Read/Write Structured Data

This page provides information on reading and writing structured data stored in Solid Pods. For information on reading and writing files (e.g., .jpg or .json) stored in Solid Pods, see Read/Write Files.

Structured data, in this context, refer to data structured as Things in SolidDatasets. For example, if you want to save information (such as the required books, etc.) about a course, you can have a SolidDataset that corresponds to the course, and within that SolidDataset, contain the Things that correspond to various data about course, such as the books.

A Pod represented as a directory structure, containing a folder "profile" with a document "card", and a folder "fall2021", which in turn contains a folder "courses", which contains a document "Writing101". Alongside the directory structure, the "Writing101" document is also shown individually, containing separate areas labelled "#book1", "#book2" and "#otherThing".

For more information, see Structured Data.

Prerequisite for Restricted Data

By default, solid-client functions make unauthenticated requests. To access restricted data, you must authenticate as a user who has been granted appropriate access to that data. For example, to write to a resource, the user must have Write Access on the Resource. To make authenticated requests, you can use one of Inrupt’s authentication library to login and pass the fetch() function as an option to solid-client functions.

Inrupt provides the following libraries for authentication:

  • solid-client-authn-browser library to authenticate in a browser.

  • solid-client-authn-node library to authenticate in Node.js.

The following example uses solid-client-authn-browser to authenticate a user, which provides a fetch() function. You can pass this function as an option to solid-client functions, as it includes the user’s credentials. These credentials will automatically be provided in requests to Solid servers, thereby allowing the authenticated user to access protected resources if the user has the required permissions/authorization.

import { handleIncomingRedirect, login, fetch, getDefaultSession } from '@inrupt/solid-client-authn-browser'
import { getSolidDataset, saveSolidDatasetAt } from "@inrupt/solid-client";

async function loginAndFetch() {
  // 1. Call the handleIncomingRedirect() function,
  //    - Which completes the login flow if redirected back to this page as part of login; or
  //    - Which is a No-op if not part of login.
  await handleIncomingRedirect();

  // 2. Start the Login Process if not already logged in.
  if (!getDefaultSession().info.isLoggedIn) {
    await login({
      oidcIssuer: "https://broker.pod.inrupt.com",
      redirectUrl: window.location.href,
      clientName: "My application"
    });
  }

  // 3. Make authenticated requests by passing `fetch` to the solid-client functions.
  // For example, the user must be someone with Read access to the specified URL.
  const myDataset = await getSolidDataset(
    "https://pod.inrupt.com/docsteam/profile/card", {
    fetch: fetch
  });

  // ...
  
  // For example, the user must be someone with Write access to the specified URL.
  const savedSolidDataset = await saveSolidDatasetAt(
    "https://pod.inrupt.com/docsteam/profile/card",
    myChangedDataset, {
    fetch: fetch
  });
}

loginAndFetch();

For more information on authentication, see Authentication.

For more information on access management, see:

Read Data

Read Access

To read private data (i.e., it is not configured for public read access), the user must first authenticate as someone with the appropriate access. To make authenticated requests (such as read and write), pass in a fetch function that includes the user’s credentials to the various read/write functions. For an example, see the Prerequisite section.

To read data with solid-client,

  1. You first use getSolidDataset to fetch the dataset from which you want to read your data.

  2. Then, use either:

    • getThing to get a single data entity from the dataset, or

    • getThingAll to get all data entities from the dataset.

  3. Then, from the data entity, you can get specific data. For a list of the get functions, see thing/get module.

0. Import

Import from the client libraries those objects to be used in the example:

import {
  getSolidDataset,
  getThing,
  getStringNoLocale,
  getUrlAll
} from "@inrupt/solid-client";

import { FOAF, VCARD } from "@inrupt/vocab-common-rdf";

1. Fetch the SolidDataset

To access data, first use getSolidDataset to fetch the SolidDataset, such as a profile document, that contains the data.

