This document explains how Active Data Shapes can be used to define Web Services. The DASH namespace includes the class dash:Service that can be instantiated with an ontology editor. Each service has a JavaScript snippet as its implementation, for server-side execution against a rich model-driven API. The URI and parameter declarations of the services are used to generate intuitive web service URLs and Swagger schemas.

Scope of This Document

This document is part of the Active Data Shapes framework. We assume that you are familiar with the Active DASH Tutorial.

Overview

If you want to repeatedly execute scripts you should turn them into a format that is friendly for execution as Web Services. Any ADS script can be exposed through the TopBraid EDG server's tbl/service servlet. In order to define such web services you need to create instances

The main difference between those types is that you need to link a dash:ResourceService with a class before you can use it. The property dash:resourceService is used to point from a class at the services that applies to instances of that class.

Here is a simple Resource Service that applies to instances of the class g:Country and produces a JSON object with some information about that country. Note that the input country is bound to the variable focusNode when the service executes.

g:CountrySummary
    a dash:ResourceService ;
    rdfs:label "Country Summary" ;
    rdfs:comment "A Service for Country instances, returning a simple JSON object with some information about the country." ;
    dash:js """
        ({
            uri: focusNode.uri,
            label: focusNode.toString(),
            callingCode: focusNode.callingCode
        })
    """ .
    
g:Country
    dash:resourceService g:CountrySummary .

In the TopBraid Ontology editor this would look as follows:

You should place such services in an Ontology but they may potentially also be stored in different asset collection types. Any graph (asset collection) that can use these services must include (owl:import) the ontology with the service declations. So for example, if you have a Geo Services ontology then the Geo Taxonomy would owl:import the Geo Services ontology.

Once the services apply to an asset collection, the Reports tab will have a link such as this:

This link takes you to an automatically generated Swagger page that explains how to invoke those services and even allows you to test them:

Web Service URL Scheme

All ADS web services can be invoked with the following URL template:

/tbl/service/{dataGraphId}/{servicePrefix}/{serviceLocalName}

The following example calls the graph service ex:MyService against the production copy of the asset collection with identifier geo:

/tbl/service/geo/ex/MyService

Identifying the Graph

The dataGraphId specifies the graph that the service operates on (by default). In TopBraid, this will use the asset collection ID, for example geo for the Geography Taxonomy. Optionally, a workflow/working copy can be specified after the dot. Examples:

As usual, scripts should avoid making large-scale changes to graphs that have the change history on, including workflows. Batch import jobs should go directly against the bare production copy.

The query graphs will always include the transitive owl:imports, but write operations can only modify the base graph. Any of these subgraphs may contain the instances of dash:Service that declare the actual services.

Identifying the Service

servicePrefix and serviceLocalName are used to determine the URI of the dash:Service. This means that all services must be in a namespace that has a prefix. The service from the following Turtle graph can be called using /tbl/service/{dataGraphId}/ex/MyService:

PREFIX ex: <http://example.org/ns#> .
...

ex:MyService
	a dash:GraphService ;
	... 

Identifying the Focus Node

All ADS scripts are initialized with a variable focusNode that points at a NamedNode. In the case of dash:GraphServices this is simply the URI resource of the master graph (as instance of NamedNode). For dash:ResourceServices this will be the resource specified in the two path fragments after the service identifier. The following example will use g:Iceland as focus node and will choose the most suitable ADS JavaScript class for it:

/tbl/service/geo/ex/MyService/g/Iceland

This mechanism is straight-forward when the resource is from a prefixed namespace - the first name part is the namespace prefix while the second name part is the local name.

Alternatively, use the URI scheme where the second name part is the URL-encoded full URI:

/tbl/service/geo/ex/MyService/URI/http%3A%2F%2Ftopquadrant.com%2Fns%2Fexamples%2Fgeography%23Iceland

In the rare case that you need to specify blank nodes, use _/{blankNodeId}.

Parameters

ADS web services may declare any number of parameters, using the standard sh:parameter vocabulary from SHACL. At execution time, these will be mapped to the parameters of the web service invocation.

Syntax of Parameters

A simple parameter declaration taking integers looks as follows:

