jsonreststores

A module to create full Json REST stores in minutes

JsonRestStores

JsonRestStores is the best way to create REST stores that return JSON data. JsonRestStores is in RC1 status, and the API is locked. Please (find and) file bugs and requests as issues against this repo.

Rundown of features:

  • DRY approach. Everything works as you'd expect it to, even though you are free to tweak things.
  • Down-to-earth. It does what developers actually need, using existing technologies.
  • Database-agnostic. You can either use a generic database connector, or implement the data-manipulation methods yourself.
  • Protocol-agnostic. For now, only HTTP is implemented. However, with JsonRestStores the protocol used to make REST calls doesn't actually matter.
  • Schema based. Anything coming from the client will be validated and cast to the right type.
  • API-ready. Every store function can be called via API, which bypass permissions constraints
  • Tons of hooks. You can hook yourself to every step of the store processing process: afterValidate(), afterCheckPermissions(), afterDbOperation(), afterEverything()
  • Authentication hooks. Only implement things once, and keep authentication tight.
  • Mixin-based. You can add functionality easily.
  • Inheriting stores. You can easily derive a store from another one.
  • Simple error management. Errors can be chained up, or they can make the store return them to the client.
  • Great documentation. Every aspect of JsonRestStores is carefully explained and documented. Note that every day usage doesn't require knowlege of every single aspect of JsonRestStores.

JsonRestStores even comes with its own database layer mixin, SimpleDbLayerMixin, which will implement all of the important methods that will read, write and delete elements from a database. The mixin uses simpledblayer to access the database. For now, only MongoDb is supported but more will surely come.

Introduction to (JSON) REST stores

Here is an introduction on REST, JSON REST, and this module. If you are a veteran of REST stores, you can probably just skim through this.

Imagine that you have a web application with bookings, and users connected to each booking, and that you want to make this information available via a JSON Rest API. You would have to define the following routes in your application:

  • GET /bookings/
  • GET /bookings/:bookingId
  • PUT /bookings/:bookingId
  • POST /bookings/
  • DELETE /bookings:/bookingId

And then to access users for that booking:

  • GET /bookings/:bookingId/users/
  • GET /bookings/:bookingId/users/:userId
  • PUT /bookings/:bookingId/users/:userId
  • POST /bookings/:bookingId/users/
  • DELETE /bookings/:bookingId/users/:userId

