Access points

Introduction

One of the main goals of a Linked Data Platform is to link data (obviously). Such links can exist within a domain or between different domains.

With Carbon LDP you can link data in two different ways:

  • Parent-child relation (hard link)
    Denotes a direct parent-child relation in which a document can have child documents. This means that whenever the parent is deleted, the children and the children of the children (and so on…) will also be deleted.
  • Membership relation (soft link)
    A soft link can be made if you want to list a document, let’s say document B as a member of another document, such as document A. By creating this type of link, if you delete the document linking the members (document A) only that document is deleted; any documents that were a member of it (e.g. document B) are left untouched.

Membership relation

Notably, the core of a soft link is listing documents as members of another one. Then, how is a membership relation made with Carbon?

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

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

These properties are configured in the document that will maintain the member 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: Returns an array of UNRESOLVED Document objects (Shallow Documents) for each member
  • $getMembers: Returns an array of RESOLVED Document objects for each member
  • $removeMember/$removeMembers: Removes the specified members from the relationship (without deleting their actual documents). 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" };

// ... project, task1, task2 and task3 persistence (create, etc)

project.$addMembers( [ task1, task2, task3 ] )
    .then( () => {
        return project.$resolve();
    } )
    .then( ( project ) => {
        console.log( project.tasks ); // Shallow Documents of 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)

        return project.$listMembers();
    } )
    .then( ( unresolvedTasks ) => {
        console.log( unresolvedTasks ); // Shallow 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 ); // Shallow Document of the project. Added due to the configured isMemberOfRelation
    } )
    .catch( error => console.error( error ) );
