Step 4: Setting up a Data Model
Now it's time to start working on some actual functionality: we'll be making an app that allows people to keep notes. In this step, we'll prepare their Pod for our data model — much as you might prepare database tables in a traditional application with a relational back-end.
At a high level, we'll set up the data model as follows:
- Check if a Document tracking our notes already exists.
- If it doesn't exist, create it.
- Fetch that Document.
So to start with the first step: in which Document should we track notes? The answer can be found in the concept of the Public Type Index.
The Public Type Index is itself a publicly accessible Document stored in the user's Pod. This
Document contains a list of links to other Documents, along with the type of data that is to be
included in those Documents. To store notes, the data type we will use is the
TextDigitalDocument
, defined by
Schema.org. Every time the user saves a note, we will store it as a
TextDigitalDocument.
If the Document containing these notes was located in the user's Pod at /public/notes.ttl
, their
Public Type Index could refer to it like this:
Subject | Predicate | Object |
---|---|---|
#notes | rdf:type | solid:TypeRegistration |
#notes | solid:forClass | schema:TextDigitalDocument |
#notes | solid:instance | /public/notes.ttl |
The above Type Index includes one Type Registration, identified by #notes
, that registers
/public/notes.ttl
for the data type schema:TextDigitalDocument
.
So how do we find the user's Public Type Index? It's usually listed in their profile, i.e. the Document accessible at their WebID:
Subject | Predicate | Object |
---|---|---|
#me | solid:publicTypeIndex | /settings/publicTypeIndex.ttl |
This is a pattern you'll often encounter when writing Solid apps: you start with the user's WebID, read the profile Document located there, and from there you can find the data you need.
So let's see what this looks like in code:
import { fetchDocument } from 'tripledoc';
import { solid, schema } from 'rdf-namespaces';
async function getNotesList(profile) {
/* 1. Check if a Document tracking our notes already exists. */
const publicTypeIndexRef = profile.getRef(solid.publicTypeIndex);
const publicTypeIndex = await fetchDocument(publicTypeIndexRef);
const notesListEntry = publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument);
/* 2. If it doesn't exist, create it. */
if (notesListEntry === null) {
// We will define this function later:
return initialiseNotesList(profile, publicTypeIndex);
}
/* 3. If it does exist, fetch that Document. */
const notesListRef = notesListEntry.getRef(solid.instance);
return await fetchDocument(notesListRef);
}
Given the profile
we fetched before, we can find a reference to the public Type
Index under solid:publicTypeIndex
. Then in the public Type Index, we can find the #notes
entry
by looking for the Subject that has a Statement saying it is for the class
schema:TextDigitalDocument
.
If that Subject is not found, we initialise a new Document to contain the notes — more on that
later. If it is found, we find the URL of the Document that should contain notes under the
solid:instance
key. All we then have to do is to call fetchDocument
to retrieve the Document at
that URL.
One thing to keep in mind, again, is that you can make no assumptions about the data in the user's
Pod. Thus, to be safe you should always check whether the return value of getRef()
is null
.
Consider using TypeScript to get a warning when you forget to do
so.
That leaves us with one loose thread: the function initialiseNotesList()
, which creates a new
Document that will contain the notes, and adds it to the Public Type Index. Let's take a look:
import { createDocument } from 'tripledoc';
import { space, rdf, solid, schema } from 'rdf-namespaces';
async function initialiseNotesList(profile, typeIndex) {
// Get the root URL of the user's Pod:
const storage = profile.getRef(space.storage);
// Decide at what URL within the user's Pod the new Document should be stored:
const notesListRef = storage + 'public/notes.ttl';
// Create the new Document:
const notesList = createDocument(notesListRef);
await notesList.save();
// Store a reference to that Document in the public Type Index for `schema:TextDigitalDocument`:
const typeRegistration = typeIndex.addSubject();
typeRegistration.addRef(rdf.type, solid.TypeRegistration)
typeRegistration.addRef(solid.instance, notesList.asRef())
typeRegistration.addRef(solid.forClass, schema.TextDigitalDocument)
await typeIndex.save([ typeRegistration ]);
// And finally, return our newly created (currently empty) notes Document:
return notesList;
}
(Keep in mind that, in order to write to the user's Pod, the user will need to have given your app explicit permission to write when they signed in.)
If all the above looks terribly complicated: that's because it is. Work is underway to make this easier in the future, allowing you to point a library at any data model, and having the library make sure that the user's Pod is automatically prepared to handle that model. For now though, you'll have to work through these step-by-step instructions.