It sounds simple enough (although it's only two tables and it already looks rather boring). It gets tricky when you consider that:

  • You need to make sure that permissions are always carefully checked. For example, only users that are part of booking 1234 can GET /bookings/1234/users
  • When implementing GET /bookings/, you need to parse the URL in order to enable data filtering (for example, GET /bookings?dateFrom=1976-01-10&name=Tony will need to filter, on the database, all bookings made after the 10th of January 1976 by Tony).
  • When implementing GET /bookings/, you need to return the right Content-Range HTTP headers in your results so that the clients know what range they are getting.
  • When implementing GET /bookings/, you also need to make sure you take into account any Range header set by the client, which might only want to receive a subset of the data
  • With POST and PUT, you need to make sure that data is validated against some kind of schema, and return the appropriate errors if it's not.
  • With PUT, you need to consider the HTTP headers If-match and If-none-match to see if you can//should//must overwrite existing records
  • All unimplemented methods should return a 501 Unimplemented Method server response

This is only a short list of obvious things: there are many more to consider. The point is, when you make a store you should be focusing on the important parts (the data you gather and manipulate, and permission checking) rather than repetitive, boilerplate code.

With JsonRestStores, you can create JSON REST stores without ever worrying about any one of those things. You can concentrate on what really matters: your application's data and logic.

If you are new to REST and web stores, you will probably benefit by reading a couple of important articles. Understanding the concepts behind REST stores will make your life easier.

I suggest you read John Calcote's article about REST, PUT, POST, etc.. It's a fantastic read, and I realised that it was written by John, who is a long term colleague and fellow writer at Free Software Magazine, only after posting this link here!

You should also read my small summary of what a REST store actually provides.

At this stage, the stores are 100% compatible with Dojo's JsonRest as well as Sitepen's dstore.

Dependencies overview

Jsonreststores is a module that creates managed routes for you, and integrates very easily with existing ExpressJS applications.

Here is a list of modules used by JsonRestStores. You should be at least slightly familiar with them.

  • SimpleDeclare - Github. This module makes creation of constructor functions/classes a breeze. Using SimpleDeclare is a must when using JsonRestStores -- unless you want to drown in unreadable code.

  • SimpleSchema - Github. This module makes it easy (and I mean, really easy) to define a schema and validate/cast data against it. It's really simple to extend a schema as well. It's a no-fuss module.

  • Allhttperrors. A simple module that creats Error objects for all of the possible HTTP statuses.

  • SimpleDbLayer. The database layer used to access the database

Note that all of these modules are fully unit-tested, and are written and maintained by me.

It is recommended that you have a working knowledge of SimpleDbLayer (focusing on querying and automatic loading of children) before delving too deep into JsonRestStores, as JsonRestStores uses the same syntax to create queries and to define nested layers.

Your first Json REST store

Creating a store with JsonRestStores is very simple. Here is how you make a fully compliant store, ready to be added to your Express application:

      var JsonRestStores = require('jsonreststores'); // The main JsonRestStores module
      var Schema = require('simpleschema');  // The main schema module
      var SimpleDbLayer = require('simpledblayer');
      var MongoMixin = require('simpledblayer-mongo')
      var declare = require('simpledeclare');
 
      // The DbLayer constructor will be a mixin of SimpleDbLayer (base) and
      // MongoMixin (providing mongo-specific driver to SimpleDbLayer)
      var DbLayer = declare( SimpleDbLayer, MongoMixin, { db: db } );
 
      // Basic definition of the managers store
      var Managers = declare( JsonRestStores, JsonRestStores.HTTPMixin, JsonRestStores.SimpleDbLayerMixin, {
 
        // Constructor class for database-access objects, which in this case
        // will access MongoDNB collections
        DbLayer: DbLayer,
 
        schema: new Schema({
          name   : { type: 'string', trim: 60 },
          surname: { type: 'string', searchable: true, trim: 60 },
        }),
 
        storeName: 'managers',
        publicURL: '/managers/:id',
 
        handlePut: true,
        handlePost: true,
        handleGet: true,
        handleGetQuery: true,
        handleDelete: true,
      });
 
      var managers = new Managers(); 
 
      JsonRestStores.init();
      managers.protocolListen( 'HTTP', { app: app } );;

Note that since you will be mixing in JsonRestStores with JsonRestStores.HTTPMixin and JsonRestStores.SimpleDbLayerMixin for every single store you create (more about mixins shortly), you might decide to create the mixin once for all making the code less verbose:

    var JsonRestStores = require('jsonreststores'); // The main JsonRestStores module
    var Schema = require('simpleschema');  // The main schema module
    var SimpleDbLayer = require('simpledblayer');
    var MongoMixin = require('simpledblayer-mongo')
    var declare = require('simpledeclare');
 
    // The DbLayer constructor will be a mixin of SimpleDbLayer (base) and
    // MongoMixin (providing mongo-specific driver to SimpleDbLayer)
    var DbLayer = declare( SimpleDbLayer, MongoMixin, { db: db } );
 
    // Mixin of JsonRestStores, JsonRestStores.HTTPMixin and JsonRestStores.SimpleDbLayerMixin
    // with the DbLayer parameter already set
    var Store = declare( JsonRestStores, JsonRestStores.HTTPMixin, JsonRestStores.SimpleDbLayerMixin, { DbLayer: DbLayer } );
 
    // Basic definition of the managers store
    var Managers = declare( Store, {
 
      schema: new Schema({
        name   : { type: 'string', trim: 60 },
        surname: { type: 'string', searchable: true, trim: 60 },
      }),
 
      storeName: 'managers',
      publicURL: '/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
 
    var managers = new Managers();
 
    JsonRestStores.init();
    protocolListen( 'HTTP', { app: app } );

That's it: this is enough to add, to your Express application, a a full store which will handly properly all of the HTTP calls.

  • Managers is a new constructor function that inherits from JsonRestStores (the main constructor for JSON REST stores) mixed in with JsonRestStores.HTTPMixin (which ensures that protocolListen() works with the HTTP parameter, allowing clients to connect using HTTP) and JsonRestStores.SimpleDbLayerMixin (which gives JsonRestStores the ability to manipulate data on a database automatically).
  • DbLayer is a SimpleDbLayer constructor mixed in with MongoMixin, the MongoDB-specific layer for SimpleDbLayer. So, DbLayer will be used by Managers to manipulate MongoDB collections.
  • schema is an object of type Schema that will define what's acceptable in a REST call.
  • publicURL is the URL the store is reachable at. The last one ID is the most important one: the last ID in publicURL (in this case it's also the only one: id) defines which field, within your schema, will be used as the record ID when performing a PUT and a GET (both of which require a specific ID to function).
  • storeName (mandatory) needs to be a unique name for your store.
  • handleXXX are attributes which will define how your store will behave. If you have handlePut: false and a client tries to PUT, they will receive an NotImplemented HTTP error.
  • protocolListen( 'HTTP', { app: app } ) creates the right Express routes to receive HTTP connections for the GET, PUT, POST and DELETE methods.
  • JsonRestStores.init() should always be run once you have declared all of your stores. This function will run the initialisation code necessary to make nested stores work properly.

JsonRestStores is very unobtrusive of your Express application. In order to make everything work, you can just:

  • Generate a new ExpressJS application
  • Connect to the database
  • Define the stores using the code above.

This is how the stock express code would change to implement the store above (please note that this is mostly code autogenerated when you generate an Express application):

    var express = require('express');
    var path = require('path');
    var favicon = require('serve-favicon');
    var logger = require('morgan');
    var cookieParser = require('cookie-parser');
    var bodyParser = require('body-parser');
 
    var routes = require('./routes/index');
    var users = require('./routes/users');
 
    var app = express();
 
    // CHANGED: ADDED AN INCLUDE `dbConnect`
    var dbConnect = require('./dbConnect');
 
    // view engine setup
    app.set('views', path.join(__dirname, 'views'));
    app.set('view engine', 'jade');
 
    // uncomment after placing your favicon in /public
    //app.use(favicon(__dirname + '/public/favicon.ico'));
    app.use(logger('dev'));
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(cookieParser());
    app.use(express.static(path.join(__dirname, 'public')));
 
    app.use('/', routes);
    app.use('/users', users);
 
    // CHANGED: Added call to dbConnect, and waiting for the db
    dbConnect( function( db ){
 
      // ******************************************************
      // ********** CUSTOM CODE HERE **************************
      // ******************************************************
 
      var JsonRestStores = require('jsonreststores'); // The main JsonRestStores module
      var Schema = require('simpleschema');  // The main schema module
      var SimpleDbLayer = require('simpledblayer');
      var MongoMixin = require('simpledblayer-mongo')
      var declare = require('simpledeclare');
 
      // The DbLayer constructor will be a mixin of SimpleDbLayer (base) and
      // MongoMixin (providing mongo-specific driver to SimpleDbLayer)
      var DbLayer = declare( SimpleDbLayer, MongoMixin, { db: db } );
 
      // Common mixin of JsonRestStores, JsonRestStores.SimpleDbLayerMixin and the DbLayer parameter
      // already set
 
      var Store = declare( JsonRestStores, JsonRestStores.SimpleDbLayerMixin, { DbLayer: DbLayer } );
 
      var Managers = declare( Store, {
 
        schema: new Schema({
          name   : { type: 'string', trim: 60 },
          surname: { type: 'string', searchable: true, trim: 60 },
        }),
 
        storeName: 'managers',
        publicURL: '/managers/:id',
 
        handlePut: true,
        handlePost: true,
        handleGet: true,
        handleGetQuery: true,
        handleDelete: true,
      });
      var managers = new Managers(); 
 
      JsonRestStores.init();
      managers.protocolListen( 'HTTP', { app: app } );;
 
      // ******************************************************
      // ********** END OF CUSTOM CODE      *******************
      // ******************************************************
  
      // catch 404 and forward to error handler
      app.use(function(req, res, next) {
          var err = new Error('Not Found');
          err.status = 404;
          next(err);
      });
 
      // error handlers
 
      // development error handler
      // will print stacktrace
      if (app.get('env') === 'development') {
          app.use(function(err, req, res, next) {
              res.status(err.status || 500);
              res.render('error', {
                  message: err.message,
                  error: err
              });
          });
      }
 
      // production error handler
      // no stacktraces leaked to user
      app.use(function(err, req, res, next) {
          res.status(err.status || 500);
          res.render('error', {
              message: err.message,
              error: {}
          });
      });
 
 
    });
    module.exports = app;

The dbConnect.js file is simply something that will connect to the database and all the callback with the db instance:

var mongo = require('mongodb');
exports = module.exports = function( done ){
  // Connect to the database
  mongo.MongoClient.connect('mongodb://localhost/storeTesting', {}, function( err, db ){
    if( err ){
      console.error( "Error connecting to the database: ", err );
      process.exit( 1 );
    }   
    return done( db );
  }); 
}

This store is actually fully live and working! It will manipulate your database and will respond to any HTTP requests appropriately.

A bit of testing with curl:

$ curl -i -XGET  http://localhost:3000/managers/
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: "223132457"
Date: Mon, 02 Dec 2013 02:20:21 GMT
Connection: keep-alive

[]

curl -i -X POST -d "name=Tony&surname=Mobily"  http://localhost:3000/managers/
HTTP/1.1 201 Created
X-Powered-By: Express
Location: /managers/2
Content-Type: application/json; charset=utf-8
Content-Length: 54
Date: Mon, 02 Dec 2013 02:21:17 GMT
Connection: keep-alive

{
  "id": 2,
  "name": "Tony",
  "surname": "Mobily"
}

curl -i -X POST -d "name=Chiara&surname=Mobily"  http://localhost:3000/managers/
HTTP/1.1 201 Created
X-Powered-By: Express
Location: /managers/4
Content-Type: application/json; charset=utf-8
Content-Length: 54
Date: Mon, 02 Dec 2013 02:21:17 GMT
Connection: keep-alive

{
  "id": 4,
  "name": "Chiara",
  "surname": "Mobily"
}

$ curl -i -GET  http://localhost:3000/managers/
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 136
ETag: "1058662527"
Date: Mon, 02 Dec 2013 02:22:29 GMT
Connection: keep-alive

[
  {
    "id": 2,
    "name": "Tony",
    "surname": "Mobily"
  },
  {
    "id": 4,
    "name": "Chiara",
    "surname": "Mobily"
  }
]


$ curl -i -GET  http://localhost:3000/managers/?surname=mobily
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 136
ETag: "15729456527"
Date: Mon, 02 Dec 2013 02:22:35 GMT
Connection: keep-alive

[
  {
    "id": 2,
    "name": "Tony",
    "surname": "Mobily"
  },
  {
    "id": 4,
    "name": "Chiara",
    "surname": "Mobily"
  }
]

$ curl -i -GET  http://localhost:3000/managers/?surname=fabbietti
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: "1455673456"
Date: Mon, 02 Dec 2013 02:22:42 GMT
Connection: keep-alive

[]

$ curl -i -X PUT -d "name=Merc&surname=Mobily"  http://localhost:3000/managers/2
HTTP/1.1 200 OK
X-Powered-By: Express
Location: /managers/2
Content-Type: application/json; charset=utf-8
Content-Length: 54
Date: Mon, 02 Dec 2013 02:23:50 GMT
Connection: keep-alive

{
  "id": 2,
  "name": "Merc",
  "surname": "Mobily"
}

$ curl -i -XGET  http://localhost:3000/managers/2
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 54
ETag: "-264833935"
Date: Mon, 02 Dec 2013 02:24:58 GMT
Connection: keep-alive

{
  "id": 2,
  "name": "Merc",
  "surname": "Mobily"
}

It all works!

Mixins are a powerful way to specialise a generic constructor.

For example, the constructor JsonRestStores on its own is hardly useful as it doesn't allow you to wait for request and actually serve them. On its own, calling protocolListen( 'HTTP', { app: app } ); will fail, because protocolListen() will attempt to run the method protocolListenHTTP( { app: app } ), which isn't defined.

The good news is that the mixin JsonRestStores.HTTPMixin implements protocolListenHTTP() (as well as the corresponding protocolSendHTTP()), which makes protocolListen( 'HTTP', { app: app } ); work.

You can mix a store with as many protocol mixins as you like (although at this stage only HTTP is currently implemented).

HTTPMixin is only one piece of the puzzle: on its own, it's not enough. JsonRestStores mixed with HTTPMixin creates JSON REST stores with the following data-manipulation methods left unimplemented (they will throw an error if they are run):

  • implementFetchOne: function( request, cb )
  • implementInsert: function( request, forceId, cb )
  • implementUpdate: function( request, deleteUnsetFields, cb )
  • implementDelete: function( request, cb )
  • implementQuery: function( request, next )
  • implementReposition: function( doc, where, beforeId, cb )

Implementing these methods is important to tell JsonRestStores how to actualy manipulate the store's data. You can do it yourself by hand, but if you want to save a few hundred hours, this is exactly what JsonRestStores.SimpleDbLayerMixin does: it's a mixin that enriches the basic JsonRestStore objects with all of the methods listed above, using a database as data storage.

So when you write:

var Managers = declare( JsonRestStores, JsonRestStores.HTTPMixin, JsonRestStores.SimpleDbLayerMixin, {

You are creating a constructor, Managers, mixing in the prototypes of JsonRestStores (the generic, unspecialised constructor for Json REST stores), HTTPMixin (which makes protocolListen( 'HTTP', { app: app } ); work) and JsonRestStores.SimpleDbLayerMixin (which provides the implementations of implementFetchOne(), implementInsert(), etc. to manipulate data).

SimpleDbLayerMixin will use the DbLayer attribute of the store as the constructor used to create "table" objects, and will manipulate data with them.

DbLayer itself is created using the same pattern as Managers.

SimpleDbLayer on its own is useless: it creates a DB layer with the following methods left unimplemented:

  • select( filters, options, cb )
  • update( conditions, updateObject, options, cb )
  • insert( record, options, cb )
  • delete( conditions, options, cb )
  • reposition: function( record, where, beforeId, cb )

The implementation will obviously depend on the database layer. So, when you type:

var DbLayer = declare( SimpleDbLayer, MongoMixin );

You are creating a constructor, DbLayer, that is the mixin of SimpleDbLayer (where select() update() etc. are not implemented) and MongoMixin (which implements select(), update() etc. using MongoDB as the database layer).

This is the beauty of mixins: they implement the missing methods in a generic, unspecialised constructor.

When you define a store like this:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  hardLimitOnQueries: 50,
});

managers.protocolListen( 'HTTP', { app: app } );;

The publicURL is used to:

  • Add id: { type: id } to the schema automatically. This is done so that you don't have to do the grunt work of defining id in the schema if they are already in publicURL.
  • Create the paramIds array for the store. In this case, paramIds will be [ 'id' ].

So, you could reach the same goal without publicURL:

var Managers = declare( Store, {

  schema: new Schema({
    id     : { type: 'id' },
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', trim: 60 },
  }),

  storeName: 'managers',
  paramIds: [ 'id' ],

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  hardLimitOnQueries: 50,
});

var managers = new Managers();
JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );; // This will throw()

Note that:

  • The id parameter had to be defined in the schema
  • The paramIds array had to be defined by hand
  • managers.protocolListen( 'HTTP', { app: app } ); can't be used as the public URL is not there

This pattern is much more verbose, and it doesn't allow the store to be placed online with protocolListen().

In any case, the property idProperty is set as last element of paramIds; in this example, it is id.

In the documentation, I will often refers to paramIds, which is an array of element in the schema which match the ones in the route. However, in all examples I will declare stores using the "shortened" version.

How stores work: a walk-through

Here is a walk-through on how stores actually work, and how requests are fulfilled. Note that this refers very specifically to stores mixing in with HTTPMixin and with SimpleDbLayerMixin.

When you define a store like this:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  hardLimitOnQueries: 50,
});

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;

The last line is the one that makes the store "active": managers.protocolListen()will actually runmanagers.protocolListenHTTP(), which is defined thanks to HTTPMixin. protocolListenHTTP() will define the appropriate routes using Express' app (passed to it as a parameter). The code in HTTPMixin looks like this:

    // Make entries in "app", so that the application
    // will give the right responses
    app.get(      url + id, this._getRequestHandler( 'Get' ) );
    app.get(      url,      this._getRequestHandler( 'GetQuery') );
    app.put(      url + id, this._getRequestHandler( 'Put') );
    app.post(     url,      this._getRequestHandler( 'Post') );
    app.delete(   url + id, this._getRequestHandler( 'Delete') );

So, the following routes are defined:

GET /managers/:id -- returns a specific manager. Handler: `store._makeGet()`
GET /managers/ -- returns a collection of elements; you can filter by surname, which is searchable. Handler: `store._makeGetQuery()`
PUT /managers/:id -- writes over an existing manager object. Handler: `store._makePut()`
POST /managers/ -- creates a new manager object. Handler: `store._makePost()`
DELETE /managers/:id -- deletes a manager. Handler: `store._makeDelete()`

The method this._getRequestHandler(), also defined in HTTPMixin, will be responsible of creating a plain Javascript object called request, and enrich it with the following attributes:

  • remote: Set to true.
  • protocol: set to HTTP.
  • params: set to the URL parameters. For example, a request like this: PUT /managers/10/cars/20 will have params set as { managerId: 10, id: 20 }
  • body: set to the request's body.
  • session: set to the request's session.
  • options: set based on the request's query string and headers, see the next section for more details
  • _req and _res: set to the request's req and res parameters -- this is specific to HTTPMixin.

After defining this object, this._getRequestHandler() will finally be ready to call one of the following methods (depending on the method):

  • _makeGet( request, next ) (implements GET for one single document)
  • _makeGetQuery( request, next ) (implements GET for a collection, no ID passed)
  • _makePut( request, next ) (implements PUT for a collection)
  • _makePost( request, next ) (implements POST for a collection)
  • _makeDelete( request, next ) (implements DELETE for a collection)

These methods are the heart of JsonRestStores: they will handle the request by calling implementFetchOne(), implementInsert(), implementUpdate(), implementDelete(), implementQuery() and implementReposition() (conveniently provided in this case by SimpleDbLayerMixin).

The options object is the most complex and the most useful. Each request handlers will consider different attributes:

  • putBefore. If set, and if the store supports positioning, the entry will be placed before the entry with id putBefore.
  • putDefaultPosition. If set, and if the store supports positioning, this option will instruct JsonRestStores where to place the entry: start or end (only used when putBefore isn't set)
  • overwrite. If set to true, the put will only be successful if the record is an existing one. If set to false, the put will only be successful if the record didn't exist. If not set, the put will work either way.
  • putBefore. Same as the handler _makePut
  • putDefaultPosition. Same as the handler _makePut
  • conditions. An hash object containing the filter criteria, which will have to match onlineSearchSchema.
  • ranges. It can have skip and count.
  • sort. An hash that defines how to sort the query. For example { model: -1, maker: 1 }. Note that each key needs to be listed in the sortableFields element of the store.
  • skipHardLimitOnQueries. If set to true, the attribute hardLimitOnQueries will be ignored when making getQuery calls.
  • delete. If set to true, each record will be deleted after retrieval. Note that if the store's self.deleteAfterGetQuery is set to true, then delete will automatically be set to true for each request.

Protocol mixins (in this case, HTTPMixin) have the task of accepting the request, and make sure that the options object passed to the request handler is adequately filled depending on the request itself.

Specifically:

  • putBefore. From header x-put-before
  • putDefaultPosition. From header x-put-default-position
  • overwrite. If the header if-match is set to *, it's set to true. If the header if-none-match is set to *, is set to false.
  • putBefore. Same as the handler _makePut
  • putDefaultPosition. Same as the handler _makePut
  • conditions. Worked out from the query string.
  • ranges. Worked out from the range header; if the header is 3-10, then ranges will be assigned skip: 3, limit: 8 } (it will skip to the third element, and will fetch 8 elements at the most).
  • sort. Worked out from the sortBy element in the query string; if it is for example ?sortBy=-model,+maker, options.sort will be { model: -1, maker: 1 }.

Please note that you can easily overload the specific methods in HTTPMixin if you want store parameters to be taken from the store differently.

The _make???() request handlers will use the self.sendData( request, method, returnObject ); method to send data out to the client. sendData(), in turn, will call protocolSendHTTP( request, method, returnObject ). This method has has access to the request attribute, which (as mentioned earlier) was assigned _req and _res. req._res is used by protocolSendHTTP() to set the right HTTP headers and status, and deliver the response to the client:

  • status is 200 by default.
  • if there is an error (the method is set as error, and returnObject is therefore an Error object), the status is set to the httpError attribute of the error, and the response will be the responseBody attribute of the error.
  • post and putNew and putExisting methods will set the Location header
  • post and putNew will set the status to 201. delete will set the status to 204.
  • getQuery will set the Content-Range headers, like items 3-10/100 (which will tell the client what was actually fetched in terms of range, and what the total count is).

A nested store

Stores are never "flat" as such: you have workspaces, and then you have users who "belong" to a workspace. Here is how you create a "nested" store:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,
});
var managers = new Managers(); 