// ... additional imports
import { Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

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

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

let task1:Task = { name: "Task 1" }; // Being "Task" a business specific interface defined somewhere
let task2:Task = { name: "Task 2" };
let task3:Task = { name: "Task 3" };

// ... project, task1, task2 and task3 persistence (create, etc)

projectDocument.$addMembers( [ task1, task2, task3 ] )
    .then( () => {
        return projectDocument.$resolve<Project>();
    } )
    .then( ( project:Project & Document ) => {
        console.log( projectDocument.tasks ); // Shallow Documents of task1, task2 and task3. Added because of the configured hasMemberRelation
        console.log( projectDocument.tasks.indexOf( task1 ) !== - 1 ); // true (remember, shallow documents are cached and reused)

        return projectDocument.$listMembers<Task>();
    } )
    .then( ( unresolvedTasks:Task & Document[] ) => {
        console.log( unresolvedTasks ); // Shallow Documents of task1, task2 and task3

        return projectDocument.$removeMember( task2 );
    } )
    .then( () => {
        console.log( "Task 2 is no longer a member of the project" );

        return projectDocument.$getMembers<Task>();
    } )
    .then( ( resolvedTasks:Task & Document ) => {
        console.log( resolvedTasks ); // task1 and task3 documents
        console.log( resolvedTasks[ 0 ].project ); // Shallow document 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" };

// ... project, task1, task2 and task3 persistence (create, etc)

project.$addMembers( [ task1, task2, task3 ] )
    .then( function() {
        return project.$resolve();
    } )
    .then( function( project ) {
        console.log( project.tasks ); // Shallow Documents of 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)

        return project.$listMembers();
    } )
    .then( function( unresolvedTasks ) {
        console.log( unresolvedTasks ); // Shallow Documents of 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( ( resolvedTasks ) => {
        console.log( resolvedTasks ); // task1 and task3 documents
        console.log( resolvedTasks[ 0 ].project ); // Shallow Documents of the project. Added due to the configured isMemberOfRelation
    } )
    .catch( function( error ) { console.error( error ); } );

So far we’ve seen how to configure a membership relation between documents. In this case, we used the membership relation to list tasks of a project.

Membership relations have a limitation, however: a document can only maintain one list through one property (by default members). This means that if we now want to add people to the project, whenever we call the $addMembers method, it would mix the people with the tasks of the project.

So, how would you add/list other documents through another property? This can be accomplished by using access points.

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

Like normal documents, access points can be customized by specifying a hasMemberRelation and an isMemberOfRelation.

To create an access point for a document, you must use the create method from the AccessPoint class to create an in-memory AccessPoint object. You can then pass it to the $create method of the documents endpoint or to a Document object to persist it.

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

let project;
let person1;
let person2;

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( ( _projectMembersAccessPoint ) => {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        // Add members to the Access Point
        return _projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        // Refresh the project so it can have the property listing the members
        return project.$refresh();
    } )
    .then( ( refreshedProject ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh();
    } )
    .then( ( refreshedPerson1 ) => {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Documents of project
    } )
    .catch( error => console.error( error ) );
// ... additional imports
import { AccessPoint, TransientAccessPoint } from "carbonldp/AccessPoint";
import { Document } from "carbonldp/Document";

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

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( ( _projectMembersAccessPoint:AccessPoint ) => {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        // Add members to the Access Point
        return _projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        // Refresh the project so it can have the property listing the members
        return project.$refresh<Project>();
    } )
    .then( ( refreshedProject:Project & Document ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh<Person>();
    } )
    .then( ( refreshedPerson1:Person & Document ) => {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Documents of project
    } )
    .catch( error => console.error( error ) );
// ... imports

var project;
var person1;
var person2;

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( function( _projectMembersAccessPoint ) {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        // Add members to the Access Point
        return _projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( function() {
        // Refresh the project so it can have the property listing the members
        return project.$refresh();
    } )
    .then( function( refreshedProject ) {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh();
    } )
    .then( function( refreshedPerson1 ) {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Document of project
    } )
    .catch( function( error ) { console.error( error ); } );

At the end, an access point is a document with added functionality. Because it’s a document, it behaves by the rules of a regular document, giving you access to the same methods that standard documents have.

One such method is the $delete method of the Document class. To delete an access point you can use the $delete method of the documents endpoint or of a Document object.

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

let project;
let person1;
let person2;

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( ( _projectMembersAccessPoint ) => {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        // Add members to the Access Point
        return _projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        // Refresh the project so it can have the property listing the members
        return project.$refresh();
    } )
    .then( ( refreshedProject ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Delete the access point
        return projectMembersAccessPoint.$delete();
    } )
    .then( () => {

        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh();
    } )
    .then( ( refreshedPerson1 ) => {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Documents of project
    } )
    .catch( error => console.error( error ) );
// ... additional imports
import { AccessPoint, TransientAccessPoint } from "carbonldp/AccessPoint";
import { Document } from "carbonldp/Document";

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

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( ( _projectMembersAccessPoint:AccessPoint ) => {
        console.log( projectMembersBaseAccessPoint === _projectMembersAccessPoint ); // true

        projectMembersAccessPoint = _projectMembersAccessPoint;

        // Add members to the Access Point
        return projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( () => {
        // Refresh the project so it can have the property listing the members
        return project.$refresh<Project>();
    } )
    .then( ( refreshedProject:Project & Document ) => {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Delete the access point
        return projectMembersAccessPoint.$delete();
    } )
    .then( () => {
        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh<Person>();
    } )
    .then( ( refreshedPerson1:Person & Document ) => {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Documents of project
    } )
    .catch( error => console.error( error ) );
// ... imports

var project;
var person1;
var person2;

// ... project, person1 and person2 document retrieval

// 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
} );

// Persist the Access Point
project.$create( projectMembersAccessPoint, "people" );
    .then( function( _projectMembersAccessPoint ) {
        console.log( projectMembersAccessPoint === _projectMembersAccessPoint ); // true

        // Add members to the Access Point
        return _projectMembersAccessPoint.$addMembers( [ person1, person2 ] );
    } )
    .then( function() {
        // Refresh the project so it can have the property listing the members
        return project.$refresh();
    } )
    .then( function( refreshedProject ) {
        console.log( project === refreshedProject ); // true
        console.log( refreshedProject.people ); // Shallow Documents of person1 and person2

        // Delete the access point
        return projectMembersAccessPoint.$delete();
    } )
    .then( function() {

        // Refresh a person so it can have the property pointing back to the document a person is member of
        return person1.$refresh();
    } )
    .then( function( refreshedPerson1 ) {
        console.log( person1 === refreshedPerson1 ); // true
        console.log( refreshedPerson1.projects ); // Shallow Documents of project
    } )
    .catch( function( error ) { console.error( error ); } );

Because access points create soft links between documents, the member documents (person1 and person2) will still exist, but the property that linked them back to the document they were members of will be deleted from them.

Conclusion

Overall, there are two types of links between documents: hard links (direct parent-child relation) and soft links (document referencing other documents). You can set a soft link relationship with the membership relation. You can configure the membership relation by using the hasMemberRelation and the isMemberOfRelation.

A document can only maintain one list through one property. If you want to add more members through another property you can use access points. Access points are special documents whose purpose is to maintain a list of members for another document. Because access points are documents with added functionality, they can still use the same methods as any document, so you can $create or $delete them using the documents endpoint or the Document.