RDF 1.1 data modelling, graph assembly and serialisation.
@metreeca/trio provides an RDF substrate for TypeScript applications, covering the term and graph data model, declarative combinators for assembling graphs from domain objects, and codecs for standard RDF serialisations. Terms and graphs are built through validating factories and combinators, so that nothing malformed reaches a serialisation:
npm install @metreeca/trio
TypeScript consumers must use "moduleResolution": "nodenext"/"node16"/"bundler" in tsconfig.json.
The legacy "node" resolver is not supported.
This section introduces essential concepts; for complete coverage, see the API reference:
| Module | Description |
|---|---|
| @metreeca/trio | RDF 1.1 data model |
| @metreeca/trio/builder | Builders for RDF graphs. |
| @metreeca/trio/ntriples | N-Triples codec |
Terms are built through validating factories, each returning a frozen record: named boxes an absolute IRI, blank
mints a document-scoped label, and tagged and typed pair a lexical form with a language tag or a datatype IRI.
Statements assemble with triple, graphs with graph:
import { xsd } from "@metreeca/core/datatype";
import { createNamespace } from "@metreeca/core/resource";
import { graph, named, rdf, tagged, triple, typed } from "@metreeca/trio";
const ex = createNamespace("https://example.org/");
const widget = named(ex["products/123"]);
const statements = graph(
triple(widget, rdf.type, named(ex.Product)),
triple(widget, named(ex.name), tagged("Widget", "en")),
triple(widget, named(ex.price), typed(99.99, xsd.decimal))
);
Malformed components are rejected where the term is built, rather than escaping into a serialisation: a relative IRI, a
blank-node label outside the intersection of the N-Triples, Turtle and SPARQL 1.1 grammars, a language tag outside BCP
47, or a lexical form holding an isolated UTF-16 surrogate all throw a RangeError.
lexical reads back the bare string a term carries, undecorated by serialisation syntax: the blank-node label, the
absolute IRI, or the literal lexical form, whatever the form of the term.
Blank-node identity is scoped to the document that introduced it: skolemize replaces every blank node of a graph with
a minted urn:uuid: resource, correlating repeated labels to the same resource.
The @metreeca/trio/builder module maps domain objects to the graphs representing them: resource anchors a
description at a chosen subject, description concatenates per-property graphs, and property links subjects to
objects, taken either as ready terms or as values run through a mapper and embedded at their own root:
import { xsd } from "@metreeca/core/datatype";
import { createNamespace } from "@metreeca/core/resource";
import { blank, type Graph, named, rdf } from "@metreeca/trio";
import { data, description, link, property, resource, text } from "@metreeca/trio/builder";
const ex = createNamespace("https://example.org/");
function encodeVendor(vendor: Vendor): Graph {
return resource(named(vendor.id), id => description( // anchored at the vendor's own IRI
property(id, rdf.type, link(vendor.types)), // typing statements
property(id, ex.name, text(vendor.name)), // language map to tagged/plain literals
property(id, ex.founded, data(vendor.founded, xsd.gYear)), // scalar to a datatype-typed literal
property(id, ex.address, vendor.address, encodeAddress), // embed a sub-resource at its own root
property(link(vendor.products), ex.vendor, id) // inverse: each product links back to it
));
}
function encodeAddress(address: Address): Graph {
return resource(blank(), anchor => description( // self-anchored at a fresh blank node
property(anchor, ex.street, data(address.street)),
property(anchor, ex.city, data(address.city))
));
}
Every combinator returns a graph, so a resource and its embedded sub-resources assemble as one nested expression.
resource checks that every statement of a non-empty built graph hangs off its subject, walking statements in either
direction, so an inverse statement anchors its own subtree just as a direct one does; an embedded graph must likewise
hang off a single root, never pointed back at from the outside. Graphs failing either check throw a RangeError
instead of being silently mislinked.
The @metreeca/trio/ntriples module decodes and encodes N-Triples documents,
alongside the NTriples media type for content negotiation:
import { decodeNTriples, encodeNTriples, NTriples } from "@metreeca/trio/ntriples";
const document = encodeNTriples(statements);
// <https://example.org/products/123> <https://example.org/name> "Widget"@en .
await fetch(ex["graphs/products"], {
method: "PUT",
headers: { "content-type": NTriples },
body: document
});
const decoded = decodeNTriples(document);
Encoding takes terms as valid, escaping rather than checking them, so a graph assembled through the factories is
serialised as it stands. Decoding skips blank lines and comments, correlates repeated blank-node labels within the
document, and throws a SyntaxError on a malformed statement; it is verified against the
W3C RDF 1.1 N-Triples syntax test suite.
This project is licensed under the Apache 2.0 License – see LICENSE file for details.