var ManagersCars = declare( Store, {

  schema: new Schema({
    make     : { type: 'string', trim: 60, required: true },
    model    : { type: 'string', trim: 60, required: true },
  }),

  storeName: 'managersCars',
  publicURL: '/managers/:managerId/cars/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,
});
var managersCars = new ManagersCars();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;
managersCars.protocolListen( 'HTTP', { app: app } );;

You have two stores: one is the simple managers store with a list of names and surname; the other one is the managersCars store: note how the URL for managersCars includes managerId.

The managersCars store will will respond to GET /managers/2222/cars/3333 (to fetch car 3333 of manager 2222), GET /workspace/2222/users (to get all cars of manager 2222), and so on.

Remember that in managersCars remote queries will always honour the filter on managerId, both in queries (GET without an id as last parameter) and single-record operations (GET with a specific id). This happens thanks to SimpleDbLayerMixin (more about this later).

If you have two nested tables like the ones shown above, you might want to be able to look up fields automatically. JsonRestStores allows you to to so using the nested property.

For example:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  nested: [
    {
      type: 'multiple',
      store: 'managersCars',
      join: { managerId: 'id' },
    }
  ],

});
var managers = new Managers(); 

var ManagersCars = declare( Store, {

  schema: new Schema({
    make     : { type: 'string', trim: 60, required: true },
    model    : { type: 'string', trim: 60, required: true },
  }),

  storeName: 'managersCars',
  publicURL: '/managers/:managerId/cars/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  nested: [
    {
      type: 'lookup',
      localField: 'managerId',
      store: 'managers',
    }
  ],
});
var managersCars = new ManagersCars();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;
managersCars.protocolListen( 'HTTP', { app: app } );;