ex:MyService
	a dash:GraphService ;
	sh:parameter ex:MyService-number ;
	...
	
ex:MyService-number
	a sh:Parameter ;
	sh:path <urn:param:number> ;
	sh:datatype xsd:integer ;
	sh:optional true ;
	sh:description "A numeric input parameter of the service." ;
.

Parameters are mandatory by default, unless sh:optional true.

File Upload Parameters

File upload parameters are identified by having a value for dash:mimeTypes. (The example uses a blank node for the parameter declaration, just to show how that would look like).

ex:MyService
	...
	sh:parameter [
		a sh:Parameter ;
		sh:path <urn:param:file> ;
		dash:mimeTypes ".csv,.tsv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ;
		sh:datatype xsd:string ;
		sh:description "The spreadsheet file to upload." ;
		sh:name "file" ;
	] ;

At execution the file will be uploaded and is the available to the script using IO.uploadedFile(file) where the name of the variable file is the local name of the sh:path. See Importing Data using Active Data Shapes on how to process such files further.

Mapping to JavaScript Variables

At execution time, each declared parameter of the ADS service will be declared (using an automatically generated let assignment) and available to the script. The local name of the parameter's sh:path will be used as JS variable name. For example sh:path ex:myParam can be used by the script as myParam. The values of the variables is determined by the parameter declarations:

Write Permission versus Read-Only

Unless dash:canWrite is true, web services operate in read-only mode. Such services cannot modify the underlying graph. Read-only services can be invoked using GET or POST unless one of the parameters is a file upload, in which case POST is the only option. Services that can write always must be called with the POST method. All POST requests use multipart/form-data encryption type only.

Response

When a web service is invoked, the body of the ADS script (dash:js) is evaluated, and the result of the last expression will be used to create a response. By default, ADS web services are assumed to return a JSON value, with application/json content type. To return any other mime type, specify a value for dash:responseContentType. Typical values are:

Alternatively, set dash:responseContentType to "void" to not return any web service response. This is common for services that only perform side effects such as edits to the graph.

Tip: For simple JSON services such as the example from the beginning of the document, note that JSON objects are best wrapped with (...) so that they get evaluated as an expression, not as a JavaScript code block.

Producing Spreadsheets

A service will produce a (downloadable) spreadsheet if the dash:responseContentType contains either

and if the result object (i.e. the result of the last expression in the script) is a JavaScript object with the following fields:

This is exactly the format that graph.select produces, i.e. you can simply end a script with a SPARQL SELECT query:

ex:FromSPARQL
    a dash:GraphService ;
    rdfs:label "From SPARQL" ;
    dash:responseContentType "text/csv" ;
    dash:js "graph.select('SELECT * { ?s ?p ?o }')" .

As an alternative to SPARQL, you can construct such objects programmatically. This example produces a spreadsheet with three columns. It will first iterate over all instances of Country, drops those that do not have any value for isoCountryCode2 and then sorts the countries by that country code. Finally it maps the resulting array into JavaScript objects with name-value pairs suitable for spreadsheet rendering.

ex:CountriesList
    a dash:GraphService ;
    rdfs:label "Countries list" ;
    dash:responseContentType "text/tab-separated-values" ;
    dash:js """
let spreadsheet = {
    vars: [ 'Code', 'Country', 'URI' ],
    bindings: g.everyCountry().
        filter((country) => country.isoCountryCode2).
        sort((a, b) => a.isoCountryCode2.localeCompare(b.isoCountryCode2)).
        map((country) => (
            {
                Country: country.toString(),
                URI: country,
                Code: country.isoCountryCode2
            }))
}
spreadsheet;
""" .

Note that if the values are RDF non-literals, the system will insert the URI as value. To avoid that, convert it explicitly to labels using .toString().

If the column names match the values of the JavaScript fields that are generated by Active Data Shapes, you can write some really compact exporters. The following variation produces almost the same output as above, but directly using the country objects. We have dropped the Country column as the countries may have multiple skos:prefLabels. Note that this snippet only shows the JavaScript code (value of dash:js):

