diff --git a/docs/about.ttl b/docs/about.ttl index 8312121..4086262 100644 --- a/docs/about.ttl +++ b/docs/about.ttl @@ -2,6 +2,7 @@ @prefix rdf: . @prefix dh: . @prefix dct: . +@prefix schema: . <> a dh:Item ; dct:title "About LinkedDataHub" ; @@ -11,7 +12,13 @@ <#overview-video> a ldh:Object ; dct:title "LinkedDataHub 6 overview" ; - rdf:value . + rdf:value ; + ldh:graph <> . + + a schema:VideoObject ; + schema:name "LinkedDataHub 6 overview" ; + schema:url ; + schema:thumbnailUrl . <#content> a ldh:XHTML ; rdf:value """
@@ -37,10 +44,6 @@

Knowledge management and collaboration

-
- -

Screen recording: LinkedDataHub overview

-

As an RDF-native CMS, LinkedDataHub provides these end-user features:

  • creating structured content from Linked Data and SPARQL resources
  • @@ -48,7 +51,7 @@
  • browsing Linked Data
  • copying RDF resources into the local dataspace
  • searching and querying SPARQL endpoints
  • -
  • creating collections from SPARQL queries
  • +
  • creating collections from SPARQL queries
  • editing content in place, with RDFa annotations linking text to resources
  • importing CSV data by mapping it to RDF using SPARQL
  • visualizing query results as graphs, charts etc.
  • diff --git a/docs/extending.ttl b/docs/extending.ttl index 830c8f0..b1102e9 100644 --- a/docs/extending.ttl +++ b/docs/extending.ttl @@ -12,7 +12,7 @@ <#content> a ldh:XHTML ; rdf:value """

    Extending LinkedDataHub with your own code: XSLT stylesheets, Java endpoints, Docker images

    -

    Everything in the tutorial and the user guide happens through the UI, the CLI and RDF files. The pages in this section pick up where low-code ends: they show how to override the rendering with your own XSLT stylesheet and how to package an app with custom Java endpoints and a dedicated Docker image.

    +

    Everything in the tutorial and the user guide happens through the UI, the CLI and RDF files. The pages in this section pick up where low-code ends: they show how to override the rendering with your own XSLT stylesheet, how to create a package for your own vocabulary, and how to package an app with custom Java endpoints and a dedicated Docker image.

    """^^rdf:XMLLiteral . <#select-children> a ldh:Object ; diff --git a/docs/extending/create-packages.ttl b/docs/extending/create-packages.ttl new file mode 100644 index 0000000..d71caa7 --- /dev/null +++ b/docs/extending/create-packages.ttl @@ -0,0 +1,73 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Create packages" ; + dct:description "Bundle the ontology and the stylesheet for your own vocabulary as a reusable package" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Bundle the ontology and the stylesheet for your own vocabulary as a reusable package

    +

    A package is a namespace ontology and an XSLT stylesheet, published as Linked Data behind a package URI. + Developers can create custom packages for their own domain vocabularies. The process has four steps.

    +
    +

    Write package ontology

    +

    Create ns.ttl describing both the document the registry serves it as and the ontology inside it, with the vocabulary import and the views:

    +
    <> a dh:Item ;
    +    dct:title "Schema.org ontology" ;
    +    foaf:primaryTopic <#> .
    +
    +<#> a owl:Ontology ;
    +    owl:imports <https://schema.org/> .
    +
    +# Attach view to a property
    +schema:knows ldh:view :PersonKnows .
    +
    +:PersonKnows a ldh:View ;
    +    dct:title "Knows" ;
    +    spin:query :SelectPersonKnows .
    +
    +:SelectPersonKnows a sp:Select ;
    +    sp:text \"\"\"
    +    SELECT DISTINCT ?person
    +    WHERE { GRAPH ?graph { $about schema:knows ?person } }
    +    ORDER BY ?person
    +    \"\"\" .
    +
    + +
    +

    The dct:title on <> is not optional. A write is held to the constraints of the document as it will be written, and a LinkedDataHub document must be titled, so an ns.ttl that describes only the ontology is refused 422 Unprocessable Entity when it is pushed.

    +
    +
    +
    +
    +

    Write XSLT stylesheet

    +

    Create the stylesheet — name the file for the vocabulary it covers — with XSLT templates using system modes like ac:*, ldh:* and xhtml:*. See the Stylesheets reference for template customization patterns.

    +
    <xsl:template match="schema:knows" mode="ac:PropertyEditor"/>
    +
    +
    +

    Publish package metadata

    +

    Publish the descriptor as Linked Data at your package URI, naming the ontology beside it:

    +
    <> a dh:Container ;
    +    dct:title "Schema.org" ;
    +    foaf:primaryTopic <#this> .
    +
    +<#this> a lds:Package ;
    +    dct:title "Schema.org Package" ;
    +    dct:description "Schema.org vocabulary support" ;
    +    lds:ontology <ns/#> .
    +

    Then upload the stylesheet into the package document and add ac:stylesheet naming that upload, which is where a registry published with ldh push gets it:

    +
    package="https://packages.example.com/schema.org/"
    +
    +upload=$(ldh add file --title schema.xsl --file schema.xsl "$package")
    +echo "INSERT { <${package}#this> <https://w3id.org/atomgraph/client#stylesheet> <$upload> } WHERE { }" | ldh patch "$package"
    +

    A dataspace importing the package dereferences its descriptor, ontology and stylesheet anonymously, so all three have to be readable without a certificate.

    +
    +
    +

    Test installation

    +

    Declare the ldh:import triple in a test dataspace's settings document — see Management for the command.

    +
    +
    """^^rdf:XMLLiteral . diff --git a/docs/get-started.ttl b/docs/get-started.ttl index fab954b..08e4cbd 100644 --- a/docs/get-started.ttl +++ b/docs/get-started.ttl @@ -35,7 +35,7 @@

Connect an agent

-

AI agents such as Claude Code or Cursor work with LinkedDataHub through the same HTTP API, using Web-Algebra's MCP server. Connected that way, an agent can declare a working application in a dataspace — containers, views and charts — instead of generating code for one.

+

AI agents such as Claude Code or Cursor work with LinkedDataHub through the same HTTP API, with the ldh Claude Code plugin or over Web-Algebra's MCP server. Connected that way, an agent can declare a working application in a dataspace — containers, views and charts — instead of generating code for one.

Read instructions on connecting an agent.

diff --git a/docs/get-started/agents.ttl b/docs/get-started/agents.ttl index fb76bb3..991dd0c 100644 --- a/docs/get-started/agents.ttl +++ b/docs/get-started/agents.ttl @@ -5,21 +5,66 @@ <> a dh:Item ; dct:title "Connect an agent" ; - dct:description "Give an AI agent such as Claude Code or Cursor access to a LinkedDataHub instance over MCP, using Web-Algebra" ; + dct:description "Give an AI agent such as Claude Code or Cursor access to a LinkedDataHub instance, with the ldh Claude Code plugin or over MCP with Web-Algebra" ; rdf:_1 <#content> . <#content> a ldh:XHTML ; rdf:value """
-

Give an AI agent such as Claude Code or Cursor access to a LinkedDataHub instance over MCP, using Web-Algebra

-

Agents use the same HTTP API as the browser and the CLI. The bridge is - Web-Algebra, which exposes LinkedDataHub operations as - Model Context Protocol (MCP) tools: creating containers and items, adding views, - queries and charts, reading and writing RDF, and running SPARQL. An agent connected this way can build a working application in a dataspace — +

Give an AI agent such as Claude Code or Cursor access to a LinkedDataHub instance, with the ldh Claude Code plugin or over MCP with Web-Algebra

+

Agents use the same HTTP API as the browser and the CLI, and act as a WebID: an agent can read and + write only what its WebID is authorized to. There are two ways to connect one. The Claude Code plugin gives Claude Code + skills for building on LinkedDataHub with the ldh CLI. Web-Algebra exposes LinkedDataHub operations to any + Model Context Protocol (MCP) client as tools: creating containers and items, adding views, + queries and charts, reading and writing RDF, and running SPARQL. Either way, an agent can build a working application in a dataspace: its classes, containers, views and charts are documents like any other.

The conventions agents should follow when writing to LinkedDataHub are in - AGENTS.md.

+ AGENTS.md. To ask questions about the data in a document + from the browser instead, use the assistant.

-

Prerequisites

+

Claude Code plugin

+

The ldh plugin ships with LinkedDataHub. + Its marketplace is the LinkedDataHub repository, so Claude Code is told about it once and then installs the plugin from it. In Claude Code:

+
/plugin marketplace add AtomGraph/LinkedDataHub
+/plugin install ldh@linkeddatahub
+

The skills drive the ldh CLI, so they need a LinkedDataHub instance, the owner's WebID certificate and the CLI on the PATH + (see installing the CLI). Claude loads a skill by itself when a request matches what it + covers. To call one explicitly, type /ldh: followed by its name.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Skills of the ldh plugin
SkillCovers
ldh:dataspaceProvisioning a new dataspace on a self-hosted instance: the configuration files, the restart, and checking that it is up
ldh:datasetThe namespace ontology and its install, CSV and RDF imports, federation with remote SPARQL endpoints, and building a large dataset standalone and bulk-loading it
ldh:appDocuments, blocks, views and charts, and an app kept as a directory and installed with ldh push
ldh:packageA reusable package: an ontology and an XSLT stylesheet, published to a registry and imported with ldh packages add
+

A request can span several skills, and Claude uses them in turn:

+

Here are products.csv and categories.csv. Model them with schema.org, import them into the inventory dataspace, and make each category's page list its products.

+
+
+

Web-Algebra over MCP

+

Web-Algebra expresses LinkedDataHub operations, SPARQL queries and Linked Data + reads as plans that run as one unit, and its MCP server offers them as tools. Any MCP client can use it.

+
+

Prerequisites

  • A running LinkedDataHub instance. The examples below assume the default local setup at https://localhost:4443/.
  • The WebID certificate the agent will act as. For your own instance that is the owner certificate, ssl/owner/cert.pem, with the password from @@ -28,7 +73,7 @@
-

Claude Code

+

Claude Code

Register Web-Algebra as an MCP server, run from the LinkedDataHub directory:

claude mcp add web-algebra \\
   --env CERT_PEM_PATH="$PWD/ssl/owner/cert.pem" \\
@@ -37,7 +82,7 @@
         

Start claude and check that /mcp lists web-algebra as connected.

-

Cursor and Claude Desktop

+

Cursor and Claude Desktop

Both read an mcpServers entry: ~/.cursor/mcp.json for Cursor, claude_desktop_config.json for Claude Desktop. Use absolute paths and the literal password, since these files are not run by a shell.