This is an example where using JsonRestStores really shines: when you use GET to fetch a manager, the object's attribute manager._children.managersCars will be an array of all cars joined to that manager. Also, when you use GET to fetch a car, the object's attribute car._children.managerId will be an object representing the correct manager. This is immensely useful in web applications, as it saves tons of HTTP calls for lookups. NOTE: The child's store's extrapolateDoc() and prepareBeforeSend() methods will be called on the child's data (as you would expect). Keep in mind that when those methods are being called on bested data, request.nested will be set to true.

Note that in nested objects the store names are passed as strings, rather than objects; this is important: in this very example, you can see store: 'managersCars', as a nested store, but at that point managersCars hasn't been declared yet. The store names in nested will be resolved later, by the JsonRestStores.init() function, using JsonRestStores' registry for the lookup. This is why it's crucial to run JsonRestStores.init() only when all of your stores have been created (and are therefore in the registry).

Fetching of nested data is achieved by SimpleDbLayerMixin by using SimpleDbLayer's nesting abilities, which you should check out. If you do check it out, you will see strong similarities between JsonRestStores' nested parameter and SimpleDbLayer. If you have used nested parameters in SimpleDbLayer, then you easily see that JsonRestStores will simply make sure that the required attribute for nested entries are there; for each nested entry it will add a layer property (based on the store's own collectionName) and a layerField property (based on the store's own idProperty).