Use getSolidDataset() to get the Profile Document

To fetch a SolidDataset, pass its URL to getSolidDataset as in the following example:

const myDataset = await getSolidDataset(
  "https://pod.inrupt.com/docsteam/profile/card"
);

The example retrieves a SolidDataset that is accessible by the public. To make an authenticated request for a SolidDataset that requires the user to have read access, pass in a fetch function that includes the user’s credentials. For an example, see the Prerequisite section.

2. Get the Data Entity Thing

From the fetched SolidDataset, you can use either:

  • getThing with the Thing’s URL to get a single data entity, or

  • getThingAll to get all data entities from the dataset.

Use getThing() to get profile data associated with WebID from SolidDataset

The following example passes in the WebID to the getThing to retrieve the profile (i.e., the Thing) from the previously fetched SolidDataset (i.e., the profile document).

const profile = getThing(
  myDataset,
  "https://pod.inrupt.com/docsteam/profile/card#me"
);

3. Read Data Attribute of a Thing

A property (i.e. data) about a Thing is identified by a URL. Like the Thing and SolidDataset, each property (such as name, etc.) of a Thing is also identified by a URL. That is, when storing the name of a book, you do not store the data under the identifier name. Instead, you use a URL identifier, such as https://schema.org/name from the https://schema.org Vocabulary (or some other URL).

A property can have zero, one or more values, and the value is typed; e.g., a string, an integer, or a URL if pointing to other Things. To retrieve the data, use the appropriate get function depending on the data type and the number of values for the property.

Note

These types are specific to Solid, i.e. they are not JavaScript types. This is the reason there is a distinction between integers and decimals, and why there is a separate type for URLs.

To the get function, pass the URL that identifies the property to fetch. To encourage interoperability, various Vocabularies exist to identify common data. For example:

  • http://www.w3.org/2006/vcard/ns#fn is part of the vCard Vocabulary. http://www.w3.org/2006/vcard/ns#fn is understood to be a string representing a formatted name.

  • http://xmlns.com/foaf/0.1/knows is part of the Friend of a Friend (FOAF) Vocabulary. http://xmlns.com/foaf/0.1/knows is understood to be a URL and a Thing can have more than one knows relationship.

Tip

Many Vocabularies already exist that define various identifying URLs for various concepts (e.g., Organization, Person) and properties (e.g., name, address). Inrupt’s vocab-common-rdf library bundles constants that refer to terms in popular pre-existing vocabularies. Applications can use these constants instead of specifying the full URL. For more information on Vocabulary usage, see Vocabulary.

Use various get() functions to get properties/data from the profile.

From the retrieved profile (i.e. the Thing), the following operations gets the following properties:

  • http://www.w3.org/2006/vcard/ns#fn

  • http://xmlns.com/foaf/0.1/knows.

For simplicity, the example uses the corresponding constants from the @inrupt/vocab-common-rdf library for the URLs.

// 3. Retrieve specific data from the retrieved thing; i.e. profile.

// 3a. Get a single string data value from the profile.
// Specifically, the operation returns the profile's VCARD.fn 
// (i.e., "http://www.w3.org/2006/vcard/ns#fn") value as a string.

const fn = getStringNoLocale(profile, VCARD.fn);

// 3b. Get multiple URL data from the profile.
// Specifically , the operation returns the profile's FOAF.knows
// (i.e., "http://xmlns.com/foaf/0.1/knows") values as an array of URLs. 
// The URLs point to the Things representing Persons.

const acquaintances = getUrlAll(profile, FOAF.knows);

For a list of the get functions, see the thing/get.

Write Data

Write Access

To write data to a Pod requires that the user have appropriate access. That is, to identify the user as someone with the appropriate access, the user must first authenticate. To make authenticated requests (such as read and write), pass in a fetch function that includes the user’s credentials to the various read/write functions.

For an example, see the Prerequisite section.

