Getting started with the JavaScript SDK

This guide introduces you to the use of the Carbon LDP™ JavaScript SDK. In it you will find an installation guide and the basic methods needed to start using our platform.

Prerequisite knowledge

To better understand this guide and its code examples, a working knowledge of the following prerequisite topics is required.

Additionally, knowledge on the following topics is highly recommended:

  • TypeScript: Carbon LDP’s JavaScript SDK is written in TypeScript, a JavaScript superset language, maintained by Microsoft. See: TypeScript.
  • Package Managers: Carbon LDP recommends NPM
  • ES2015 Modules: ES2015 modules let you “import” a file from another one so you can modularize your application. See: Modules (Exploring ES6, by Dr. Axel Rauschmayer)
Before starting this guide, please join our mailing list and follow the steps in the quick start guide to install the Carbon Platform and Workbench. If you have already done so, let the fun begin!

Installing the SDK

Installing the SDK depends on what package manager you are using:

Package Manager

If you are not using a package manager you can clone the source code with the following command:

git clone https://github.com/CarbonLDP/CarbonLDP-JS-SDK.git

After cloning the project, and after running npm install and npm start, you’ll find the compiled SDK in /dist/bundles/. An unminified version is available as CarbonLDP.sfx.js, and a minified version as CarbonLDP.sfx.min.js.

npm
npm install carbonldp --save
jspm
jspm install npm:carbonldp
# We also need to install one of carbonldp's dependencies
jspm install npm:jsonld

After it has been installed, the way you’ll use it will depend on your development environment:

Browser - Global Window Object

The SDK comes with a bundled version that you can import directly into your html:

<script src="--sdk-directory--/dist/bundles/CarbonLDP.sfx.js"></script>

Note: You must replace --sdk-directory-- with the path on your system where you’ve placed the bundled version.

After that, a variable named CarbonLDP will be accessible in the global scope. This variable will serve as both a class constructor and a namespace. It’s usage as a class constructor is the same as when using ES2015 Modules, but its namespace nature is unique to the Global approach.

CarbonLDP Variable as a Namespace

The CarbonLDP object chains together all the CarbonLDP modules, so you can access them even if you are using the Global approach. Each time you see in an example something like:

import { NotFoundError } from "carbonldp/HTTP/Errors";

You can take the portion after carbonldp/, replace the /s with . and append that to the CarbonLDP namespace to access the same module. For example, accessing that module using the global namespace would be:

CarbonLDP.HTTP.Errors.NotFoundError
Browser - ES2015 Modules

To be able to use ES2015 modules a transpiler like Babel, Traceur or TypeScript needs to be used. This documentation won’t cover the details of setting one up, but we recommend using NPM as a package manager, configured to use TypeScript as a transpiler. That way you can use ES2015 features and bundle your dependencies together!

Each module in Carbon LDP will export at least one named export and you can either make use of selective imports or import all statements to import them. For example:

// This: ( Selective import )
import { Document, TransientDocument } from "carbonldp/Document";
let myDocument:TransientDocument = Document.$create();

// Is the same as this: ( Import all )
import * as Document from "carbonldp/Document";
let myDocument:Document.TransientDocument = Document.Document.$create();

The semantics behind Carbon LDP’s modules are based on the usage of selective imports statements. We recommend not using import all statements because IDEs will not be able to auto-complete while coding but also because you’ll likely end up with long and repetitive statements, like in the example above.

For each folder inside the CarbonLDP source, there is a module at the same level with the same name that exports everything contained by the folder. Example:

import { Errors, RequestService } from "carbonldp/HTTP";

import * as HTTP from "carbonldp/HTTP";

HTTP.Errors === Errors; // true
HTTP.RequestService === RequestService; // true

Next, you need to decide what language (and what version) you are going to use. The SDK can be used with the following ones:

  • TypeScript: a JavaScript superset language, maintained by Microsoft. See: TypeScript.
  • JavaScript ES2015+: JavaScript with the new features introduced in the ES2015 standard. In order for you to use it, you’ll likely need a transpiler like BabelJS or Traceur
  • JavaScript ES5: The good old JS, no arrow functions, let declarations or any of that magic. To use the SDK with this JavaScript version you’ll need to include the following polyfill before including the SDK:
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.min.js"></script>

Our code examples are written in all of these languages.

If you decide to use Typescript, keep in mind that both the SDK and SPARQLER work with Typescript 2.8 and above.

Creating a CarbonLDP object

To start creating, saving, linking documents, among other things with the SDK, you need to create a CarbonLDP object.

The CarbonLDP object represents the instance of the CarbonLDP platform you will be working with; helping you to communicate directly with your platform. When creating this object you need to specify the domain where your platform lives, and pass it as a parameter of the CarbonLDP object; other settings can be passed as parameters as well. You can view the API documentation for more information.

Our code examples are written in all of these languages.

import { CarbonLDP } from "carbonldp";

// initialize your CarbonLDP object with the domain where your platform lives
let carbonldp = new CarbonLDP( "http://my-carbonldp-instance.com:8083" );
import { CarbonLDP } from "carbonldp";

// initialize your CarbonLDP object with the domain where your platform lives
let carbonldp:CarbonLDP = new CarbonLDP( "http://my-carbonldp-instance.com:8083" );
// initialize your CarbonLDP object with the domain where your platform lives
var carbonldp = new CarbonLDP( "http://my-carbonldp-instance.com:8083" );

Instead of passing a simple string to construct the object, you can also pass a CarbonLDPSettings type of object to configure the CarbonLDP object. This is useful when you want to dynamically configure settings like host, port or SSL.

The properties you can set with the CarbonLDPSettings object are the following:

Let’s see an example using the Settings object to create the CarbonLDP object, but this time also specifying the URL that will be used for your Carbon LDP vocabulary.

import { CarbonLDP } from "carbonldp";

// The settings object
let settings = {
    host: "my-carbon-instance.com", 	// or localhost
    port: 8083,
    ssl: false,
    vocabulary: "http://schema.org/"
};

// initialize your Carbon object with the domain where your platform lives
let carbonldp = new CarbonLDP( settings );
import { CarbonLDP } from "carbonldp";
import { CarbonLDPSettings } from "carbonldp/CarbonLDPSettings";