Naming conventions for stores

It's important to be consistent in naming conventions while creating stores. In this case, code is clearer than a thousand bullet points:

var Managers = declare( Store, { 

  schema: new Schema({
    // ...
  });

  publicUrl: '/managers/:id',

  storeName: `managers`
  // ...
}
var managers = new Managers();

var People = declare( Store, { 

  schema: new Schema({
    // ...
  });

  publicUrl: '/people/:id',

  storeName: `people`
  // ...
}
var people = new People();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;
people.protocolListen( 'HTTP', { app: app } );;
  • Store names anywhere lowercase and are plural (they are collections representing multiple entries)
  • Irregulars (Person => People) are a fact of life
  • Store constructors (derived from Store) are in capital letters (as constructors, they should be)
  • Store variables are in small letters (they are normal object variables)
  • storeName attributes are in small letters (to follow the lead of variables)
  • URL are in small letters (following the stores' names, since everybody knows that /Capital/Urls/Are/Silly)
var Managers = declare( Store, { 

  schema: new Schema({
    // ...
  });

  publicUrl: '/managers/:id',

  storeName: `managers`
  // ...
}
var managers = new Managers();

var ManagersCars = declare( Store, { 

  schema: new Schema({
    // ...
  });

  publicUrl: '/managers/:managerId/cars/:id',

  // ...
  storeName: `managersCars`
  // ...
}
var managerCars = new ManagersCars();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;    
managerCars.protocolListen( 'HTTP', { app: app } );;
  • The nested store's name starts with the parent store's name (managers) keeping pluralisation
  • The URL is in small letters, starting with the URL of the parent store

Customise search rules

In the previous examples, I explained how marking a field as searchable in the schema has the effect of making it searchable in queries:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,
});

var managers = new Managers(); 

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;

If you query the store with http://localhost:3000/managers/?surname=mobily, it will only return elements where the surname field matches.

In JsonRestStores you actually define what fields are acceptable as filters with the parameter onlineSearchSchema, which is defined exactly as a schema. So, writing this is equivalent to the code just above:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  onlineSearchSchema: new Schema( {
    surname: { type: 'string', trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,
});

var managers = new Managers(); 

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;

If onlineSearchSchema is not defined, JsonRestStores will create one based on your main schema by doing a shallow copy, excluding paramIds (which means that, in this case, id is not added automatically to onlineSearchSchema, which is most likely what you want).

If you define your own onlineSearchSchema, you are able to decide exactly how you want to filter the values. For example you could define a different default, or trim value, etc. However, in common applications you can probably live with the auto-generated onlineSearchSchema.

You can decide how the elements in onlineSearchSchema will be turned into a search with the queryConditions parameter.

queryConditions is normally automatically generated for you if it's missing. So, not passing it is the same as writing:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  onlineSearchSchema: new Schema( {
    surname: { type: 'string', trim: 60 },
  }),

  queryConditions: { 
    type: 'eq', 
    args: [ 'surname', '#surname#']
  },

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,
});