{
@@ -55,7 +100,7 @@
         

If the client cannot find uv, replace "uv" with its absolute path, as printed by which uv.

-

Streamable HTTP transport

+

Streamable HTTP transport

Clients that connect to an MCP URL instead of launching a process can use Web-Algebra's HTTP server:

CERT_PEM_PATH="$PWD/ssl/owner/cert.pem" \\
 CERT_PASSWORD="$(cat secrets/owner_cert_password.txt)" \\
@@ -70,6 +115,7 @@ uv run --with web-algebra uvicorn web_algebra.server:app
+

Try it

Ask the agent for something small that it has to declare rather than code. With the Northwind Traders demo installed, for example:

diff --git a/docs/reference.ttl b/docs/reference.ttl index 8c8e5dd..6656f2c 100644 --- a/docs/reference.ttl +++ b/docs/reference.ttl @@ -13,7 +13,7 @@ rdf:value """

Feature-by-feature reference for the LinkedDataHub platform, its data model, HTTP API and administration

The concept pages describe the dataspace, its dataset and the data model. - The interface pages cover the user interface, the HTTP API and the command line interface. + The interface pages cover the user interface, the HTTP API, the command line interface and the assistant. Customization is covered by the stylesheets and imports pages, and operations by the configuration, triplestores, versioning and administration pages. Start with the dataspace and data model pages; an application or deployment moving off 5.x starts at diff --git a/docs/reference/administration/acl.ttl b/docs/reference/administration/acl.ttl index 67f85c8..2ef119b 100644 --- a/docs/reference/administration/acl.ttl +++ b/docs/reference/administration/acl.ttl @@ -14,7 +14,7 @@

LinkedDataHub access control is based on the W3C ACL ontology.

Access modes

-

There are four access modes (classes of operation) that map to HTTP methods:

+

There are four access modes (classes of operation) that map to HTTP methods:

diff --git a/docs/reference/administration/ontologies.ttl b/docs/reference/administration/ontologies.ttl index 4d76679..4fe72a3 100644 --- a/docs/reference/administration/ontologies.ttl +++ b/docs/reference/administration/ontologies.ttl @@ -210,7 +210,7 @@ WHERE {}

Views

-

A property can carry a view that lists the resources it relates: ldh:view attaches +

A property can carry a view that lists the resources it relates: ldh:view attaches the view in the property's forward direction, ldh:inverseView in its inverse. The view is stored in no document. LinkedDataHub derives it whenever it renders a resource, and appends it to that resource's row.

Which resources get the view is decided by the property's declaration, not by the data. An ldh:view renders on every resource @@ -240,7 +240,7 @@ schema:customer ldh:inverseView :OrdersFromCustomer .

Since the view renders on every customer, ldh:showWhenEmpty false hides it on a customer that has placed no orders. The view's ac:mode fixes its layout mode, and the view renders a Create button for - inline creation of the class at the property's other end.

+ inline creation of the class at the property's other end.

The attachments are read from the namespace ontology and its imports closure, so a view declared in ns.ttl and one declared in a package ontology behave the same. A derived view has no edit or drag controls: it is changed by editing its declaration in the ontology.

diff --git a/docs/reference/administration/packages.ttl b/docs/reference/administration/packages.ttl index 78d0721..17a3f6f 100644 --- a/docs/reference/administration/packages.ttl +++ b/docs/reference/administration/packages.ttl @@ -196,56 +196,8 @@ WHERE {}

Creating custom packages

-

Developers can create custom packages for their own domain vocabularies. The process has four steps.

-

Write package ontology

-

Create ns.ttl describing both the document the registry serves it as and the ontology inside it, with the vocabulary import and the views:

-
<> a dh:Item ;
-    dct:title "Schema.org ontology" ;
-    foaf:primaryTopic <#> .
-
-<#> a owl:Ontology ;
-    owl:imports <https://schema.org/> .
-
-# Attach view to a property
-schema:knows ldh:view :PersonKnows .
-
-:PersonKnows a ldh:View ;
-    dct:title "Knows" ;
-    spin:query :SelectPersonKnows .
-
-:SelectPersonKnows a sp:Select ;
-    sp:text \"\"\"
-    SELECT DISTINCT ?person
-    WHERE { GRAPH ?graph { $about schema:knows ?person } }
-    ORDER BY ?person
-    \"\"\" .
-
- -
-

The dct:title on <> is not optional. A write is held to the constraints of the document as it will be written, and a LinkedDataHub document must be titled, so an ns.ttl that describes only the ontology is refused 422 Unprocessable Entity when it is pushed.

-
-
-

Write XSLT stylesheet

-

Create the stylesheet — name the file for the vocabulary it covers — with XSLT templates using system modes like ac:*, ldh:* and xhtml:*. See the Stylesheets reference for template customization patterns.

-
<xsl:template match="schema:knows" mode="ac:PropertyEditor"/>
-

Publish package metadata

-

Publish the descriptor as Linked Data at your package URI, naming the ontology beside it:

-
<> a dh:Container ;
-    dct:title "Schema.org" ;
-    foaf:primaryTopic <#this> .
-
-<#this> a lds:Package ;
-    dct:title "Schema.org Package" ;
-    dct:description "Schema.org vocabulary support" ;
-    lds:ontology <ns/#> .
-

Then upload the stylesheet into the package document and add ac:stylesheet naming that upload, which is where a registry published with ldh push gets it:

-
package="https://packages.example.com/schema.org/"
-
-upload=$(ldh add file --title schema.xsl --file schema.xsl "$package")
-echo "INSERT { <${package}#this> <https://w3id.org/atomgraph/client#stylesheet> <$upload> } WHERE { }" | ldh patch "$package"
-

A dataspace importing the package dereferences its descriptor, ontology and stylesheet anonymously, so all three have to be readable without a certificate.

-

Test installation

-

Declare the ldh:import triple in a test dataspace's settings document — see Management for the command.

+

Developers can create custom packages for their own domain vocabularies: write the package ontology and stylesheet, publish the package + metadata and test the installation. The Create packages guide walks through the steps.

Available packages

diff --git a/docs/reference/assistant.ttl b/docs/reference/assistant.ttl new file mode 100644 index 0000000..d801e2f --- /dev/null +++ b/docs/reference/assistant.ttl @@ -0,0 +1,158 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Assistant" ; + dct:description "The built-in assistant: how questions become Web-Algebra plans, how it is set up, and how conversations are stored" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

The built-in assistant: how questions become Web-Algebra plans, how it is set up, and how conversations are stored

+

The assistant turns a question into a Web-Algebra plan, shows the plan, and runs + it when the reader says so. It runs in the browser as part of the client stylesheet and talks to a web-algebra service beside the platform, + which is optional and off by default — see Setup. + The user guide covers using it. To work with LinkedDataHub from an agent of your own instead, see + Connect an agent.

+
+

How it works

+
    +
  1. The client sends the question to /webalgebra/plans on the dataspace's own origin, with the current document, the dataspace's + SPARQL endpoint and ontology, and the last three turns of the conversation that ran, each with its plan, its outcome and up to a hundred + of the rows it returned.
  2. +
  3. nginx forwards /webalgebra to the web-algebra service together with the client certificate of the connection. A + request without a client certificate is answered with 401, since the service has nobody to act for.
  4. +
  5. The service asks the model for a plan, checks it against its executor, and answers with the plan, a one-sentence summary and the operations + it uses. The client shows them as a card.
  6. +
  7. On Execute, the client submits the plan to /webalgebra. The service answers 202 with an execution id, and the + client polls /webalgebra/{id}/result once a second, showing each step as the executor reports it.
  8. +
  9. When the plan has run, the client sends the result to /webalgebra/answers for a sentence that sums it up, then + stores the turn in the conversation's document and reloads the documents the plan wrote.
  10. +
+

A plan that only reads may run as soon as it arrives. One that writes always waits for the reader to press Execute.

+
+
+

Access control

+

The service runs every plan as the reader who asked. It presents the secretary's WebID certificate to the platform, with an + On-Behalf-Of header naming the reader's WebID. The platform honours it because every agent's WebID document that the platform creates, + at sign-up or at the first Google or ORCID login, states that the secretary delegates that agent + (<secretary> acl:delegates <agent>). Plans are therefore subject to the reader's own + authorizations: a plan can read and write what the reader can, and nothing else.

+

nginx clears any On-Behalf-Of and Client-Cert headers arriving from outside, so identity comes only from the TLS + connection. The web-algebra service is reachable only through nginx.

+

On a deployment that enables the assistant, the Assistant button + appears for a signed-in agent with append access to the document, and a conversation accepts new questions under the same condition.

+
+
+

Setup

+
+ +
+

The web-algebra service runs the ghcr.io/atomgraph/webalgebra-server-ldh image, which is proprietary: AtomGraph publishes the image but not its source, and LinkedDataHub's license does not cover it. The assistant is therefore opt-in.

+
+
+

docker-compose.yml declares the web-algebra service under the assistant Compose + profile, so it does not start unless the profile is active, and + the Assistant button stays hidden until the platform is told that the service runs. Writing plans from questions also needs an + OpenAI API key:

+
    +
  1. Enable the profile in .env: +
    COMPOSE_PROFILES=assistant
    + The profile starts the service, and the platform reads the same variable: it then publishes lds:assistant true for every + dataspace in its /context, which is what the button is shown on. Set it in .env rather than with + docker compose --profile, which the platform does not see.
  2. +
  3. Put the key in secrets/openai_api_key.txt
  4. +
  5. Declare the secret and hand it to the service in docker-compose.override.yml: +
    secrets:
    +  openai_api_key:
    +    file: ./secrets/openai_api_key.txt
    +services:
    +  web-algebra:
    +    environment:
    +      - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key
    +    secrets:
    +      - secretary_cert_password
    +      - openai_api_key
    +
  6. +
  7. Recreate the services: make up
  8. +
+

Without a key the service still runs plans, but declines to write them: a question is answered with a message saying so, shown in the card. + The sentence that sums up a result also comes from the model; without it, the card shows the rows alone.

+

The service's variables are listed in the configuration reference.

+
+

Running without the service

+

Without the profile, the rest of the stack runs as usual: nginx looks the service up per request instead of at startup, and answers + /webalgebra with 502. Conversations stored while the assistant was enabled are still drawn from the store, but a + new question in them fails with an error in its card.

+
+
+
+

Data model

+

A conversation is an ldh:Chat resource, placed in a document by an object block + like a view or a chart. Its turns are ldh:ChatTurn resources in the same document, and the chat's rdf:_1, + rdf:_2, … members in the order they were asked. The first question writes the chat, the object block and the document's next + rdf:_N for the block in one request; the chat and its block share a fragment suffix, and the chat's title is the first question, cut near + 80 characters. Each turn is written when its answer has landed, or has failed to.

+
Access modes and the HTTP methods they permit
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Properties of an assistant turn
PropertyValue
ldh:questionWhat the reader asked
ldh:answerWhat the plan did, read by the model, as an rdf:XMLLiteral: an XHTML div of paragraphs, lists, emphasis, code and links to what the plan returned, when the model gave one. Other markup is dropped, and so are links that are not http(s). Turns stored before answers were XHTML hold a plain string
ldh:planThe Web-Algebra plan as an rdf:XMLLiteral: its summary, its operations and the plan itself
ldh:executionWhat running the plan reported, as an rdf:XMLLiteral: the steps, the documents written, and the first hundred rows or resources of the result
ldh:outcomeHow the turn went, in one line: rows or resources returned, documents written, no results, the failure's message, or declined when the service answered in words instead of with a plan
dct:createdWhen the turn was stored
+

A stored conversation is drawn from these resources when the document is rendered again, so it shows the result as it was when the turn ran; + nothing is run again. Since the chat is an ordinary resource, its object block can also be placed in another document, and questions asked + there are stored in the chat's own document.

+
@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/> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+<https://localhost:4443/customers/> rdf:_3 <https://localhost:4443/customers/#block-91c0…> .
+
+<https://localhost:4443/customers/#block-91c0…> a ldh:Object ;
+    rdf:value <https://localhost:4443/customers/#chat-91c0…> .
+
+<https://localhost:4443/customers/#chat-91c0…> a ldh:Chat ;
+    dct:title "Which five customers ordered the most in 1997?" ;
+    rdf:_1 <https://localhost:4443/customers/#turn-5d7e…> .
+
+<https://localhost:4443/customers/#turn-5d7e…> a ldh:ChatTurn ;
+    ldh:question "Which five customers ordered the most in 1997?" ;
+    ldh:answer "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>The five customers with the most orders in 1997 were:</p><ol><li><a href=\"…\">Save-a-lot Markets</a>: 17 orders</li>…</ol></div>"^^rdf:XMLLiteral ;
+    ldh:plan "<wa:plan …>…</wa:plan>"^^rdf:XMLLiteral ;
+    ldh:execution "<wa:execution …>…</wa:execution>"^^rdf:XMLLiteral ;
+    ldh:outcome "5 rows" ;
+    dct:created "2026-10-17T09:12:44Z"^^xsd:dateTime .
+
+
"""^^rdf:XMLLiteral . diff --git a/docs/reference/configuration.ttl b/docs/reference/configuration.ttl index 0756362..7a6a1e7 100644 --- a/docs/reference/configuration.ttl +++ b/docs/reference/configuration.ttl @@ -178,7 +178,15 @@ HOST=ec2-54-235-229-141.compute-1.amazonaws.com

Linked Data

-

ENABLE_LINKED_DATA_PROXY takes false to disable the Linked Data proxy, which is enabled by default.

+

ENABLE_LINKED_DATA_PROXY takes false to disable the Linked Data proxy, which is enabled by default.

+
+
+

Assistant

+

COMPOSE_PROFILES, the Compose variable, is passed to the platform too. When it includes assistant, which also starts the web-algebra service, the platform offers the assistant: it publishes lds:assistant true for every dataspace in its /context, and documents show the Assistant button. The assistant is disabled by default.

+
+
+

Sitemap

+

ENABLE_SITEMAP takes true to serve the sitemap.xml of every end-user dataspace, which is disabled by default: its queries walk every public document of the dataspace. Without it, every robots.txt disallows crawling. The sample .env turns it on, since a new install is small; a large deployment can leave it unset.

HTTP(S)

@@ -214,7 +222,7 @@ HOST=ec2-54-235-229-141.compute-1.amazonaws.com

HTTP client timeouts

-

Timeouts and connection lifetimes for LinkedDataHub's pooled HTTP clients, used for the Linked Data proxy and for accessing SPARQL services. All values are in milliseconds and are passed as CATALINA_OPTS system properties.

+

Timeouts and connection lifetimes for LinkedDataHub's pooled HTTP clients, used for the Linked Data proxy and for accessing SPARQL services. All values are in milliseconds and are passed as CATALINA_OPTS system properties.

CLIENT_SOCKET_TIMEOUT
Socket (read) timeout — how long to wait for data on an established connection. Defaults to 120000.
@@ -278,6 +286,43 @@ HOST=ec2-54-235-229-141.compute-1.amazonaws.com

SEF_ROOT and SEF_COMPILER reach the platform through CATALINA_OPTS and are optional: without them packages compose server-side only, and the client runs the stock stylesheet. A deployment with its own compose file adds the service, the platform's depends_on on it and the bind mount.

CLIENT_STYLESHEET names the client stylesheet the SEF is composed from and defaults to the platform's client.xsl. A deployment whose page bootstraps its own client stylesheet sets it to that stylesheet's webapp path - it must import the platform's client.xsl directly, by an href that resolves from that path - and its layout prefers ldh:client-stylesheet() over its own SEF; otherwise the site's rules vanish from a package dataspace on the first client-side navigation, while a reload brings them back.

+
+

web-algebra service

+

Writes and runs the assistant's Web-Algebra plans. + nginx forwards /webalgebra to it with the caller's client certificate, and the service is not exposed otherwise. It runs a plan as + the caller by presenting the secretary's certificate with an On-Behalf-Of header, and reaches the instance through nginx's client + certificate port. Its outbound requests to public destinations, such as the model's API or a SPARQL endpoint a plan names, go through + egress.

+

The service is opt-in: it belongs to the assistant Compose profile and starts only with COMPOSE_PROFILES=assistant in .env, which the platform reads as well. Its image, ghcr.io/atomgraph/webalgebra-server-ldh, is proprietary: AtomGraph publishes the image but not its source, and LinkedDataHub's license does not cover it.

+
+

Secrets

+
+
secretary_cert_password
+
Password of the secretary's WebID certificate, which the service presents to the instance
+
openai_api_key
+
OpenAI API key the service writes plans with (optional). Declared in the override together with OPENAI_API_KEY_FILE, as shown in the assistant setup. Without it the service runs plans but does not write them
+
+
+
+

Environment

+
+
WEBALGEBRA_VERSION
+
Compose-level variable: the tag of the ghcr.io/atomgraph/webalgebra-server-ldh image. Defaults to latest
+
OPENAI_API_KEY_FILE
+
Path of the openai_api_key secret inside the container, /run/secrets/openai_api_key
+
OPENAI_MODEL
+
The model plans are written with. Defaults to gpt-4o
+
WEBALGEBRA_TRUSTED_ORIGIN
+
The origin the service passes the caller's identity to: this instance, with every dataspace subdomain under it. Set from PROTOCOL, HOST and HTTPS_PORT
+
WEBALGEBRA_PROXY_HOST, WEBALGEBRA_PROXY_PORT
+
Where requests for the trusted origin are sent inside the Docker network: nginx's client certificate listener, nginx:9443
+
WEBALGEBRA_KEYSTORE, WEBALGEBRA_KEYSTORE_PASSWORD_FILE
+
The secretary's PKCS12 keystore and its password file
+
WEBALGEBRA_TRUSTSTORE
+
The server certificate, as PEM, which the service trusts when it connects to nginx
+
+
+

nginx service

@@ -291,8 +336,20 @@ HOST=ec2-54-235-229-141.compute-1.amazonaws.com
MAX_BODY_SIZE
Maximum allowed request body size (linkeddatahub has a separate setting for this). Defaults to 2097152.
-

By default nginx is configured to guard against DoS by limiting the rate of requests per second, which can be necessary on a public instance. The limiting can be disabled in platform/nginx.conf.template - by commenting out all lines starting with limit_req using #.

+

By default nginx is configured to guard against DoS by limiting the rate of requests per second, which can be necessary on a public instance. The limiting can be disabled in the nginx configuration inlined in docker-compose.yml + by commenting out all lines starting with limit_req and limit_conn using #.

+
+

SPARQL endpoint limits

+

An external client's request to a dataspace's /sparql is capped by concurrency as well as by rate, since a few heavy queries in flight at once can exhaust the triplestore's memory however slowly they arrive. A request beyond any of the limits is answered with 429 Too Many Requests. Other paths, and callers from private (RFC 1918) addresses such as the platform itself, are not counted.

+
+
NGINX_SPARQL_CONN_PER_CLIENT
+
Maximum /sparql requests in flight per client. Defaults to 2.
+
NGINX_SPARQL_CONN_TOTAL
+
Maximum /sparql requests in flight across all clients. Defaults to 8.
+
NGINX_SPARQL_RATE
+
Request rate per client, with a burst of 10. Defaults to 2r/s.
+
+

Server certificates

The certificates generated by the server-cert-gen.sh script are self-signed and therefore are shown as diff --git a/docs/reference/data-model.ttl b/docs/reference/data-model.ttl index 3a0f166..d07048d 100644 --- a/docs/reference/data-model.ttl +++ b/docs/reference/data-model.ttl @@ -12,7 +12,7 @@ <#content> a ldh:XHTML ; rdf:value """

Document and content structure, and the actions that can be performed on documents, blocks and resources

-

Start with the Documents page, then read Content blocks and Resources.

+

Start with the Documents page, then read Content blocks, Resources and Views.

"""^^rdf:XMLLiteral . <#select-children> a ldh:Object ; diff --git a/docs/reference/data-model/blocks.ttl b/docs/reference/data-model/blocks.ttl index 231fae1..9be31e0 100644 --- a/docs/reference/data-model/blocks.ttl +++ b/docs/reference/data-model/blocks.ttl @@ -39,7 +39,9 @@

Blocks other than XHTML content are called objects and must have a dereferenceable URI. Objects are embedded (transcluded) into the HTML page. You can use any RDF resource or uploaded file as an object.

LinkedDataHub will first attempt to load RDF data from the object URI and render it as a block. If that fails, it will simply embed it using the HTML <object> element. Object blocks can be used to embed queries, charts, and other LinkedDataHub system resources.

An object can also be a page on the web, which LinkedDataHub reads as data when it loads it. A web page that embeds JSON-LD in a <script type="application/ld+json"> element contributes that data: a product, an article, an event or a recipe the page describes is rendered as a resource. A YouTube video, given by its https://www.youtube.com/watch?v=… or https://youtu.be/… URL, has its metadata translated to RDF on the fly as a schema:VideoObject with its title, channel and thumbnail, and the block shows the video's player. Support for other video and media services is planned. To embed a video or a page from the web, create an object block with the page's URL as its value rather than pasting an <iframe> into an XHTML block: the object block is a resource LinkedDataHub describes and renders, and the iframe is markup it knows nothing about.

-

Built-in block types render as cards: a header carries the block's title and actions such as the links popover and the copy-URI button, and the block's content follows below. Block-type-specific controls join the header area — a view block, for example, adds its facet toolbar and parallax row there. The layout of the content may depend on the active mode of the block.

+

Built-in block types render as cards: a header carries the block's title and actions such as the links popover and the copy-URI button, and the block's content follows below. Block-type-specific controls join the header area — a view block, for example, adds its facet toolbar and parallax row there. The layout of the content may depend on the active mode of the block.

+

A conversation with the assistant is an object block too: its value is an ldh:Chat resource, and the block + renders the conversation with its question field.

RDF serialization

@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
@@ -52,8 +54,16 @@
     rdf:value <https://localhost:4443/employees/2/#this> .

This block from the Northwind dashboard embeds a plain data resource — the Vice President of Sales, rendered with his description and photo. The object can equally be a query, a chart, a - view, or an external Linked Data URI, and an optional ac:mode on the block picks the + view, or an external Linked Data URI, and an optional ac:mode on the block picks the layout mode it renders in.

+

The block reads the object's description from the object's own document, which for an external URI means through the Linked Data proxy. An optional ldh:graph on the block names a document to read it from instead, so a dataspace can show an external resource it describes in one of its own documents: one whose URI serves no RDF, one the proxy cannot reach or is disabled for, or one whose remote description is not the one the dataspace keeps. Editing the embedded resource writes to that document too. From the command line it is the --graph option of ldh add object-block. The About page of these docs embeds its YouTube video this way, with the video's schema:VideoObject description in the page itself:

+
<#overview-video> a ldh:Object ;
+    dct:title "LinkedDataHub 6 overview" ;
+    rdf:value <https://www.youtube.com/watch?v=g3i-yH95WZA> ;
+    ldh:graph <> .
+
+<https://www.youtube.com/watch?v=g3i-yH95WZA> a schema:VideoObject ;
+    schema:name "LinkedDataHub 6 overview" .
diff --git a/docs/reference/data-model/documents.ttl b/docs/reference/data-model/documents.ttl index db8863e..d3e60c3 100644 --- a/docs/reference/data-model/documents.ttl +++ b/docs/reference/data-model/documents.ttl @@ -37,7 +37,7 @@

Containers

RDF serialization

-

Document properties such as sioc:has_parent, dct:created, dct:modified, acl:owner are automatically managed by LinkedDataHub.

+

Document properties such as sioc:has_parent, dct:created, dct:modified, acl:owner are automatically managed by LinkedDataHub.

@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
 @prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@@ -66,7 +66,7 @@
         

Items

RDF serialization

-

Document properties such as sioc:has_container, dct:created, dct:modified, acl:owner are automatically managed by LinkedDataHub.

+

Document properties such as sioc:has_container, dct:created, dct:modified, acl:owner are automatically managed by LinkedDataHub.

@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
 @prefix dct: <http://purl.org/dc/terms/> .
 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
diff --git a/docs/reference/data-model/resources.ttl b/docs/reference/data-model/resources.ttl
index e4f6b58..386a8d4 100644
--- a/docs/reference/data-model/resources.ttl
+++ b/docs/reference/data-model/resources.ttl
@@ -95,7 +95,7 @@
                     
                         View
                         ldh:View
-                        View based on SELECT query results
+                        View based on SELECT query results
                     
                 
             
@@ -131,7 +131,7 @@ ORDER BY ?categoryName''' .
  • tabular SELECT results (ldh:ResultSetChart)
  • graph results of DESCRIBE and CONSTRUCT (ldh:GraphChart)
  • -

    A chart resource is easily confused with the chart layout mode in views. The layout mode renders +

    A chart resource is easily confused with the chart layout mode in views. The layout mode renders view results as a chart on the fly, while a chart resource additionally stores the chart type as well as the category and series information: variable names in the case of tabular results, and property URIs in the case of graph results.

    @@ -217,95 +217,8 @@ ORDER BY DESC(?revenue)''' .

    Views

    - A view block with paginated results -

    A view renders the results of a SPARQL SELECT query as an interactive, paginated and optionally ordered list. - The UI renders the descriptions of the resources selected by the result set, rather than the tabular result itself. - That is achieved by on-the-fly SPARQL query rewriting: the SELECT is wrapped into - a DESCRIBE query; DESCRIBE reuses the same variables from the SELECT projection. - The first projected variable, plain or aliased as in (?s AS ?resource), names the resources the view lists. When it binds - values rather than resources, as in (YEAR(NOW()) AS ?year), there is nothing to describe and the block says the query lists no resources. - This will not work for all SELECT queries.

    -

    To render paginated lists of resources, traditional applications would normally have a dedicated API endpoint that - supports pagination, ordering etc. In LinkedDataHub, views achieve the same functionality by simply building the - SPARQL query string on the client-side. A view is therefore a client-side counterpart of a container: it produces a - listing without a server-side endpoint.

    -

    A view is defined in one of two ways. A standalone view is a resource like a query or a chart: - it is stored in a document and placed on a page by an object block whose value is its URI. - A property-attached view is declared in the dataspace's ontology with ldh:view or ldh:inverseView. It is stored - in no document and is rendered on every resource of the property's domain or range, as the - ontologies reference describes. Both kinds offer the same pagination, - faceted search and parallax navigation.

    -

    A view references its query and optionally fixes a layout mode:

    -
    <#select-categories-view> a ldh:View ;
    -    spin:query <#select-categories-query> ;
    -    ac:mode <https://w3id.org/atomgraph/client#GridMode> .
    - -

    Views can be rendered in multiple layout modes: properties, list, grid, table, map, chart, and graph. They also show - the total number of results and allow result ordering by property. In the table mode, columns can be sorted by clicking - their headers; the sort order follows each column's datatype.

    -
    -

    Projecting variables

    -

    Because the SELECT is rewritten into a DESCRIBE over its projected variables, a view can only render terms that the triplestore is able to describe:

    -
      -
    • Project resources (IRIs) that exist in the dataset — their labels, images and links are resolved from each resource's own description. Projecting literals (a ?label, a ?date, or any computed value) describes nothing, so the view renders empty.
    • -
    • Project IRIs that are stored locally. An IRI that only occurs as an object but is not itself described in the dataset — for example an external image URL — contributes no triples.
    • -
    • Project a single entity type. Projecting several resource variables of different types blends their descriptions into one result set, which rarely renders coherently; select the one type you want to list and reach the related resources through their properties instead.
    • -
    -

    The query that drives the categories grid above projects the category resource, so the view renders the category documents:

    -
    SELECT DISTINCT ?category
    -WHERE {
    -    GRAPH ?doc {
    -        ?category
    -            a schema:ProductGroup ;
    -            schema:name ?categoryName
    -    }
    -}
    -

    This query looks equivalent, but it projects the name literal instead of the category resource — so it describes nothing and renders an empty view:

    -
    SELECT DISTINCT ?categoryName
    -WHERE {
    -    GRAPH ?doc {
    -        ?category
    -            a schema:ProductGroup ;
    -            schema:name ?categoryName
    -    }
    -}
    -
    -
    -

    Layout modes

    -

    View results are rendered in the view's own set of layout modes — properties, list, grid, table, map, chart, and graph — a larger set than the document layout modes.

    -
    -
    -

    Inline creation

    -

    A view attached to a property in the dataspace's ontology renders a Create button in its header, so a view can double as an entry point for the data it lists. The class to construct is inferred from the attaching property: its rdfs:range for a forward ldh:view, its rdfs:domain for an inverse ldh:inverseView. A property whose range is not a named class — an owl:unionOf list, say — gets no button, because there is nothing to construct.

    -

    Where the new instance is stored is not declared anywhere. LinkedDataHub works it out from the view's own results: a second query, derived from the view's SELECT pattern with LIMIT, OFFSET and ORDER BY removed, asks which containers hold the documents that describe the existing solutions. A view is a projection and its rows are its solutions, so creating a row means making a new solution appear in that projection — and the existing solutions already say where their descriptions live.

    -
      -
    • Exactly one container — the button renders and targets it.
    • -
    • No container, because the result set is empty — no button. There is nothing to generalise from, so the first instance of a kind is created through its container the usual way.
    • -
    • More than one container, because the results span several — no button. Nothing in the data says where a new row belongs, and deciding by majority would be placement nobody stated.
    • -
    -

    The question is put to the whole result set rather than to the page on screen, so the destination cannot depend on which page the reader happens to be looking at. The determined container is then checked for acl:Write access, which is why the button appears a moment after the rest of the view header; a forward view also writes the linking triple into the current document, so it needs write access there as well.

    -

    A view with ldh:showWhenEmpty set to false is hidden while its query returns no results — the - demo's property-attached views - use it so "Orders from this customer" only appears on customers that have orders.

    -
    <#select-products-view> a ldh:View ;
    -    spin:query <#select-products-query> ;
    -    ac:mode <https://w3id.org/atomgraph/client#TableMode> ;
    -    ldh:showWhenEmpty false .
    -
    -
    - -

    Views provide faceted search in the toolbar at the top of the view, filtering the result set. The toolbar renders one dropdown pill per facetable property; opening a pill loads that property's values together with their result counts, and checking a value filters the results. Applying or removing a filter restarts paging from the first page.

    -

    By default the facets are generic and inferred from the triple patterns of the SPARQL SELECT query used by the view. They can be customized using XSLT.

    -
    -
    -

    Parallax navigation

    - Parallax navigation jumping from a result set to a related one -

    Parallax is an unusual navigation approach that suits graph data particularly well. It allows jumping from a result set to a related result set by following an RDF property. The available pivots render as a row of chips directly under the view toolbar, one chip per property linking the result set to the rest of the graph; the row stays hidden while there are no pivots. Parallax navigates in both directions: a chip with a forward arrow follows a property of the results onto its objects, a chip with a backward arrow follows a property pointing at the results onto its subjects. The pivots are discovered by querying the whole result set, so they cover results beyond the current page.

    -

    Clicking a chip pivots the view onto the related result set and restarts paging from the first page. The applied steps appear in the toolbar next to the facets as removable chips reading via (forward) or linked by (backward); removing one rewinds the view to that step. Parallax works in - combination with faceted search which can be used to filter the initial result set.

    -

    For example, facets can be used to filter a set of products that belong to a certain category, and then parallax can be used to jump to a set of companies that provide those products, and then further on to - a set of representatives of those companies.

    -
    +

    A view renders the results of a SPARQL SELECT query as an interactive, paginated list of the resources the query selects, + with faceted search and parallax navigation. Views have their own page: Views.

    """^^rdf:XMLLiteral . diff --git a/docs/reference/data-model/resources-3.webp b/docs/reference/data-model/views-1.webp similarity index 100% rename from docs/reference/data-model/resources-3.webp rename to docs/reference/data-model/views-1.webp diff --git a/docs/reference/data-model/resources-4.webp b/docs/reference/data-model/views-2.webp similarity index 100% rename from docs/reference/data-model/resources-4.webp rename to docs/reference/data-model/views-2.webp diff --git a/docs/reference/data-model/views.ttl b/docs/reference/data-model/views.ttl new file mode 100644 index 0000000..48def7a --- /dev/null +++ b/docs/reference/data-model/views.ttl @@ -0,0 +1,103 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Views" ; + dct:description "Paginated, faceted lists of the resources a SPARQL query selects" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Paginated, faceted lists of the resources a SPARQL query selects

    + A view block with paginated results +

    A view renders the results of a SPARQL SELECT query as an interactive, paginated and optionally ordered list. + The UI renders the descriptions of the resources selected by the result set, rather than the tabular result itself. + That is achieved by on-the-fly SPARQL query rewriting: the SELECT is wrapped into + a DESCRIBE query; DESCRIBE reuses the same variables from the SELECT projection. + The first projected variable, plain or aliased as in (?s AS ?resource), names the resources the view lists. When it binds + values rather than resources, as in (YEAR(NOW()) AS ?year), there is nothing to describe and the block says the query lists no resources. + This will not work for all SELECT queries.

    +

    To render paginated lists of resources, traditional applications would normally have a dedicated API endpoint that + supports pagination, ordering etc. In LinkedDataHub, views achieve the same functionality by simply building the + SPARQL query string on the client-side. A view is therefore a client-side counterpart of a container: it produces a + listing without a server-side endpoint.

    +

    A view is defined in one of two ways. A standalone view is a resource like a query or a chart: + it is stored in a document and placed on a page by an object block whose value is its URI. + A property-attached view is declared in the dataspace's ontology with ldh:view or ldh:inverseView. It is stored + in no document and is rendered on every resource of the property's domain or range, as the + ontologies reference describes. Both kinds offer the same pagination, + faceted search and parallax navigation.

    +

    A view references its query and optionally fixes a layout mode:

    +
    <#select-categories-view> a ldh:View ;
    +    spin:query <#select-categories-query> ;
    +    ac:mode <https://w3id.org/atomgraph/client#GridMode> .
    + +

    Views can be rendered in multiple layout modes: properties, list, grid, table, map, chart, and graph. They also show + the total number of results and allow result ordering by property. In the table mode, columns can be sorted by clicking + their headers; the sort order follows each column's datatype.

    +
    +

    Projecting variables

    +

    Because the SELECT is rewritten into a DESCRIBE over its projected variables, a view can only render terms that the triplestore is able to describe:

    +
      +
    • Project resources (IRIs) that exist in the dataset — their labels, images and links are resolved from each resource's own description. Projecting literals (a ?label, a ?date, or any computed value) describes nothing, so the view renders empty.
    • +
    • Project IRIs that are stored locally. An IRI that only occurs as an object but is not itself described in the dataset — for example an external image URL — contributes no triples.
    • +
    • Project a single entity type. Projecting several resource variables of different types blends their descriptions into one result set, which rarely renders coherently; select the one type you want to list and reach the related resources through their properties instead.
    • +
    +

    The query that drives the categories grid above projects the category resource, so the view renders the category documents:

    +
    SELECT DISTINCT ?category
    +WHERE {
    +    GRAPH ?doc {
    +        ?category
    +            a schema:ProductGroup ;
    +            schema:name ?categoryName
    +    }
    +}
    +

    This query looks equivalent, but it projects the name literal instead of the category resource — so it describes nothing and renders an empty view:

    +
    SELECT DISTINCT ?categoryName
    +WHERE {
    +    GRAPH ?doc {
    +        ?category
    +            a schema:ProductGroup ;
    +            schema:name ?categoryName
    +    }
    +}
    +
    +
    +

    Layout modes

    +

    View results are rendered in the view's own set of layout modes — properties, list, grid, table, map, chart, and graph — a larger set than the document layout modes.

    +
    +
    +

    Inline creation

    +

    A view attached to a property in the dataspace's ontology renders a Create button in its header, so a view can double as an entry point for the data it lists. The class to construct is inferred from the attaching property: its rdfs:range for a forward ldh:view, its rdfs:domain for an inverse ldh:inverseView. A property whose range is not a named class — an owl:unionOf list, say — gets no button, because there is nothing to construct.

    +

    Where the new instance is stored is not declared anywhere. LinkedDataHub works it out from the view's own results: a second query, derived from the view's SELECT pattern with LIMIT, OFFSET and ORDER BY removed, asks which containers hold the documents that describe the existing solutions. A view is a projection and its rows are its solutions, so creating a row means making a new solution appear in that projection — and the existing solutions already say where their descriptions live.

    +
      +
    • Exactly one container — the button renders and targets it.
    • +
    • No container, because the result set is empty — no button. There is nothing to generalise from, so the first instance of a kind is created through its container the usual way.
    • +
    • More than one container, because the results span several — no button. Nothing in the data says where a new row belongs, and deciding by majority would be placement nobody stated.
    • +
    +

    The question is put to the whole result set rather than to the page on screen, so the destination cannot depend on which page the reader happens to be looking at. The determined container is then checked for acl:Write access, which is why the button appears a moment after the rest of the view header; a forward view also writes the linking triple into the current document, so it needs write access there as well.

    +

    A view with ldh:showWhenEmpty set to false is hidden while its query returns no results — the + demo's property-attached views + use it so "Orders from this customer" only appears on customers that have orders.

    +
    <#select-products-view> a ldh:View ;
    +    spin:query <#select-products-query> ;
    +    ac:mode <https://w3id.org/atomgraph/client#TableMode> ;
    +    ldh:showWhenEmpty false .
    +
    +
    + +

    Views provide faceted search in the toolbar at the top of the view, filtering the result set. The toolbar renders one dropdown pill per facetable property; opening a pill loads that property's values together with their result counts, and checking a value filters the results. Applying or removing a filter restarts paging from the first page.

    +

    By default the facets are generic and inferred from the triple patterns of the SPARQL SELECT query used by the view. They can be customized using XSLT.

    +
    +
    +

    Parallax navigation

    + Parallax navigation jumping from a result set to a related one +

    Parallax is an unusual navigation approach that suits graph data particularly well. It allows jumping from a result set to a related result set by following an RDF property. The available pivots render as a row of chips directly under the view toolbar, one chip per property linking the result set to the rest of the graph; the row stays hidden while there are no pivots. Parallax navigates in both directions: a chip with a forward arrow follows a property of the results onto its objects, a chip with a backward arrow follows a property pointing at the results onto its subjects. The pivots are discovered by querying the whole result set, so they cover results beyond the current page.

    +

    Clicking a chip pivots the view onto the related result set and restarts paging from the first page. The applied steps appear in the toolbar next to the facets as removable chips reading via (forward) or linked by (backward); removing one rewinds the view to that step. Parallax works in + combination with faceted search which can be used to filter the initial result set.

    +

    For example, facets can be used to filter a set of products that belong to a certain category, and then parallax can be used to jump to a set of companies that provide those products, and then further on to + a set of representatives of those companies.

    +
    +
    """^^rdf:XMLLiteral . diff --git a/docs/reference/dataset.ttl b/docs/reference/dataset.ttl index f227698..207bd76 100644 --- a/docs/reference/dataset.ttl +++ b/docs/reference/dataset.ttl @@ -16,7 +16,7 @@

    The default LinkedDataHub dataset structure follows these conventions:

    1. The default graph is not used.
    2. -
    3. Each document's description is stored in an RDF named graph whose name is the same as the document URI. The graph can be managed using the Graph Store Protocol.
    4. +
    5. Each document's description is stored in an RDF named graph whose name is the same as the document URI. The graph can be managed using the Graph Store Protocol.
    6. Documents form a parent/child hierarchy. There are two types of documents: containers, which can have other documents as children, and items, which cannot.

    The default dataset is installed into a dataspace service during setup.

    @@ -50,7 +50,7 @@ https://localhost:4443/categories/1/#this # the category itself — a fragment

    The default datasets of administration and end-user dataspaces can be found in platform/datasets/admin.trig and platform/datasets/end-user.trig, respectively.

    The end-user dataset contains the root container document and descriptions of the public DBpedia and Wikidata SPARQL services. The administration dataset contains the root container, - the sign-up and ontology-clearing endpoint documents, the acl/ document tree — containers for agents, public keys, users, groups, + the sign-up and ontology-clearing endpoint documents, the acl/ document tree — containers for agents, public keys, users, groups, authorization requests and authorizations, the default authorizations, and the owners, writers and readers groups — as well as the ontologies/ container.

    RDF terms (classes, properties etc.) used in the default datasets come from the well-known FOAF and SIOC vocabularies (with additional LinkedDataHub-specific assertions) as well as system ontologies.

    diff --git a/docs/reference/dataspace.ttl b/docs/reference/dataspace.ttl index 3ec9f8c..f29dce8 100644 --- a/docs/reference/dataspace.ttl +++ b/docs/reference/dataspace.ttl @@ -58,7 +58,7 @@ derives the counterpart from the host in either direction. You do not configure the relationship.

    LinkedDataHub publishes the pairing so that clients never derive it themselves. Responses from an end-user dataspace carry a Link header with the https://w3id.org/atomgraph/linkeddatahub/dataspaces#admin relation, whose target is the administration - dataspace's base URI. The CLI's admin commands follow it, so they take the end-user base URI.

    + dataspace's base URI; responses from an administration dataspace carry the same relation with their own base URI. The CLI's admin commands follow it, so they take either base URI.

    diff --git a/docs/reference/http-api.ttl b/docs/reference/http-api.ttl index c22bc82..7f1993d 100644 --- a/docs/reference/http-api.ttl +++ b/docs/reference/http-api.ttl @@ -4,16 +4,19 @@ @prefix dh: . @prefix dct: . -<> a dh:Item ; +<> a dh:Container ; dct:title "HTTP API" ; dct:description "Read and write RDF data from and to LinkedDataHub dataspaces over HTTP" ; - rdf:_1 <#content> . + rdf:_1 <#content> ; + rdf:_2 <#select-children> . <#content> a ldh:XHTML ; rdf:value """

    Read and write RDF data from and to LinkedDataHub dataspaces over HTTP

    LinkedDataHub implements a uniform, generic RESTful Linked Data API as defined by the - SPARQL 1.1 Graph Store HTTP Protocol. It adds a few conventions of its own, however — such as automatic document metadata — as well as some constraints.

    + SPARQL 1.1 Graph Store HTTP Protocol. It adds a few conventions of its own, however — such as automatic document metadata — as well as some constraints.

    +

    This page covers what applies to every request. Documents describes the methods for managing document data, SPARQL endpoint the query protocol, + Linked Data proxy browsing external data and other dataspaces, and System endpoints the paths served outside the document hierarchy.

    Authentication

    The LinkedDataHub UI supports two authentication methods:

    @@ -31,7 +34,7 @@

    Access control

    -

    All HTTP access to documents is subject to access +

    All HTTP access to documents is subject to access control. Requesting a document with insufficient access rights will result in a 403 Forbidden response. That means either:

    • the user is not authenticated and public access to the document is not allowed
    • @@ -107,316 +110,6 @@ ETag: "1e635b4-ntriples" http:statusCodeValue "403"^^xsd:long ] .
    -
    -

    Managing documents

    -

    Every document is also a named graph in the dataspace's RDF dataset. LinkedDataHub supports the SPARQL Graph Store Protocol's direct graph identification as the HTTP CRUD protocol for managing document data.

    -
    - -
    -

    GSP indirect graph identification is not supported starting with LinkedDataHub version 5.x.

    -
    -
    -

    The API also supports the PATCH HTTP method which is optional in GSP. It accepts graph-scoped SPARQL updates that will modify the requested document. Only the INSERT/WHERE and DELETE WHERE forms are supported; GRAPH patterns are not allowed.

    -

    Trailing slashes in document URIs are enforced using 308 Permanent Redirect responses.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    HTTP methods and the responses they return
    MethodDescriptionSuccessFailureReason
    GETReturns the data of a document200 OK404 Not FoundDocument with request URI not found
    406 Not AcceptableMedia type not supported
    POSTAppends data to a document204 No Content400 Bad RequestRDF syntax error
    404 Not FoundDocument with request URI not found
    413 Payload Too LargeRequest body too large
    415 Unsupported Media TypeMedia type not supported
    422 Unprocessable EntityConstraint violation in the resulting document
    PUTUpserts a document200 OK

    201 Created

    308 Permanent Redirect
    400 Bad RequestRDF syntax error
    Malformed document URI
    413 Payload Too LargeRequest body too large
    415 Unsupported Media TypeMedia type not supported
    422 Unprocessable EntityConstraint violation in the resulting document
    DELETERemoves the requested document204 No Content404 Not FoundDocument with request URI not found
    405 Method Not AllowedDeleting root, owner, or secretary documents is not allowed
    PATCHModifies a document using SPARQL Update204 No Content422 Unprocessable EntitySPARQL update is not a single graph-scoped INSERT/DELETE operation, or removes an rdf:type
    Constraint violation in the updated document
    -

    Any method may additionally answer 403 Forbidden — see Access control. Any method that writes may answer 428 Precondition Required when the request carries no precondition, or 412 Precondition Failed when the one it carries is out of date — see Conditional writes.

    -
    -

    Document metadata

    -

    Since LinkedDataHub 5, the document hierarchy is managed automatically.

    -

    By default, LinkedDataHub treats an RDF document as an item by giving it the dh:Item type and attaching it to the parent container using sioc:has_container. If the client wants to create a container instead, it has to explicitly add the dh:Container type on the document resource; the new container will be attached to its parent using sioc:has_parent. In either case, the URI of the new document will be relative to its parent's.

    -

    LinkedDataHub will also manage additional document metadata, such as its owner and creation/modification timestamps.

    -

    For example, this HTTP request creating the Northwind regions container (Turtle syntax):

    -
    PUT /regions/ HTTP/1.1
    -Host: localhost:4443
    -Content-Type: text/turtle
    -
    -@prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    -@prefix dct:    <http://purl.org/dc/terms/> .
    -
    -<> a dh:Container ;
    -    dct:title "Regions" .
    -

    will produce the following document triples:

    -
    @prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    -@prefix dct:    <http://purl.org/dc/terms/> .
    -@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
    -@prefix sioc:   <http://rdfs.org/sioc/ns#> .
    -@prefix acl:    <http://www.w3.org/ns/auth/acl#> .
    -
    -<https://localhost:4443/regions/>
    -    a dh:Container ;
    -    dct:created "2025-03-31T21:46:21.984Z"^^xsd:dateTime ;
    -    dct:creator <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> ;
    -    dct:title "Regions" ;
    -    sioc:has_parent <https://localhost:4443/> ;
    -    acl:owner <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> .
    -

    The HTTP request to produce a new item can be empty:

    -
    PUT /regions/1/ HTTP/1.1
    -Host: localhost:4443
    -Content-Type: text/turtle
    -

    It will create an item document with the following triples:

    -
    @prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    -@prefix dct:    <http://purl.org/dc/terms/> .
    -@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
    -@prefix sioc:   <http://rdfs.org/sioc/ns#> .
    -@prefix acl:    <http://www.w3.org/ns/auth/acl#> .
    -
    -<https://localhost:4443/regions/1/>
    -    a dh:Item ;
    -    dct:created "2025-03-31T20:45:42.802Z"^^xsd:dateTime ;
    -    dct:creator <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> ;
    -    sioc:has_container <https://localhost:4443/regions/> ;
    -    acl:owner <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> .
    -
    -
    -

    Graph-scoped updates

    -

    A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Appending a description to the Chai product:

    -
    PATCH /products/1/ HTTP/1.1
    -Host: localhost:4443
    -Content-Type: application/sparql-update
    -
    -PREFIX schema: <https://schema.org/>
    -
    -INSERT
    -{
    -    <https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
    -}
    -WHERE {}
    -

    The update executes against that one graph, so GRAPH patterns are not allowed — this request is rejected with - 422 Unprocessable Entity:

    -
    PATCH /products/1/ HTTP/1.1
    -Host: localhost:4443
    -Content-Type: application/sparql-update
    -
    -PREFIX schema: <https://schema.org/>
    -
    -INSERT
    -{
    -    GRAPH <https://localhost:4443/products/1/>    # GRAPH is implied by the request URI — not allowed here
    -    {
    -        <https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
    -    }
    -}
    -WHERE {}
    -
    -
    -

    Built-in constraints

    -

    LinkedDataHub has a few built-in constraints that are not found in the standard Graph Store Protocol:

    -
      -
    • It is not possible to delete the root document (returns 405 Method Not Allowed)
    • -
    • It is not possible to modify or delete the documents of the owner agent and the secretary agent (returns 405 Method Not Allowed)
    • -
    • A document can only be created with a URL relative to an existing container (i.e. resolving .. against the new document's URL must identify an existing container)
    • -
    -

    The built-in constraints are similar to, but separate from the ontology constraints — - SPIN constraints and - SHACL shapes declared in the dataspace's ontology. Those are checked against the - document as the write leaves it, not the request body alone, so a POST or PATCH need not repeat the properties the - document already holds, and one that leaves the document invalid is answered 422 Unprocessable Entity.

    -
    -
    -
    -

    Conditional writes

    -

    A write to a document that already exists must say which state it was written against. Every write reads the document's graph, changes it in memory and writes the whole graph back, so two writers that name no precondition overwrite each other with nothing to show that anything was lost.

    -

    Such a write is answered 428 Precondition Required. To make it conditional, send either:

    -
      -
    • If-Match with the document's current ETag, to modify what you read
    • -
    • If-None-Match: *, to create a document only if it does not exist yet
    • -
    -

    Creating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted as it always was.

    -

    If the document changed since you read it, the write is refused 412 Precondition Failed, and that response carries the document's current ETag so you can re-read, reapply your change and retry without an extra request. If-None-Match: * against a document that already exists is likewise refused 412, which is how a client can create-or-append in two steps.

    -
    - -
    -

    An ETag identifies a negotiated representation, not the graph alone: the same document answers a different tag as RDF/XML than as Turtle. Read the validator with the same Accept the write will send, or the write is refused 412.

    -
    -
    -
    HEAD /my/document/ HTTP/1.1
    -Accept: application/n-triples
    -
    -HTTP/1.1 200 OK
    -ETag: "f731c06f5ac4fec6490dff1399bd5440"
    -
    -PATCH /my/document/ HTTP/1.1
    -Accept: application/n-triples
    -Content-Type: application/sparql-update
    -If-Match: "f731c06f5ac4fec6490dff1399bd5440"
    -
    -HTTP/1.1 204 No Content
    -

    HEAD is answered for any access mode the agent holds, not acl:Read alone, so an agent granted acl:Write or acl:Append can read the validator its writes have to quote without being able to read the document. GET still requires acl:Read.

    -

    The command line interface does this for you: every ldh command that writes reads the document's validator first and sends it.

    -
    -
    -

    Versioned documents

    -

    In dataspaces with versioning enabled, document URLs accept additional query parameters that expose the version - history through the standard Memento (RFC 7089) protocol:

    -
    -
    ?version=<commit-sha>
    -
    A historical version of the document, served with a Memento-Datetime header and an immutable Cache-Control. Snapshots are read-only: write methods answer 405 Method Not Allowed
    -
    ?timemap
    -
    The document's version history — a TimeMap described with PROV-O in RDF formats, or in application/link-format as RFC 7089 requires
    -
    ?timegate
    -
    Datetime negotiation: a request with an Accept-Datetime header answers 302 Found with the closest version in Location
    -
    -

    See the versioning reference for details.

    -
    -
    -

    Executing SPARQL

    -

    Every LinkedDataHub dataspace provides a SPARQL endpoint on sparql path (relative to the dataspace's base URI). It supports the - SPARQL 1.1 Protocol and serves as a proxy for the backend endpoint of the - dataspace.

    -

    The protocol's dataset parameters can be used to scope a query to a single document's graph: passing default-graph-uri=<document-uri> - makes that document's named graph the query's default graph. The dataset specified this way overrides any FROM/FROM NAMED - clauses in the query, and GRAPH patterns match nothing since no named graphs are part of the dataset. This is the read-side counterpart - of the graph-scoped PATCH method. Reading the Chai product's name out of its document alone:

    -
    curl -k -G "https://localhost:4443/sparql" \\
    -  -H "Accept: application/sparql-results+json" \\
    -  --data-urlencode 'query=PREFIX schema: <https://schema.org/> SELECT ?name WHERE { ?product a schema:Product ; schema:name ?name }' \\
    -  --data-urlencode 'default-graph-uri=https://localhost:4443/products/1/'
    -
    -
    -

    Linked Data proxy

    -

    LinkedDataHub works as a Linked Data proxy (from the end-user perspective, as a Linked Data browser) when a URL is provided using the uri query parameter. - All HTTP methods are supported.

    -

    If the URL dereferences successfully as RDF, LinkedDataHub forwards its response body (re-serializing it to enable content negotiation). - During a write request, the request body is forwarded to the provided URL.

    -

    If the URL returns an HTML document, LinkedDataHub extracts every embedded JSON-LD script - (<script type="application/ld+json">) and parses it as RDF using JSON-LD 1.1. Pages annotated with schema.org markup - can therefore be browsed as Linked Data. The schema.org JSON-LD context is bundled with LinkedDataHub and served locally, so no external network request is made to resolve it. A supplier's homepage carrying this markup:

    -
    <script type="application/ld+json">
    -{
    -    "@context": "https://schema.org/",
    -    "@type": "Corporation",
    -    "@id": "#this",
    -    "legalName": "Exotic Liquids",
    -    "telephone": "(171) 555-2222"
    -}
    -</script>
    -

    browses as these triples:

    -
    <https://supplier.example/#this> a schema:Corporation ;
    -    schema:legalName "Exotic Liquids" ;
    -    schema:telephone "(171) 555-2222" .
    -

    The proxy only accepts external (non-relative to the current dataspace's base URI) URLs; local URLs have to be dereferenced directly.

    -

    The proxy forwards the origin's ETag and Last-Modified validators and passes conditional request headers - (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since) through. Preconditioned - writes against proxied documents are therefore evaluated at the origin. Error statuses from the origin reach the client unchanged.

    -
    -
    -

    Federation

    -

    The Linked Data proxy makes dataspaces interoperable across origins: one dataspace's client can browse, query, and write to another - origin's dataspace, whether on the same LinkedDataHub instance or a different one.

    -

    Browsing a UNESCO Thesaurus concept from the Northwind dataspace, for example, the proxy forwards the remote dataspace's hypermedia along - with the RDF:

    -
    GET /?uri=https%3A%2F%2Funesco-thesaurus.demo.linkeddatahub.com%2Fconcepts%2Fconcept3683%2F HTTP/1.1
    -Host: northwind-traders.demo.linkeddatahub.com
    -Accept: application/rdf+xml
    -
    -HTTP/1.1 200 OK
    -Content-Type: application/rdf+xml
    -Link: <https://unesco-thesaurus.demo.linkeddatahub.com/sparql>; rel="http://www.w3.org/ns/sparql-service-description#endpoint"
    -
      -
    • The remote dataspace's SPARQL endpoint is discovered from the Link headers the proxy forwards, so views and charts over - remote documents query the remote endpoint
    • -
    • Writes are graph-scoped SPARQL updates (PATCH) or appends (POST) sent through the proxy under the origin's - If-Match precondition
    • -
    • Requests carry the delegated identity of the requesting agent, so the remote dataspace's access control - arbitrates every operation
    • -
    -

    In the user interface this means you can browse a remote dataspace and - add data to its documents the same way as to local ones, provided the remote access control allows it.

    -

    The example above uses the public demo hosts because they are reachable without any setup, but nothing about federation requires a second - instance: two dataspaces on one LinkedDataHub are distinct origins, so the same exchange happens between - northwind-traders.demo.localhost:4443 and unesco-thesaurus.demo.localhost:4443 once both are - declared.

    -

    Hypermedia

    Responses describe their context in Link headers (RFC 8288), so a client @@ -445,7 +138,7 @@ Link: <https://unesco-thesaurus.demo.linkeddatahub.com/sparql>; rel="http: https://w3id.org/atomgraph/linkeddatahub/dataspaces#admin - On end-user dataspace responses only: the base URI of its administration dataspace + The base URI of the administration dataspace: on end-user dataspace responses its pair, on administration dataspace responses its own https://w3id.org/atomgraph/client#stylesheet @@ -462,95 +155,19 @@ Link: <https://unesco-thesaurus.demo.linkeddatahub.com/sparql>; rel="http: timemap, timegate, original, self - On versioned documents: the Memento relations of RFC 7089 + On versioned documents: the Memento relations of RFC 7089 -

    On a proxied response the local relations are left out and the remote resource's own Link headers are forwarded - instead, which is how federation finds the remote SPARQL endpoint; acl:agent is kept, since it describes the request.

    +

    On a proxied response the local relations are left out and the remote resource's own Link headers are forwarded + instead, which is how federation finds the remote SPARQL endpoint; acl:agent is kept, since it describes the request.

    curl -k -I -E cert.pem:password https://localhost:4443/
    -
    -

    System endpoints

    -

    Endpoints that LinkedDataHub serves in addition to the document hierarchy. Paths are relative to the dataspace's base URI; the admin app's - are relative to the administration dataspace's base, e.g. https://admin.localhost:4443/clear.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    System endpoints by path
    PathDescription
    Admin and end-user apps
    nsIn-memory namespace ontology as well as its SPARQL endpoint
    sitemap.xmlThe dataspace's public documents, in the sitemaps protocol: generated at startup from the public read rules of the dataspace's own admin service, so an origin lists only its own documents. An end-user dataspace with nothing public has none, and neither does an admin dataspace
    robots.txtAllows crawling and names the sitemap when the dataspace has one; disallows everything when it has none
    contextThe public description of every dataspace of the instance: its type, title, origin, ontology and stylesheet, and the lds:admin base URI of each end-user dataspace (see dataspace context). Readable by anyone; it never contains service endpoints or credentials
    settingsThe dataspace's own description in the dataspace configuration. PATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable Entity
    Admin app only
    accessAccess metadata (for the authenticated agent)
    access/requestAccess request (for the authenticated agent)
    sign%20upWebID-TLS agent signup
    oauth2/login/googleOpenID Connect with Google login
    oauth2/login/orcidOpenID Connect with ORCID login
    oauth2/authorize/googleOpenID Connect with Google callback
    oauth2/authorize/orcidOpenID Connect with ORCID callback
    clearClears every cached ontology graph and imports closure from memory. With an optional uri form parameter, also purges that ontology's proxy caches and reloads its closure from the admin SPARQL endpoint
    -

    Caching

    GET and HEAD RDF responses from the backend triplestores (not LinkedDataHub responses) are cached automatically by LinkedDataHub using Varnish as an HTTP proxy cache. You can check the age of the response by inspecting the Age response header (the value is in seconds).

    -

    LinkedDataHub sends ETag response headers derived from a digest of the document's URL and its RDF content. Every representation (HTML, RDF/XML, Turtle, etc.) gets a distinct ETag value, and language-negotiated HTML representations factor the accepted languages into the tag. Treat the value as opaque: it is a validator to quote back in a conditional request, and nothing about the document can be read from it.

    +

    LinkedDataHub sends ETag response headers derived from a digest of the document's URL and its RDF content. Every representation (HTML, RDF/XML, Turtle, etc.) gets a distinct ETag value, and language-negotiated HTML representations factor the accepted languages into the tag. Treat the value as opaque: it is a validator to quote back in a conditional request, and nothing about the document can be read from it.

    HTML responses carry a Content-Language negotiated against the languages the UI ships with, and advertise Vary: Accept-Language so shared caches keep the renderings apart.

    Caching of LinkedDataHub responses can be enabled on the nginx HTTP proxy server by uncommenting the add_header Cache-Control directives in the platform/nginx.conf.template file. Caching of /uploads/ and /static/ namespaces is enabled by default.

    @@ -559,3 +176,6 @@ Link: <https://unesco-thesaurus.demo.linkeddatahub.com/sparql>; rel="http: rdfs:seeAlso . dct:title "SPARQL 1.1 Graph Store HTTP Protocol" . + +<#select-children> a ldh:Object ; + rdf:value ldh:ChildrenView . diff --git a/docs/reference/http-api/documents.ttl b/docs/reference/http-api/documents.ttl new file mode 100644 index 0000000..e04467d --- /dev/null +++ b/docs/reference/http-api/documents.ttl @@ -0,0 +1,259 @@ +@prefix ldh: . +@prefix rdf: . +@prefix rdfs: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Documents" ; + dct:description "Create, read, update and delete documents using the Graph Store Protocol" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Create, read, update and delete documents using the Graph Store Protocol

    +

    Every document is also a named graph in the dataspace's RDF dataset. LinkedDataHub supports the SPARQL Graph Store Protocol's direct graph identification as the HTTP CRUD protocol for managing document data.

    +
    + +
    +

    GSP indirect graph identification is not supported starting with LinkedDataHub version 5.x.

    +
    +
    +
    +

    Methods

    +

    The API also supports the PATCH HTTP method which is optional in GSP. It accepts graph-scoped SPARQL updates that will modify the requested document. Only the INSERT/WHERE and DELETE WHERE forms are supported; GRAPH patterns are not allowed.

    +

    Trailing slashes in document URIs are enforced using 308 Permanent Redirect responses.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    HTTP methods and the responses they return
    MethodDescriptionSuccessFailureReason
    GETReturns the data of a document200 OK404 Not FoundDocument with request URI not found
    406 Not AcceptableMedia type not supported
    POSTAppends data to a document204 No Content400 Bad RequestRDF syntax error
    404 Not FoundDocument with request URI not found
    413 Payload Too LargeRequest body too large
    415 Unsupported Media TypeMedia type not supported
    422 Unprocessable EntityConstraint violation in the resulting document
    PUTUpserts a document200 OK

    201 Created

    308 Permanent Redirect
    400 Bad RequestRDF syntax error
    Malformed document URI
    413 Payload Too LargeRequest body too large
    415 Unsupported Media TypeMedia type not supported
    422 Unprocessable EntityConstraint violation in the resulting document
    DELETERemoves the requested document204 No Content404 Not FoundDocument with request URI not found
    405 Method Not AllowedDeleting root, owner, or secretary documents is not allowed
    PATCHModifies a document using SPARQL Update204 No Content422 Unprocessable EntitySPARQL update is not a single graph-scoped INSERT/DELETE operation, or removes an rdf:type
    Constraint violation in the updated document
    +

    Any method may additionally answer 403 Forbidden — see Access control. Any method that writes may answer 428 Precondition Required when the request carries no precondition, or 412 Precondition Failed when the one it carries is out of date — see Conditional writes.

    +
    +
    +

    Document metadata

    +

    Since LinkedDataHub 5, the document hierarchy is managed automatically.

    +

    By default, LinkedDataHub treats an RDF document as an item by giving it the dh:Item type and attaching it to the parent container using sioc:has_container. If the client wants to create a container instead, it has to explicitly add the dh:Container type on the document resource; the new container will be attached to its parent using sioc:has_parent. In either case, the URI of the new document will be relative to its parent's.

    +

    LinkedDataHub will also manage additional document metadata, such as its owner and creation/modification timestamps.

    +

    For example, this HTTP request creating the Northwind regions container (Turtle syntax):

    +
    PUT /regions/ HTTP/1.1
    +Host: localhost:4443
    +Content-Type: text/turtle
    +
    +@prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    +@prefix dct:    <http://purl.org/dc/terms/> .
    +
    +<> a dh:Container ;
    +    dct:title "Regions" .
    +

    will produce the following document triples:

    +
    @prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    +@prefix dct:    <http://purl.org/dc/terms/> .
    +@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
    +@prefix sioc:   <http://rdfs.org/sioc/ns#> .
    +@prefix acl:    <http://www.w3.org/ns/auth/acl#> .
    +
    +<https://localhost:4443/regions/>
    +    a dh:Container ;
    +    dct:created "2025-03-31T21:46:21.984Z"^^xsd:dateTime ;
    +    dct:creator <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> ;
    +    dct:title "Regions" ;
    +    sioc:has_parent <https://localhost:4443/> ;
    +    acl:owner <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> .
    +

    The HTTP request to produce a new item can be empty:

    +
    PUT /regions/1/ HTTP/1.1
    +Host: localhost:4443
    +Content-Type: text/turtle
    +

    It will create an item document with the following triples:

    +
    @prefix dh:     <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
    +@prefix dct:    <http://purl.org/dc/terms/> .
    +@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
    +@prefix sioc:   <http://rdfs.org/sioc/ns#> .
    +@prefix acl:    <http://www.w3.org/ns/auth/acl#> .
    +
    +<https://localhost:4443/regions/1/>
    +    a dh:Item ;
    +    dct:created "2025-03-31T20:45:42.802Z"^^xsd:dateTime ;
    +    dct:creator <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> ;
    +    sioc:has_container <https://localhost:4443/regions/> ;
    +    acl:owner <https://admin.localhost:4443/acl/agents/865c2431-8436-4ae8-b300-2a531a013cd0/#this> .
    +
    +
    +

    Graph-scoped updates

    +

    A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Appending a description to the Chai product:

    +
    PATCH /products/1/ HTTP/1.1
    +Host: localhost:4443
    +Content-Type: application/sparql-update
    +
    +PREFIX schema: <https://schema.org/>
    +
    +INSERT
    +{
    +    <https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
    +}
    +WHERE {}
    +

    The update executes against that one graph, so GRAPH patterns are not allowed — this request is rejected with + 422 Unprocessable Entity:

    +
    PATCH /products/1/ HTTP/1.1
    +Host: localhost:4443
    +Content-Type: application/sparql-update
    +
    +PREFIX schema: <https://schema.org/>
    +
    +INSERT
    +{
    +    GRAPH <https://localhost:4443/products/1/>    # GRAPH is implied by the request URI — not allowed here
    +    {
    +        <https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
    +    }
    +}
    +WHERE {}
    +
    +
    +

    Built-in constraints

    +

    LinkedDataHub has a few built-in constraints that are not found in the standard Graph Store Protocol:

    +
      +
    • It is not possible to delete the root document (returns 405 Method Not Allowed)
    • +
    • It is not possible to modify or delete the documents of the owner agent and the secretary agent (returns 405 Method Not Allowed)
    • +
    • A document can only be created with a URL relative to an existing container (i.e. resolving .. against the new document's URL must identify an existing container)
    • +
    +

    The built-in constraints are similar to, but separate from the ontology constraints — + SPIN constraints and + SHACL shapes declared in the dataspace's ontology. Those are checked against the + document as the write leaves it, not the request body alone, so a POST or PATCH need not repeat the properties the + document already holds, and one that leaves the document invalid is answered 422 Unprocessable Entity.

    +
    +
    +

    Conditional writes

    +

    A write to a document that already exists must say which state it was written against. Every write reads the document's graph, changes it in memory and writes the whole graph back, so two writers that name no precondition overwrite each other with nothing to show that anything was lost.

    +

    Such a write is answered 428 Precondition Required. To make it conditional, send either:

    +
      +
    • If-Match with the document's current ETag, to modify what you read
    • +
    • If-None-Match: *, to create a document only if it does not exist yet
    • +
    +

    Creating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted as it always was.

    +

    If the document changed since you read it, the write is refused 412 Precondition Failed, and that response carries the document's current ETag so you can re-read, reapply your change and retry without an extra request. If-None-Match: * against a document that already exists is likewise refused 412, which is how a client can create-or-append in two steps.

    +
    + +
    +

    An ETag identifies a negotiated representation, not the graph alone: the same document answers a different tag as RDF/XML than as Turtle. Read the validator with the same Accept the write will send, or the write is refused 412.

    +
    +
    +
    HEAD /my/document/ HTTP/1.1
    +Accept: application/n-triples
    +
    +HTTP/1.1 200 OK
    +ETag: "f731c06f5ac4fec6490dff1399bd5440"
    +
    +PATCH /my/document/ HTTP/1.1
    +Accept: application/n-triples
    +Content-Type: application/sparql-update
    +If-Match: "f731c06f5ac4fec6490dff1399bd5440"
    +
    +HTTP/1.1 204 No Content
    +

    HEAD is answered for any access mode the agent holds, not acl:Read alone, so an agent granted acl:Write or acl:Append can read the validator its writes have to quote without being able to read the document. GET still requires acl:Read.

    +

    The command line interface does this for you: every ldh command that writes reads the document's validator first and sends it.

    +
    +
    +

    Versioned documents

    +

    In dataspaces with versioning enabled, document URLs accept additional query parameters that expose the version + history through the standard Memento (RFC 7089) protocol:

    +
    +
    ?version=<commit-sha>
    +
    A historical version of the document, served with a Memento-Datetime header and an immutable Cache-Control. Snapshots are read-only: write methods answer 405 Method Not Allowed
    +
    ?timemap
    +
    The document's version history — a TimeMap described with PROV-O in RDF formats, or in application/link-format as RFC 7089 requires
    +
    ?timegate
    +
    Datetime negotiation: a request with an Accept-Datetime header answers 302 Found with the closest version in Location
    +
    +

    See the versioning reference for details.

    +
    +
    """^^rdf:XMLLiteral ; + rdfs:seeAlso . + + dct:title "SPARQL 1.1 Graph Store HTTP Protocol" . diff --git a/docs/reference/http-api/linked-data-proxy.ttl b/docs/reference/http-api/linked-data-proxy.ttl new file mode 100644 index 0000000..ff3384b --- /dev/null +++ b/docs/reference/http-api/linked-data-proxy.ttl @@ -0,0 +1,72 @@ +@prefix ldh: . +@prefix rdf: . +@prefix rdfs: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Linked Data proxy" ; + dct:description "Browse, query and write to external Linked Data and other dataspaces" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Browse, query and write to external Linked Data and other dataspaces

    +
    +

    Linked Data proxy

    +

    LinkedDataHub works as a Linked Data proxy (from the end-user perspective, as a Linked Data browser) when a URL is provided using the uri query parameter. + All HTTP methods are supported.

    +

    If the URL dereferences successfully as RDF, LinkedDataHub forwards its response body (re-serializing it to enable content negotiation). + During a write request, the request body is forwarded to the provided URL.

    +

    If the URL returns an HTML document, LinkedDataHub extracts every embedded JSON-LD script + (<script type="application/ld+json">) and parses it as RDF using JSON-LD 1.1. Pages annotated with schema.org markup + can therefore be browsed as Linked Data. The schema.org JSON-LD context is bundled with LinkedDataHub and served locally, so no external network request is made to resolve it. A supplier's homepage carrying this markup:

    +
    <script type="application/ld+json">
    +{
    +    "@context": "https://schema.org/",
    +    "@type": "Corporation",
    +    "@id": "#this",
    +    "legalName": "Exotic Liquids",
    +    "telephone": "(171) 555-2222"
    +}
    +</script>
    +

    browses as these triples:

    +
    <https://supplier.example/#this> a schema:Corporation ;
    +    schema:legalName "Exotic Liquids" ;
    +    schema:telephone "(171) 555-2222" .
    +

    An HTML page with no JSON-LD has no RDF representation, so the proxy answers 406 Not Acceptable. The user interface then opens the + page in a new browser tab, as an ordinary external link would.

    +

    The proxy only accepts external (non-relative to the current dataspace's base URI) URLs; local URLs have to be dereferenced directly.

    +

    The proxy forwards the origin's ETag and Last-Modified validators and passes conditional request headers + (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since) through. Preconditioned + writes against proxied documents are therefore evaluated at the origin. Error statuses from the origin reach the client unchanged.

    +
    +
    +

    Federation

    +

    The Linked Data proxy makes dataspaces interoperable across origins: one dataspace's client can browse, query, and write to another + origin's dataspace, whether on the same LinkedDataHub instance or a different one.

    +

    Browsing a UNESCO Thesaurus concept from the Northwind dataspace, for example, the proxy forwards the remote dataspace's hypermedia along + with the RDF:

    +
    GET /?uri=https%3A%2F%2Funesco-thesaurus.demo.linkeddatahub.com%2Fconcepts%2Fconcept3683%2F HTTP/1.1
    +Host: northwind-traders.demo.linkeddatahub.com
    +Accept: application/rdf+xml
    +
    +HTTP/1.1 200 OK
    +Content-Type: application/rdf+xml
    +Link: <https://unesco-thesaurus.demo.linkeddatahub.com/sparql>; rel="http://www.w3.org/ns/sparql-service-description#endpoint"
    +
      +
    • The remote dataspace's SPARQL endpoint is discovered from the Link headers the proxy forwards, so views and charts over + remote documents query the remote endpoint
    • +
    • Writes are graph-scoped SPARQL updates (PATCH) or appends (POST) sent through the proxy under the origin's + If-Match precondition
    • +
    • Requests carry the delegated identity of the requesting agent, so the remote dataspace's access control + arbitrates every operation
    • +
    +

    In the user interface this means you can browse a remote dataspace and + add data to its documents the same way as to local ones, provided the remote access control allows it.

    +

    The example above uses the public demo hosts because they are reachable without any setup, but nothing about federation requires a second + instance: two dataspaces on one LinkedDataHub are distinct origins, so the same exchange happens between + northwind-traders.demo.localhost:4443 and unesco-thesaurus.demo.localhost:4443 once both are + declared.

    +
    +
    """^^rdf:XMLLiteral . diff --git a/docs/reference/http-api/sparql.ttl b/docs/reference/http-api/sparql.ttl new file mode 100644 index 0000000..0a970f7 --- /dev/null +++ b/docs/reference/http-api/sparql.ttl @@ -0,0 +1,37 @@ +@prefix ldh: . +@prefix rdf: . +@prefix rdfs: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "SPARQL endpoint" ; + dct:description "Query the dataspace using the SPARQL 1.1 Protocol" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Query the dataspace using the SPARQL 1.1 Protocol

    +
    +

    Executing SPARQL

    +

    Every LinkedDataHub dataspace provides a SPARQL endpoint on sparql path (relative to the dataspace's base URI). It supports the + SPARQL 1.1 Protocol and serves as a proxy for the backend endpoint of the + dataspace.

    +

    A GET on the endpoint without a query parameter returns its + SPARQL 1.1 Service Description, which advertises queries only: + documents are written through the Graph Store Protocol, not SPARQL Update.

    +

    External clients are limited in how many queries they may have in flight and how often they may send them; a query beyond the + limits is answered with 429 Too Many Requests.

    +

    The protocol's dataset parameters can be used to scope a query to a single document's graph: passing default-graph-uri=<document-uri> + makes that document's named graph the query's default graph. The dataset specified this way overrides any FROM/FROM NAMED + clauses in the query, and GRAPH patterns match nothing since no named graphs are part of the dataset. This is the read-side counterpart + of the graph-scoped PATCH method. Reading the Chai product's name out of its document alone:

    +
    curl -k -G "https://localhost:4443/sparql" \\
    +  -H "Accept: application/sparql-results+json" \\
    +  --data-urlencode 'query=PREFIX schema: <https://schema.org/> SELECT ?name WHERE { ?product a schema:Product ; schema:name ?name }' \\
    +  --data-urlencode 'default-graph-uri=https://localhost:4443/products/1/'
    +
    +
    """^^rdf:XMLLiteral ; + rdfs:seeAlso . + + dct:title "SPARQL 1.1 Protocol" . diff --git a/docs/reference/http-api/system-endpoints.ttl b/docs/reference/http-api/system-endpoints.ttl new file mode 100644 index 0000000..dbc6d7d --- /dev/null +++ b/docs/reference/http-api/system-endpoints.ttl @@ -0,0 +1,91 @@ +@prefix ldh: . +@prefix rdf: . +@prefix rdfs: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "System endpoints" ; + dct:description "Endpoints served in addition to the document hierarchy" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Endpoints served in addition to the document hierarchy

    +
    +

    System endpoints

    +

    Endpoints that LinkedDataHub serves in addition to the document hierarchy. Paths are relative to the dataspace's base URI; the admin app's + are relative to the administration dataspace's base, e.g. https://admin.localhost:4443/clear.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    System endpoints by path
    PathDescription
    Admin and end-user apps
    nsIn-memory namespace ontology as well as its SPARQL endpoint
    sitemap.xmlThe dataspace's public documents, in the sitemaps protocol: queried on request from the public read rules of the dataspace's own admin service, so an origin lists only its own documents. Past 50,000 documents it is a sitemap index over sitemap.xml?bucket=… pages, which group the documents by the MD5 prefix of their URI. Served only when ENABLE_SITEMAP is on; an end-user dataspace with nothing public has none, and neither does an admin dataspace
    robots.txtAllows crawling and names the sitemap when the sitemap is enabled and the dataspace has at least one public document; disallows everything otherwise
    contextThe public description of every dataspace of the instance: its type, title, origin, ontology and stylesheet, and the lds:admin base URI of each end-user dataspace (see dataspace context). Readable by anyone; it never contains service endpoints or credentials
    settingsThe dataspace's own description in the dataspace configuration. PATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable Entity
    Admin app only
    accessAccess metadata (for the authenticated agent)
    access/requestAccess request (for the authenticated agent)
    sign%20upWebID-TLS agent signup
    oauth2/login/googleOpenID Connect with Google login
    oauth2/login/orcidOpenID Connect with ORCID login
    oauth2/authorize/googleOpenID Connect with Google callback
    oauth2/authorize/orcidOpenID Connect with ORCID callback
    clearClears every cached ontology graph and imports closure from memory. With an optional uri form parameter, also purges that ontology's proxy caches and reloads its closure from the admin SPARQL endpoint
    +
    +
    """^^rdf:XMLLiteral . diff --git a/docs/reference/imports/rdf.ttl b/docs/reference/imports/rdf.ttl index 68d1523..23f1de1 100644 --- a/docs/reference/imports/rdf.ttl +++ b/docs/reference/imports/rdf.ttl @@ -25,7 +25,7 @@
    Transformation query (optional)
    A user-defined CONSTRUCT query that transforms the input RDF into a different RDF representation (spin:query)
    Graph (optional)
    -
    The target graph (document) to which converted items are appended with POST (sd:name)
    +
    The target graph (document) to which converted items are appended with POST (sd:name)
    @@ -104,8 +104,8 @@ WHERE <https://localhost:4443/categories/1/#this> a skos:Concept ; skos:closeMatch <https://unesco-thesaurus.demo.linkeddatahub.com/concepts/concept3683/#this> .

    The mapped concept URIs stay remote on purpose: following one leads into the UNESCO Thesaurus dataspace through the - Linked Data proxy, and queries can join the two dataspaces over their - federated SPARQL endpoints.

    + Linked Data proxy, and queries can join the two dataspaces over their + federated SPARQL endpoints.

    diff --git a/docs/reference/migrating-from-5x.ttl b/docs/reference/migrating-from-5x.ttl index 3a0a15c..828d2b5 100644 --- a/docs/reference/migrating-from-5x.ttl +++ b/docs/reference/migrating-from-5x.ttl @@ -185,7 +185,7 @@

    A client that wrote unconditionally now reads the tag first, as the ldh CLI does for you. An entity tag identifies a negotiated variant, so the read supplying the validator must send the same Accept as the write quoting it, or the write is refused - 412. The HTTP API page has the request shapes.

    + 412. The HTTP API page has the request shapes.

    Every write is validated against the constraints as the document it leaves, which PATCH already was in 5.10. A POST used to be checked on its body alone and is now checked merged with the document, so appending to a document that violates a constraint is refused 422 until the violation is repaired. A PUT whose body does diff --git a/docs/reference/stylesheets.ttl b/docs/reference/stylesheets.ttl index 75edf06..6c7c5bb 100644 --- a/docs/reference/stylesheets.ttl +++ b/docs/reference/stylesheets.ttl @@ -335,7 +335,7 @@ $ac:uri xs:anyURI? - URI of the external resource when the request goes through the Linked Data proxy; absent for local documents (prefer the ac:uri() function) + URI of the external resource when the request goes through the Linked Data proxy; absent for local documents (prefer the ac:uri() function) @@ -386,7 +386,7 @@ ac:uri() xs:anyURI? Both - URI of the external resource being viewed through the Linked Data proxy; absent for local documents. The server reads it from the $ac:uri parameter, the browser from the uri query parameter + URI of the external resource being viewed through the Linked Data proxy; absent for local documents. The server reads it from the $ac:uri parameter, the browser from the uri query parameter lds:origin() @@ -596,7 +596,7 @@ sd:endpoint() xs:anyURI Both - SPARQL endpoint of the current data pane — the local endpoint, or the remote dataspace's when browsing another dataspace through the proxy. Replaces the former $sd:endpoint parameter + SPARQL endpoint of the current data pane — the local endpoint, or the remote dataspace's when browsing another dataspace through the proxy. Replaces the former $sd:endpoint parameter lds:dataspace() diff --git a/docs/reference/user-interface.ttl b/docs/reference/user-interface.ttl index 58a0d0d..ec10674 100644 --- a/docs/reference/user-interface.ttl +++ b/docs/reference/user-interface.ttl @@ -26,7 +26,7 @@

    The navigation bar with the address bar and menus

    The dataspace title or logo in the top-left always links to the root container of the current dataspace.

    -

    The address bar in the middle accepts the URI of a resource to navigate to. Entering an external http:// or https:// URL loads it through the Linked Data proxy so that it can be browsed within LinkedDataHub.

    +

    The address bar in the middle accepts the URI of a resource to navigate to. Entering an external http:// or https:// URL loads it through the Linked Data proxy so that it can be browsed within LinkedDataHub.

    On the right, dropdown menus provide access to the list of dataspaces, to the settings, and to your user account. For authenticated users, the settings menu opens the dataspace settings form and links to the administration dataspace and to the dataspace's namespace ontology.

    @@ -87,7 +87,7 @@ The links popover open on a Northwind order block, backlink rows visible

    Backlinks list the resources that link to a resource — that is, resources that have a property whose value is that resource. They complement the outgoing properties shown in the resource's own description.

    Backlinks are per block: every block whose resource has a URI carries its own links button and popover, so in Properties mode each resource in the document offers the backlinks of that resource. The button sits among the block header's actions; XHTML blocks anchor it in the card's top-right corner, and views place it in the view toolbar. The backlink rows are loaded when the popover is first opened, and opening one popover closes the others.

    -

    For view result sets, parallax navigation plays the complementary role: it lets you jump from the current results to a related set by following a property in either direction (for example, from a set of products to the companies that supply them). It works together with the view's faceted search.

    +

    For view result sets, parallax navigation plays the complementary role: it lets you jump from the current results to a related set by following a property in either direction (for example, from a set of products to the companies that supply them). It works together with the view's faceted search.

    Layout modes

    @@ -108,7 +108,7 @@ force-directed layout. Clicking a node shows its details, double-clicking loads the resource, and right-clicking a URI node loads its backlinks (resources pointing at it). The canvas can be toggled to fullscreen (press Esc to exit), and the graph state is preserved across search and filter re-renders. -

    Views have their own, larger set of layout modes.

    +

    Views have their own, larger set of layout modes.

    Creation bar

    @@ -120,12 +120,14 @@
    Properties
    The Create dropdown creates a new resource of the selected type
    +

    For a signed-in agent, the bar also holds the Assistant button, which opens a conversation with the + assistant above the bar. See using the assistant.

    Document tabs

    The tab bar with a UNESCO Thesaurus concept open in a second tab next to the Northwind dashboard

    LinkedDataHub navigates within a single page. When you follow a link to an external document (for example a resource fetched through the - Linked Data proxy), it opens in its own tab so that several documents can be kept open side by side. + Linked Data proxy), it opens in its own tab so that several documents can be kept open side by side. A sticky tab bar appears at the top of the page once the first external tab is opened.

    Each tab keeps its own content pane, and modal dialogs (such as creation and edit forms) are scoped to the active tab pane. Switching between tabs is cached, so returning to a previously opened tab does not re-fetch it. Closing a tab removes its pane and falls back to the base document's tab.

    diff --git a/docs/reference/versioning.ttl b/docs/reference/versioning.ttl index e28d6f3..5350d00 100644 --- a/docs/reference/versioning.ttl +++ b/docs/reference/versioning.ttl @@ -69,7 +69,7 @@

    Historical versions are read-only: only acl:Read is advertised in the Link headers and write methods answer 405 Method Not Allowed. In the user interface a historical version page shows a notice banner with a link back to the current version.

    Historical versions are also outside the scope of SPARQL: the dataspace's RDF dataset holds only the current version of every - document, while the history lives solely in the Git repository. Queries on the SPARQL endpoint + document, while the history lives solely in the Git repository. Queries on the SPARQL endpoint therefore always answer over the latest versions.

    GET /orders/10248/?version=8c9f2b1 HTTP/1.1
     Host: localhost:4443
    diff --git a/docs/tutorial/app-as-repository.ttl b/docs/tutorial/app-as-repository.ttl
    index 1605616..e8d595c 100644
    --- a/docs/tutorial/app-as-repository.ttl
    +++ b/docs/tutorial/app-as-repository.ttl
    @@ -74,7 +74,7 @@ ldh push "$LDH_BASE"

    How this works

    • Command line interface — one command per HTTP API operation, scriptable and replayable
    • -
    • HTTP API — the PUT semantics that make the replay idempotent
    • +
    • HTTP API — the PUT semantics that make the replay idempotent

    Next: Beyond low-code

    diff --git a/docs/tutorial/hello-dataspace.ttl b/docs/tutorial/hello-dataspace.ttl index ca5b88c..4293e8f 100644 --- a/docs/tutorial/hello-dataspace.ttl +++ b/docs/tutorial/hello-dataspace.ttl @@ -66,7 +66,7 @@
    • Documents — the document hierarchy and how documents map to named graphs
    • Content blocks — how rdf:_1, rdf:_2, … sequence a document's content
    • -
    • HTTP API — the PUT semantics behind ldh put
    • +
    • HTTP API — the PUT semantics behind ldh put

    Next: Structure

    diff --git a/docs/tutorial/insight.ttl b/docs/tutorial/insight.ttl index dd639ac..2c88270 100644 --- a/docs/tutorial/insight.ttl +++ b/docs/tutorial/insight.ttl @@ -12,7 +12,7 @@ rdf:value """

    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: an + views are resources like everything else. A chart is three resources chained together: an sp:Select query holding the SPARQL text, an ldh:ResultSetChart describing how to plot its results, and an ldh:Object block placing it on a document.

    A revenue chart

    @@ -127,7 +127,7 @@

    How this works

    • Charts — the query–chart–block chain and the chart vocabulary
    • -
    • Views — layout modes, projected variables and faceted search
    • +
    • Views — layout modes, projected variables and faceted search

    Next: Composition

    diff --git a/docs/user-guide.ttl b/docs/user-guide.ttl index 5b138c8..9179dd1 100644 --- a/docs/user-guide.ttl +++ b/docs/user-guide.ttl @@ -12,7 +12,7 @@ <#content> a ldh:XHTML ; rdf:value """

    Step-by-step guides for everyday tasks in LinkedDataHub

    -

    The Tutorial builds one dataspace end to end; these pages are standalone recipes that you can read in any order. They fall into three groups: working with data — browse, query, add, import and create it; authoring — edit content and upload files; and administering — change model, manage dataspaces, manage packages and version history. Developer extension topics live in the Extending section.

    +

    The Tutorial builds one dataspace end to end; these pages are standalone recipes that you can read in any order. They fall into three groups: working with data — browse, query, add, import and create it; authoring — edit content and upload files; asking the assistant; and administering — change model, manage dataspaces, manage packages and version history. Developer extension topics live in the Extending section.

    """^^rdf:XMLLiteral . <#select-children> a ldh:Object ; diff --git a/docs/user-guide/create-data/create-content.ttl b/docs/user-guide/create-data/create-content.ttl index b8d1f1c..502c2c6 100644 --- a/docs/user-guide/create-data/create-content.ttl +++ b/docs/user-guide/create-data/create-content.ttl @@ -117,8 +117,13 @@ --uri "#top-manager" \\ --value "${LDH_BASE}employees/2/#this" \\ "$LDH_BASE" -

    A chart or a view is embedded the same way — its URI as the +

    A chart or a view is embedded the same way — its URI as the --value, like the demo dashboard's #sales-trend chart.

    +

    An external resource described in one of the dataspace's own documents is embedded from that description with --graph, which names the document:

    +
    ldh add object-block \\
    +    --value "https://www.youtube.com/watch?v=g3i-yH95WZA" \\
    +    --graph "${LDH_BASE}videos/" \\
    +    "$LDH_BASE"
    diff --git a/docs/user-guide/use-the-assistant.ttl b/docs/user-guide/use-the-assistant.ttl new file mode 100644 index 0000000..391a0d3 --- /dev/null +++ b/docs/user-guide/use-the-assistant.ttl @@ -0,0 +1,84 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Use the assistant" ; + dct:description "Ask about the data in words, check the plan the assistant proposes, and run it" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
    +

    Ask about the data in words, check the plan the assistant proposes, and run it

    +
    + +
    +

    The assistant is optional and off by default. It is available on instances that enable the web-algebra service and give it a model key — see the setup instructions.

    +
    +
    +

    The assistant turns a question or an instruction into a Web-Algebra plan: the queries + and writes that answer it, in the order they run. You see the plan before anything runs, and the plan runs as you, so it can read and change only + what you can.

    +
    +

    Open the assistant

    +

    Sign in with your WebID certificate and open a document you have append access to. Click + Assistant in the creation bar. + A conversation block opens above the bar, with a field to type into. Each click opens another conversation, and a conversation you have not asked + anything in is closed with its button or with Escape.

    +
    +
    +

    Ask a question

    +

    Type a question about the data, or say what to change, and press Enter (Shift+Enter starts a new line). For example, + on the Northwind Traders dashboard:

    +

    Which five customers ordered the most in 1997?

    +

    The assistant knows which document you are on, its dataspace's SPARQL endpoint and the dataspace's ontology, so a question can say "this + order" or "these products". It also knows the last few questions in the same conversation and what they returned, so a follow-up such as + "chart them by country" refers to the earlier rows.

    +

    The first question saves the conversation in the document as a block. A conversation where nothing was asked leaves nothing behind.

    +
    +
    +

    Check and run the plan

    +
    + +

    Screenshot: a plan card in the assistant, with its summary, its operations and the Execute button

    +
    +

    The answer comes back as a card holding the plan: a sentence saying what it does, and its operations as rows, each marked as reading, + writing, or able to remove content. Each row folds out the plan's XML, so you can read exactly what will run.

    +

    Click Execute to run the plan. The rows report as each operation runs, and when the plan + is done the card shows what it returned (as a table, a chart or a map, when the plan says which suits the result) and links to the documents + it changed. A sentence summing up the result appears at the top of the card, and the rows fold under it.

    +
    + +

    Screenshot: an answered card, with the answer summing up the result, the result as a table and the steps folded under their count

    +
    +

    Two options above the question field change when plans run:

    +
    +
    Execute by default
    +
    On by default. A plan that only reads runs as soon as it arrives. A plan that writes always waits for you to click Execute, whatever this + option says, because a write cannot be undone from the card.
    +
    Retry on empty result
    +
    Off by default. When a plan returns nothing or fails, the assistant asks again by itself, telling the plan service what did not work, up to + three times. Every attempt is its own card, and turning the option off stops it.
    +
    +
    +
    +

    When a plan returns nothing

    +

    A query that matches nothing usually means the plan guessed a class or a property wrong. The card offers two ways on:

    +
      +
    • Revise asks the same question again, telling the plan service that this plan returned + nothing, so it does not propose it again.
    • +
    • Retry runs the same plan once more, which helps when the failure was temporary.
    • +
    +

    Rephrasing the question with the names the data uses (the class and property labels shown in the document) also helps.

    +
    +
    +

    Come back to a conversation

    +

    Every question is saved when its answer has landed, with the plan, what running it reported (the first hundred rows or resources of the result) + and how it went. When you open the document again, the conversation is there as you left it, and you can keep asking. Readers who are not + signed in, or who do not have append access, see the conversation without a question field.

    +

    A conversation is a block like any other: it is deleted the way any block is, and it can be placed in another document as an object block, + where it continues in the document it is stored in.

    +
    +

    Read more about how the assistant works and how to set it up.

    +
    """^^rdf:XMLLiteral . diff --git a/screencast/docs/manifest.mjs b/screencast/docs/manifest.mjs index 7ef82c5..5d672c9 100644 --- a/screencast/docs/manifest.mjs +++ b/screencast/docs/manifest.mjs @@ -234,14 +234,14 @@ export const SHOTS = [ of: (page) => subject(page, 'Revenue by category'), }, { - doc: 'reference/data-model/resources', n: 3, line: 228, kind: 'still', + doc: 'reference/data-model/views', n: 1, line: 14, kind: 'still', caption: 'a view block with paginated results', at: '/orders/', want: '.ldh-view-toolbar', of: (page) => subject(page, 'All orders'), }, { - doc: 'reference/data-model/resources', n: 4, line: 299, kind: 'still', + doc: 'reference/data-model/views', n: 2, line: 96, kind: 'still', caption: 'parallax navigation jumping from a result set to a related one', at: '/products/', // The pivot bar is no longer a
    with a : it is a div that the @@ -440,8 +440,6 @@ export const SHOTS = [ want: '.ldh-block-row, h1', }, - // ── blocked ──────────────────────────────────────────────────────────────── - { doc: 'about', n: 1, line: 37, kind: 'clip', caption: 'LinkedDataHub overview', blocked: 'the flagship film — its own project' }, { // The one slot that is a shell rather than a page. It runs the page's procedure end // to end in a throwaway clone — see tapes/get-started-setup.tape for why two values @@ -1117,4 +1115,50 @@ export const SHOTS = [ }, of: '.ac-modal:visible', }, + // ── user-guide/use-the-assistant ────────────────────────────────────────── + // Both need the web-algebra service with an OpenAI key (reference/assistant#setup). The + // first question stores the chat in the document it is asked on, so each asks on a + // scratch document; the frame is the card alone, so the scratch name is never shown. + { + // A read-only plan runs on arrival while "Execute by default" is on, and the card + // never shows Execute. The checkbox goes off first so the plan waits. + doc: 'user-guide/use-the-assistant', n: 1, line: 44, kind: 'still', writes: true, + caption: 'a plan card in the assistant, with its summary, its operations and the Execute button', + at: '/', + async act({ page, cursor, type, sleep, scratch }) { + await page.goto(await scratch.document('plan', 'Top customers'), { waitUntil: 'load' }); + await page.waitForTimeout(4000); + await cursor.click(page.locator('button.ldh-chat-open').first()); + const chat = page.locator('.ldh-chat').last(); + await chat.waitFor({ state: 'visible', timeout: 10000 }); + const run = chat.locator('.ldh-chat-run input[name="run"]'); + if (await run.isChecked()) await cursor.click(chat.locator('.ldh-chat-run')); + await sleep(400); + await type(chat.locator('textarea[name="question"]'), 'Which five customers ordered the most in 1997?', { base: 22, spread: 8 }); + await page.keyboard.press('Enter'); + await chat.locator('.ldh-chat-plan button.ldh-chat-execute').last().waitFor({ state: 'visible', timeout: 90000 }); + await sleep(800); + }, + want: '.ldh-chat .ldh-chat-plan button.ldh-chat-execute', + of: '.ldh-chat .ldh-chat-plan:last-of-type', + }, + { + doc: 'user-guide/use-the-assistant', n: 2, line: 53, kind: 'still', writes: true, + caption: 'an answered card, with the answer summing up the result, the result as a table and the steps folded under their count', + at: '/', + async act({ page, cursor, type, scratch }) { + await page.goto(await scratch.document('answer', 'Top customers'), { waitUntil: 'load' }); + await page.waitForTimeout(4000); + await cursor.click(page.locator('button.ldh-chat-open').first()); + const chat = page.locator('.ldh-chat').last(); + await chat.waitFor({ state: 'visible', timeout: 10000 }); + await type(chat.locator('textarea[name="question"]'), 'Which five customers ordered the most in 1997?', { base: 22, spread: 8 }); + await page.keyboard.press('Enter'); + await chat.locator('.ldh-chat-plan .ldh-chat-answer').last().waitFor({ state: 'visible', timeout: 180000 }); + await page.waitForTimeout(2000); + }, + want: async (page) => (await page.locator('.ldh-chat .ldh-chat-plan .ldh-chat-answer').count()) >= 1 + && (await page.locator('.ldh-chat .ldh-chat-plan details.ldh-chat-trace:not([open])').count()) >= 1, + of: '.ldh-chat .ldh-chat-plan:last-of-type', + }, ]; \ No newline at end of file