README
Siren.js Core
A cross-platform library that provides classes for creating objects representing components (entities, actions, etc.) of the Siren hypermedia format. The primary intent of this library is for generating or parsing Siren representations.
Installation
npm install @siren-js/core
Development Release
@siren-js/core
is currently in the development phase (v0.x) while we work to
realize the best API for working with Siren in JavaScript. This means minor
version increments may not be backward compatible, but patch version increments
will.
In order to get to a production-ready release (v1+), we need users to try out the library, find bugs, and give honest, constructive feedback on how we can improve! See the Contributing section below.
Usage
The following example demonstrates one way of building the
example entity from the Siren spec. It uses the Entity
class
which takes an object representing a Siren entity.
import * as Siren from '@siren-js/core';
const order = getOrderFromDB(orderNumber);
const entity = new Siren.Entity({
class: ['order'],
properties: {
orderNumber: order.orderNumber,
itemCount: order.items.length,
status: order.orderStatus
},
entities: [
{
class: ['items', 'collection'],
rel: ['http://x.io/rels/order-items'],
href: `http://api.x.io/orders/${order.orderNumber}/items`
},
{
class: ['info', 'customer'],
rel: ['http://x.io/rels/customer'],
properties: {
customerId: order.customer.userId,
name: order.customer.fullName
},
links: [
{
rel: ['self'],
href: `http://api.x.io/customers/${order.customer.userId}`
}
]
}
],
actions: [
{
name: 'add-item',
title: 'Add Item',
method: 'POST',
href: `http://api.x.io/orders/${order.orderNumber}/items`,
type: 'application/x-www-form-urlencoded',
fields: [
{ name: 'orderNumber', type: 'hidden', value: `${order.orderNumber}` },
{ name: 'productCode', type: 'text' },
{ name: 'quantity', type: 'number' }
]
}
],
links: [
{
rel: ['self'],
href: `http://api.x.io/orders/${order.orderNumber}`
},
{
rel: ['previous'],
href: `http://api.x.io/orders/${order.orderNumber - 1}`
},
{
rel: ['next'],
href: `http://api.x.io/orders/${order.orderNumber + 1}`
}
]
});
If you don't want to build your entity in one fell swoop (I wouldn't blame you),
you can use the other component classes: Action
, Field
, Link
,
EmbeddedEntity
, and EmbeddedLink
. Each of these accept required
members as positional constructor arguments and optional members in a final
options object.
Here's how you might break up the above code:
const orderUrl = `http://api.x.io/orders/${order.orderNumber}`;
const selfLink = new Siren.Link(['self'], orderUrl);
const itemsRel = 'http://x.io/rels/order-items';
const itemsUrl = `http://api.x.io/orders/${order.orderNumber}/items`;
const itemsLink = new Siren.EmbeddedLink([itemsRel], itemsUrl, {
class: ['items', 'collection']
});
const customerRel = 'http://x.io/rels/customer';
const customerEntity = new Siren.EmbeddedEntity([customerRel], {
class: ['info', 'customer'],
properties: {
customerId: order.customer.userId,
name: order.customer.fullName
},
links: [
{
rel: ['self'],
href: `http://api.x.io/customers/${order.customer.userId}`
}
]
});
const quantityField = new Siren.Field('quantity', {
type: 'number'
});
const addItemAction = new Siren.Action('add-item', itemsUrl, {
title: 'Add Item',
method: 'POST',
type: 'application/x-www-form-urlencoded',
fields: [
{ name: 'orderNumber', type: 'hidden', value: `${order.orderNumber}` },
{ name: 'productCode', type: 'text' },
quantityField
]
});
Now constructing the full entity is a little easier.
new Siren.Entity({
class: ['order'],
properties: {
orderNumber: order.orderNumber,
itemCount: order.items.length,
status: order.orderStatus
},
entities: [itemsLink, customerEntity],
actions: [addItemAction],
links: [
selfLink,
{
rel: ['previous'],
href: `http://api.x.io/orders/${order.orderNumber - 1}`
},
{
rel: ['next'],
href: `http://api.x.io/orders/${order.orderNumber + 1}`
}
]
});
//=> same as entity
Component Lookup
The Entity
and Action
classes each provide a method for looking up their
actions and fields by name
.
entity.getActionByName('add-item');
//=> same as addItemAction
addItemAction.getFieldByName('quantity');
//=> same as quantityField
The Entity
class also has methods for looking up sub-entities and links by
rel
and class
, as well as actions by class
.
entity.getLinksByRel('self');
//=> same as [selfLink]
entity.getEntitiesByRel(itemsRel);
//=> same as [itemsLink]
entity.getEntitiesByClass('items');
//=> same as [itemsLink]
// you can pass multiple classes/rels (order doesn't matter)
entity.getEntitiesByClass('customer', 'info');
//=> same as [customerEntity]
// components' property must contain all values
entity.getEntitiesByClass('items', 'info');
//=> []
The Action
class has a method for looking up fields by class
that works
similarly.
Generating Siren
To generate a Siren representation, use JSON.stringify()
.
const siren = JSON.stringify(entity);
Parsing Siren
To parse a string as a Siren representation, use JSON.parse()
and pass the
result to the Entity
constructor.
new Siren.Entity(JSON.parse(siren));
//=> same as entity
Extensions
The options objects of each component class allow you to extend the core Siren
spec. Need an hreflang
property on your link? No problem!
Need validation constraints on your fields? You got it!
const link = new Siren.Link(['profile'], 'http://api.example.com/profile', {
hreflang: 'en-US'
});
link.hreflang;
//=> 'en-US'
const field = new Siren.Field('quantity', { min: 1, max: 10 });
const value = 15;
if (value < field.min || value > field.max) {
// this block will execute...
}
TypeScript
Type declarations are included in the @siren-js/core
package and require at
least version 3.8.2 of TypeScript. However, TypeScript users may experience
several limitations not present for JavaScript users.
For example, class properties that are nested components can be passed as plain objects in the constructor, but not when modifying the property directly.
// this is OK
addItemAction.fields = [quantityField];
// this causes an error in TypeScript!!!
addItemAction.fields = [{ name: 'quantity' }];
Similarly, Link
's and EmbeddedLink
's href
property can be given a URL
,
which is coerced to a string
using the toString()
method. Again, this is
available when instantiating a class, but not when modifying the property.
const url = new URL(orderUrl);
// this is OK
const link = new Siren.Link(['self'], url);
link.href;
//=> same as orderUrl
// this causes an error in TypeScript!!!
link.href = url;
These limitations are caused by a requirement for getters and setters to have the same type (see TypeScript issue #2521).
Contributing
If you would like to contribute anything from a bug report to a code change, see our contribution guidelines.