var managers = new Managers(); 

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;

Basically, queryConditions is automatically generated with the name field in the database that matches the name entry in the query string (that's what #name# stands for).

Remember that here:

queryConditions: { 
  type: 'eq', 
  args: [ 'surname', '#surname#']
},

surname refers to the database field surname, whereas #surname# refers to the query string's surname element (which is cast thanks to onlineSearchSchema.

If you had defined both name and surname as searchable, queryConditions would have been generated as:

queryConditions: {
    type: 'and',
    args: [
      { type: 'eq', args: [ 'name', '#name#' ] },
      { type: 'eq', args: [ 'surname', '#surname#' ]
    ]
  },

Basically, both name and surname need to match their respective values in the query string. To know more about the syntax of queryConditions, please have a look at the conditions object in SimpleDbLayer.

Keep in mind that the syntax of JsonRestStore's queryConditions is identical to the syntax of the conditions object in SimpleDbLayer, with the following extras:

  • In JsonRestStores, when a value is in the format #something#, that something will be replaced by the value in the corresponding value in the query string when making queries. If something is not passed in the query string, that section of the query is ignored.
  • You can have the attribute ifDefined set as a value in queryConditions: in this case, that section of the query will only be evaluated if the corresponding value in the query string is defined.

For example, you could define queryConditions as:

queryConditions: {
  type: 'and',
  args: [

    {
      type: 'and', ifDefined: 'surname', args: [
        { type: 'startsWith', args: [ 'surname', '#surname#' ] },
        { type: 'eq', args: [ 'active', true ] },              
      ]
    },

    { 
      type: 'startsWith', args: [ 'name', '#name#']
    }
  ]
},

The strings #surname# and #name# are translated into their corresponding values in the query string. The ifDefined means that that whole section of the query will be ignored unless surname is passed to the query string. The comparison operators, which were eq in the generated queryConditions, are now much more useful startsWith.

You can clearly see that thanks to queryConditions you can effectively create any kind of query based on the passed parameter. For exampe, you could create a searchAll field like this:

onlineSearchSchema: new Schema( {
  searchAll: { type: 'string', trim: 60 },
}),

queryConditions: {
  type: 'or',
  ifDefined: 'searchAll',
  args: [
    { type: 'startsWith', args: [ 'number', '#searchAll#' ] },
    { type: 'startsWith', args: [ 'firstName', '#searchAll#' ] },
    { type: 'startsWith', args: [ 'lastName', '#searchAll#' ] },
  ]
},

This example highlights that onlineSearchSchema fields don't have to match existing fields in the schema: they can be anything, which is then used as a #field# value in queryConditions. They are basically values that will be used when constructing the actual query in queryConditions.

This makes JsonRestStores immensely flexible in terms of what queries can be implemented.

Thanks to queryConditions you can define any kind of query you like. The good new is that you can also search in children tables that are defined as nested in the store definitions.

For example:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', searchable: true, trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  onlineSearchSchema: new HotSchema({
    name    : { type: 'string', trim: 60 },
    surname : { type: 'string', trim: 60 },
    carInfo : { type: 'string', trim: 30 },
  }),

  queryConditions: {
    type: 'and',
    args: [

      {
        type: 'startsWith', args: [ 'surname', '#surname#']
      },

      {
        type: 'or',
        ifDefined: 'carInfo',
        args: [
          { type: 'startsWith', args: [ 'managersCars.make', '#carInfo#' ] },
          { type: 'startsWith', args: [ 'managersCars.model','#carInfo#' ] },
        ]
      }
    ]
  },

  nested: [
    {
      type: 'multiple',
      store: 'managersCars',
      join: { managerId: 'id' },
    }
  ],

});
var managers = new Managers(); 

var ManagersCars = declare( Store, {

  schema: new Schema({
    make     : { type: 'string', trim: 60, searchable: true, required: true },
    model    : { type: 'string', trim: 60, searchable: true, required: true },
  }),

  onlineSearchSchema: new HotSchema({
    make       : { type: 'string', trim: 60 },
    model      : { type: 'string', trim: 60 },
    managerInfo: { type: 'string', trim: 60 }
  }),

  queryConditions: {
    type: 'and',
    args: [

      { type: 'startsWith', args: [ 'make', '#make#'] },

      { type: 'startsWith', args: [ 'model', '#model#'] },

      {
        type: 'or',
        ifDefined: 'managerInfo',
        args: [
          { type: 'startsWith', args: [ 'managers.name', '#managerInfo#' ] },
          { type: 'startsWith', args: [ 'managers.surname','managerInfo#' ] },
        ]
      }
    ]
  },

  storeName: 'managersCars',
  publicURL: '/managers/:managerId/cars/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  nested: [
    {
      type: 'lookup',
      localField: 'managerId',
      store: 'managers',
    }
  ],
});
var managersCars = new ManagersCars();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;
managersCars.protocolListen( 'HTTP', { app: app } );;

You can see how for example in Managers, onlineSearchSchema has a mixture of fields that match the ones in the schema (name, surname) that look for a match in the correponding fields, as well as search-specific fields (like carInfo) that end up looking into the nested children.

It's totally up to you how you want organise your searches. For example, you might decide to make a searchAll field instead for Managers:

onlineSearchSchema: new HotSchema({
  searchAll : { type: 'string', trim: 60 },
}),

queryConditions: {
  type: 'or',
  ifDefined: 'searchAll',
  args: [
    { type: 'startsWith', args: [ 'name', '#searchAll#'] }
    { type: 'startsWith', args: [ 'surname', '#searchAll#'] }
    { type: 'startsWith', args: [ 'managersCars.make', '#searchAll#' ] },
    { type: 'startsWith', args: [ 'managersCars.model','#searchAhh#' ] },
  ]
},

