Read/Write 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.

When accessing data with solid-client,

  • A Thing refers to a data entity; e.g., a person. Each Thing’s URL acts as its identifier.

    The use of the Thing’s URL facilitates interoperability. That is, to combine data, you can use a Thing’s URL to link to other data.

  • A SolidDataset is a set of Things. Each SolidDataset’s URL acts as its identifier.

    Typically, all Things in a SolidDataset have URLs relative to the SolidDataset’s URL.

From a Thing, you can access specific properties (i.e., data) about the Thing. For example, if a Thing represents a person’s contact information, specific properties for a person may include the person’s name, email addresses, etc. Each property’s URL acts as its identifier.

Vocabulary

To encourage interoperability, existing Vocabularies define reusable terms describing common data (e.g., name, title, address, url). For example, http://www.w3.org/2006/vcard/ns#fn is part of the vCard Vocabulary.

Inrupt’s vocab-common-rdf library bundles constants that refer to terms in popular pre-existing vocabularies. Applications can use these constants to read and write data. For more information on Vocabulary usage, see Vocabulary.

Prerequisite for Restricted Data

By default, solid-client only enables access to public data on Solid Pods. To access other 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.

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.

Note

Use solid-client-authn-browser for client-side code only.

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 to complete the authentication process.
  //   If the page is being loaded after the redirect from the Solid Identity Provider
  //      (i.e., part of the authentication flow), the user's credentials are stored in-memory, and
  //      the login process is complete. That is, a session is logged in 
  //      only after it handles the incoming redirect from the Solid Identity Provider.
  //   If the page is not being loaded after a redirect from the Solid Identity Provider, 
  //      nothing happens.
  await handleIncomingRedirect();

  // 2. Start the Login Process if not already logged in.
  if (!getDefaultSession().info.isLoggedIn) {
    // The `login()` redirects the user to their identity provider;
    // i.e., moves the user away from the current page.
    await login({
      // Specify the URL of the user's Solid Identity Provider; e.g., "https://inrupt.net"
      oidcIssuer: "https://inrupt.net",
      // Specify the URL the Solid Identity Provider should redirect to after the user logs in,
      // e.g., the current page for a single-page app.
      redirectUrl: window.location.href,
    });
  }

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

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

loginAndFetch();

For more information on authentication, see Authenticate.

For more information on access management, see Manage Access to Data (ACP) and Manage Access to Data (WAC).

Read Data

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 Dataset

To access data, first fetch the dataset (SolidDataset) that contains the data. To fetch a SolidDataset, pass its URL to getSolidDataset as in the following example:

const myDataset = await getSolidDataset(
  "https://docs-example.inrupt.net/profile/card"
);

The examples 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 dataset, 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.

The following passes in a example entity’s URL to getThing to retrieve the entity, specifically the profile, from the previously fetched dataset.

const profile = getThing(
  myDataset,
  "https://docs-example.inrupt.net/profile/card#me"
);

3. Read Data Attribute of a Thing

A property (i.e. data) about a Thing is identified by a URL. A property can have zero, one or more values, and the value is typed; e.g., a string, an integer, or an URL if pointing to other Things.

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 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.

To access data, use the appropriate get function depending on the data type and the number of values for the property. Pass the function the URL that identifies the property to fetch.

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 that is

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://docs-example.inrupt.net/profile/card", { fetch: fetch });
const profile = getThing( myDataset, "https://docs-example.inrupt.net/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 Example");
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://docs-example.inrupt.net/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://docs-example.inrupt.net/profile/card", { fetch: fetch });
const profile = getThing( myDataset, "https://docs-example.inrupt.net/profile/card#me");

let updatedProfile = setStringNoLocale(profile, VCARD.fn, "Docs Example");
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://docs-example.inrupt.net/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.