Locating data
As described in Understanding Solid, a key concept in Solid is that data is linked. Which means that finding the data you want is done by following links, usually starting at the user’s WebID. As an example, let’s consider the data created by the Solid app Media Kraken, an app that allows you to track which movies you have seen. As a user connects their Solid Pod, the app receives their WebID, e.g. https://mypod.example/profile#me. That might contain the following data:
{ publicTypeIndex: "https://mypod.example/settings/publicTypeIndex", ~meta: { url: " https://mypod.example/profile#me", },}OK, so that is pointing to a “public type index”. Media Kraken might have created the following entry there:
{ type: "http://www.w3.org/ns/solid/terms#TypeRegistration", forClass: "http://schema.org/Movie", instance: "https://mypod.example/movies/", ~meta: { url: " https://mypod.example/settings/publicTypeIndex#forMovies", },}This says that this data is a “type registration”, for data of type “movie”, and provides a link to where data of that type could be found under instance. And so https://mypod.example/movies/ is where the user’s watched movies are stored.
However, as described under What makes Solid hard, and how does Benno help?, one challenge when writing Solid applications is that you can never be sure what data already exists. And while Benno’s models can validate that existing and new data is complete, you will still have to follow the links every time your app opens. And what’s more: if the links do not exist yet, you will have to add them yourself.
Obviously, this is a major nuisance: instead of this lousy busywork, I’d much rather focus on the interesting parts of my app! Luckily, Benno has a trick up its sleeve for this as well: Trails.
In a nutshell, Trails provide an ergonomic method to define how to find which links to follow, and how to leave new links if they don’t exist yet. But first, a word about authentication.
A word about authentication
Section titled “A word about authentication”Benno is built on top of @inrupt/solid-client, which does not handle authentication — and neither does Benno. Instead, for browser-based apps, there’s @inrupt/solid-client-authn-browser (which I lovingly refer to as SCAB).
After using SCAB to connect to the user’s Pod, in addition to the user’s WebID, it will provide you with a fetch function that has a similar API to the browser’s native one, the main difference being that requests sent with this function will be authenticated. Thus, it can be used to access data in the user’s Pod that is not publicly accessible. You can pass this fetch function as a parameter to all Benno functions that involve fetching or storing data, including when following a Trail.
Laying down your trail
Section titled “Laying down your trail”Let’s first define the models for the data as laid out above:
import { buildModel } from "benno";import { rdf, solid, space } from "rdf-namespaces";
export const profileModel = buildModel() .addProperty({ alias: "podRoot", property: space.storage, type: "url", required: true, }) .addProperty({ alias: "publicTypeIndex", property: solid.publicTypeIndex, type: "url", required: false, });
export const typeIndexModel = buildModel() .addProperty({ alias: "type", property: rdf.type, type: "url", required: true, }) .addProperty({ alias: "forClass", property: solid.forClass, type: "url", required: true, }) .addProperty({ alias: "instance", property: solid.instance, type: "url", required: true, });In addition to using these models for reading and writing data, you can also use them to define your trail. For example:
import { buildTrail } from "benno/trail";import { profileModel, typeIndexModel } from "./trail-models";import { schema, solid } from "rdf-namespaces";
export const trail = buildTrail() .via({ model: profileModel, getTargetUrl: (profile) => profile.publicTypeIndex ?? null, fallback: (rootUrl, existingProfile) => { if (!existingProfile) { // If the profile was not found at the user's WebID altogether, // something is seriously wrong with the user's Pod. // We can't fix that, so bail out: throw new Error("Profiles should always exist"); } return { ...existingProfile, publicTypeIndex: `${rootUrl}/settings/publicTypeIndex`, }; }, }) .via({ model: typeIndexModel, findPointer: (typeIndexes) => { return ( typeIndexes.find( (typeIndex) => typeIndex.type === solid.TypeRegistration && typeIndex.forClass === schema.Movie, ) ?? null ); }, getTargetUrl: (typeIndex) => typeIndex.instance, fallback: (rootUrl) => { return { type: solid.TypeRegistration, forClass: schema.Movie, instance: `${rootUrl}/movies/`, }; }, });As you can see, a Trail consists of individual steps (defined through .via calls). Each step uses a model and results in a URL. getTargetUrl defines how to get that URL from a Thing that matches the model. If that Thing is not identified by the previous step’s URL (or the starting URL, but we’ll discuss that below), you can define a function findPointer that takes a list of Things that match your model, and returns the one you care about.
And finally, we need to deal with the case in which the data does not yet exist in the user’s Pod. To do so, we can pass a fallback function that returns a new Thing. You can have that Thing point to an arbitrary URL, as long as it starts with the provided rootUrl, Benno will create that resource if it does not exist yet.
Following the trail
Section titled “Following the trail”The trail is now ready for use! All you need to do is pass it a starting point using startFrom (this will usually be the user’s WebID), and then call follow. Try it out yourself below. (Tip: open your browser’s network console to see all the requests being sent to your Pod — imagine having to write those by hand!)
import type { UrlString } from "@inrupt/solid-client";import { fetch } from "@inrupt/solid-client-authn-browser";import { trail } from "./trail";
async function fetchMovieUrl(webId: UrlString) { return trail.startFrom(webId).follow({ fetch: fetch });}