Getting started with the JavaScript SDK
Prerequisite knowledge
To better understand this guide and its code examples, a working knowledge of the following prerequisite topics is required.
- HTML, CSS, JavaScript: See: Mozilla Developer Network.
- Promises: A standard-based way of handling asynchronous JavaScript logic. See: Promise object(Mozilla Developer Network).
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)
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.
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" );
Advanced CarbonLDP object creation
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:
host | string | “my-carbonldp-instance.com” |
port (optional) | number | 8083 |
ssl (optional) | boolean | false |
vocabulary (optional) | string | “http://schema.org/” |
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 XML
representation 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 Document
s that have no other content than the $id
of the Document
they represent plus all the methods provided by the Document
class. 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.
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/";
}
responsible
property{
$id: "https://example.com/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 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, Pointer
s 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 slugresource-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.
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:
- Retrieve the document:
$get
using thedocument
id as described inretrieving a document
- Modify the
document
: Add, modify or deleteproperties
,fragments
,named fragments
. - 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 $refresh
method. 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 documents
endpoint 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 members
which 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 shallowDocument
objects for each of the children documentsdocuments.$getChildren
: Returns a list ofDocument
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!
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 defaultmembers
)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 shallowDocument
objects for each member$getMembers
: Like$getChildren
, returns an array of completeDocument
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 ); } );
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.