In this case, the only allowed field in the query string will be searchAll which will look for a match anywhere.

Sorting options and default sort

A client can require data sorting by setting the sortBy parameter in the query string. This means that there shouldn't be a sortBy element in the onlineSearchSchema attribute. JsonRestStores will parse the query string, and make sure that data is fetched in the right order.

In JsonRestStores you can also decide some default fields that will be used for sorting, in case no sorting option is defined in the query string.

The sortBy attribute is in the format +field1,+field2,-field3 which will instruct JsonRestStores to sort by field1, field2 and field3 (with field3 being sorted in reverse).

When you create a store, you can decide which fields are sortable:

For example:

var Managers = declare( Store, {

  schema: new Schema({
    name   : { type: 'string', searchable: true, trim: 60 },
    surname: { type: 'string', searchable: true, trim: 60 },
  }),

  storeName: 'managers',
  publicURL: '/managers/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  sortableFields: [ 'name', 'surname' ],

  nested: [
    {
      type: 'multiple',
      store: 'managersCars',
      join: { managerId: 'id' },
    }
  ],

});
var managers = new Managers(); 

var ManagersCars = declare( Store, {

  schema: new Schema({
    make     : { type: 'string', trim: 60, searchable: true, required: true },
    model    : { type: 'string', trim: 60, searchable: true, required: true },
  }),

  storeName: 'managersCars',
  publicURL: '/managers/:managerId/cars/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  sortableFields: [ 'make', 'model', 'managers.name' ],

  nested: [
    {
      type: 'lookup',
      localField: 'managerId',
      store: 'managers',
    }
  ],
});
var managersCars = new ManagersCars();

JsonRestStores.init();
managers.protocolListen( 'HTTP', { app: app } );;
managersCars.protocolListen( 'HTTP', { app: app } );;

In this case, I didn't define onlineSearchSchema nor queryConditions: the store will get the default ones provided by JsonRestStores.

Note how sortableFields is an array of fields that will be taken into consideration. Each element of the array will be a field in the schema itself.

