Getting Started: Part 2

This tutorial creates an introductory application that uses Inrupt’s JavaScript client libraries to write to your Pod. Alternatively, to create a sample application using Inrupt’s Solid React SDK, refer to Solid React SDK documentation.

The tutorial uses npm and Webpack to run the application locally on localhost:8080.

Locally Run Getting Started Application Part 2

Prerequisites

The tutorial follows Getting Started: Part 1. As such, the tutorial assumes that you have:

  • Registered a Pod.

  • Installed npm.

If you do not have the prerequisites, refer to Prerequisites in Getting Started: Part 1.

Note

The tutorial uses https://inrupt.net/ as the Pod Server for your example Pod. If your example Pod is not on https://inrupt.net/, you must change the oidcIssuer value in the application’s index.js file.

Build the Application

1. Initialize the Application

  1. Create the directory structure for your Webpack project:

    mkdir -p  my-demo-app2 my-demo-app2/src my-demo-app2/dist
    
  2. Go to the newly created my-demo-app2 directory.

    cd my-demo-app2
    
  3. Initialize the application.

    • To accept the default values for the application without prompts:

      npm init -y
      

    - or -

    • To be prompted to enter values for the application:

      npm init
      
      1. You can either hit return to accept the default values (including empty values) or supply your own values.

      2. When prompted Is this OK? (yes), enter to accept yes.

2. Install the Client Libraries

  1. Use npm to install the solid-client, solid-client-authn-browser, and vocab-common-rdf libraries:

    npm install @inrupt/solid-client @inrupt/solid-client-authn-browser @inrupt/vocab-common-rdf
    

3. Install Webpack

  1. Use npm to install Webpack packages:

    npm install webpack webpack-cli webpack-dev-server css-loader style-loader --save-dev
    
  2. Create a webpack.config.js file with the following content:

    const path = require("path");
    module.exports = {
       mode: "development",
       entry: "./src/index.js",
       output: {
         path: path.resolve(__dirname, "public"), 
         filename: "index.js" 
       },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [ { loader: "style-loader" }, { loader: "css-loader" } ],
          },
        ]
      },
      devServer: {
        contentBase: "./dist"
      },
      resolve: {
          fallback: { 
             stream: require.resolve("stream-browserify") ,
             crypto: require.resolve("crypto-browserify")
          }
      }
    };
    
  3. Edit the package.json file to add build and start script fields to scripts:

    Tip

    Be sure to add the comma after the preceding field value before adding the build and start fields.

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "build": "webpack",
      "start": "webpack serve --open true"
    },
    

4. Create the Application