// The settings object
let settings:CarbonLDPSettings = {
    host: "localhost",	// or localhost
    port: 8083,
    ssl: false,
    vocabulary: "http://schema.org/"
};

// initialize your Carbon object with the domain where your platform lives
let carbonldp = new CarbonLDP( settings );
// The settings object
var settings = {
    host: "my-carbon-instance.com", 	// or localhost
    port: 8083,
    ssl: false,
    vocabulary: "http://schema.org/"
};

// initialize your Carbon object with the domain where your platform lives
var carbonldp = new CarbonLDP( settings );

When specifying the vocabulary option, you must pass a string containing the URL to which a document’s properties will resolve. This means that whenever you store an object containing the property title, to give an example, Carbon by default, will store it as http://localhost:8083/vocabularies/main/#title but in this case, it will be stored as http://schema.org/#title. Notice how the property has the URL we entered for vocabulary, this is due to the way Carbon stores its data.

To know more about how CarbonLDP stores a/an document’s/object’s properties, see the section URI’s as Identifiers of the Object Model page, or visit the vocabularies section of the Object Schema page.

Creating a document

Now that you have your instance, you can start working with your data. Carbon LDP™ stores data in documents, which are basically normal JavaScript objects identified by a URI (Unique Resource Identifier). These documents are composed by properties, fragments and named fragments. For more information see: JavaScript SDK Object model.

Every CarbonLDP instance has a documents endpoint which provides methods to help you to manage your stored documents. To save an object, we have to store it as a document using the $create method of the documents endpoint, like this:

let carbonldp;

// ... initialize your CarbonLDP object

// JavaScript object to be persisted as a CarbonLDP document
let project = {
    name: "Project X",
    tasks: [
        {
            name: "Task 1",
            dueDate: new Date( "2016-04-02" )
        },
        {
            name: "Task 2",
            dueDate: new Date( "2016-04-08" )
        }
    ]
};

