Making access to Solid pods data a breeze: @solideal/storage

I already hear you : “Urgh… another library to access data on Solid pods, rdflib, tripledoc, soukai…” and you may be right but hold on!

Right now, developing for Solid is kinda frustrating. If you’re not familiar with Linked Data (as I was), every library expecting you to use triples or quads will hurt you. This library aims to help you store data easily on a pod and take care of the repetitive stuff.

Here is an overview of the library available here https://github.com/solideal/storage :

  • Lightweight with only one dependency to @inrupt/solid-client
  • Created for Solid
  • Written in Typescript
  • Easily maps between triples / javascript objects without hassle
  • Provides an easy API to create/update, read and delete data on a pod
  • Use Solid type indexes to resolve data location if you need to (and may even register the type as needed)

Here is a sample of what you can do right now with @solideal/storage:

import { Repository, is } from "@solideal/storage";

// Let's say you already have some data to persist
const myBookmark = {
  id: "some-unique-identifier",
  title: "My super duber local bookmark",
  url: "http://localhost:3000",
};

// And now you want to persist it (let's assume the user has already a type registration for this type of data
// Here I'm using the `resolve` static method which means the library will look at the webid type
// index to determine where to actually store the data.
const repository = Repository<typeof myBookmark>.resolve({
  type: "https://www.w3.org/2002/01/bookmark#Bookmark",
  schema: {
    id: is.key(), // This one is mandatory and will contains the resource location
    title: is.string("http://purl.org/dc/elements/1.1/title"),
    url: is.url("https://www.w3.org/2002/01/bookmark#recalls"),
  },
}, {
  webid: "https://yuukanoo.solid.community/profile/card#me" // needed if no global webid was defined with `configure({ webid: "..." })`
});

await repository.save(myBookmark); // myBookmark.id will equals the final url on your pod :)

I already have a lot of ideas in my head to go even further and the API may break in the future but I wanted to share what I came up with so far to see if it can help other people.

8 Likes

I like the is builder pattern, that’s cool!

What does reading look like though?

Thanks!

Reading goes like this:

import { Repository, is } from "@solideal/storage";

interface Bookmark {
  id: string;
  title: string;
  url: string;
}

const repository = new Repository<Bookmark>({
  source: "https://yuukanoo.solid.community/public/bookmarks.ttl", // When you know where you should fetch the data
  type: "https://www.w3.org/2002/01/bookmark#Bookmark",
  schema: {
    id: is.key(), // This one is mandatory and will contains the resource location
    // You can also provide multiple predicates if the vocabulary can change between applications
    title: is.string("http://purl.org/dc/elements/1.1/title", "http://purl.org/dc/terms/title"),
    url: is.url("https://www.w3.org/2002/01/bookmark#recalls"),
  },
});

const allBookmarks = await repository.find();
/**
 * Will yield something like this:
 * [
 *    {
 *      id: "https://yuukanoo.solid.community/public/bookmarks.ttl#anid",
 *      title: "My super duber local bookmark",
 *      url: "http://localhost:3000",
 *    },
 *    {
 *      id: "https://yuukanoo.solid.community/public/bookmarks.ttl#anotherone",
 *      title: "Another bookmark",
 *      url: "http://localhost:5000",
 *    },
 * ]
 */

// Apply a filter directly on retrieved data
await repository.find((bookmark) => bookmark.title.indexOf("super") !== -1);

// Will yield **a single** Bookmark with the given key
await repository.only("https://yuukanoo.solid.community/public/bookmarks.ttl#anid")
// Or same as find, apply a filter to return only the first matched element
await repository.only(((bookmark) => bookmark.title.indexOf("super") !== -1);

I’m currently working on an app making use of this library to provide a real world example :slight_smile:

1 Like

Look good, I’m looking forward to seeing what this would look like when used in an actual app :slight_smile: What would this look like if e.g. you’d have an address book (vcard:AddressBook) with contacts (vcard:Individual) in them (e.g. like described here) - can you specify links between Repositories?

That’s already planned here https://github.com/solideal/storage/issues/7 :slight_smile: The plan is to make the library evolve with real use cases. It will not cover everything but should provide a straightforward way to build applications on top of Solid.

I’m also trying to figure how to manage containers (see https://github.com/solideal/storage/issues/8). My current idea is to provide the root container of a resource type to the Repository and a new option to convert a data (managed by the repository) to the final path.

For example, if you have blog posts stored by date (http://pod/posts/2019/posts.ttl, http://pod/posts/2020/posts.ttl), you will be able to provide the root (http://pod/posts/) ldp container and an option along the lines:

interface Post {
  permalink: string
  title: string
  date: Date
}

const repo = new Repository<Post>({
  source: "http://pod/posts/",
  // The line below provide informations on how to get a dataset path based on a data to persist
  resolveTo: (post: Post): string => `${post.date.getFullYear()}/posts.ttl`,
  type: "...",
  schema: { ... }
});

And the repository will take care of the rest : creating intermediate containers if it needs to, retrieving posts in nested documents and so on without the developer having to do it itself.

The Repository represents the main (if not the only) component that a developer should be aware of.

Looks good. Two things to keep in mind while implementing that:

  1. The root of the Pod need not (currently) be the root of the domain. This might change in the future, but to be safe, it is best to read the http://www.w3.org/ns/pim/space#storage in the user’s profile to find the root.
  2. There is no need for the client to create intermediate Containers. If you create a Resource at https://example.pod/grandparent/parent/resource, and neither grandparent nor parent exist yet, the server will create them for you.
3 Likes

Oh nice to hear 2), that will make my life easier :slight_smile: