KesytoneJS List emit events, api routes, and socket routes
npm install keystone-live```
npm install keystone-live
For Keystone v0.4.x apiRoutes must be added in routes as the onMount event is fired too late. init
For keystone-live 0.2.0 you must include a keystone instance with .
`javascript
var keystone = require('keystone');
var Live = require('keystone-live');
// Add keystone.init()
// Add keystone models
// ...
Live.init(keystone);
keystone.set('routes', function(app) {
var opts = {
exclude: '_id,__v',
route: 'galleries',
paths: {
get: 'find',
create: 'new'
}
}
Live.
apiRoutes('Post').
apiRoutes('Gallery',opts);
});
keystone.start({
onStart: function() {
Live.
apiSockets().
listEvents();
}
});
`
For Keystone 0.3.x and below you can add apiRoutes in the onMount event.
`javascript
var Live = require('keystone-live');
// optionally add the keystone instance
// Live.init(keystone)
keystone.start({
onMount: function() {
Live.apiRoutes();
},
onStart: function() {
Live.
apiSockets().
listEvents();
}
});
`
#### .init ( keystone )
> @param keystone _{Instance}_ - Keystone instance
> _@return_ this
In order to use keystone-live 2.0+ you must include a keystone instance with .init(keystone). Keystone-live <2.0 .init is optional.
#### .apiRoutes ( [ list ], [ options ] )
> @param list _{String}_ - _Optional_ Keystone List key
> @param options _{Object}_ - _Optional_ Options
> _@return_ this
Set list = false to attach routes to all lists. Call multiple times to attach to chosen Lists.
For Keystone v0.4.x apiRoutes must be added in routes as the onMount event is fired too late. For Keystone 0.3.x and below you can add apiRoutes in the onMount event.
`javascript`
keystone.set('routes', function(app) {
var opts = {
exclude: '_id,__v',
route: 'galleries',
paths: {
get: 'find',
create: 'new'
}
}
live.
apiRoutes('Post').
apiRoutes('Gallery',opts);
});options is an object that may contain: true
> __skip__ - {_String_} - Comma seperated string of default Routes to skip.
> __exclude__ - {_String_} - Comma seperated string of Fields to exclude from requests (takes precedence over include)
> __include__ - {_String_} - Comma seperated string of Fields to include in requests
> __auth__ - {_...Boolean|Function_} - Global auth. sets check of req.user
> __middleware__ - {_...Array|Function_} - Global middleware routes
> __route__ - {_String_} - Root path without pre and trailing slash eg: api
> __paths__ - {_Object_} rename the default action uri paths
>> __create__ - {_String_}
>> __get__ - {_String_}
>> __list__ - {_String_}
>> __remove__ - {_String_}
>> __update__ - {_String_}
>> __updateField__ - {_String_}
> __routes__ - {_Object_} override the default routes
>> __create__ - {_...Object|Function_}
>> __get__ - {_...Object|Function_}
>> __list__ - {_...Object|Function_}
>> __remove__ - {_...Object|Function_}
>> __update__ - {_...Object|Function_}
>> __updateField__ - {_...Object|Function_}
>> __additionalCustomRoute__ - {_...Object|Function_} - add your own routes
>>
>> __Each route can be a single route function or an object that contains:__
>>> __route__ - {_Function_} - your route function
>>> __auth__ - {_...Boolean|Function_} - auth for the route. use true for the built in req.user check. '_id, __v'
>>> __middleware__ - {_...Array|Function_} - middleware for the route.
>>> __excludeFields__ - {_String_} - comma seperated string of fields to exclude. (takes precedence over include) 'name, address, city'
>>> __includeFields__ - {_String_} - comma seperated string of fields to exclude.
NOTE: include and exclude can be set for each list individually, before applying to all other lists with Live.apiRoute(null, options). exclude takes precedent over include and only one is used per request. You can override the global setting per request.
`javascript`
var opts = {
route: 'api/v2',
exclude: '_id, __v',
auth: false,
paths: {
remove: 'delete'
},
skip: 'create, remove',
routes: {
get: {
auth: false,
middleware: [],
route: function(list) {
return function(req, res) {
console.log('custom get');
list.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
var data2 = {}
data2[list.path] = item;
res.apiResponse(data2);
});
}
},
},
create: {
auth: function requireUser(req, res, next) {
if (!req.user) {
return res.apiError('not authorized', 'Please login to use the service', null, 401);
} else {
next();
}
},
},
yourCustomFunction: function(list) {
return function(req, res) {
console.log('my custom function');
list.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
var data2 = {}
data2[list.path] = item;
res.apiResponse(data2);
});
}
}
}
}
// add rest routes to all lists
Live.apiRoutes(false, opts);
// add rest routes to Post
// Live.apiRoutes('Post', opts);options
Created Routes
Each registered list gets a set of routes created and attached to the schema. You can control the uri of the routes with the object as explained above.
| action | route |
|--- |--- |
| list | /api/posts/list |
| create | /api/posts/create |
| get | /api/posts/__:id__ |
| update | /api/posts/__:id__/update |
| updateField | /api/posts/__:id__/updateField |
| remove | /api/posts/__:id__/remove |
| yourRoute | /api/posts/__:id__/yourRoute |
| yourRoute | /api/posts/yourRoute |
Modifiers: each request can have relevant modifiers added to filter the results.
> include: 'name, slug' - fields to include in result
> exclude: '__v' - fields to exclude from result
> populate: 'createdBy updatedBy' - fields to populate
> populate: 0 - do not populate - createdBy and updatedBy are defaults
> limit: 10 - limit results
> skip: 10 - skip results
> sort: {} - sort results
route requests look like
``
/api/posts/55dbe981a0699a5f76354707/?list=Post&path=posts&emit=get&id=55dbe981a0699a5f76354707&exclude=__v&populate=0
___
#### .apiSockets ( [ options ], callback )
> alias .live
> @param options _{Object}_ - Options for creating events
> _@return_ callback _{Function}_
Create the socket server and attach to events
Returns this if no callback provided.
options is an object that may contain:
> __exclude__ - {_String_} - Comma seperated Fields to exclude from requests (takes precedence over include)
> __include__ - {_String_} - Fields to include in requests
> __auth__ - {_...Boolean|Function_} - require user
> __middleware__ - {_...Array|Function_} - global middleware function stack function(socket, data, next)
> __listConfig__ - {_Object_} - configuration for lists
>> __only__ - {_String_} - comma seperated string of Lists allowed (takes first precedence)
>> __skip__ - {_String_} - comma seperated string of Lists not to allow
> __lists__ - {_Object_} - individual List config
>> __KEY__ - {_Object_} - Each Key should be a valid List with an object consisting of:
>>> __exclude__ - {_String_} - comma seperated string of routes to exclude. 'create, update, remove, updateField' true
>>> __...get|find|list__ - {_...Object|Function_} - all of the __routes__
>>> __auth__ - {_...Boolean|Function_} - global auth funtion or for default auth for all paths function(socket, data, next)
>>> __middleware__ - {_...Array|Function_} - global middleware function stack for all paths
> __routes__ - {_Object_} - override the default routes
>> __create__ - {_...Object|Function_}
>> __get__ - {_...Object|Function_} returns Object Array
>> __find__ - {_...Object|Function_} alias of list
>> __list__ - {_...Object|Function_} returns of Objects true
>> __remove__ - {_...Object|Function_}
>> __update__ - {_...Object|Function_}
>> __updateField__ - {_...Object|Function_}
>> __...customRoutes__ - {_...Object|Function_} - create your own routes
>> Each route can be a Function or an object consisting of:
>>> __route__ - {_Function_} - route to run
>>> __auth__ - {_...Boolean|Function_} - auth funtion or for default auth '_id, __v'
>>> __middleware__ - {_...Array|Function_} - middleware stack - function(socket, data, next)
>>> __excludeFields__ - {_String_} - comma seperated string of fields to exclude. (takes precedence over include) 'name, address, city'
>>> __includeFields__ - {_String_} - comma seperated string of fields to exclude.
`javascript
var opts = {
include: 'name,slug,_id,createdAt',
auth: function(socket, next) {
if (socket.handshake.session) {
console.log(socket.handshake.session)
var session = socket.handshake.session;
if(!session.userId) {
console.log('request without userId session');
return next(new Error('Authentication error'));
} else {
var User = keystone.list(keystone.get('user model'));
User.model.findById(session.userId).exec(function(err, user) {
if (err) {
return next(new Error(err));
}
if(!user.isAdmin) {
return next(new Error('User is not authorized'))
}
session.user = user;
next();
});
}
} else {
console.log('session error');
next(new Error('Authentication session error'));
}
},
routes: {
// all functions except create and update follow this argument structure
get: function(data, socket, callback) {
console.log('custom get');
if(!_.isFunction(callback)) callback = function(err,data){
console.log('callback not specified for get',err,data);
};
var list = data.list;
var id = data.id;
if(!list) return callback('list required');
if(!id) return callback('id required');
list.model.findById(id).exec(function(err, item) {
if (err) return callback(err);
if (!item) return callback('not found');
var data = {}
data[list.path] = item;
callback(null, data);
});
},
yourCustomRoute: function(data, req, socket, callback) {
// req contains a user field with the session id
console.log('this is my custom room listener function');
if(!_.isFunction(callback)) callback = function(err,data){
console.log('callback not specified for get',err,data);
};
var list = data.list;
var id = data.id;
if(!list) return callback('list required');
if(!id) return callback('id required');
list.model.findById(id).exec(function(err, item) {
if (err) return callback(err);
if (!item) return callback('not found');
var data = {}
data[list.path] = item;
callback(null, data);
});
},
// create and update follow the same argument structure
update: function(data, req, socket, callback) {
if(!_.isFunction(callback)) callback = function(err,data){
console.log('callback not specified for update',err,data);
};
var list = data.list;
var id = data.id;
var doc = data.doc;
if(!list) return callback('list required');
if(!id) return callback('id required');
if(!_.isObject(doc)) return callback('data required');
if(!_.isObject(req)) req = {};
list.model.findById(id).exec(function(err, item) {
if (err) return callback(err);
if (!item) return callback('not found');
item.getUpdateHandler(req).process(doc, function(err) {
if (err) return callback(err);
var data2 = {}
data2[list.path] = item;
callback(null, data2);
});
});
}
}
}
// start live events and add emitters to Post
Live.apiSockets(opts).listEvents('Post');
// alternate new configuration
Live.apiSockets({
auth: false,
listConfig: {
exclude: 'Tool, Brand',
},
middleware: function(data, socket, next) {
debug('should run 1st for everyone' );
data.test = 'Hello Peg!';
next();
},
routes: {
create: {
auth: true,
},
update: {
auth: true,
},
updateField: {
auth: true,
},
remove: {
auth: true,
},
},
lists: {
'RomBox': {
// exclude: 'create',
middleware: [function(data, socket, next) {
debug('run middleware', data.test, socket.handshake.session);
data.test = 'Hello Al!';
next();
}, function(data, socket, next) {
debug('should run 3rd', data.test);
next();
}],
},
'Spec': {
auth: false,
// exclude: 'create',
middleware: [function(data, socket, next) {
debug('run middleware', data.test, socket.handshake.session);
data.test = 'Hello Al!';
next();
}, function(data, socket, next) {
debug('should run 3rd', data.test);
next();
}],
create: {
auth: false,
},
update: {
auth: false,
},
updateField: {
auth: false,
},
remove: {
auth: false,
},
}
}
});
`
Modifiers: each request can have relevant modifiers added to filter the results.
> include: 'name, slug' - fields to include in result
> exclude: '__v' - fields to exclude from result
> populate: 'createdBy updatedBy' - fields to populate
> populate: 0 - do not populate - createdBy and updatedBy are defaults
> limit: 10 - limit results
> skip: 10 - skip results
> sort: {} - sort results
socket requests look like - see socket requests and client
``
var data = {
list: 'Post',
limit: 10,
skip: 10,
sort: {}
}
live.io.emit('list', data);
Listens to emitter events
`javascript
/ add Live doc events /
Live.on('doc:' + socket.id, docEventFunction);
/ add Live doc pre events /
Live.on('doc:Pre', docPreEventFunction);
/ add Live doc post events /
Live.on('doc:Post', docPostEventFunction);
/ add Live list event /
Live.on('list:' + socket.id, listEventFunction);
`
##### Socket emitters
user emitters
sent to individual sockets
`javascript
// list emit
socket.emit('list', event);
// document pre events
socket.emit('doc:pre', event);
socket.emit('doc:pre:' + event.type, event);
// document post events
socket.emit('doc:post', event);
socket.emit('doc:post:' + event.type, event);
// document event
socket.emit('doc', event);
socket.emit('doc:' + event.type, event);
``
global emitters
global events sent to rooms on change events only.javascript
// room unique identifier sent by user - emit doc
if(event.iden) {
emitter.to(event.iden).emit('doc' , event);
emitter.emit(event.iden , event);
emitter.to(event.iden).emit('doc:' + event.type , event);
}
// the doc id - doc:_id
if(event.id) {
emitter.to(event.id).emit('doc', event);
emitter.emit(event.id, event);
emitter.to(event.id).emit('doc:' + event.type, event);
}
// the doc slug - doc:slug
if(event.data && event.data.slug) {
emitter.to(event.data.slug).emit('doc', event);
emitter.to(event.data.slug).emit('doc:' + event.type, event);
emitter.emit(event.iden , event);
}
// the list path - doc:path
if(event.path) {
emitter.to(event.path).emit('doc', event);
emitter.to(event.path).emit('doc:' + event.type, event);
emitter.emit(event.path , event);
}
// individual field listening -
if(event.field && event.id) {
// room event.id:event.field emit doc
emitter.to(event.id + ':' + event.field).emit('doc', event);
emitter.to(event.id + ':' + event.field).emit('doc:' + event.type, event);
emitter.emit(event.id + ':' + event.field , event);
emitter.emit(event.field , event);
// room path emit field:event.id:event.field
emitter.to(event.path).emit('field:' + event.id + ':' + event.field, event);
emitter.to(event.path + ':field').emit('field:' + event.id + ':' + event.field, event);
}
`
___
#### .init ( [ keystone ] )
> @param keystone _{Instance}_ - Pass keystone in as a dependency
> _@return_ this
Useful for development if you want to pass Keystone in
___
#### .listEvents ( [ list ] )
> alias .list
> @param list _{String}_ - Keystone List Key
> _@return_ this
Leave blank to attach live events to all Lists.
Should be called after Live.apiSockets()
Learn about attached events
List Broadcast Events
Websocket Broadcast Events
`javascript`
keystone.start({
onStart: function() {
Live.
apiSockets().
listEvents('Post').
listEvents('PostCategory');
}
});
___
#### .router ()
> _@return_ this
`javascript`
this.MockRes = require('mock-res');
this.MockReq = require('mock-req');
___
and doc:post on the user broadcast (explained below). For greater interactivity and control use the websocket broadcast events.
|pre|post|*post|
|---|---|---|
| init:pre| init:post| |
| validate:pre | validate:post | |
| save:pre | save:post | save |
| remove:pre | remove:post | |
*Note that post save has an extra event save:post and save.
`javascript
list.schema.pre('validate', function (next) {
var doc = this;
// emit validate event to rooms
changeEvent({
type:'validate:pre',
path:list.path,
data:doc,
success: true
}, Live._live.namespace.lists);
// emit validate input locally
live.emit('doc:Pre',{
type:'validate',
path:list.path,
data:doc,
success: true
});
next();
});
` Each method will trigger a local event and a broadcast event.
Each event will send a data object similiar to:
`javascript
{
type:'save:pre',
path:list.path,
id:doc._id.toString(),
data:doc,
success: true
}
` The broadcast event is sent when each action occurs.
`javascript
changeEvent({
type:'remove:post',
path:list.path,
data:doc,
success: true
}, Live._live.namespace.lists);
`
changeEvent will send a broadcast to any of the following rooms that are available for listening:
> doc._id
> list.path
> doc.slug
>
> Each room emits
doc and doc:event.type
`javascript
// the doc id - event.id
if(event.id) {
emitter.to(event.id).emit('doc', event);
emitter.to(event.id).emit('doc:' + event.type, event);
}
// the doc slug - event.data.slug
if(event.data && event.data.slug) {
emitter.to(event.data.slug).emit('doc', event);
emitter.to(event.data.slug).emit('doc:' + event.type, event);
}
// the list path - event.path
if(event.path) {
emitter.to(event.path).emit('doc', event);
emitter.to(event.path).emit('doc:' + event.type, event);
}
` The following are valid
event.type values for List global broadcasts:
> init:pre
> init:post
> validate:pre
> validate:post
> save:pre
> save:post
> save
> remove:pre
> remove:post
> The local event will emit
doc:Pre or doc:Post for the appropriate events
`javascript
// pre
Live.emit('doc:Pre',{
type:'save',
path:list.path,
id:doc._id.toString(),
data:doc,
success: true
});
// post
Live.emit('doc:Post',{
type:'save',
path:list.path,
id:doc._id.toString(),
data:doc,
success: true
});
`
We use Live.on in app to respond and broadcast to the current user.
`javascript
/ add live doc pre events /
Live.on('doc:Pre', docPreEventFunction);/ add live doc post events /
Live.on('doc:Post', docPostEventFunction);
function docPreEventFunction(event) {
// send update info to global log
Live.emit('log:doc', event);
/ send the users change events /
socket.emit('doc:pre', event);
socket.emit('doc:pre:' + event.type, event);
}
function docPostEventFunction(event) {
Live.emit('log:doc', event);
/ send the users change events /
socket.emit('doc:post', event);
socket.emit('doc:post:' + event.type, event);
}
`
$3
Live uses socket.io v~1.3.2 to handle live event transportation. A set of CRUD routes are available and there are several rooms you can subscribe to that emit results. io is exposed via
Live.io. Our list namespace is Live.io.of('/lists').
You will connect to the /lists namespace in the client to listen to emit events.#### CRUD Listeners
There is a generic set of CRUD listeners available to control the database. You do not receive callback results with Websocket CRUD listeners. You will need to pick the best strategy to use to listen for result events from the rooms available. Each listener emits its result to
Live.on. Live.on will catch each submission and decide who should be notified. View the changeEvent() behaviour below.
###### create
`javascript
socket.emit('create',{
list: 'Post',
doc: {
title: 'Hello',
},
iden: _uniqueKey_
});socket.on('create', function(obj) {
live.emit('doc:' + socket.id,{type:'created', path:getList.path, id:doc._id, data:doc, success:true, iden: list.iden});
});
`
###### custom
`javascript
socket.emit(yourCustomRoom,{
list: 'Post', //if available
id: '54c9b9888802680b37003af1', //if available
iden: _uniqueKey_
});socket.on(custom, function(obj) {
live.emit('doc:' + socket.id,{type:'get', path:list.path, data:doc, success:true, iden: list.iden});
});
`###### get
`javascript
socket.emit('get',{
list: 'Post',
id: '54c9b9888802680b37003af1',
iden: _uniqueKey_
});socket.on('get', function(obj) {
live.emit('doc:' + socket.id,{type:'get', path:list.path, id:list.id, data:doc, success:true, iden: list.iden});
});
`###### list
`javascript
socket.emit('list',{
list: 'Post',
iden: _uniqueKey_
});socket.on('list', function(obj) {
live.emit('doc:' + socket.id,{path:list.path, data:docs, success:true});
});
`###### remove
`javascript
socket.emit('remove',{
list: 'Post',
id: '54c9b9888802680b37003af1',
iden: _uniqueKey_
});socket.on('remove', function(obj) {
live.emit('doc:' + socket.id,{type:'removed', path:list.path, id:list.id, success:true, iden: list.iden});
});
`###### update
`javascript
socket.emit('update',{
list: 'Post',
id: '54c9b9888802680b37003af1',
doc: {
title: 'Bye!',
},
iden: _uniqueKey_
});socket.on('update', function(obj) {
live.emit('doc:' + socket.id,{type:'updated', path:list.path, id:list.id, data:list.doc, success:true, iden: list.iden});
});
`###### updateField
`javascript
socket.emit('updateField',{
list: 'Post',
id: '54c9b9888802680b37003af1',
field: 'content.brief',
value: 'Help!',
iden: _uniqueKey_
});
// Hello
socket.on('updateField', function(obj) {
live.emit('doc:' + socket.id,{type:'updatedField', path:getList.path, id:list.id, data:list.doc, field:list.field, value:list.value, success:true, iden: list.iden});
});`##### Broadcast Results
Instead of returning a http response, each listener emits a local event that the app is waiting for. This event is processed and the correct rooms are chosen to broadcast the result.
There are two emitter namespaces
> doc
>
emitter.to(event.path).emit('doc', event);
> emitter.to(event.path).emit('doc:TYPE', event);
>
> list
> socket.emit('list', event);
> list is only sent to the requesting user
The following are valid
event.type values:
> created
> get
> save
> updated
> updatedField
> custom Each broadcast is sent to the global doc as well as a computed doc:event.type channel.
changeEvent will send the broadcast to the following rooms that are available for listening:
###### path
`javascript
emitter.to(event.path).emit('doc', event);
emitter.to(event.path).emit('doc:' + event.type, event);
`
###### id
the doc._id value if available
`javascript
emitter.to(event.id).emit('doc', event);
emitter.to(event.id).emit('doc:' + event.type, event);
`###### slug
document slug if available
`javascript
emitter.to(event.data.slug).emit('doc', event);
emitter.to(event.data.slug).emit('doc:' + event.type, event);
`
###### id:field
field broadcasts to the list.path room and a doc._id:fieldName room
`javascript
// room event.id:event.field emit doc
emitter.to(event.id + ':' + event.field).emit('doc', event);
emitter.to(event.id + ':' + event.field).emit('doc:' + event.type, event);// room path emit field:event.id:event.field
emitter.to(event.path).emit('field:' + event.id + ':' + event.field, event);
emitter.to(event.path + ':field').emit('field:' + event.id + ':' + event.field, event);
`
###### iden
Dynamic room. Send a unique iden with each request and the app emits back to a room named after iden
`javascript
emitter.to(event.iden).emit('doc' , event);
emitter.to(event.iden).emit('doc:' + event.type , event);
`
To use iden make sure to kill your event listeners. Here is a simple response trap function:
`javascript
var trapResponse = function(callback) {
var unique = keystone.utils.randomString();
var cb = function(data) {
socket.removeListener(unique, cb);
callback(data);
}
socket.on(unique, cb);
return unique;
}var myFn = function(data) {
// do someting
}
socket.emit('create',{
list:'Post',
doc: data,
iden: trapResponse(myFn)
});
`Additional io namespaces
The socket instance is exposed at Live.io.
The /lists and / namespaces are reserved. You can create any others of your own.`javascript
var sharedsession = require("express-socket.io-session");/ create namespace /
var myNamespace = Live.io.of('/namespace');
/ session management /
myNamespace.use(sharedsession(keystone.get('express session'), keystone.get('session options').cookieParser));
/ add auth middleware /
myNamespace.use(function(socket, next){
if(!keystone.get('auth')) {
next();
} else {
authFunction(socket, next);
}
});
/ list events /
myNamespace.on("connection", function(socket) {
var req = {
user: socket.handshake.session.user
}
socket.on("disconnect", function(s) {
// delete the socket
delete live._live.namespace.lists;
// remove the events
live.removeListener('doc:' + socket.id, docEventFunction);
live.removeListener('list:' + socket.id, listEventFunction);
live.removeListener('doc:Post', docPostEventFunction);
live.removeListener('doc:Pre', docPreEventFunction);
});
socket.on("join", function(room) {
socket.join(room.room);
});
socket.on("leave", function(room) {
socket.leave(room.room);
});
});
`Client
Your client should match up with our server version. Make sure you are using 1.x.x and not 0.x.x versions.`javascript
var socketLists = io('/lists');
socketLists.on('connect',function(data) {
console.log('connected');
});
socketLists.on('error',function(err) {
console.log('error',err);
});
socketLists.on('doc:save',function(data) {
console.log('doc:save',data);
});
socketLists.on('doc',function(data) {
console.log('doc',data);
});
socketLists.on('list',function(data) {
console.log('list data',data);
});``