({
    vars: [ 'isoCountryCode2', 'uri' ],
    bindings: g.everyCountry().
        filter((country) => country.isoCountryCode2).
        sort((a, b) => a.isoCountryCode2.localeCompare(b.isoCountryCode2))
})

Producing RDF

It is possible to produce a string rendering of RDF triples using IO.serializeRDF(), which takes an array of triple objects as input (and can therefore be combined with the results of graph.triples().

The following service produces the concise bounded description of the focus node, traversing all depending blank nodes. Note this particular implementation does not check for cyclic blank nodes. We are only showing the JavaScript code here for brevity. It could be attached to any class that is also a node shape.

function collectTriples(subject, triples) {
    graph.triples(subject, null, null, true).forEach(t => {
        triples.push(t);
        if(t.object.isBlankNode()) {
            collectTriples(t.object, triples);
        }
    })
}

let triples = [];
collectTriples(focusNode, triples);
IO.serializeRDF(triples);

Alternatively, if the dash:responseContentType is one of the following, you can simply return an array of triples and the conversion will happen automatically:

For example, a script such as graph.triples() would return the whole graph. Or, in the example above, replace the last line simply with triples.

Producing Zip Files

Starting with TopBraid 7.0.2, ADS web services can produce zip files wrapping one or more files that would otherwise be produced individually, including Spreadsheets and RDF files. Such services need to declare the dash:responseContentType "application/zip" and return a JSON object with name-value pairs so that the names (object keys) are the paths of the individual files in the zip archive, and the values are the file contents. The suffix of the individual files must be one of the following:

The following example produces a zip archive with two files: the file /triples.ttl will contain all triples of the graph in Turtle format and the file /folder/query.csv will be a CSV file nested in a sub-folder.


ex:ZipService
    a dash:GraphService ;
    rdfs:label "Zip service" ;
    dash:responseContentType "application/zip" ;
    dash:js """
let spreadsheet = {
    vars: [ 'isoCountryCode2', 'uri' ],
    bindings: g.everyCountry().
        filter((country) => country.isoCountryCode2).
        sort((a, b) => a.isoCountryCode2.localeCompare(b.isoCountryCode2))
}
let triples = graph.triples();

let zip = {
	'triples.ttl': triples,
	'folder/query.csv': spreadsheet,
};

zip;
""" .

Examples

This section illustrates the use of ADS web services for some typical example use cases.

Creating HTML Output

This script takes a value (name) as input and produces a "Hello World" style output.

ex:Hello
    a dash:GraphService ;
    rdfs:label "Hello World" ;
    dash:js """
`<div>
    <h1>Hello, ${name}!</h1>
</div>`
""" ;
    dash:responseContentType "text/html" ;
    sh:parameter [
        a sh:Parameter ;
        sh:path ex:name ;
        sh:name "name" ;
        sh:description "The name of the person to say hello to." ;
        sh:datatype xsd:string ;
    ] .

Example call:

http://localhost:4500/tbl/service/geo/ex/Hello?name=World

Modifying a Resource

This script takes a value (calling code) as input and assigns it to a given country.

g:SetCallingCode
    a dash:ResourceService ;
    rdfs:label "Set calling code" ;
    dash:canWrite true ;
    dash:js "focusNode.callingCode = callingCode" ;
    dash:responseContentType "void" ;
    sh:parameter [
        a sh:Parameter ;
        sh:path g:callingCode ;
        sh:name "calling code" ;
        sh:description "The calling code to assign to the Country." ;
        sh:datatype xsd:integer ;
    ] .
    
g:Country
    dash:resourceService g:SetCallingCode .

Example call:

curl -X POST "http://localhost:4500/tbl/service/geo/g/SetCallingCode/g/Iceland" -H  "accept: */*" -H  "Content-Type: multipart/form-data" -F "callingCode=43"

Tip: If you add the following triples, the service will also be available to end users of the editor from the Modify menu of any Country:

g:Country
    dash:resourceAction g:SetCallingCode .
    			
g:SetCallingCode
    a dash:ModifyAction ;
    dash:actionGroup g:MyActions ;
    ...

g:MyActions
    a dash:ActionGroup ;
    rdfs:label "My actions" .