In the my-demo-app2 directory, create the files for the application. The tutorial provides an explanation of the JavaScript code at the end of the tutorial.

  1. In the my-demo-app2/dist folder, create a my-demo.css file with the following content:

    h2,h3 {
        margin: 1rem 1.2rem 1rem 1.4rem;
    }
    
    body * {
       margin-left: .5rem;
       margin-right: 1rem;
    }
    
    header {
       border-bottom: #5795b9 solid;
       padding-left: .5rem;
    }
    
    .panel {
       border: 1px solid #005b81;
       border-radius: 4px;
       box-shadow: rgb(184, 196, 194) 0px 4px 10px -4px;
       box-sizing: border-box;
    
       padding: 1rem 1.5rem;
       margin: 1rem 1.2rem 1rem 1.2rem;
    }
    
    #login {
       background: white;
    }
    
    #labelStatus[role="alert"] {
       padding-left: 1rem;
       color: purple;
    }
    
    
    #write {
      background: #e6f4f9;
    }
    
    #titles {
       margin-left: 1.3rem;
    }
    
    label {
       vertical-align: top;
    }
    #savedtitles {
       border: none;
       background: #f8fbfd;
       margin-left: 6rem;
    }
    
    #labelRetrieved {
       margin-left: 1.2rem;
    }
    
  2. In the my-demo-app2/dist, create an index.html file with the following content:

     
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Getting Started: Inrupt JavaScript Client Libraries</title>
        <script defer src="./index.js" ></script>
        <link rel="stylesheet" href="my-demo.css" />
      </head>
      <body>
        
        <header>
            <h2>Getting Started</h2>
            <h3>with Inrupt JavaScript Client Libraries: Write Operation</h3>
        </header>
        <section id="login" class="panel">
            <div class="row">
              <label id="labelLogin" for="btnLogin">1. Click the button to log in: </label>
              <button name="btnLogin" id="btnLogin">Login</button> 
              <p id="labelStatus"></p>
            </div>
        </section>
    
        <div id="write" class="panel" >
          <div class="row">
            <label id="writelabel">2. Create a private reading list in your Pod.</label>
          </div>
          <div class="row">
            <label id="podlabel" for="PodURL">a. Enter your Pod URL: </label>
            <input type="url" id="PodURL" name="PodURL"  size="50" pattern="https://.*"  placeholder="e.g., https://<username>.inrupt.net">/getting-started/readingList/myList
          </div>
          <div class="row">
            <label id="listLabel" for="titles">b. Enter items to read: </label>
            <textarea id="titles" name="titles" rows="5" cols="42" >
    Leaves of Grass
    RDF 1.1 Primer</textarea>
            <button name="btnCreate" id="btnCreate">Create</button> <span id="labelCreateStatus"></span>
          </div>
          <div class="row"></div>
            <label id="labelRetrieved" for="savedtitles">Retrieved:</label>
            <textarea id="savedtitles" name="savedtitles" rows="5" cols="42" readonly></textarea>
          </div>
        </div>
      </body>
    </html>
    
  3. In the my-demo-app2/src, create an index.js file with the following content:

    Note

    If your Pod is not on https://inrupt.net, modify the oidcIssuer value.

    import {
       createSolidDataset,
       createThing,
       setThing,
       addInteger,
       addUrl,
       addStringNoLocale,
       saveSolidDatasetAt,
       getSolidDataset,
       getThingAll,
       getStringNoLocale
    } from "@inrupt/solid-client";
    
    import { Session } from "@inrupt/solid-client-authn-browser";
    
    import { SCHEMA_INRUPT_EXT, RDF, AS } from "@inrupt/vocab-common-rdf";
    
    const session = new Session();
    
    const buttonLogin = document.querySelector("#btnLogin");
    const buttonCreate = document.querySelector("#btnCreate");
    buttonCreate.disabled=true;
    const labelCreateStatus = document.querySelector("#labelCreateStatus");
    
    // 1a. Start Login Process. Call session.login() function.
    async function login() {
      if ( !session.info.isLoggedIn ) {
        await session.login({
          oidcIssuer: "https://inrupt.net",
          redirectUrl: window.location.href,
        });
      }
    }
    
    // 1b. Login Redirect. Call session.handleIncomingRedirect() function.
    // When redirected after login, finish the process by retrieving session information.
    async function handleRedirectAfterLogin() {
    
        await session.handleIncomingRedirect(window.location.href);
        if (session.info.isLoggedIn) {
          // Update the page with the status.
          document.getElementById("labelStatus").textContent = "Your session is logged in.";
          document.getElementById("labelStatus").setAttribute("role", "alert");
          // Enable Create button
          buttonCreate.disabled=false;
        }
    }
    
    // The example has the login redirect back to the index.html.
    // This calls the function to process login information.
    // If the function is called when not part of the login redirect, the function is a no-op.
    handleRedirectAfterLogin();
    
    // 2. Create the Reading List
    async function createList() {
      labelCreateStatus.textContent = "";
      const podUrl = document.getElementById("PodURL").value;
     
      // For simplicity and brevity, this tutorial hardcodes the SolidDataset URL.
      // In practice, you should add a link to this resource in your profile that applications can follow.
      const readingListUrl = `${podUrl}/getting-started/readingList/myList`;
     
      let titles = document.getElementById("titles").value.split("\n");
    
      let myReadingList  = createSolidDataset();
       
      // Add titles to the Dataset
      for (let i = 0; i < titles.length; i++) {
        let title = createThing({name: "title" + i});
        title = addUrl(title, RDF.type, AS.Article);
        title = addStringNoLocale(title, SCHEMA_INRUPT_EXT.name, titles[i]);
        myReadingList = setThing(myReadingList, title);
      }
    
      try {
        let savedReadingList = await saveSolidDatasetAt(
          readingListUrl,
          myReadingList,
          { fetch: session.fetch }
        );
    
        labelCreateStatus.textContent = "Saved";
        // Disable Create button
        buttonCreate.disabled=true;
    
        // Refetch the Reading List
        savedReadingList = await getSolidDataset(
          readingListUrl,
          { fetch: session.fetch } 
        );
    
        let items = getThingAll(savedReadingList);
    
        let listcontent="";
        for (let i = 0; i < items.length; i++) {
           let item = getStringNoLocale(items[i], SCHEMA_INRUPT_EXT.name);
           if (item != null) {
              listcontent += item + "\n";
           }
        }
    
        document.getElementById("savedtitles").value = listcontent; 
    
      } catch (error) {
        console.log(error);
        labelCreateStatus.textContent = "Error" + error;
        labelCreateStatus.setAttribute("role", "alert");
      }
     
    }
    
    
    buttonLogin.onclick = function() {  
      login();
    };
    
    buttonCreate.onclick = function() {  
      createList();
    };
    

    For details about the JavaScript code, see Examination of the Code.

