Object model

Documents

The main model object handled by Carbon LDP™ is called a Document.

A Document begins as a normal JavaScript object containing any number of data attributes that you care to define (key/value pairs). Typically, a Document represents a data entity of a given type (e.g. Person, Project, Product, Invoice). It can have either literal data-type properties (e.g. string, date, number) or pointers that link to other resources.

When a JavaScript object is saved to Carbon, it is given a URI that uniquely identifies it on a network or the web. That URI can then later be used to retrieve the document or to query for specific data attributes within it. Queries can also be used to find data across multiple documents.

URIs as identifiers

Using a numerical value as an ID is prone to collisions. Using a UUID solves the collision problem but makes the ID relative to the application (making the ID hard to resolve without knowing where it came from). That’s why Carbon uses Unique Resource Identifiers (or URIs).

URIs are strings that identify resources in a network. Any URL you can think of is also considered a URI, but not all URIs are URLs. The main difference is that a URL can change the resource it is pointing at, while URIs can also belong to a resource forever (in which case they would be URNs (Unique Resource Names).

Basically, URI = URL + URN

URIs follow a scheme. Carbon’s document URIs, follow the http or https scheme (depending on the platform’s configuration).

Using URIs as identifiers allows Carbon documents to be globally unique, resolvable and
relative to the network.

Properties

Like any JavaScript object, Carbon documents store data in properties. Properties can be named and can contain whatever values your application needs, but there are some properties already used by the system, and therefore reserved:

  • $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.
  • types: An array holding one or more named classes that describe the type of the document (e.g. Person, Project, Product, Invoice).
  • created: Date when the document was created.
  • modified: Date when the document was last modified.
  • hasMemberRelation: Configures the property that will hold the array of Document members.
  • isMemberOfRelation: Specifies the property that a member Document will acquire that links back to its container Document.

Fragments and named fragments

Documents may also contain nested objects. There are two types of nested objects, fragments and named fragments:

var document = {                                        // =================== Document
        $id: "https://.../projects/project-x/",
        $slug: "project-x"
        name: "Project X",
        description: {                                      // -------- Fragment
            format: "html",
            content: "<div>Some content</div>"
        },                                                  // -------- End: Fragment
        sow: {                                              // -------- Named Fragment
            $id: "#sow",
            $slug: "sow"
            signedOn: new Date( "2016-04-03" ),
            clauses: [ // ... ]
        }                                                   // -------- End: Named Fragment
    };                                                      // =================== End: Document

Fragments are identified by an ID, just like documents. However, their ID is not a URI. Instead, fragments use IDs of the form _:RANDOM-STRING. These IDs are local to the document, making it impossible to link to fragments from outside of the document. Their purpose is to allow a document to refer, internally, to segments of itself.

Named fragments, on the other hand, are identified by a relative URI and can be referenced from outside the document. Their URIs have the form DOCUMENT-URI#NAMED-FRAGMENT-SLUG. Nevertheless, there are times (as shown in the example) where they can be written relative to the document like #NAMED-FRAGMENT-SLUG.

In a Nutshell

You can save JavaScript objects with simple key/value pairs as Carbon documents. The value of a given key may be a nested object which, in turn, becomes either a fragment or a named fragment.

If it doesn’t make sense to reference the inner object from outside of the containing document, you can save the inner object as a fragment. If it is an essential part of the document, but it makes sense to reference or link to it from outside, you should create a named fragment.

Fragment Management

Regular fragments are the most common types of fragments, they are also called “Blank Nodes” (or bNodes for short).

Carbon offers several methods to help you manage your bNodes and named fragments. You can create them using the $createFragment method. If you either:

  • Pass an ID as a parameter
  • Pass a fragment with an $id property
  • Pass a fragment with a $slug property

A new named fragment will be created, otherwise, a bNode will be created.

   let document;
  //  ... get Document from CarbonLDP

   let fragment;
   // ... Create your fragment object with the desired properties

   document.$createFragment(fragment); // please notice that since you are not providing an id, you are creating a bNode
   document.$createFragment(fragment, "fragment-id") // This would create a named fragment.
   document.$save();

You can remove the fragments from your document using the $removeFragment method.

   // ... Application processes and we don't need the fragment anymore in that object

   document.$removeFragment(fragment); // ... or
   document.$removeFragment("fragment-id");
   document.$save();

You can also use the $getFragment and $getFragments methods to retrieve the fragments within a document:

    let document = await this.carbon.documents.$get("fragment-id"); // gets the document with object ID.

    document.$getFragment("_:bNode-id"); // returns the bNode associated with "_:bNode-id"
    document.$getFragments(); // Returns an array of all fragments within documentObject

When you create a bNode, an id is generated using UUID which starts with “_:”, the ids from named fragments however, start with “#”.

In any method, if you write the id as fragment-id, carbon will treat is as a named fragment id, and convert it to #fragment-id. If you want to specify that the id you’re providing is for a bNode, make sure your id starts with “_:” (_:bNode-id).

Object types

Any object stored in Carbon (documents, fragments or named fragments), can be marked with “types”. These types can be thought of as classes or classifications that describe the type of the object.

You can classify an object as any custom type, but be aware that Carbon also automatically assigns certain system types when an object is saved.

The types property will contain an array of types. To determine whether a document represents an object of a given type, you can use the document’s $hasType method:

// ... imports
let project;

// ... project retrieval

project.types.push( "project", "important-project" );

console.log( project.$hasType( "project" ) ); // true
console.log( project.types.length !== 2 ); // true, remember Carbon may add more types to the document
// ... imports
import { Document } from "carbonldp/Document";

let project:Project & Document;

// ... project retrieval

project.types.push( "project", "important-project" );

console.log( project.$hasType( "project" ) ); // true
console.log( project.types.length !== 2 ); // true, remember Carbon may add more types to the document
// ... imports
var project;

// ... project retrieval

project.types.push( "project", "important-project" );

console.log( project.$hasType( "project" ) ); // true
console.log( project.types.length !== 2 ); // true, remember Carbon may add more types to the document

Shallow documents

Shallow documents are unresolved Documents that have no other content than the $id of the Document they represent -hence the unresolved adjective- plus all the methods provided by the Document class.

This type of document is commonly returned when retrieving documents that have properties that link to other documents.

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

{
        $id: "https://.../tasks/task-x/",
        $slug: "task-x"
        description: "Call the project manager to set the release date",
        budget: 500000,
        responsible: "https://.../employees/employee-x/";
}
The Task linking an Employee through the responsible property
{
        $id: "https://.../employees/employee-x/",
        $slug: "employee-x"
        firstName: "John",
        lastName: "Smith"
};
The Employee linked by the Task 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 nested calls that may be caused by linked objects contained in the original linked object, and so on…

Lastly, 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 Document class.

// ... imports
let carbonldp;

// ... initialize your CarbonLDP object

let taskDocument;

// ... task retrieval
carbonldp.documents.$get( "tasks/task-x/" )
    .then( ( _taskDocument ) => {
        taskDocument = _taskDocument;

        console.log( taskDocument.$isResolved() ); // true
        console.log( taskDocument.responsible.$isResolved() ); // false

        return taskDocument.responsible.$resolve();
    } )
    .then( ( employeeDocument ) => {

        console.log( employeeDocument.$isResolved() ); // true
        console.log( taskDocument.responsible.$isResolved() ); // true
        console.log( taskDocument.$isResolved() ); // true
    } )
    .catch( error => console.error( error ) );
// ... import your Task and Employee interfaces
let carbonldp:CarbonLDP = new CarbonLDP( "http://localhost:8083" );

let taskDocument:Task & Document;

// ... task retrieval
carbonldp.documents.$get<Task>( "tasks/task-x/" )
    .then( ( _taskDocument:Task & Document ) => {
        taskDocument = _taskDocument;

        console.log( taskDocument.$isResolved() ); // true
        console.log( (<any>taskDocument.responsible).$isResolved() ); // false

        return (<any>taskDocument.responsible).$resolve();
    } )
    .then( ( employeeDocument:Employee & Document ) => {

        console.log( employeeDocument.$isResolved() ); // true
        console.log( (<any>taskDocument.responsible).$isResolved() ); // true

        console.log( employeeDocument.firstName ); // John
        console.log( taskDocument.responsible.firstName ); // John
    } )
    .catch( error => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object
var taskDocument;

// ... task retrieval
carbonldp.documents.$get( "tasks/task-x/" )
    .then( function( _taskDocument ) {
        taskDocument = _taskDocument;

        console.log( taskDocument.$isResolved() ); // true
        console.log( taskDocument.responsible.$isResolved() ); // false

        return taskDocument.responsible.$resolve();
    } )
    .then( function( employeeDocument ) {

        console.log( employeeDocument.$isResolved() ); // true
        console.log( taskDocument.responsible.$isResolved() ); // true
        console.log( taskDocument.$isResolved() ); // true
    } )
    .catch( function( error ) { console.error( error ); } );

Creating endpoints

Thanks to the characteristics of shallow documents (having all the Document methods + having the $id of the document they point to), we can use them as endpoints to create, read or delete resources.

Think of endpoints as “services” that let you create, read or delete children of a given resource. For example, suppose that you have a document called customers which stores and manages every customer as its children. The $id of this document would be http://localhost:8083/customers/, whereas the $id of each children (customer) would be http://localhost:8083/customers/customer-x/.

Usually, a service with HTTP calls to create, read or delete resources is required in the majority of web applications. With the SDK, you don’t have to manually create these HTTP calls because the SDK does that for you. This is the benefit of using endpoints as the base of your services.

To register an endpoint, the SDK offers a registry that lets you do exactly that. Once you register the endpoint, you can use it in your service to create, read or delete your resources.

The following is an example of how to create a CustomerService with operations to create, read or delete clients using a custom endpoint. Notice how the service uses an endpoint as the base of the service’s HTTP calls:

// ... imports
let carbonldp;

// ... initialize your CarbonLDP object

// Declaring the service
class CustomerService {

    constructor( carbonldp ) {
        this.carbonldp = carbonldp;
        // Using the registry to obtain the endpoint of "http://localhost:8083/customers/"
        this._endpoint = this.carbonldp.registry.register( "customers/" );
    }

    create( name, numberOfEmployees, slug ) {

        let newClient = {
            name: name,
            numberOfEmployees: numberOfEmployees
        };

        return this._endpoint.$create( newClient, slug );
    }

    get( slug ) {

        return this._endpoint.$get( slug );
    }

    // Updates are only possible using the modifying document's
    //  save() or saveAndRefresh() methods
    update( modifiedClient ) {

        return modifiedClient.$saveAndRefresh();
    }

    delete( slugOrURI ) {

        return this._endpoint.$delete( slugOrURI );
    }

}

(function () {
    // Creating the service passing the CarbonLDP object we initialized
    let customerService = new CustomerService( carbonldp );

    customerService.create( "Base22", 100, "base22" )
        .then( ( createdClient ) => {
            console.log( createdClient.$slug ); // base22
        } );
})();
// ... import your Client interface
import { Document } from "carbonldp/Document";

let carbonldp;

// ... initialize your CarbonLDP object

// Declaring the service
class CustomerService {

    private carbonldp:CarbonLDP;
    private _endpoint:Document;

    constructor( carbonldp:CarbonLDP ) {
        this.carbonldp = carbonldp;
        // Using the registry to obtain the endpoint of "http://localhost:8083/customers/"
        this._endpoint = this.carbonldp.registry.register( "customers/" );
    }

    public create( name:string, numberOfEmployees:number, slug?:string ):Promise<Client & Document> {

        let newClient:Client = {
            name: name,
            numberOfEmployees: numberOfEmployees
        };

        return this._endpoint.$create( newClient, slug );
    }

    public get( slug:string ):Promise<Client & Document> {

        return this._endpoint.$get( slug );
    }

    // Updates are only possible using the modifying document's
    //  save() or saveAndRefresh() methods
    public update( modifiedClient:Client & Document ):Promise<Client & Document> {

        return modifiedClient.$saveAndRefresh();
    }

    public delete( slugOrURI:string ):Promise<void> {

        return this._endpoint.$delete( slugOrURI );
    }

}

(function () {
    // Creating the service passing the CarbonLDP object we initialized
    let customerService:CustomerService = new CustomerService( carbonldp );

    customerService.create( "Base22", 100, "base22" )
        .then( ( createdClient:Client & Document ) => {
            console.log( createdClient.$slug ); // base22
        } );
})();
var carbonldp;

// ... initialize your CarbonLDP object

// Declaring the service
var CustomerService = (function() {

    function CustomerService( carbonldp ) {
        this.carbonldp = carbonldp;
        // Using the registry to obtain the endpoint of "http://localhost:8083/customers/"
        this._endpoint = this.carbonldp.registry.register( "customers/" );
    }

    CustomerService.prototype.create = function( name, numberOfEmployees, slug ) {

        var newClient = {
            name             : name,
            numberOfEmployees: numberOfEmployees
        };

        return this._endpoint.$create( newClient, slug );
    };

    CustomerService.prototype.get = function( slug ) {

        return this._endpoint.$get( slug );
    };

    // Updates are only possible using the modifying document's
    //  save() or saveAndRefresh() methods
    CustomerService.prototype.update = function( modifiedClient ) {

        return modifiedClient.$saveAndRefresh();
    };

    CustomerService.prototype.delete = function( slugOrURI ) {

        return this._endpoint.$delete( slugOrURI );
    };

    return CustomerService;
})();

(function () {
    // Creating the service passing the CarbonLDP object we initialized
    var customerService = new CustomerService( carbonldp );

    customerService.create( "Base22", 100, "base22" )
        .then( function( createdClient ) {
            console.log( createdClient.$slug ); // base22
        } );
})();

The documents endpoint

The SDK also comes with some predefined endpoints that let you create child documents. In fact, if you’ve been following the documentation since the getting started guide, you have already been making use of a default SDK endpoint: the documents endpoint.

Unlike custom endpoints, the documents endpoint points to the root of your platform (/). That’s why whenever you create a resource using the documents endpoint, it will be created as a direct child of /. Additionally, that’s the same reason why when reading or deleting a document using the documents endpoint, you can pass either a full URI (http://localhost:8083/customers/customer-x/) or a relative URI (customers/customer-x/).

Conclusion

JavaScript objects can be defined with data-type properties, pointers (links) to other resources, fragments (inner-objects), and named fragments (externally referable inner-objects). These objects, once saved and/or retrieved using the JavaScript SDK are Document objects. Each Document object is uniquely identified by a URI, which can be used to reference, retrieve, update, or delete the Document.

Documents will always be classified with certain system types, but can also be classified with custom types (e.g. Person, Project, Product, Invoice).

Document objects are the main data objects you work with when using the JavaScript SDK and the primary means by which you create, read, update, and delete data.

Documents are resolved objects that contain all their properties, whereas Shallow Documents are unresolved Documents that only carry their own $id.