carbonldp.documents.$create( "/", project ).then(
    ( projectDocument ) => {
        console.log( project === projectDocument ); // true
        console.log( project.$id ); // document's URI
    }
).catch( error => console.error( error ) );
// ... import your Project interface
import { Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

// JavaScript object to be persisted as a CarbonLDP document
let project:Project = {
    name: "Project X",
    tasks: [
        {
            name: "Task 1",
            dueDate: new Date( "2016-04-02" )
        },
        {
            name: "Task 2",
            dueDate: new Date( "2016-04-08" )
        }
    ]
};

carbonldp.documents.$create( "/", project ).then(
    ( projectDocument:Project & Document ) => {
        console.log( project === projectDocument ); // true
        console.log( project.$id ); // document's URI
    }
).catch( error => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

// JavaScript object to be persisted as a CarbonLDP document
var project = {
    name: "Project X",
    tasks: [
        {
            name: "Task 1",
            dueDate: new Date( "2016-04-02" )
        },
        {
            name: "Task 2",
            dueDate: new Date( "2016-04-08" )
        }
    ]
};

carbonldp.documents.$create( "/", project ).then(
    function( projectDocument ) {
        console.log( project === projectDocument ); // true
        console.log( project.$id ); // document's URI
    }
).catch( function( error ) { console.error( error ); } );

After executing that, the information inside of project will have been persisted in Carbon LDP™. Nested objects will also be saved inside of the document, so pretty much any JSON object can be saved as a Carbon LDP document (as long as the JSON’s information is contained inside of an object, not an array).

Notice how the promise returned by $create is returning a Document object. This type of object represents a saved document in Carbon LDP™. It may or may not have the latest server information. You can make sure that you have its latest changes by using its $refresh method.

If you’ve just executed this, you’ll notice that the document’s $id (or URI) ends with something like 1264563453436/. Carbon LDP™ automatically assigns URIs to newly created documents. If you want to customize the ending portion of the URI, a third parameter can be passed to the $create method. This parameter is called $slug and it serves as a suggestion for Carbon LDP™ when forging the document’s URI:

// ...

carbonldp.documents.$create( "/", project, "My sUp4 Project!" ).then(
    ( projectDocument ) => {
        console.log( projectDocument.$id ); // .../my-sup4-project/
    }
).catch( ( error ) => console.error( error ) );
// ...

carbonldp.documents.$create<Project>( "/", project, "My sUp4 Project!" ).then(
    ( projectDocument:Project & Document ) => {
        console.log( projectDocument.$id ); // .../my-sup4-project/
    }
).catch( ( error ) => console.error( error ) );
// ...

carbonldp.documents.$create( "/", project, "My sUp4 Project!" ).then(
    function( projectDocument ) {
        console.log( projectDocument.$id ); // .../my-sup4-project/
    }
).catch( function( error ) { console.error( error ); } );

In Carbon LDP™, all stored documents are children of another document. In this case we are specifying our newly created document’s parent through the first parameter "/". That string represents a relative URI; one that can be resolved against something else. In this case it is being resolved to the base URI our instance has, which is our domain. To see to what that would resolve you can use a $resolve method:

// ... imports

let carbonldp;

// ... initialize your CarbonLDP object

console.log( carbonldp.$resolve( "/" ) ); // https://your-domain.com/
// ... imports

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

console.log( carbonldp.$resolve( "/" ) ); // https://your-domain.com/
var carbonldp;

// ... initialize your CarbonLDP object

console.log( carbonldp.$resolve( "/" ) ); // https://your-domain.com/

Retrieving a document

Documents can be retrieved by their URIs or by following other documents’ links. In fact, you can paste the $id you just retrieved when persisting your first document in your browser and you should see an XMLrepresentation of it.

Like creating documents, retrieving them can be done using the documents endpoint inside of any Carbon LDP instance. To do so, you can use its $get method:

// ... imports
let carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to retrieve
let projectID = "https://example.com/your-document-id/";

carbonldp.documents.$get( projectID )
    .then( ( project ) => {
        console.log( project.$id ); // "https://example.com/your-document-id/"
        console.log( project.name ); // "Project X"
    } )
    .catch( ( error ) => console.error( error ) );
// ... import your Project interface
import { Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to retrieve
let projectID:string = "https://example.com/your-document-id/";

carbonldp.documents.$get<Project>( projectID )
    .then( ( project:Project & Document ) => {
            console.log( project.$id ); // "https://example.com/your-document-id/"
            console.log( project.name ); // "Project X"
    })
    .catch( ( error ) => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to retrieve
var projectID = "https://example.com/your-document-id/";

carbonldp.documents.$get( projectID )
    .then( function( project ) {
        console.log( project.$id ); // "https://example.com/your-document-id/"
        console.log( project.name ); // "Project X"
    } )
    .catch( function( error ) { console.error( error ); } );

Shallow Documents

In the example from above we used the ID of the document to retrieve it, but there are times that instead of having the ID, you’ll have a partial or shallow Document object. This can happen for example when a document points to another document through a property.

Shallow documents are unresolved Documents that have no other content than the $id of the Document they represent plus all the methods provided by the Documentclass. Think of shallow documents as Documents that haven’t been resolved because they don’t have all their properties. To retrieve those missing properties, we have to resolve the unresolved Document by using the $resolve method of the Documentclass.

For example, imagine that a Project has a property called responsible which links to the Employee in charge of that Project.

{
    $id: "https://example.com/projects/my-project/",
    $slug: "my-project"
    responsible: "https://example.com/employees/employee-x/";
}
The Task linking an Employee through the responsible property
{
    $id: "https://example.com/employees/employee-x/",
    $slug: "employee-x"
    firstName: "John",
    lastName: "Smith"
};
The Employee linked by the Project object

When retrieving the task, the value of the responsible property will be a shallow document with no other content than the $id of the linked document. The reason for omitting the linked document’s other properties is to avoid calls that may end up creating more calls because the linked object may also have linked objects on it and so on…

To retrieve a shallow document use the $resolve method, like this:

// ... imports
let carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to retrieve
let projectID = "https://example.com/projects/my-project/";

carbonldp.documents.$get( projectID )
    .then( ( project ) => {
        console.log( project.$id ); // "https://example.com/projects/my-project/"
        console.log( project.$slug ); // "my-project"
        console.log( project.responsible.$isResolved() ); // false
        console.log( responsible.firstName ); // undefined

        return project.$resolve();
    } )
    .then( ( responsible ) => {
        console.log( responsible.$id ); // "https://example.com/employees/employee-x/"
        console.log( responsible.$slug ); // "employee-x"
        console.log( responsible.$isResolved() ); // true
        console.log( responsible.firstName ); // John
    } )
    .catch( ( error ) => console.error( error ) );
// ... additional imports
import { Document } from "carbonldp/Document";

let carbonldp;

// ... initialize your CarbonLDP object

let projectID:string = "https://example.com/projects/my-project/";

carbonldp.documents.$get( projectID )
    .then( ( project:Project & Document ) => {
        console.log( project.$id ); // "https://example.com/projects/my-project/"
        console.log( project.$slug ); // "my-project"
        console.log( project.responsible.$isResolved() ); // false
        console.log( responsible.firstName ); // undefined

        return project.$resolve();
    } )
    .then( ( responsible:Employee & Document ) => {
        console.log( responsible.$id ); // "https://example.com/employees/employee-x/"
        console.log( responsible.$slug ); // "employee-x"
        console.log( responsible.$isResolved() ); // true
        console.log( responsible.firstName ); // John
    } )
    .catch( ( error ) => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to retrieve
var projectID = "https://example.com/projects/my-project/";

carbonldp.documents.$get( projectID )
    .then( function( project ) {
        console.log( project.$id ); // "https://example.com/projects/my-project/"
        console.log( project.$slug ); // "my-project"
        console.log( project.responsible.$isResolved() ); // false
        console.log( responsible.firstName ); // undefined

        return project.$resolve();
    } )
    .then( function( responsible ) {
        console.log( responsible.$id ); // "https://example.com/employees/employee-x/"
        console.log( responsible.$slug ); // "employee-x"
        console.log( responsible.$isResolved() ); // true
        console.log( responsible.firstName ); // John
    } )
    .catch( ( error ) => console.error( error ) );

As you could see, Shallow Documents have two states, resolved and unresolved. When a document isn’t resolved, it doesn’t contain the document’s information but it can be used to link other documents to it as we’ll see later.

Pointers

There are times that documents point to a resource located outside of your platform (external resource) instead to other documents living in it (internal resource). This can happen when a document points to an external resource through a property. When retrieving such type of documents the object’s property will contain a Pointer object.

Pointer objects were designed just to contain the $id property which has the URI of the external resource. Unlike shallow documents, Pointers don’t provide functionality aside of containing the URI of the external resource.

Dollar sign properties ($)

Dollar sign ($) properties are created and managed by the SDK; their purpose is to avoid name collisions when users save new properties. The SDK achieves this by prepending a dollar sign ($) to its reserved system properties.

An example of a dollar sign property preventing a name collision is a document’s ID. An ID is a very common property found throughout applications. However, the SDK returns object’s URIs using an id property of its own. Without the dollar sign prepending the property’s name, users wouldn’t be able to declare their own id’s within their object’s implementations.

Therefore, the SDK will prepend a dollar sign to the property storing a document’s id, turning it into $id. As a result, a user could make use of the .$id and the .id properties, and both of them will serve their own purpose.

Some dollar sign $ properties provided by the SDK are:

  • $id: URI of the document.
  • $slug: Holds the last part of the URI the document is identified with. For example the URI: https://example.com/resource-1/ would have as a slug resource-1. This property can be useful when used as a relative URI; for example when retrieving a child Document from a parent.

Bear in mind that the SDK includes more dollar sign properties, you will find other examples when working with the SDK.

Properties with underscores

In JavaScript there’s a common practice followed by developers in which properties starting with underscores (_) are considered exclusive for the library or the class they belong to.

The SDK follows this practice by including some variables starting with an underscore. Please, do not use them since we cannot guarantee their compatibility in newer versions of the SDK.

Modifying and saving a document

All documents in CarbonLDP can be modified. However, all the changes made to a document are done in memory. To persist the modifications, the document needs to be saved.

To modify a document you should:

  1. Retrieve the document:
    1. $get using the document id as described in retrieving a document
  2. Modify the document: Add, modify or delete properties, fragments, named fragments.
  3. Save the changes through document.$save method.

Document properties can be added/modified/removed as with any normal JavaScript object.

// ... imports

let project;

// ID of the Carbon LDP document you want to retrieve
let projectID = "https://example.com/your-document-id/";

// 1) Retrieve the document
carbonldp.documents.$get( projectID )
    .then( ( retrievedProject ) => {

        // 2) Modify the document
        retrievedProject.name = "Project's New Name";
        retrievedProject.tasks.push( {
            name: "Task 3",
            dueDate: new Date( "2016-05-02" )
        } );
        retrievedProject.description = {
            type: "markdown",
            content: "# Some description here"
        };
        project = retrievedProject;

        // 3) Save the changes
        return retrievedProject.$save();
    } )
    .then( ( savedProject ) => {

        console.log( savedProject === project ); // true
        return savedProject.$refresh();
    })
    .then( ( refreshedProject ) => {

        console.log( refreshedProject === project ); // true
        // Continue doing stuff...
    })
    .catch( ( error ) => console.error( error ) );
// ... import your Project interface
import { Document } from "carbonldp/Document";

let project:Project & Document;

// ID of the Carbon LDP document you want to retrieve
let projectID:string = "https://example.com/your-document-id/";

// 1) Retrieve the document
carbonldp.documents.$get<Project>( projectID )
    .then( ( retrievedProject:Project & Document ) => {

        // 2) Modify the document
        retrievedProject.name = "Project's New Name";
        retrievedProject.tasks.push( {
            name: "Task 3",
            dueDate: new Date( "2016-05-02" )
        } );
        retrievedProject.description = {
            type: "markdown",
            content: "# Some description here"
        };
        project = retrievedProject;

        // 3) Save the changes
        return retrievedProject.$save();
    } )
    .then( ( savedProject:Document ) => {

        console.log( savedProject === project ); // true
        return savedProject.$refresh();
    })
    .then( ( refreshedProject:Document ) => {

        console.log( refreshedProject === project ); // true
        // Continue doing stuff...
    })
    .catch( ( error ) => console.error( error )
var project;

// ID of the Carbon LDP document you want to retrieve
var projectID = "https://example.com/your-document-id/";

// 1) Retrieve the document
carbonldp.documents.$get( projectID )
    .then( function( retrievedProject ) {

        // 2) Modify the document
        retrievedProject.name = "Project's New Name";
        retrievedProject.tasks.push( {
            name: "Task 3",
            dueDate: new Date( "2016-05-02" )
        } );
        retrievedProject.description = {
            type: "markdown",
            content: "# Some description here"
        };
        project = retrievedProject;

        // 3) Save the changes
        return retrievedProject.$save();
    } )
    .then( function( savedProject ) {

        console.log( savedProject === project ); // true
        return savedProject.$refresh();
    })
    .then( function( refreshedProject ) {

        console.log( refreshedProject === project ); // true
        // Continue doing stuff...
    })
    .catch( function( error ) { console.error( error ); } );

Notice that we used the $save method and immediately after, we called the $refreshmethod. Every document has this method and it helps you sync the in memory representation of the document with the server’s information. In this case we used it because after the document was saved, Carbon LDP™ could have added or modified some of its properties (noticeably properties like modified, created, among others…).

This is a very common thing to do, therefore persisted documents also have the $saveAndRefresh method which does both things at once.

Deleting a document

As when creating and retrieving documents, the documents endpoint can help you delete a persisted document through its $delete method. To do so, you need to provide the document’s URI:

// ... imports

let carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to delete
let projectID = "https://example.com/your-document-id/";

carbonldp.documents.$delete( projectID )
    .then( () => {
        // continue doing stuff...
    } )
    .catch( ( error ) => console.error( error ) );
// ... additional imports

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to delete
let projectID:string = "https://example.com/your-document-id/";

carbonldp.documents.$delete( projectID )
    .then( () => {
        // continue doing stuff...
    } )
    .catch( ( error ) => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

// ID of the Carbon LDP document you want to delete
var projectID = "https://example.com/your-document-id/";

carbonldp.documents.$delete( projectID )
    .then( () => {
        // continue doing stuff...
    } )
    .catch( function( error ) { console.error( error ); } );

Like in “Retrieving a document”, a document can also be deleted through the Document‘s $delete method:

// ... imports

let project;

// persisted document retrieval

project.$delete().then(
    () => {
        // continue doing stuff...
    }
).catch( error => console.error );
// ... import your Project interface
import { Document } from "carbonldp/Document";

let project:Project & Document;

// persisted document retrieval

project.$delete().then(
    () => {
        // continue doing stuff...
    }
).catch( error => console.error );
var project;

// persisted document retrieval

project.$delete().then(
    function() {
        // continue doing stuff...
    }
).catch( error => console.error );

In fact, this pattern can be seen across several methods, where both the documentsendpoint and Document expose similar methods. For example, the $create method of the documents endpoint is also exposed by a Document object and allows you to create a direct child of that document. Basically, if you don’t have a resolved reference of the document, use documents endpoint. But if you already have one, you can call the action through the Document directly (which may make code more readable, and in some cases more efficient).

Linking documents

One of the core features of Carbon LDP™, is linking data (after all, it’s a Linked Data Platform). Documents can be linked to other documents through normal JavaScript properties:

// ... imports

let project1;
let project2;

// ... persisted documents retrieval

project1.relatedProject = project2;
// ... import your Project interface
import { Document } from "carbonldp/Document";

let project1:Project & Document;
let project2:Project & Document;

// ... persisted documents retrieval

project1.relatedProject = project2;
var project1;
var project2;

// ... persisted documents retrieval

project1.relatedProject = project2;

There are also special properties that Carbon LDP™ handles for you. The most important ones are contains, which represents the document parent-child relation, and memberswhich represents membership relation.

Parent-child relation

Like we said before, every document that is saved in Carbon LDP™ must have a parent. When a document is created, it’s automatically added to the array contained in its parent’s contains property. The SDK gives you convenient methods to handle this array, such as:

  • documents.$listChildren: Returns a list of shallow Document objects for each of the children documents
  • documents.$getChildren: Returns a list of Document objects for each of the document children
// ... imports

let carbonldp

// ... initialize your CarbonLDP object

carbonldp.documents.$listChildren( "projects/" ).then(
    ( shallowProjects ) => {
        console.log( shallowProjects ); // array of shallow documents
    }
);

carbonldp.documents.$getChildren( "projects/" ).then(
    ( projects ) => {
        console.log( projects ); // array of documents
    }
);
// ... import your Project interface

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$listChildren<Project>( "projects/" ).then(
    ( shallowProjects:( Project & Document )[] ) => {
        console.log( shallowProjects ); // array of shallow documents
    }
);

carbonldp.documents.$getChildren<Project>( "projects/" ).then(
    ( projects:( Project & Document )[] ) => {
        console.log( projects ); // array of documents
    }
);
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$listChildren( "projects/" ).then(
    function( shallowProjects ) {
        console.log( shallowProjects ); // array of shallow documents
    }
);

carbonldp.documents.$getChildren( "projects/" ).then(
    function( projects ) {
        console.log( projects ); // array of documents
    }
);

Like $create and $delete, $listChildren and $getChildren are also accessible through a Document.
See, there’s a pattern!

The last example could show you weird results. The documentsendpoint caches shallow documents, so listing children of a document while at the same time retrieving them will return you the same objects.

It’s worth mentioning that parent-child relations are considered hard links. They are directly related to the structure of the data. Meaning, if you delete the relation, the child gets deleted too. By consequence, if you delete a parent, all of its children will also be deleted (and the children of its children, and so on).

Membership Relation

Because parent-child relations are hard links, it is not recommended to use them in your app logic. Instead, Carbon LDP™ helps you manage another type of relation, considered as a soft link: membership relations.

The meaning of a membership relation is up to your application logic to define it. It can mean things like, a project’s list of tasks, a person’s friends, a person’s assets, etc. You must be thinking “well I can already do that with normal JavaScript properties, what are the advantages of treating them as ‘membership relations’?”. It’s true that you can manually manage lists, but to do so, you would need to manage the whole document. By configuring a property to act as a “membership relation”, you enable things like retrieving the list without retrieving the complete document, adding new elements without retrieving the list, etc.

All Carbon LDP™ documents can maintain a list of members. The relation between the document and its members can be configured, through the properties:

  • hasMemberRelation: Configures the property that will hold the array of members (by default members)
  • isMemberOfRelation: Configures the property that each member will have, that links back to the document they are member of (none by default)

These properties are set in the document that will hold the members array. Members can be added, removed, listed or retrieved through the following methods in the documents endpoint (and the document itself too):

  • $addMember/$addMembers: Adds the provided shallow documents as members of the document
  • $listMembers: Like $listChildren, returns an array of shallow Document objects for each member
  • $getMembers: Like $getChildren, returns an array of complete Document objects for each member
  • $removeMember/$removeMembers: Removes the specified members (without deleting them)
    NOTICE: When using $removeMembers without passing an array of members, all the members will be removed.
// ... imports

let carbonldp;

// ... initialize your CarbonLDP object

let project = {
    name: "Important project",

    hasMemberRelation: "tasks",
    isMemberOfRelation: "project"
};

let task1 = { name: "Task 1" };
let task2 = { name: "Task 2" };
let task3 = { name: "Task 3" };

// ... get the containers of projects and tasks
let getContainers = Promise.all( [
    carbonldp.documents.$get( "tasks/" ),
    carbonldp.documents.$get( "projects/" )
] );

getContainers
    .then( ( [ tasks, projects ] ) => {

        // Persist the project and the tasks into their respective containers
        return Promise.all( [
            projects.$create( project, "project-1" ),
            tasks.$create( task1 ),
            tasks.$create( task2 ),
            tasks.$create( task3 )
        ] );
    } )
    .then( ( [ projectDocument, task1Document, task2Document, task3Document, ] ) => {

        console.log( project === projectDocument ); // true
        console.log( task1 === task1Document ); // true
        console.log( task2 === task2Document ); // true
        console.log( task3 === task3Document ); // true

        return project.$addMembers( [ task1, task2, task3 ] );
    } )
    .then( () => {
        return project.$resolve();
    } )
    .then( ( project ) => {

        console.log( project.tasks ); // Documents for task1, task2 and task3. Added because of the configured hasMemberRelation
        console.log( project.tasks.indexOf( task1 ) !== -1 ); // true (remember, shallow documents are cached and reused)
        console.log( project.tasks.indexOf( task2 ) !== -1 ); // true
        console.log( project.tasks.indexOf( task3 ) !== -1 ); // true

        return project.$listMembers();
    } )
    .then( ( shallowTasks ) => {

        console.log( shallowTasks ); // Documents of task1, task2 and task3

        return project.$removeMember( task2 );
    } )
    .then( () => {

        console.log( "Task 2 is no longer a member of the project" );

        return project.$getMembers();
    } )
    .then( ( resolvedTasks ) => {

        console.log( resolvedTasks ); // task1 and task3 documents
        console.log( resolvedTasks[ 0 ].project ); // Document of the project. Added due to the configured isMemberOfRelation
    } )
    .catch( error => console.error( error ) );
// ... import your Project and Task interfaces
import { BaseDocument, Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

let project:Project & BaseDocument = {
    name: "Important project",

    hasMemberRelation: "tasks",
    isMemberOfRelation: "project"
};

let task1:Task & BaseDocument = {name: "Task 1"};
let task2:Task & BaseDocument = {name: "Task 2"};
let task3:Task & BaseDocument = {name: "Task 3"};

let storedProject:Project & Document;	// Unlike JavaScript, Typescript does not mutate its objects, meaning that we
let storedTask2:Task & Document;		// have to make use of helping objects to use them in the thenables chain

// ... get the containers of projects and tasks
let getContainers = Promise.all( [
    carbonldp.documents.$get( "tasks/" ),
    carbonldp.documents.$get( "projects/" )
] );

getContainers
    .then( ( [ tasks, projects ]:[ Document, Document ] ) => {

        // Persist the project and the tasks into their respective containers
        return Promise.all( [
            projects.$create( project, "project-1" ),
            tasks.$create( task1 ),
            tasks.$create( task2 ),
            tasks.$create( task3 )
        ] );
    } )
    .then( ( [ projectDocument, task1Document, task2Document, task3Document ]:[ Project & Document, Task & Document, Task & Document, Task & Document ] ) => {

        storedProject= projectDocument;	// Assigning the variables we are going to use in the thenables chain
        storedTask2 = task2Document;

        console.log( project === projectDocument ); // true
        console.log( task1 === task1Document ); // true
        console.log( task2 === task2Document ); // true
        console.log( task3 === task3Document ); // true

        return projectDocument.$addMembers( [ task1Document, task2Document, task3Document ] );
    } )
    .then( () => {

        console.log( storedProject.tasks ); // Documents for task1, task2 and task3. Added because of the configured hasMemberRelation
        console.log( storedProject.tasks.indexOf( task1 ) !== - 1 ); // true (remember, shallow documents are cached and reused)
        console.log( storedProject.tasks.indexOf( task2 ) !== - 1 ); // true
        console.log( storedProject.tasks.indexOf( task3 ) !== - 1 ); // true

        return storedProject.$listMembers();
    } )
    .then( ( shallowTasks:Task & Document[] ) => {

        console.log( shallowTasks ); // Shallow Documents for task1, task2 and task3

        return storedProject.$removeMember( storedTask2 );
    } )
    .then( () => {

        console.log( "Task 2 is no longer a member of the project" );

        return storedProject.$getMembers();
    } )
    .then( ( resolvedTasks:Task & Document[] ) => {

        console.log( resolvedTasks ); // task1 and task3 documents
        console.log( resolvedTasks[ 0 ].project ); // Documents of the project. Added due to the configured isMemberOfRelation
    } )
    .catch( error => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

var project = {
    name: "Important project",

    hasMemberRelation : "tasks",
    isMemberOfRelation: "project"
};

var task1 = { name: "Task 1" };
var task2 = { name: "Task 2" };
var task3 = { name: "Task 3" };

// ... get the containers of projects and tasks
var getContainers = Promise.all( [
    carbonldp.documents.$get( "tasks/" ),
    carbonldp.documents.$get( "projects/" )
] );

getContainers
    .then( function( promisesResults ) {

        var tasks = promisesResults[ 0 ];
        var projects = promisesResults[ 1 ];

        // Persist the project and the tasks into their respective containers
        return Promise.all( [
            projects.$create( project, "project-1" ),
            tasks.$create( task1 ),
            tasks.$create( task2 ),
            tasks.$create( task3 )
        ] );
    } )
    .then( function( promisesResults ) {

        var projectDocument = promisesResults[ 0 ];
        var task1Document = promisesResults[ 1 ];
        var task2Document = promisesResults[ 2 ];
        var task3Document = promisesResults[ 3 ];


        console.log( project === projectDocument ); // true
        console.log( task1 === task1Document ); // true
        console.log( task2 === task2Document ); // true
        console.log( task3 === task3Document ); // true

        return project.$addMembers( [ task1, task2, task3 ] );
    } )
    .then( function() {
        return project.$resolve();
    } )
    .then( function( project ) {

        console.log( project.tasks ); // Documents for task1, task2 and task3. Added because of the configured hasMemberRelation
        console.log( project.tasks.indexOf( task1 ) !== - 1 ); // true (remember, shallow documents are cached and reused)
        console.log( project.tasks.indexOf( task2 ) !== - 1 ); // true
        console.log( project.tasks.indexOf( task3 ) !== - 1 ); // true

        return project.$listMembers();
    } )
    .then( function( shallowTasks ) {

        console.log( shallowTasks ); // Shallow Documents for task1, task2 and task3

        return project.$removeMember( task2 );
    } )
    .then( function() {

        console.log( "Task 2 is no longer a member of the project" );

        return project.$getMembers();
    } )
    .then( function( resolvedTasks ) {

        console.log( resolvedTasks ); // task1 and task3 documents
        console.log( resolvedTasks[ 0 ].project ); // Shallow document of the project. Added due to the configured isMemberOfRelation
    } )
    .catch( function( error ) { console.error( error ); } );
More on $listMembers(), $getMembers(), $listChildren() and $getChildren() can be found here:

Reading documents

Creating an access point

Membership relations have a limitation though, a document can only maintain one list through one property. So what happens when you want to manage members of another property? For example, what happens if you want to manage a list of people that are members of the project? Well, this can be accomplished using access points.

Access points are special documents whose purpose is to maintain a list of members of another document. Any document can have any number of access points, removing the limitation we talked about.

Like normal documents, access points can be customized by specifying a hasMemberRelation and an isMemberOfRelation. To create an access point for a document, you have to use the create method from the AccessPoint class to create an in-memory AccessPoint object. Once you have that object, you can pass it to the $create method of the documents endpoint or of a Document object to persist it.

// ... imports
import { AccessPoint } from "carbonldp/AccessPoint";

let project;
let person1;
let person2;

// Creating an in-memory access point that we will persist later
let projectMembersAccessPoint = AccessPoint.create( {
    hasMemberRelation: "people", // property used to store the member list in the accessPoint's document
    isMemberOfRelation: "projects" // property used to link back members to the document they are members of
} );

// ... project, person1 and person2 document retrieval
let getPeopleAndProject = Promise.all( [
    carbonldp.documents.$get( "projects/project-1/" ),
    carbonldp.documents.$get( "people/person-1/" ),
    carbonldp.documents.$get( "people/person-2/" )
] );

getPeopleAndProject
    .then( ( [ projectDocument, person1Document, person2Document ] ) => {
        project = projectDocument;
        person1 = person1Document;
        person2 = person2Document;

        return project.$create( projectMembersAccessPoint, "people" )
    } )
    .then( ( _projectMembersAccessPoint ) => {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        return projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        return project.$refresh();
    } )
    .then( ( refreshedProject ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Documents of person1 and person2

        return person1.$refresh();
    } )
    .then( ( refreshedPerson1 ) => {
            console.log( person1 === refreshedPerson1 ); // true
            console.log( refreshedPerson1.projects ); // Document of project-1
        }
    ).catch( error => console.error( error ) );
// ... import your Project and Person interfaces
import { Document } from "carbonldp/Document";
import { AccessPoint, TransientAccessPoint } from "carbonldp/AccessPoint";

let project:Project & Document;
let person1:Person & Document;
let person2:Person & Document;

// Creating an in-memory access point that we will persist later
let projectMembersAccessPoint:TransientAccessPoint = AccessPoint.create( {
    hasMemberRelation: "people", // property used to store the member list in the accessPoint's document
    isMemberOfRelation: "projects" // property used to link back members to the document they are members of
} );

// ... project, person1 and person2 document retrieval
let getPeopleAndProject = Promise.all( [
    carbonldp.documents.$get( "projects/project-1/" ),
    carbonldp.documents.$get( "people/person-1/" ),
    carbonldp.documents.$get( "people/person-2/" )
] );

getPeopleAndProject
    .then( ( [ projectDocument, person1Document, person2Document ]:[ Project & Document, Person & Document, Person & Document ] ) => {

        project = projectDocument;
        person1 = person1Document;
        person2 = person2Document;

        return project.$create( projectMembersAccessPoint, "people" )
    } )
    .then( ( _projectMembersAccessPoint:AccessPoint ) => {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        return projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        return project.$refresh();
    } )
    .then( ( refreshedProject:Project & Document ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Documents of person1 and person2

        return person1.$refresh();
    } )
    .then( ( refreshedPerson1:Person & Document ) => {
            console.log( person1 === refreshedPerson1 ); // true
            console.log( refreshedPerson1.projects ); // Document of project-1
        }
    ).catch( error => console.error( error ) );
var project;
var person1;
var person2;

// Creating an in-memory access point that we will persist later
var projectMembersAccessPoint = CarbonLDP.AccessPoint.create( {
    hasMemberRelation: "people", // property used to store the member list in the accessPoint's document
    isMemberOfRelation: "projects" // property used to link back members to the document they are members of
} );

// ... project, person1 and person2 document retrieval
var getPeopleAndProject = Promise.all( [
    carbonldp.documents.$get( "projects/project-1/" ),
    carbonldp.documents.$get( "people/person-1/" ),
    carbonldp.documents.$get( "people/person-2/" )
] );

getPeopleAndProject
    .then( function( promisesResults ) {

        var projectDocument, = promisesResults[ 0 ];
        var person1Document, = promisesResults[ 1 ];
        var person2Document = promisesResults[ 2 ];

        project = projectDocument;
        person1 = person1Document;
        person2 = person2Document;

        return project.$create( projectMembersAccessPoint, "people" )
    } )
    .then( function( _projectMembersAccessPoint ) {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        return projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( function() {
        return project.$refresh();
    } )
    .then( function( refreshedProject ) {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Documents of person1 and person2

        return person1.$refresh();
    } )
    .then( function( refreshedPerson1 ) {
            console.log( person1 === refreshedPerson1 ); // true
            console.log( refreshedPerson1.projects ); // Document of project-1
        }
    ).catch( error => console.error( error ) );

Putting it all together

Here’s an example of all the actions we talked about, joined together into a single script:

import { CarbonLDP } from "carbonldp";
import { AccessPoint } from "carbonldp/AccessPoint";

let project = {
    name: "Project X"
};
let projectDocument;
let tasksAccessPoint;
let task1Document;
let task2Document;

let carbonldp = new CarbonLDP( "http://localhost:8083" );


carbonldp.documents
    .$create( "/", project )
    .then( ( _projectDocument ) => {
        projectDocument = _projectDocument;

        console.log( project === projectDocument ); // true
        console.log( projectDocument.$id ); // document's URI
        console.log( "created" in projectDocument ); // false - The project was persisted but it's latest state hasn't been retrieved from the server

        // Read the just created document
        return carbonldp.documents.$get( projectDocument.$id ); // Can also be `return projectDocument.$refresh();`
    } )
    .then( ( projectDocument ) => {
        console.log( projectDocument === projectDocument ); // true - The same object was updated with its latest properties
        console.log( projectDocument.created ); // Creation date

        projectDocument.description = {
            type   : "markdown",
            content: "# Some description here"
        };

        // Save the latest state to the server
        return projectDocument.$save(); // Can also be `carbonldp.documents.$save( projectDocument );`
    } )
    .then( ( projectDocument ) => {
        console.log( project === projectDocument ); // true

        // Creating an access point for the project to manage tasks
        let accessPoint = AccessPoint.create({
            hasMemberRelation: "tasks"
        })

        return projectDocument.$create( accessPoint, "tasks" );
    } )
    .then( ( _tasksAccessPoint ) => {
        tasksAccessPoint = _tasksAccessPoint;

        // Creating a task so we can add it to the project later on
        return carbonldp.documents.$create( "/", {
            name   : "Do something",
            dueDate: new Date()
        } );
    } )
    .then( ( _task1Document ) => {
        task1Document = _task1Document;

        // Creating another task so we can add it to the project later on
        return carbonldp.documents.$create( "/", {
            name   : "Do something else",
            dueDate: new Date()
        } );
    } )
    .then( ( _task2Document ) => {
        task2Document = _task2Document;

        // Adding them to the project through the AccessPoint
        return tasksAccessPoint.$addMembers( [ task1Document, task2Document ] );
    } )
    .then( () => {

        // Refreshing the project so we can check if the tasks were added
        return projectDocument.$refresh();
    } )
    .then( ( _projectDocument ) => {
        projectDocument = _projectDocument;

        console.log( project === _projectDocument ); // true - Still the same object
        console.log( "tasks" in _projectDocument ); // true
        console.log( _projectDocument.tasks.indexOf( task1Document ) !== - 1 ); // true - There's the first task
        console.log( _projectDocument.tasks.indexOf( task2Document ) !== - 1 ); // true - There's the other task

        // Deleting everything
        let promises = [];
        promises.push( projectDocument.$delete() );
        promises.push( task1Document.$delete() );
        promises.push( task2Document.$delete() );

        return Promise.all( promises );
    } )
    .then( () => {

        console.log( "All done!" );
    } )
    .catch( error => console.error( error ) );
import { CarbonLDP } from "carbonldp";
import { AccessPoint, TransientAccessPoint } from "carbonldp/AccessPoint";
import { Document } from "carbonldp/Document";

interface Task {
    name:string;
    dueDate:Date;
}

interface Project {
    name:string;
    description?:{
        type:string;
        content:string;
    };
    tasks?:Task[];  // If there are no tasks this property won't exist
}

let project:Project = {
    name: "Project X"
};
let projectDocument:Project & Document;
let tasksAccessPoint:AccessPoint;
let task1Document:Task & Document;
let task2Document:Task & Document;

let carbonldp:CarbonLDP = new CarbonLDP( "http://localhost:8083" );


carbonldp.documents
    .$create( "/", project )
    .then( ( savedProjectDocument:Project & Document ) => {
        projectDocument = savedProjectDocument;

        console.log( project === projectDocument ); // true
        console.log( projectDocument.$id ); // document's URI
        console.log( "created" in projectDocument ); // false - The project was persisted but it's latest state hasn't been retrieved from the server

        // Read the just created document
        return carbonldp.documents.$get<Project>( projectDocument.$id ); // Can also be `return persistedProject.$refresh();`
    } )
    .then( ( projectDocument:Project & Document ) => {
        console.log( project === projectDocument ); // true - The same object was updated with its latest properties
        console.log( projectDocument.created ); // Creation date

        projectDocument.description = {
            type: "markdown",
            content: "# Some description here"
        };

        // Save the latest state to the server
        return projectDocument.$save<Project>(); // Can also be `carbonldp.documents.$save<Project>( projectDocument );`
    } )
    .then( ( projectDocument:Project & Document ) => {
        console.log( project === projectDocument ); // true

        // Creating an access point for the project to manage tasks
        let accessPoint:TransientAccessPoint = AccessPoint.create({
            hasMemberRelation: "tasks"
        })

        return projectDocument.$create( accessPoint, "tasks" );
    } )
    .then( ( _tasksAccessPoint:AccessPoint ) => {
        tasksAccessPoint = _tasksAccessPoint;

        // Creating a task so we can add it to the project later on
        return carbonldp.documents.$create<Task>( "/", {
            name: "Do something",
            dueDate: new Date()
        } );
    } )
    .then( ( _task1Document:Task & Document ) => {
        task1Document = _task1Document;

        // Creating another task so we can add it to the project later on
        return carbonldp.documents.$create<Task>( "/", {
            name: "Do something else",
            dueDate: new Date()
        } );
    } )
    .then( ( _task2Document:Task & Document ) => {
        task2Document = _task2Document;

        // Adding them to the project through the AccessPoint
        return tasksAccessPoint.$addMembers( [ task1Document, task2Document ] );
    } )
    .then( () => {

        // Refreshing the project so we can check if the tasks were added
        return projectDocument.$refresh<Project>();
    } )
    .then( ( projectDocument:Project & Document ) => {
        console.log( project === projectDocument ); // true - Still the same object
        console.log( "tasks" in projectDocument ); // true
        console.log( projectDocument.tasks.indexOf( task1Document ) !== - 1 ); // true - There's the first task
        console.log( projectDocument.tasks.indexOf( task2Document ) !== - 1 ); // true - There's the other task

        // Deleting everything
        let promises:Promise<void>[] = [];
        promises.push( projectDocument.$delete() );
        promises.push( task1Document.$delete() );
        promises.push( task2Document.$delete() );

        return Promise.all( promises );
    } )
    .then( () => {

        console.log( "All done!" );
    } )
    .catch( error => console.error( error ) );
var project = {
    name: "Project X"
};
var projectDocument;
var tasksAccessPoint;
var task1Document;
var task2Document;

var carbonldp = new CarbonLDP( "http://localhost:8083" );


carbonldp.documents
    .$create( "/", project )
    .then( function( _projectDocument ) {
        projectDocument = _projectDocument;

        console.log( project === projectDocument ); // true
        console.log( projectDocument.$id ); // document's URI
        console.log( "created" in projectDocument ); // false - The project was persisted but it's latest state hasn't been retrieved from the server

        // Read the just created document
        return carbonldp.documents.$get( projectDocument.$id ); // Can also be `return projectDocument.$refresh();`
    } )
    .then( function( projectDocument ) {
        console.log( projectDocument === projectDocument ); // true - The same object was updated with its latest properties
        console.log( projectDocument.created ); // Creation date

        projectDocument.description = {
            type   : "markdown",
            content: "# Some description here"
        };

        // Save the latest state to the server
        return projectDocument.$save(); // Can also be `carbonldp.documents.$save( projectDocument );`
    } )
    .then( function( projectDocument ) {
        console.log( project === projectDocument ); // true

        // Creating an access point for the project to manage tasks
        var accessPoint = AccessPoint.create({
            hasMemberRelation: "tasks"
        })

        return projectDocument.$create( accessPoint, "tasks" );
    } )
    .then( function( _tasksAccessPoint ) {
        tasksAccessPoint = _tasksAccessPoint;

        // Creating a task so we can add it to the project later on
        return carbonldp.documents.$create( "/", {
            name   : "Do something",
            dueDate: new Date()
        } );
    } )
    .then( function( _task1Document ) {
        task1Document = _task1Document;

        // Creating another task so we can add it to the project later on
        return carbonldp.documents.$create( "/", {
            name   : "Do something else",
            dueDate: new Date()
        } );
    } )
    .then( function( _task2Document ) {
        task2Document = _task2Document;

        // Adding them to the project through the AccessPoint
        return tasksAccessPoint.$addMembers( [ task1Document, task2Document ] );
    } )
    .then( function() {

        // Refreshing the project so we can check if the tasks were added
        return projectDocument.$refresh();
    } )
    .then( function( _projectDocument ) {
        projectDocument = _projectDocument;

        console.log( project === _projectDocument ); // true - Still the same object
        console.log( "tasks" in _projectDocument ); // true
        console.log( _projectDocument.tasks.indexOf( task1Document ) !== - 1 ); // true - There's the first task
        console.log( _projectDocument.tasks.indexOf( task2Document ) !== - 1 ); // true - There's the other task

        // Deleting everything
        var promises = [];
        promises.push( projectDocument.$delete() );
        promises.push( task1Document.$delete() );
        promises.push( task2Document.$delete() );

        return Promise.all( promises );
    } )
    .then( function() {

        console.log( "All done!" );
    } )
    .catch( function( error ) { console.error( error ); } );

This was a short introduction to what the SDK and Carbon LDP™ can do. You should be able to develop simple applications after reading this document, but to really master the inner works of the SDK you can continue reading about the Object Model.