5. Run the Application

  1. In the my-demo-app2 directory, run:

    npm run build && npm run start
    

    The output resembles the following:

    ℹ 「wds」: Project is running at http://localhost:8080/
    ...
    ℹ 「wdm」: Compiled successfully.
    
  2. Open localhost:8080 in a browser.

  3. Login.

    1. Click Login to login.

    2. If you have logged out of your Pod, you are prompted to log in. Enter your username and password and log in.

    3. If you have not already authorized the application, you will be prompted to authorize this application to access your Pod. To allow the application to read and write to your Pod, click Authorize.

    4. You are redirected back to your page.

    Tip

    You must be logged in to enable the Create button.

  4. Write your reading list.

    1. Enter your Pod URL; e.g., https://<username>.inrupt.net.

    2. Add your reading list.

    3. Click Create to create.

    The reading list is created in your <PodURL>/getting-started/readingList/myList.

6. Exit the Application

To exit the application, stop the npm run server process; e.g., Ctrl-C.

Examination of the Code

Login Code

The example uses the solid-client-authn-browser library to log in. solid-client-authn-browser is for client-side code only.

From @inrupt/solid-client-authn-browser, import Session:

import { Session } from "@inrupt/solid-client-authn-browser";

Create a new Session:

const session = new Session();

Start the login process by calling Session.login().

For example, to login to a Pod on https://inrupt.net:

// 1a. Start Login Process. Call session.login() function.
async function login() {
  if ( !session.info.isLoggedIn ) {
    await session.login({
      oidcIssuer: "https://inrupt.net",
      redirectUrl: window.location.href,
    });
  }
}

This function sends the user to the identity provider oidcIssuer, and once logged in, redirects back to the specified redirectURL.

Note

If your Pod is not on https://inrupt.net, modify the oidcIssuer value.

When redirected back from the identity provider, use Session.handleIncomingRedirect() to complete the login process:

// 1b. Login Redirect. Call session.handleIncomingRedirect() function.
// When redirected after login, finish the process by retrieving session information.
async function handleRedirectAfterLogin() {

    await session.handleIncomingRedirect(window.location.href);
    if (session.info.isLoggedIn) {
      // Update the page with the status.
      document.getElementById("labelStatus").textContent = "Your session is logged in.";
      document.getElementById("labelStatus").setAttribute("role", "alert");
      // Enable Create button
      buttonCreate.disabled=false;
    }
}

// The example has the login redirect back to the index.html.
// This calls the function to process login information.
// If the function is called when not part of the login redirect, the function is a no-op.
handleRedirectAfterLogin();

The function collects and verifies the information provided by the identity provider.

For more information on using the library to authenticate, see Authenticate.

Write Reading List

The example uses the solid-client and vocab-common-rdf libraries to write data to your Pod.

Tip

For the sake of simplicity and brevity, this getting started guide hardcodes the SolidDataset URL. In practice, you should add a link to this resource in your profile that applications can follow.

From @inrupt/solid-client and @inrupt/vocab-common-rdf, import the objects used in the application:

import {
   createSolidDataset,
   createThing,
   setThing,
   addInteger,
   addUrl,
   addStringNoLocale,
   saveSolidDatasetAt,
   getSolidDataset,
   getThingAll,
   getStringNoLocale
} from "@inrupt/solid-client";
import { Session } from "@inrupt/solid-client-authn-browser";
import { SCHEMA_INRUPT_EXT, RDF, AS } from "@inrupt/vocab-common-rdf";

Use createSolidDataset to create a new SolidDataset (i.e., the reading list).

  let myReadingList  = createSolidDataset();

The application uses createThing to create a new data entity (i.e., Thing) for each title to save. 1

To the Thing, the application uses the following functions to add data to the Thing:

  • addUrl to add the RDF.type property with AS.Article as its value.

  • addStringNoLocale to add the SCHEMA_INRUPT_EXT.name property with the title as its value.

Then, the application uses setThing to add the Thing to the SolidDataset (i.e., the reading list).

  // Add titles to the Dataset
  for (let i = 0; i < titles.length; i++) {
    let title = createThing({name: "title" + i});
    title = addUrl(title, RDF.type, AS.Article);
    title = addStringNoLocale(title, SCHEMA_INRUPT_EXT.name, titles[i]);
    myReadingList = setThing(myReadingList, title);
  }

RDF, SCHEMA_INRUPT_EXT, and AS provide convenience objects for many Solid-related terms:

  • RDF provides convenience objects for the RDF Vocabulary. For example, the RDF.type is a convenience object that includes the http://www.w3.org/1999/02/22-rdf-syntax-ns#type IRI.

  • SCHEMA_INRUPT_EXT is Inrupt’s extension of the schema.org Vocabulary. It provides convenience objects for a subset of terms from the schema.org Vocabulary, adding language tags/translations to labels and comments if missing from schema.org.

    By limiting the number of terms, SCHEMA_INRUPT_EXT aims to make working with select terms from Schema.org easier as Schema.org currently defines over 2,500 terms. For more information, see SCHEMA_INRUPT_EXT.

  • AS provides convenience objects for the ActivityStreams Vocabulary. For example, the AS.Article is a convenience object that includes the https://www.w3.org/ns/activitystreams#Article IRI.

Although you can use the IRI string instead of the convenience objects, these objects represent many of the ideas and concepts that are useful in Solid itself as well as in Solid applications.

The identifiers RDF.type, SCHEMA_INRUPT_EXT.name, and AS.Article (or their IRI equivalents) impose no restrictions on the data saved for the titles. For example, adding the RDF.type property with value AS.Article to the data entity imposes no conditions about the data properties of the entity. See Vocabularies vs. Data Schemas for more information.

The solid-client library’s functions (such as the various add/set 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.

1

The application specifies the Thing’s name (optional) during its instantiation. A Thing’s URL is its Dataset URL appended with # and the Thing’s name; in this case:

  • ${podURL}/getting-started/readingList/myList#title1,

  • ${podURL}/getting-started/readingList/myList#title2, etc.

Use saveSolidDatasetAt to save the SolidDataset with the titles data to <PodURL>/getting-started/readingList/myList. saveSolidDatasetAt creates any intermediate folders/containers as needed. 2

let savedReadingList = await saveSolidDatasetAt(
  readingListUrl,
  myReadingList,
  { fetch: session.fetch }
);

Upon successful save, saveSolidDatasetAt returns a SolidDataset whose state reflect the data that was sent to be saved.

2

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

Upon save, the application returns the SolidDataset (the reading list) whose state reflect the data that was sent to be saved. The savedReadingList may not accurately reflect the saved state of the data if concurrent operations have modified additional fields.

To retrieve the saved reading list, the tutorial uses getSolidDataset.

// Refetch the Reading List
savedReadingList = await getSolidDataset(
  readingListUrl,
  { fetch: session.fetch } 
);

let items = getThingAll(savedReadingList);

let listcontent="";
for (let i = 0; i < items.length; i++) {
   let item = getStringNoLocale(items[i], SCHEMA_INRUPT_EXT.name);
   if (item != null) {
      listcontent += item + "\n";
   }
}

document.getElementById("savedtitles").value = listcontent; 

The application uses SCHEMA_INRUPT_EXT.name convenience object from the vocab-common-rdf library to specify the property to retrieve.

  • For more information on property identifiers and vocabularies, see Vocabulary.

  • For more information on write operations, see Write Data.

Additional Information

React SDK Availability

To create a sample application using Inrupt’s Solid React SDK, refer to the Solid React SDK documentation.