To write data with solid-client,

  1. Create a data entity (i.e., a Thing) with the updates.

    1. Start with a Thing to modify.

      • Use getThing to start with an existing data entity, or

      • Use createThing to create a new data entity.

    2. With this Thing as a starting point, use the following functions to create a new Thing with the modifications:

      • thing/add functions to return a new Thing modified with new data added,

      • thing/set functions to return a new Thing modified with the specified data, and

      • thing/remove functions return a new Thing modified with specified data removed.

  2. Use setThing to return a new dataset with the updated Thing.

  3. Use saveSolidDataSetAt to save the dataset to the Pod and return a new SolidDataSet.

Important

  • The solid-client library’s functions (such as the various add/set/remove functions) do not modify the objects that are passed in as arguments. Instead, the library’s functions return a new object with the requested changes, and do not modify the passed-in objects.

  • A SolidDataset keeps track of the data changes compared to the data in the Pod. For set operations on exisitng properties, the changelog tracks both the old value and new values of the property being modified.

    The save operation applies the changes from the changelog to the current SolidDataset. If the old value specified in the changelog does not correspond to the value currently in the Pod, the save operation will error.

Assumptions

The following examples assumes:

  • The application has used the solid-client-authn-browser library to handle login and has an authenticated fetch function.

  • The logged-in user has permission to read and write the specified SolidDataset.

See the Prerequisite section for details on authenticating the user’s session.

0. Import

Import from the client libraries those objects to be used in the example:

import {
  getSolidDataset,
  getThing,
  setStringNoLocale,
  addStringNoLocale,
  setThing,
  saveSolidDatasetAt,
} from "@inrupt/solid-client";
import { fetch } from "@inrupt/solid-client-authn-browser";
import { FOAF, VCARD } from "@inrupt/vocab-common-rdf";

1. Create Thing with Updated Data

To start, you need a data entity (a Thing) entity from which to create the updated Thing:

  • Use getThing to start with an existing data entity, or

  • Use createThing to create a new data entity.

The following example uses an existing profile as the Thing to update:

// 1a. Start with an existing Thing (i.e., profile).
// Note: Login code has been omitted for brevity. See the Prerequisite section above.
// ...

const myDataset = await getSolidDataset( "https://pod.inrupt.com/docsteam/profile/card", { fetch: fetch });
const profile = getThing( myDataset, "https://pod.inrupt.com/docsteam/profile/card#me");

With this profile as a starting point, use the following functions to create a new Thing with the modifications:

For example, the following example creates a new Thing updatedProfile with the following changes:

  • Update http://www.w3.org/2006/vcard/ns#fn to "Docs Example".

  • Add http://xmlns.com/foaf/0.1/nick to "docs".

  • Add http://xmlns.com/foaf/0.1/nick to "example".

For simplicity, the example uses the corresponding constants from the @inrupt/vocab-common-rdf library for the URLs.

// 1b. Modify the thing; specifically, update the name and add two nicknames.
// Note: solid-client functions do not modify objects passed in as arguments. 
// Instead the functions return new objects with the modifications.
// That is, setStringNoLocale and addStringNoLocale return a new Thing and
// - profile remains unchanged and 
// - updatedProfile is changed only because it is explicitly set to the object returned from addStringNoLocale.

let updatedProfile = setStringNoLocale(profile, VCARD.fn, "Docs Team");
updatedProfile = addStringNoLocale(updatedProfile, FOAF.nick, "docs");
updatedProfile = addStringNoLocale(updatedProfile, FOAF.nick, "example");

The solid-client library’s functions (such as the various add/set/remove functions) do not modify the objects that are passed in as arguments. Instead, the library’s functions return a new object with the requested changes, and the functions do not modify the passed-in objects.

In the above example:

  • profile remains unchanged after the setStringNoLocale, and

  • updatedProfile is explicitly set to the new object returned from the addStringNoLocale calls. Otherwise, the updatedProfile would remain unchanged from the calls.