It is interesting how one of the sortable fields is managers.name: since managers is a nested table, its sub-fields can be used as sorting fields (as long as they are declared as searchable in their store's schema).

If the client doesn't provide any sorting options, you can decide a list of default fields that will be applied automatically. This is useful when you want to retrieve, for example, a list of comments and want to make sure that they are returned in chronological order without having to get the client to specify any sorting optinons.

For example:

var Comments = declare( Store, {

  schema: new Schema({
    subject: { type: 'string', searchable: true, trim: 60 },
    body   : { type: 'string', searchable: true, trim: 4096 },
    posted : { type: 'date',   searchable: true, protected: true, default: function(){ return new Date() } },
  }),

  storeName: 'comments',
  publicURL: '/comments/:id',

  handlePut: true,
  handlePost: true,
  handleGet: true,
  handleGetQuery: true,
  handleDelete: true,

  defaultSort: {
    posted: -1
  },

});
var comments = new Comments(); 

JsonRestStores.init();
comments.protocolListen( 'HTTP', { app: app } );;

This will ensure that comments are always retrieved in reversed order, newest first. Since sortableFields is not defined, the default order (by posted) is the only possible one for this store.

The position attribute

When creating a store, you can set the position parameter as true. For example:

    var Managers= declare( Store, {
 
      schema: new Schema({
        name   : { type: 'string', trim: 60 },
        surname: { type: 'string', trim: 60 },
      }),
 
      position: true,
 
      storeName: 'managers',
      publicURL: '/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
    var managers = new Managers();
 
    JsonRestStores.init();
    managers.protocolListen( 'HTTP', { app: app } );;

The position attribute means that PUT and POST calls will have to honour positioning based on options.putBefore and options.putDefaultPosition.

The main use of position: true is that when no sorting is requested by the client, the items will be ordered correctly depending on their "natural" positioning.

Positioning will keep into account the store's paramIds when applying positioning. This means that if you have a store like this:

    var Managers= declare( Store, {
 
      schema: new Schema({
        workspaceId: { type: 'id' },
        name       : { type: 'string', trim: 60 },
        surname    : { type: 'string', trim: 60 },
      }),
 
      position: true,
 
      storeName: 'managers',
      publicURL: '/workspaces/:workspaceId/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
    var managers = new Managers();

Positioning will have to take into account workspaceId when repositioning: if an user in workspace A repositions an item, it mustn't affect positioning in workspace B. Basically, when doing positioning, paramIds define the domain of repositioning (in this case, elements with matching workspaceIds will belong to the same domain).

deleteAfterGetQuery: automatic deletion of records after retrieval

If your store has the deleteAfterGetQuery set to true, it will automatically delete any elements fetched with a getQuery method (that is, a GET run without the final id, and therefore fetching elements based on a filter). This is done by forcing options.delete to true (unless it was otherwise defined) in makeGetQuery() .

This is especially useful when a store has, for example, a set of records that need to be retrieved by a user only once (like message queues).

hardLimitOnQueries: limit the number of records

If your store has the hardLimitOnQueries set, any getQuery method (that is, a GET without the final id, and therefore fetching elements based on a filter) will never return more than hardLimitOnQueries results (unless you are using JsonRestStore's API, and manually set options.skipHardLimitOnQueries to true).

Stores and collections when using SimpleDbLayerMixin

When using SimpleDbLayerMixin (which is the most common case, unless you are implementing data manipulation functions on your own), a SimpleDbLayer collection will be created using the following attributes passed to the store:

  • idProperty: the same as store.idProperty

  • schema: the same as store.schema

  • nested: the same as store.nested

  • hardLimitOnQueries: the same as store.hardLimitOnQueries

  • strictSchemaOnFetch: the same as store.strictSchemaOnFetch

  • schemaError: set as store.UnprocessableEntityError, which is the same as e.UnprocessableEntityError (from the allhttperrors module)

  • fetchChildrenByDefault: set to true

  • positionField: set to __position if store.position is set to true

  • positionBase: set as a copy of store.paramIds, after cutting out the last item

The collection's name will match storeName, unless you pass a store.collectionName attribute.

Note that if a collection with a matching collectionName was already defined, then that collection is effectively reused by SimpleDbLayerMixin. In this case, the following attribute in the JsonRestStore store will be forced to match the SimpleDbLayer's collection's attributes:

  • idProperty (actually if the collection's idProperty doesn't match the store's, an error is thrown)
  • store.schema
  • store.nested (see next section)
  • store.hardLimitOnQueries
  • store.strictSchemaOnFetch

This allows you to potentially define SimpleDbLayer's layers beforehand, and then use them in JsonRestStores by defining a collectionName matching an existing table's name. If you decide to do so, remember to set fetchChildrenByDefault to true, and schemaError to e.UnprocessableEntityError (where e comes from the module allhttperrors). You will also need to set your own positionField and positionBase manually if you want positioning to happen. Generally speaking, it's just easier and better to let JsonRestStores create your SimpleDbLayer collections.

Using SimpleDbLayerMixin implies that you are using an indexed collection. SimpleDbLayer's layer have a method called generateSchemaIndexes( options ) which will generate indexed for the collections based on the schema. These indexes are most likely all you will ever need. If not, please refer to the Indexing section in SimpleDbLayer to know more about indexing, remembering that you can always access the SimpleDbLayer instance for a table through store.dbLayer.

While developing, you should also remember to run:

store.dbLayer.generateSchemaIndexes( options, function( err ){
// ...
});

Alternatively, you can just run one command that will cover all of your collections:

DbLayer.generateSchemaIndexesAllLayers( options, function( err ){
// ...
});

Automatic schema changes done by SimpleDbLayerMixin

The searchable attribute in the schema is really important: in SimpleDbLayer, for example, only searchable fields are actually searchable, and indexes are created automatically for them.

When defining a schema in JsonRestStores with SimpleDbLayerMixin mixed in, the following happens automatically:

  • Any element in paramIds will be marked as searchable in the store's schema. This means that writing:
    var Managers= declare( Store, {
 
      schema: new Schema({
        workspaceId: { type: 'id' },
        name       : { type: 'string', trim: 60 },
        surname    : { type: 'string', trim: 60 },
      }),
 
      position: true,
 
      storeName: 'managers',
      publicURL: '/workspaces/:workspaceId/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
    var managers = new Managers();

Is the same as writing:

    var Managers= declare( Store, {
 
      schema: new Schema({
        id         : { type: 'id', searchable: true },
        workspaceId: { type: 'id', searchable: true },
        name       : { type: 'string', trim: 60 },
        surname    : { type: 'string', trim: 60 },
      }),
 
      position: true,
 
      storeName: 'managers',
      publicURL: '/workspaces/:workspaceId/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
    var managers = new Managers();

Note that searchable is set both for id and for workspaceId (which are the store's paramIds, as they are defined in publicURL).

  • Any database field mentioned anywhere in queryConditions will also be made searchable in the main schema. This means that writing:
    var Managers = declare( Store, {
 
      schema: new Schema({
        name   : { type: 'string', trim: 60 },
        surname: { type: 'string', trim: 60 }, // Note: surname is NOT searchable
      }),
 
      onlineSearchSchema: new Schema( {
        surnameSearch: { type: 'string', trim: 60 },
      }),
 
      queryConditions: { 
        type: 'startsWith', 
        args: [ 'surname', '#surnameSearch#']
      },
 
      storeName: 'managers',
      publicURL: '/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
 
    var managers = new Managers(); 
 
    JsonRestStores.init();
    managers.protocolListen( 'HTTP', { app: app } );;

Is the same as writing:

    var Managers = declare( Store, {
 
      schema: new Schema({
        name   : { type: 'string', trim: 60 },
        surname: { type: 'string', searchable: true, trim: 60 }, // Note: surname IS searchable
      }),
 
      onlineSearchSchema: new Schema( {
        surnameSearch: { type: 'string', trim: 60 },
      }),
 
      queryConditions: { 
        type: 'startsWith', 
        args: [ 'surname', '#surnameSearch#']
      },
 
      storeName: 'managers',
      publicURL: '/managers/:id',
 
      handlePut: true,
      handlePost: true,
      handleGet: true,
      handleGetQuery: true,
      handleDelete: true,
    });
 
    var managers = new Managers(); 
 
    JsonRestStores.init();
    managers.protocolListen( 'HTTP', { app: app } );;

This is accomplished by SimpleDbLayerMixin by actually going through the whole queryConditions and checking that every database field mentioned in it is made searchable in the main schema.

Inheriting a store from another one (advanced)

At this point it's clear that stores are defined as constructor classes, which are then used -- only once -- to create a store variable. For example the constructor Managers() is used to create the managers store with managers = new Managers().

This allows you to define a base store, and derive stores off that base store. For example:

 
    // The base WorkspacesUsersBase constructor
    // Note that the collectionName is defined to something different to
    // storeName
 
    var WorkspacesUsersBase = declare( Store, {
 
      schema: new HotSchema({
        id         : { type: 'id', searchable: true },
        userId     : { type: 'id', searchable: true },
        workspaceId: { type: 'id', searchable: true },
      }),
 
      storeName: 'workspacesUsersBase',
      collectionName: 'workspacesUsers',
 
      idProperty: 'id',
 
      // NOTE: no paramIds nor publicURL is defined.
    });
    stores.workspacesUsersBase = new WorkspacesUsersBase();
 
    // The specialised WorkspacesUsers constructor, which
    // define an onlineSearchSchema and publicURL
 
    var WorkspacesUsers = declare( WorkspacesUsersBase, {
 
      storeName:  'workspacesUsers',
      collectionName: 'workspacesUsers',
 
      publicURL: '/workspaces/:workspaceId/users/:id',
 
      handleGetQuery: true,
 
    });
    stores.workspacesUsers = new WorkspacesUsers();
 
    // The specialised UsersWorkspaces constructor, which
    // define an onlineSearchSchema and publicURL
 
    var UsersWorkspaces = declare( WorkspacesUsersBase, {
 
      storeName:  'usersWorkspaces',
      collectionName: 'workspacesUsers',
 
      publicURL: '/users/:userId/workspaces/:id',
      
      handleGetQuery: true,
 
    });
    stores.usersWorkspaces = new UsersWorkspaces();
``