diff --git a/docs/about.ttl b/docs/about.ttl index c696636..7110522 100644 --- a/docs/about.ttl +++ b/docs/about.ttl @@ -25,7 +25,7 @@
Checkout the user guide on application building.
+Follow the tutorial to build your first app, then see the user guide on application building for the extension layers.
All the basics of LinkedDataHub. From installation to customizing the model and user interface.
This guide will show how a LinkedDataHub application can be used to manage domain-specific RDF classes and instances. As an example, we will use SKOS concepts and concept schemes.
Note that most management actions can also be performed using the CLI (Command Line Interface). Where applicable, the UI and CLI instructions are shown side by side. The UNESCO Thesaurus demo app demonstrates how SKOS vocabularies can be managed in LinkedDataHub.
+New to LinkedDataHub? The tutorial builds a complete application, from an empty dataspace to a published app.
Setup is only required if you plan to run your own instance of LinkedDataHub. It consists of few steps, which involve creating a configuration file and running a
diff --git a/docs/tutorial.ttl b/docs/tutorial.ttl
new file mode 100644
index 0000000..77c0b8e
--- /dev/null
+++ b/docs/tutorial.ttl
@@ -0,0 +1,37 @@
+@prefix ldh: Build your first Knowledge Graph application, from an empty dataspace to a published app This tutorial rebuilds the Northwind Traders demo application: the iconic sample
+ database, reborn as an RDF Knowledge Graph. Every stage ends with a result you can see in the browser. Most steps are shown two ways: point-and-click in the user interface, and as
+ CLI commands that you can script, version and replay. Before you start you need a running LinkedDataHub instance and the owner's WebID certificate. The Get started section covers both. The command line
+ examples assume the Make the app reproducible: a git repository that installs into any instance Every stage so far produced files — document descriptions, mapping queries, CSV data, media. Kept in a repository with a small install script, they are the application: versionable,
+ diffable, reviewable, and replayable into any LinkedDataHub instance. This is the layout the Northwind Traders repository uses: The document URL is derived from the file path: categories.ttl installs to ${base}categories/, root.ttl to ${base} itself.
+ update-folder.sh walks the tree applying that rule — resolving each file's relative URIs against its document URL and Re-running the install converges instead of duplicating: prompts for the base URI, certificate and password, then runs install.sh: make public, import the ontology, replay the documents, upload the files, run the CSV imports. The
+ entire tutorial, replayed in one command — against localhost today, against production tomorrow. Next: Beyond low-code Where to go when data alone is not enough Everything in this tutorial was done with data: documents, queries and authorizations, applied over HTTP. That is the ceiling of the low-code approach — and there is a door in it. Each layer
+ of LinkedDataHub can be extended without forking it: If you get stuck or build something worth showing, open a thread on GitHub Discussions. Back to the Tutorial overview Assemble the landing page from content blocks Everything the app can show is now in place — text, data, charts, views. The landing page is their composition: an ordered sequence of
+ content blocks on the root document. Two block types exist: The demo's finished root document interleaves them into an executive dashboard: The narrative rhythm — prose, then evidence, then prose — is an editorial choice, and it is made entirely in data: reordering the page is renumbering The homepage is the finished application: branded header, live charts over the imported graph, and navigation into the containers — every pixel of it a rendering of one document's
+ description. Next: Publish Import CSV data using SPARQL mappings Northwind's data ships as plain CSV files. LinkedDataHub imports CSV by running a SPARQL The three Create the import from the UI — upload the CSV file, pick the mapping query and the target container — following the Import CSV data
+ guide. Each import pairs a mapping query with a CSV file and a target container. The demo lists them in a manifest, imports.csv: and replays the manifest with a small script built on the CLI's Imports run asynchronously — each one is itself a document that records its status. The Categories container lists eight category documents. Open one: the category renders with its name, description and identifier, typed as a product group — with the labels coming from
+ the Model stage. Repeat the pattern for the remaining entities and the Knowledge Graph fills in, orders linking to products, products to suppliers and
+ categories. Next: Media Give your app an identity: a title, a description and a landing page In LinkedDataHub everything is a document, and every document is a named graph whose name is the document's URL. Your
+ dataspace starts with a single document: the root document at the base URI. It doubles as the app's homepage, so making the app yours starts with describing this one
+ document. In this stage you give the root document a title, a description and a first content block — and see the result render as your
+ homepage. Open the base URL (e.g. https://localhost:4443/) and authenticate with the owner's WebID certificate. Switch the root document into edit mode, change the title to
+ Northwind Traders, fill in the description, and add an XHTML content block with a short welcome text. The Edit
+ content guide walks through the block editor in detail. The same change, expressed as data. Describe the root document in a root.ttl file: Then replace the root document's description with it using Reload the base URL: the homepage now carries your title and welcome text. You have not touched a template, a controller or a database schema — you replaced the description of one
+ document, and the UI is a rendering of that description. Next: Structure Turn SPARQL queries into charts and views With the graph populated, analysis is a query away — and in LinkedDataHub, queries, charts and
+ views are resources like everything else. A chart is three resources chained together: a
+ On the Categories container, revenue per category — the query joins order items to products to categories across their documents: Add Views render query results as document listings instead of plots — the container's own children listing is one. A grid of all categories: The same resources can be created interactively: save a query from the query editor, then add a chart block that references
+ it. The Categories container is a small dashboard: a revenue bar chart, a products-per-category chart, and a grid of the categories themselves — all live views over the graph, recomputed on
+ each request. Next: Composition Upload files and link them to resources Northwind comes with images: a photo per employee, an icon per category. Uploaded files are stored content-addressed — the file's URL is derived from the SHA-1 hash of its
+ content, under ${base}uploads/. The same bytes always land at the same URL, which is what makes file references stable enough to bake into import mappings. Upload a file with the file upload form as shown in the Upload file guide. A file becomes part of the graph the moment something points at it. The category mapping from the Data stage does exactly that — the CSV carries each image's hash,
+ and the mapping mints the upload URL from it: Category and employee documents now render with their images — Next: Insight Define the vocabulary that describes your domain Each dataspace has a namespace ontology where you define the classes and properties of your domain. It lives in the admin application, which shares your base URI with
+ an admin. subdomain prefix: for the end-user app at https://localhost:4443/, the ontology document is
+ https://admin.localhost:4443/ontologies/namespace/. Northwind reuses Schema.org terms rather than inventing its own, so "modeling" here means declaring which classes the app works with and how
+ they should be labeled: Open the admin application and edit the namespace ontology document directly, or follow the Change model guide, which also
+ shows how to add SPIN constructors to classes — the templates that drive "create instance" forms in the UI. Append the class definitions from ns.ttl to the namespace ontology document with Ontologies are cached in memory, so tell the application to reload it: The ontology document in the admin app renders your classes with their labels. The visible payoff on the end-user side comes in the next stage: resources imported as
+ Next: Data Grant public access and ship it So far only the owner's WebID certificate can see the app. Access is governed by ACL authorizations — documents in the admin
+ app, like everything else. Publishing means creating one authorization: read access on the whole dataspace for all agents. The script creates the public-read authorization in the admin application. Authorizations are data, so you can inspect the document it created — and delete it to unpublish. The same authorization can be created with the access control forms in the admin application — see the ACL reference for
+ the authorization model: who (agents, agent classes), what (documents, document classes), and which Open the base URL in a private browser window, with no certificate: the Northwind Traders app renders, charts and all. It is now a public Linked Data dataset and a public web
+ application — the same URLs serve both, negotiated by content type. Next: App as a repository Lay out the document hierarchy that will hold your data Documents come in two kinds: containers, which hold child documents, and
+ items. Northwind's domain gives us the container layout directly — one container per entity type: Create each container from the root document using the document creation form — see the Create documents guide. A container is just a document whose description says it is one. categories.ttl: Repeat for the remaining eight containers — or skip ahead to App as a repository to see how the demo derives the document URL from the filename and
+ replays a whole folder in one command. The root document now lists the containers as its children, and each container URL resolves to an (empty) listing page. The URL space of your app is its information
+ architecture. Next: Model Using LinkedDataHub as a low-code platform for Knowledge Graph applications If you are building your first app, start with the tutorial — this guide covers the layers beyond it. Every component in LinkedDataHub is data-driven and was designed with extensibility in mind. You can override behavior (e.g. Java method or XSLT template) without having to modify LinkedDataHub's
codebase, and more importantly, without having to write the same logic from scratch. The following sections are split by component/layer and explain how to extend them when building bespoke apps.bin/ scripts are on your PATH and use these shell variables throughout:base="https://localhost:4443/"
+cert_pem_file="ssl/owner/cert.pem"
+cert_password="..."
+ Stages
+
+
+ northwind-traders/
+├── root.ttl # one .ttl per document...
+├── categories.ttl
+├── categories/ # ...and a folder for the files it imports
+│ ├── categories.csv
+│ ├── categories.rq
+│ └── *.gif
+├── admin/model/
+│ ├── ns.ttl # the namespace ontology
+│ ├── patch-ontology.ru # resets it before re-import
+│ └── import-ns.sh
+├── imports.csv # the CSV import manifest
+├── install.sh # replays everything against a base URI
+├── update-folder.sh
+├── .ldhignore # files that are not documents
+└── Makefile
+ The convention
+ PUTting the result — and skips anything
+ matched by .ldhignore.Idempotency
+ PUT replaces each document's description outright, and the one place that appends rather than replaces — the
+ namespace ontology — is first reset by patch-ontology.ru, a SPARQL update that clears the previously imported terms.Installing
+ make install
+
+
+ ldh:XHTML holds authored markup, and
+ ldh:Object embeds any other resource — a chart, a view, a document. Anything that is not markup gets wrapped in an object block.<> a def:Root ;
+ dct:title "Northwind Traders" ;
+ rdf:_1 <#page-header> ; # XHTML — title and pitch
+ rdf:_2 <#overview-intro> ; # XHTML — dashboard intro
+ rdf:_3 <#sales-trend-block> ; # Object — monthly sales line chart
+ rdf:_4 <#revenue-by-country-block> ; # Object — geo chart
+ rdf:_5 <#top-selling-products> ; # Object — product ranking
+ rdf:_6 <#top-manager-header> ; # XHTML — section heading
+ rdf:_7 <#top-manager> ; # Object — employee of the quarter
+ rdf:_8 <#navigation-prompt> ; # XHTML — where to go next
+ rdf:_9 <#select-children> . # Object — children view
+ rdf:_N. In the browser the
+ same composition is drag-and-drop in edit mode; from the command line it is the root.ttl from the first stage grown to its final form, applied
+ with the same PUT.What you now see
+ CONSTRUCT mapping over each
+ row: the row's cells are bound as properties of a row resource, and the query builds the RDF you want from them. The mapping for categories, categories.rq:PREFIX foaf: <http://xmlns.com/foaf/0.1/>
+PREFIX dct: <http://purl.org/dc/terms/>
+PREFIX schema: <https://schema.org/>
+
+CONSTRUCT
+{
+ GRAPH ?graph
+ {
+ ?graph dct:title ?categoryName ;
+ foaf:primaryTopic ?category .
+
+ ?category a schema:ProductGroup ;
+ dct:title ?categoryName ;
+ schema:name ?categoryName ;
+ schema:identifier ?categoryID ;
+ schema:description ?description .
+ }
+}
+WHERE
+{
+ ?category_row <#categoryID> ?categoryID ;
+ <#description> ?description ;
+ <#categoryName> ?categoryName .
+
+ BIND(uri(concat(str($base), "categories/")) AS ?container)
+ BIND(uri(concat(str(?container), encode_for_uri(?categoryID), "/")) AS ?graph)
+ BIND(uri(concat(str(?graph), "#this")) AS ?category)
+}
+ BINDs carry the whole document model of the import:
+
+ ?container — the target container from the Structure stage, resolved against $base?graph — one document (named graph) per row, minted inside the container from the row's identifier?category — the document's topic: the document is about the category, so the category is a #this fragment of itIn the browser
+ From the command line
+ query_filename,csv_filename,target,title
+categories/categories.rq,categories/categories.csv,categories/,Categories
+products/products.rq,products/products.csv,products/,Products
+orders/orders.rq,orders/orders.csv,orders/,Orders
+...
+ create-csv-import.sh:./import-csv.sh "$base" "$cert_pem_file" "$cert_password" "$PWD/imports.csv"
+ What you now see
+ In the browser
+ From the command line
+ @prefix def: <https://w3id.org/atomgraph/linkeddatahub/default#> .
+@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix dct: <http://purl.org/dc/terms/> .
+
+<> a def:Root ;
+ dct:title "Northwind Traders" ;
+ dct:description "Knowledge Graph representation of the Northwind Traders sample database" ;
+ rdf:_1 <#intro> .
+
+<#intro> a ldh:XHTML ;
+ rdf:value '''<div xmlns="http://www.w3.org/1999/xhtml">
+ <div class="page-header">
+ <h2>Northwind Traders</h2>
+ <p class="lead">The iconic sample database converted to an RDF Knowledge Graph</p>
+ </div>
+ </div>'''^^rdf:XMLLiteral .
+ PUT. The turtle command resolves the relative URIs in the file against the document URL, so the same
+ file installs against any base URI:cat root.ttl | turtle --base="$base" | put.sh \\
+ -f "$cert_pem_file" \\
+ -p "$cert_password" \\
+ -t "application/n-triples" \\
+ "$base"
+ What you now see
+ How this works
+
+
+ rdf:_1, rdf:_2, … sequence a document's contentPUT semantics behind put.shsp:Select query holding the SPARQL text, a ldh:ResultSetChart describing how to plot its results, and a ldh:Object block placing it on a document.A revenue chart
+ <#category-revenue-query> a sp:Select ;
+ dct:title "Category revenue" ;
+ sp:text '''PREFIX schema: <https://schema.org/>
+
+SELECT ?categoryName (SUM(?sale) AS ?revenue)
+WHERE {
+ GRAPH ?orderGraph {
+ ?order schema:orderedItem ?orderItem .
+ ?orderItem schema:orderedItem ?product ;
+ schema:orderQuantity ?quantity ;
+ schema:price ?price .
+ BIND(?quantity * ?price AS ?sale)
+ }
+ GRAPH ?productGraph {
+ ?product schema:category ?category .
+ }
+ GRAPH ?categoryGraph {
+ ?category schema:name ?categoryName .
+ }
+}
+GROUP BY ?category ?categoryName
+ORDER BY DESC(?revenue)''' .
+
+<#category-revenue> a ldh:ResultSetChart ;
+ dct:title "Revenue by category" ;
+ spin:query <#category-revenue-query> ;
+ ldh:chartType <https://w3id.org/atomgraph/client#BarChart> ;
+ ldh:categoryVarName "categoryName" ;
+ ldh:seriesVarName "revenue" .
+
+<#category-revenue-block> a ldh:Object ;
+ rdf:value <#category-revenue> .
+ <#category-revenue-block> to the container's block sequence (rdf:_2 after the intro block) and PUT the document as before.A view
+ <#select-categories-view> a ldh:View ;
+ spin:query <#select-categories-query> ;
+ ac:mode <https://w3id.org/atomgraph/client#GridMode> .
+ In the browser
+ What you now see
+ In the browser
+ From the command line
+ add-file.sh \\
+ -b "$base" \\
+ -f "$cert_pem_file" \\
+ -p "$cert_password" \\
+ --title "Nancy Davolio" \\
+ --file "$PWD/employees/nancy.jpg" \\
+ --content-type "image/jpeg" \\
+ "$base"
+ Linking files to resources
+ ?category foaf:depiction ?picture .
+...
+BIND(uri(concat(str($base), "uploads/", encode_for_uri(?pictureHash))) AS ?picture)
+ What you now see
+ foaf:depiction is picked up by the default layout, no template work required.@prefix : <#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix schema: <https://schema.org/> .
+
+: a owl:Ontology .
+
+schema:ProductGroup a owl:Class ;
+ rdfs:label "Product group" ;
+ rdfs:isDefinedBy : .
+
+schema:Product a owl:Class ;
+ rdfs:label "Product" ;
+ rdfs:isDefinedBy : .
+
+schema:Order a owl:Class ;
+ rdfs:label "Order" ;
+ rdfs:isDefinedBy : .
+ In the browser
+ From the command line
+ POST. The prepended @base makes the : prefix resolve to
+ ${base}ns# — the terms belong to the end-user namespace even though the ontology document lives in the admin app:admin_base="https://admin.localhost:4443/"
+
+{ echo "@base <${base}ns> ."; cat ns.ttl; } | post.sh \\
+ -f "$cert_pem_file" \\
+ -p "$cert_password" \\
+ --content-type "text/turtle" \\
+ "${admin_base}ontologies/namespace/"
+ clear-ontology.sh \\
+ -f "$cert_pem_file" \\
+ -p "$cert_password" \\
+ -b "$admin_base" \\
+ --ontology "${base}ns#"
+ What you now see
+ schema:Product or schema:Order arrive typed and labeled, and the model is what the UI uses to render and create them.How this works
+
+
+ From the command line
+ make-public.sh -b "$base" -f "$cert_pem_file" -p "$cert_password"
+ In the browser
+ acl:mode.What you now see
+ https://localhost:4443/
+├── categories/
+├── customers/
+├── employees/
+├── orders/
+├── products/
+├── regions/
+├── shippers/
+├── suppliers/
+└── territories/
+ In the browser
+ From the command line
+ @prefix dh: <https://www.w3.org/ns/ldt/document-hierarchy#> .
+@prefix dct: <http://purl.org/dc/terms/> .
+
+<> a dh:Container ;
+ dct:title "Categories" .
+ PUT it to the container URL, the same way the root document was updated:cat categories.ttl | turtle --base="${base}categories/" | put.sh \\
+ -f "$cert_pem_file" \\
+ -p "$cert_password" \\
+ -t "application/n-triples" \\
+ "${base}categories/"
+ What you now see
+