title | description | source | edit | masthead |
---|---|---|---|---|
Resource Definitions |
Documentation for configuring Resources, including attributes and behaviors. |
/img/digital-texture.jpg |
Resources define the "object model" that your application exposes. Here's how a single resource is created:
maki.define('Person');
In general, Resources should be defined as singular items – this is important as
it makes heuristics in various places much easier. Person
in this case will
become /people
(automatically!) as a collection, and it contains a list of
person
items. That's pretty neat!
Applications built with Maki provide sane defaults, but can be completely customized. The directory structure for customization can be automatically created for you, by simply calling bootstrap
after your Resources have been defined:
maki.define('Person', {
attributes: ...
});
maki.bootstrap();
This is a chainable method, so maki.bootstrap().start();
will also work.
Defining a Resource in Maki has but one requirement, the list of attributes
it
provides. You can think of this like a Type Definition in a more strongly-typed
language:
maki.define('Person', {
attributes: { name: String }
});
The attributes associated with a Resource will enforce type, too! The names are used to control parameters when interacting with the resource, so choose them wisely.
Attributes support a compact-form definition (as above), but also a long-form declaration for more complex behaviors:
maki.define('Person', {
attributes: {
name: { type: String , max: 140 }
}
});
Default values for a Resource attribute can be any object type, including a function:
maki.define('Person', {
attributes: {
name: { type: String , max: 140 },
created: { type: Date , default: Date.now }
}
});
Remember – do not call the function here, simply set it.
The max
attribute can be used to define a maximum value (for type Number
) or
length (for type String
).
Similarly, min
can be used to specify a minimum.
You can prevent a field from rendering in various contexts by providing a map of boolean values and the Maki method upon which to restrict that attribute.
maki.define('Person', {
attributes: {
name: { type: String , max: 140 },
created: { type: Date , default: Date.now , render: {
create: false
} }
}
});
You can choose to auto-populate Resource attributes on specific methods, such as
.query
and .get
. Supply an array of either strings for the method name, or
an object with additional parameters, including a list of fields to include.
maki.define('Widget', {
attributes: {
name: { type: String , max: 140 },
_owner: { type: ObjectId , ref: 'Person', populate: ['get'] }
}
});
maki.define('Widget', {
attributes: {
name: { type: String , max: 140 },
_owner: { type: ObjectId , ref: 'Person', populate: [
{
method: 'get',
fields: { username: 1 , slug: 1 }
}
] }
}
});
When specifying a value for a referenced field (that is, ref:
), several types
can be used.
simple
for a top-level link (defers to consensus). Example: person: 'martindale'
path
for a hinted type (defers to local node). Example: person: '/people/martindale'
target
for another namespace's version (defers to remote node). Example: person: soundtrack/people/martindale
exact
for a specific document (defers to the content). Example: https://maki.io/messages/328250f68b47a76cdeee7be76526f560ce959d70969978c74e60d89a2c13b43e
Certain special types of Resource attributes exist. These control some behavior unique to these types.
The 'File'
type will create a specially aliased attribute that exposes a raw
file stream. Files can be uploaded to fields labeled type: 'File'
, and are
downloadable via a special Files
resource (exposed over HTTP at /files
).
These field types are rendered in HTML as file upload input elements, and offer the user the ability to select one (or multiple) files for upload.
The default datastore for these uploads is the datastore – in the current version of Maki (0.2.0 as of time of writing), this is a GridFS filestore that never touches disk.
All Maki Resources expose exactly five (5) methods:
- query to select a list of documents,
- get to get a single instance of a document by its identifier (ID),
- create to create a new instance of a document,
- update to change properties of a document, and
- destroy to remove a document.
These methods can be used to construct any more complex behavior, such as when multiple Resources might need to be involved. The ideal place for these types of behaviors is in the Pipeline.
The Methods exposed by Maki Resource are subject to a pipeline of "transformer"
functions, which can be used to perform authorization, transformation, or any
other number of things. Pipeline functions can be of two types; pre
or
post
.
var Person = maki.define('Person', {
attributes: { name: String }
});
Person.pre('create', function(done) {
var person = this;
// trim any whitespace from the individual's name.
person.name = person.name.trim();
done();
});
Person.post('create', function(done) {
var person = this;
// create some sort of entry somewhere...
Activity.create({ ref: person._id } , done );
});
All Maki Resources also emit events when certain operations take place – and they even describe the specifics of those operations in an atomic fashion.
var Person = maki.define('Person', {
attributes: { name: String }
});
Person.on('create', function( data ) {
console.log('A Person was created!', data );
});
When using the HTTP service (enabled by default), a pub/sub architecture is exposed via the WebSocket protocol. This is, by default, completely routable:
Server
var Person = maki.define('Person', {
attributes: { name: String }
});
Client
var ws = new WebSocket('ws://example.com/people');
ws.onmessage = function( data ) {
console.log('new event received!', data );
};
These WebSockets speak JSON-RPC, and specifically expose these methods:
ping
, which should be responded to with a "pong" result.patch
, which provides an array of operations to execute on a resource (via the JSON-PATCH specification)subscribe
will expect achannel
parameter that matches the "collection" name of the expected resource (see below).unsubscribe
is the inverse ofsubscribe
, as you've surely gathered.
WebSockets are not limited to a single resource – you can submit a subscribe
(or an unsubscribe
) RPC call to add additional subscriptions to an existing
connection:
var ws = new WebSocket('ws://localhost:9200/people');
ws.on('open', function() {
var JSONRPCEvent = {
jsonrpc: '2.0',
method: 'subscribe',
data: {
channel: '/examples'
}
};
ws.send( JSON.stringify( JSONRPCEvent ) );
});
The ws
connection will now receive updates to both the Example
and the
People
Resources.
Resources can automatically be collected from outside sources, including local files and even remote HTTP services. This is useful for querying APIs, or keeping some versioned data in an accessible JSON-formatted data file.
Sources are added via the source
parameter on a resource:
maki.define('Example', {
attributes: { name: String , description: String },
source: './data/examples.json'
});
The content in this attribute will be passed to procure, which will then collect the data and cache it locally.
Currently, this is done one time, at Maki startup. The data is never again queried, and the in-memory version is kept until the worker restarts.
Important: Resources currently bypass any middleware. Do not expect hooks to work on static content.
Sourced data can have a transformation function, or a mapper
, applied to it at
runtime:
maki.define('Release', {
attributes: { name: String },
source: 'https://api.github.com/repos/martindale/maki/releases',
map: function( release ) {
return {
name: release.name
};
}
});
Mappers expect a single parameter, and expect a single return parameter –
identical to Array.prototype.map
.
Sourced data can be formatted in a number of ways. For example, as Markdown:
maki.define('Release', {
attributes: {
name: { type: String , render: 'markdown' }
},
source: 'https://api.github.com/repos/martindale/maki/releases',
map: function( release ) {
return {
name: release.name
};
}
});
markdown
is currently the only supported renderer. Future versions of Maki
will likely include the ability to pass a function in this field.
Any resource can have additional requirements when requested via a non-
programmatic endpoint (such as a rendered HTML view). You can them via the
requires
property:
maki.define('Example', {
requires: {
'Release': {
filter: { isExample: true }
}
}
});
You can also supply a function, which will be executed at query time, with
this
supplied as the context:
maki.define('Example', {
requires: {
'Release': {
filter: function() {
return { _example: this._id }
}
}
}
});
Requirements can be named arbitrarily, by simply supplying the property name as
the key, and providing a resource
string to the configuration:
maki.define('Example', {
requires: {
'featured': {
resource: 'Example',
filter: function() {
return { featured: true }
}
}
}
});
Responses will now have a named attribute featured
, which contains the results
from the requirement collector.
Not unlike Requirements, Resources with nested objects can have
populate()
called on it.
maki.define('Example', {
requires: {
'Release': {
populate: '_person'
}
}
});
This is passed directly to the internal query, and will be attached to the required subdocuments.
Indexes (or indices) can be supplied to any Resource via the indices
option,
which expects an array of objects, as follows:
indices: [ { fields: ['type', 'id'] , unique: true } ]