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 slugresource-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
.
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 Document
s 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/";
}
responsible
property{
$id: "https://.../employees/employee-x/",
$slug: "employee-x"
firstName: "John",
lastName: "Smith"
};
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 Document
s 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
.