README
Solid Soukai
Solid engine for Soukai ODM.
Table of contents
Getting Started
This library allows you to store and read data from a Solid POD using the Soukai ODM. Before going into Solid specifics, you should be familiar with Soukai basics so make sure to read the Soukai documentation first.
There are two extensions to the core Soukai library, a Solid engine and a some Solid models (with their respective relationships). To get started, you can install both packages as npm dependencies:
npm install soukai soukai-solid
Managing the authentication is outside the scope of this package, so you'll need to provide a fetch method to perform network requests (if you want to learn more about authentication, you can check out this repository: noeldemartin/ramen).
To get started, initialize the engine and make sure to call loadSolidModels
to load models that are provided by this library. Please note that this is just an example to get up and running, but you should define some Solid specific properties in the model for a real application. Make sure to read on after this.
import SoukaiSolid, { SolidEngine, SolidModel } from 'soukai-solid';
import Soukai from 'soukai';
class Person extends SolidModel {}
SoukaiSolid.loadSolidModels();
Soukai.loadModels({ Person });
// If you want to make authenticated requests, you should use the fetch method from an authentication library.
Soukai.useEngine(new SolidEngine(window.fetch.bind(window)));
// You would normally get the url dynamically, we're hard-coding it here as an example.
Person.at('https://example.org/people/').create({ name: 'John Doe' });
Solid Models vs Solid Documents
Soukai is a library designed to work with document databases, hence calling it an ODM (Object Document Mapper). This usually means that a Soukai model maps to a database document, and documents are stored within collections in the database.
However, in Solid things work a little different. A Solid container is the equivalent of a collection, but Solid documents don't map directly to Soukai models. Instead, Soukai models represent RDF resources. This is an irrelevant distinction when a Solid document only contains a single RDF resource, but that's rarely the case. Proper RDF modeling often results in documents containing information about multiple resources.
All of this complexity is dealt with under the hood, but it is important to be aware of the distinction. There are also some practical implications that you'll see in the following sections dealing with collections. Internally, SolidModel
is serialized to a JSON-LD graph and different models can end up modifying the same document.
The way that models are stored in documents can be configured with relations, and there are some methods to get document information. getDocumentUrl()
returns the document information from the model url, and getSourceDocumentUrl()
returns information about the document where the RDF resource was read from. Most of the time both should be the same document, but there is nothing in Solid that forbids doing otherwise.
Defining Solid Models
All standard model definition rules apply, with some extra things to keep in mind.
Primary keys
By default, the attribute that serves as the primary key for a SolidModel
is url
instead of id
. As its name suggests, this is the url of the RDF resource that represents the model.
Collections
The concept of collections in Solid is represented with Solid containers. Given the dynamic nature of Solid, collection names should not be hard-coded in the model class. Instead, they will be inferred from the model url. Since the url of a model is not always available, there are a couple of helper methods in the SolidModel
class to make this easier.
You can use both from
and at
, which are only syntactic sugar to set the static collection
property of a model at runtime:
// This is something you would obtain at runtime from other models or libraries.
const containerUrl = 'https://example.org/people/';
// Get persons from the container.
const persons = await Person.from(containerUrl).all();
// Create a new person in the container.
const person = await Person.at(containerUrl).create({ name: 'Amy Doe' });
The find
, delete
and save
methods infer the container url from the model url, so calling from
or at
is not necessary. However, the save
accepts the container url as a first argument. This can be ignored if the model url has been minted before by calling mintUrl
or setting the url
attribute:
const person = new Person({ name: 'Amy Doe' });
// You can either do this...
await person.save('https://example.org/people/');
// or this...
person.mintUrl('https://example.org/people/');
await person.save();
// or this.
person.url = 'https://example.org/people/amy#it';
await person.save();
RDF definitions
When working with Solid entities, there are a couple of things that have to be defined for a model to be serialized properly as an RDF resource.
You can indicate the resource types using the rdfsClasses
static property (these will be serialized as http://www.w3.org/1999/02/22-rdf-syntax-ns#type
RDF properties).
Property names can be defined using the rdfProperty
key within the field definition.
Here's an example illustrating both:
class Person extends SolidModel {
static rdfsClasses = ['http://xmlns.com/foaf/0.1/Person'];
static fields = {
name: {
type: FieldType.String,
rdfProperty: 'http://xmlns.com/foaf/0.1/name',
},
};
}
Doing this all the time can become cumbersome, given that the same namespaces will probably be used multiple times. You can define reusable prefixes using the rdfContexts
static property.
Here's a definition equivalent to the previous snippet:
class Person extends SolidModel {
static rdfContexts = {
'foaf': 'http://xmlns.com/foaf/0.1/',
};
static rdfsClasses = ['foaf:Person'];
static fields = {
name: {
type: FieldType.String,
rdfProperty: 'foaf:name',
},
};
}
As we've seen in the Getting Started example, those properties are optional. Here's their default values:
rdfContexts
has the following prefixes included out of the box. If you define your own, these will be merged and not overridden:Prefix Url solid http://www.w3.org/ns/solid/terms#
rdfs http://www.w3.org/2000/01/rdf-schema#
rdf http://www.w3.org/1999/02/22-rdf-syntax-ns#
ldp http://www.w3.org/ns/ldp#
purl http://purl.org/dc/terms/
rdfsClasses
is an empty array by default, but this should rarely be left empty for proper RDF modeling. Values that are not urls or short-hand definitions using contexts will use the default context (the first one defined inrdfContexts
).rdfProperty
within field definitions will use the field name with the default context (the first one defined inrdfContexts
).
The Person
class we defined before can be defined more concisely like this:
class Person extends SolidModel {
static rdfContexts = {
// This will be the default context, because it is defined first.
'foaf': 'http://xmlns.com/foaf/0.1/',
};
// Because "Person" is not a url or short-hand definition using contexts,
// the default context will be used. This will be interpreted as "foaf:Person".
static rdfsClasses = ['Person'];
static fields = {
// Because "foaf" is the default context and "name" is the name of the field,
// the rdfProperty will be interpreted as "foaf:name".
name: FieldType.String,
};
}
There is also a SolidContainerModel
class that should be used to declare container models. In addition to everything from a SolidModel
, it has the following built-in definitions:
- The
rdfsClasses
array contains theldp:Container
type, it'll be merged with definitions in the model. So it actually makes sense to leaverdfsClasses
undefined for container models. - The
resourceUrls
field is defined as an array mapped from theldp:contains
RDF property. - The relationship
documents
is defined as a belongsToMany relation withSolidDocument
models, using theresourceUrls
property as the foreign key. It also provides a specialcontains
multi-model relationship (read more about relationships below). - Minted urls will use the
name
field if it exists to generate a slug instead of a UUID, and they won't use a hash at the end (read more about url minting below).
Url minting
The default behavior when creating new models is that the url will be minted in the client side, using the container url and generating a UUID. It will also add a hash at the end which can be configured defining the defaultResourceHash
static property in the model (it'll be defined as "it" by default).
If the model is a SolidContainerModel
and has a name
attribute, this will be used to create a slug instead. Container models don't use a hash in the url and end with a trailing slash.
Url minting is useful in order to perform operations with a new model before the server request has been resolved, but it can be disabled by setting the mintsUrls
property to false
. You can also mint it manually calling the mintUrl
method or setting the url
attribute:
class Person extends SolidModel {
static mintsUrls = false;
}
// You can do this...
const person = new Person({ name: 'Amy Doe' });
person.mintUrl();
await person.save();
// Or you could set the url yourself.
const person = new Person({
url: 'http://example.org/amy#it',
name: 'Amy Doe',
});
await person.save();
Relations
In addition to the relations included with the core library, other relations are provided in this package and some are extended with more functionality.
hasMany
This relation comes with some helper methods.
create
, save
and add
can be used to associate models, setting foreign keys. Models that are stored in the same document of a related model will be saved automatically when the parent model (the one who defines the relationship) is created if it didn't exist. This can be configured using the usingSameDocument
method.
When related models are stored in the same document, the hash of those models will be a UUID instead of the one defined in their defaultResourceHash
static property.
For example, let's say that we want to model a Music Band with all their members, and we want to store all the RDF resources in the same document:
class Band extends SolidModel {
static rdfContexts = {
'schema': 'https://schema.org/',
};
static rdfsClasses = ['MusicGroup'];
static fields = {
name: FieldType.String,
};
membersRelationship() {
return this.hasMany(Person, 'bandUrl').usingSameDocument(true);
}
}
class Person extends SolidModel {
static rdfContexts = {
'schema': 'https://schema.org/',
};
static rdfsClasses = ['Person'];
static fields = {
name: FieldType.String,
bandUrl: {
type: FieldType.Key,
rdfProperty: 'schema:memberOf',
},
};
}
And here's an example using those models:
const acdc = await Band.find('https://example.org/bands/ac-dc');
// You can create the model yourself, and it'll be stored when the parent is saved.
acdc.relatedMembers.add(new Person({ name: 'Bon Scott' }));
await acdc.save();
// Or you can use the create method.
// Notice how we're not specifying the bandUrl in either scenario.
await acdc.relatedMembers.create({ name: 'Angus Young' });
Note: Given the nature of Solid, related models defined with hasMany
will only be loaded if they can be found in the same document as the parent model (the one who defines the relationship). This also works if the foreign key is the only attribute found in the document.
contains and isContainedBy
There is a couple of relationships that are helpful to work with Solid containers: contains
and isContainedBy
.
Here's an example:
class MoviesContainer extends SolidModel {
static fields = {
name: {
type: FieldType.String,
rdfProperty: 'rdfs:label',
},
};
moviesRelationship() {
return this.contains(Movie);
}
}
class Movie extends SolidModel {
static rdfContexts = {
'schema': 'https://schema.org/',
};
static rdfsClasses = ['Movie'];
static fields = {
title: {
type: FieldType.String,
rdfProperty: 'schema:name',
},
};
moviesContainerRelationship() {
return this.isContainedBy(MoviesContainer);
}
}
These methods don't take foreign and local keys because they rely on the url
of the models to resolve collections and model ids.
Going Further
If you want to see more examples, you can find some models defined under tests/stubs/models and files ending with .test.ts
throughout the source code.
If you want to see some real-life applications using this library, check out Media Kraken and Solid Focus.