2. Update SolidDataset with the Updated Thing

After creating a new Thing with updated data, use setThing to insert the Thing into a SolidDataset.

If the Thing was based on an existing Thing from the SolidDataset, the function replaces the existing Thing with the new Thing.

// 2. Update SolidDataset with the updated Thing (i.e., updatedProfile). 
// Note:  solid-client functions do not modify objects passed in as arguments. 
// Instead the functions return new objects with the modifications.
// That is, setThing returns a new SolidDataset and
// - myDataset remains unchanged.
// - updatedProfile remains unchanged.

const myChangedDataset = setThing(myDataset, updatedProfile);

3. Save the SolidDataset to a Pod

To save the updated dataset to a Pod, use saveSolidDatasetAt, passing in its URL as well as the updated dataset. The saveSolidDatasetAt function creates any intermediate folders/containers as needed for the URL. 1

// 3. Save the new dataset.
// The function returns a SolidDataset that reflects its state after the save.
const savedProfileResource = await saveSolidDatasetAt(
  "https://pod.inrupt.com/docsteam/profile/card",
  myChangedDataset,
  { fetch: fetch }
);

If a SolidDataset exists at the specified URL, the changes from the updated SolidDataset are applied.

Note

A SolidDataset keeps track of the data changes compared to the data in the Pod. The save operation applies the changes from the changelog to the current SolidDataset.

For set operations on existing properties, the changelog tracks both the old value and new values of the property being modified. If the old value specified in the changelog does not correspond to the value currently in the Pod, the save operation will error.

Upon successful save, saveSolidDatasetAt returns a SolidDataset that reflects the sent data. That means, if another process made separate non-conflicting modifications to the SolidDataset after you retrieved the SolidDataset but before you successfully saved, the returned SolidDataset only reflects your changes. To make sure you have the latest data, call getSolidDataset again after saving the data.

1

The solid-client library also provides the saveSolidDatasetInContainer. However, unlike saveSolidDatasetAt which creates any intermediate folders/containers as needed, saveSolidDatasetInContainer requires the specified destination container already exists.

Changelog Considerations

A SolidDataset keeps track of the data changes compared to the data in the Pod. The save operation applies the changes from the changelog to the current SolidDataset:

  • For add operations, the changelog keeps track of additions.

  • For remove operations, the changelog keeps track of the deletions.

  • For set operations, the changelog keeps track of the old and new values. As such, if the old value specified in the changelog does not correspond to the value currently in the Pod, the save operation will error with 409 Conflict response.

Consider the previous example and the changes it makes to the profile:

const myDataset = await getSolidDataset( "https://pod.inrupt.com/docsteam/profile/card", { fetch: fetch });
const profile = getThing( myDataset, "https://pod.inrupt.com/docsteam/profile/card#me");

let updatedProfile = setStringNoLocale(profile, VCARD.fn, "Docs Team");
updatedProfile = addStringNoLocale(updatedProfile, FOAF.nick, "docs");
updatedProfile = addStringNoLocale(updatedProfile, FOAF.nick, "example");

const myChangedDataset = setThing(myDataset, updatedProfile);

// The function returns a SolidDataset that reflects its state after the save.
const savedProfileResource = await saveSolidDatasetAt(
  "https://pod.inrupt.com/docsteam/profile/card",
  myChangedDataset,
  { fetch: fetch }
);

The myChangedDataset has a changelog that reflects:

  • modification of the formatted name (from old value to new value).

  • addition of the two nicknames.

Scenario 1: If another operation has modified the formatted after you have retrieved myDataset but before you save myChangedDataset, the save operation errors.

Scenario 2: If another operation has separately added the nickname docs after you have retrieved myDataset but before you save myChangedDataset, the save operation succeeds, but the returned SolidDataset only reflects your changes. To make sure you have the latest data, call getSolidDataset again after saving the data.