Skip to content

Hypermedia API

Thom van Kalkeren edited this page Sep 12, 2019 · 10 revisions

In short, Link provides an API to write linked data by calling a resource of type schema:Action with a graph as its argument (optional). This is a first implementation, holes in the specification will be filled as the project develops.

Though generality is kept in mind building this library, it places some requirements on the targeted API;

  • The API should be a Linked Data serving RESTful API (So it should return some LD format like turtle, nquads, JSON-LD).
  • Each IRI SHOULD be a URL. All resources should be fetchable on their own IRI, subresources (#subdoc) or blank nodes should always be included in the resource which mentions them.

To read data, these are the only requirements. To create an interactive experience, additional mechanisms are explained below.

Hypermedia actions

The library exposes the execActionByIRI method which can be used to execute schema:Action resources to write data to a service driven by hypermedia. The schema.org model isn't very specific about how to actually implement the model so we've done some assumptions as to how the system will work.

Let's say you have a resource about mt. Everest which has a schema:potentialAction to some schema:CreateAction with an associated entry point which you want to add a new image to.

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix schema: <http://schema.org/> .
 
<https://example.org/location/everest/pictures/create>
  rdf:type schema:CreateAction ;
  schema:name "Upload a picture of Mt. Everest!" ;
  schema:object <https://example.org/location/everest> ;
  schema:result schema:ImageObject ;
  schema:target  <https://example.org/location/everest/pictures/create#entrypoint>.

<https://example.org/location/everest/pictures/create#entrypoint>
  rdf:type schema:EntryPoint ;
  schema:httpMethod "POST" ;
  schema:url <https://example.org/location/everest/pictures> ;
  schema:image <http://fontawesome.io/icon/plus> ;
  schema:name "Add picture" .

The difference between the action and the entry point is somewhat arbitrary, but basically the action describes what happens and the entry point how it should happen.

We diverge a little from schema here by using schema:url over schema:urlTemplate since schema doesn't specify how to fill the template (the google examples don't even use a template, but a plain string, so let's just use schema:url).

Setting up the request

The only part missing (for non-trivial requests) is the actual body. The graph is created from a plain JS object passed to the function (call the low-level function in LinkedDataAPI directly to pass a custom graph). Resolving the actual fields isn't documented yet, we're using SHACL shapes linked to under the ll:actionBody property from the parent schema:EntryPoint.

So lets execute our function with an object with an attached file;

const lrs = new LinkedRenderStore();
const action = defaultNS.ex('location/everest/pictures/create')

/**
* See [DataToGraph](https://github.com/fletcher91/link-lib/blob/master/src/processor/DataToGraph.ts) for the conversion logic.
*/
const data = {
  'http://schema.org/name': 'My everest picture',
  'schema:contentUrl': new File(),                 // Reference to a File instance from some html file input
  'schema:dateCreated': new Date(),                // Most native types should be converted to proper rdf literals
  'schema:author': {                               // Nested resources can be added, they are included with blank nodes.
    'rdf:type': defaultNS.schema('Person'),
    'schema:name': 'D. Engelbart',
  }
}

The key names for the data object are processed by LinkedRenderStore#expandProperty, so it is very lenient with resolving the input to NamedNode's.

Executing the request

Now that we have a request description and the appropriate graph, we can tell the LRS to execute the request;

result = await lrs.execActionByIRI(action, data)
 
// The data is already in the store at this point, but it is included for additional processing.
console.log(`Created new resource with iri ${result.iri} and statements ${result.data.join()}`)

By now, if the request was successful, the picture has been uploaded to the service (and probably added to the collection) visible for all. The IRI of the created resource will be retrieved from the Location header. Normally, the client implements some post-request processing in order to update the client state to e.g. redirect to the parent resource and display the created picture. Further information about state-changes are be described in the chapter below.

We currently use a multipart form body to fulfill non-GET requests, where the main resource is marked as ll:targetResource* and additional attached files have some other IRI** (currently ll:blobs/[random-21-char-string]).

* Subject to change to the document-relative URI to conform to LDP. Though this makes referring to the delta .

** Blank nodes can't be used since we need to refer to the item in the form-data, but the parser may rename blank nodes server-side creating a mismatch

Processing state changes

Generally, the current paradigm is to add application code on what to do after some request is done, usually based on the status code and the body (display a message, redirect the user, update counters, etc.). Though this is supported with execActionByIRI, it is somewhat of an anti-pattern to duplicate such logic or to change the language of the back-end to mitigate code duplication.

This library takes the recent uptake in functional concepts in front-end design (by the likes of elm and react) to be extended to the server-client model (Coming full-circle to Fieldings HATEOAS). The back-end is a state machine which can be transitioned by executing requests. The front-end is seen as a function with 2 arguments; the server state and the application state. When a request is done, the server state is mutated, which causes our front-end to be out of sync, so we place a constraint on the server that it should send back the side-effects it created in order to fulfill the request, the front-end can then process those side-effects causing the client to be in sync with the server, and the user can execute the next request.

So in short, the server should send a delta of the server state before and after the request was processed. The current implementation encodes the delta in quads describing what happened. See the linked-delta document on more information about the protocol and data model.

Back to our example, the server will return the state delta, namely the created resource, some member/association statements, and derived data such as collection counts;

HTTP/1.1 201 CREATED
Location: https://example.org/pictures/203165
Content-Type: application/trig

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix schema: <http://schema.org/> .
@prefix ex: <http://example.org/ns#> .

<http://purl.org/linked-delta/add> {
    <https://example.org/pictures/203165>
      rdf:type schema:MediaObject ;
      schema:name "My everest picture" ;
      schema:dateCreated "Thu, 12 Jun 1996 18:26:59 GMT" ;
      schema:dateUploaded "Thu, 1 Mar 2006 20:64:32 GMT" ;
      schema:author <https://example.org/people/584329> ;
      schema:contentUrl <https://static.example.org/pictures/203165.jpeg> ;
      schema:isPartOf <https://example.org/location/everest/pictures> ; // Really stretching the definition here..
      schema:thumbnailUrl <https://static.example.org/pictures/203165_thumb.png> .

    <https://example.org/people/584329>
      rdf:type schema:Person ;
      schema:name "D. Engelbart" .
      ex:pictures <https://example.org/people/584329/pictures> .
}

<http://purl.org/linked-delta/replace> {
    <https://example.org/location/everest/pictures>
      hydra:totalItems 42 .
}

We have created two new resources, a picture and a person, which data should be added to the store. Since collections in this API may only have 1 totalItems property, the server indicates that its value should be replaced rather than added.