Engine Constructor

Marak edited this page Aug 23, 2012 · 1 revision

In general, it is safe to attach instance methods to your new engine. For example, memory.js keeps a counter (called this.counter) for creating new documents without a specified name.

var engine = new Engine({
  uri: 'protocol://path/to/database'
});

At a minimum, the constructor should:

Interpret the 'uri' argument

The 'uri' argument should be treated as a unique ID to your particular data store. For example, this is a couchdb uri for the couchdb store.

In most cases the uri argument will correspond to a database url, but that's not always true. In the case of "memory", it's simply a legal javascript object property name.

Initialize a store

A constructed engine should, in some way or another, initialize a connection to its data store. For couchdb, this means opening a new connection object with cradle and attaching it as this.connection. However, this isn't a good fit for all cases; the memory store, for example, simply creates a new property to a "stores" object if stores["storeName"] doesn't exist.

Engine Instance Members

protocol

Resourceful will parse out the "couchdb" from the protocol and attempt to use an included resource with that string as its resource.protocol.

For third-party engines this may not seem critical but it's good practice to include anyway, for the purposes of inspection if nothing else.

Engine.prototype.protocol = 'file';

The protocol method sets the protocol member is used by resourceful to add syntactic sugar such that you may do:

Resource.connect('couchdb://example.nodejitsu.com');

Engine Instance Methods

Resourceful allows flexibility in some prototype methods, but not in others. Authors are encouraged to add prototype methods that feel natural to expose; for instance, the couchdb engine exposes this.prototype.head for sending http HEAD requests.

request()

Unlike some of the other prototype methods, request does not have to follow any particular contract, as it's used by your engine internally to encapsulate an asynchronous request to your particular datastore.

this.request(function () {

  var update = key in this.store;
  this.store[key] = val;
  callback(null, resourceful.mixin({ status: update ? 200 : 201 }, val));
});

In the case of the memory datastore, this simply involves a process.nextTick helper:

Memory.prototype.request = function (fn) {

  var self = this;

  process.nextTick(function () {
    fn.call(self);
  });
};

In the couchdb engine, requests look more like:

this.request('post', doc, function (e, res) {

  if (e) {
    return callback(e);
  }

  res.status = 201;
  callback(null, resourceful.mixin({}, doc, res));
});

An engine should expose the request interface that feels most natural given the transport. However, there are some conventions to follow:

  1. this.request should be asynchronous.
  2. The callback should set 'this' to be the same context as outside the callback

save()

Because the engines api was written with couchdb in mind, 'doc' should include an appropriate http status under doc.status.

save can be implemented using a combination of 'head', 'put' and 'post', as in the case of the couchdb engine. However, in the memory engine case put is an alias to save and update is implemented separately. See below: head, put and update. The following pattern should be followed across all engines:

engine.save('key', value, function (err, doc) {

  if (err) {
    throw err;
  }

  if (doc.status == 201) {
    // Will be 201 instead of 200 if the document is created instead of modified
    console.log('New document created!');
  }

  console.log(doc);
});

put()

put is typically used to represent operations that update or modify the database without creating new resources. However, it is acceptable to alias the 'save' method and allow for the creation of new resources.

Because the engines api was written with couchdb in mind, 'doc' should include an appropriate http status under doc.status. The expected status is '201'. See below: post. This pattern should be followed across all engines:

engine.put('key', value, function (err, doc) {

  if (err) {
    throw err;
  }

  if (doc.status === 201) {
    console.log('Document updated!');
  }
  else {
    throw new Error('Document did not update.');
  }

  console.log(doc);
});

post()

create()

This pattern should be followed across all engines for implementations of these methods. However, they are optional. The memory engine defines Engine.prototype.load instead. For instance:

engine.create('key', value, function (err, doc) {

  if (err) {
    throw err;
  }

  if (doc.status === 201) {
    console.log('Document updated!');
  }
  else {
    throw new Error('Status: '+doc.status);
  }

  console.log(doc);
});

post is typically used to represent operations that create new resources without modifying or updating existing ones. create should be implemented as an alias for post.

Because the engines api was written with couchdb in mind, 'doc' should include an appropriate http status under doc.status. The expected status is '201'.

load()

This method is optional and is used to more or less replace the "create" and "post" methods along with "put" and "save".

//
// Example with the memory transport
//
var memory = new Memory();

memory.load([ { 'foo': 'bar' }, { 'bar': 'baz' }]);

In the above example, each object passed to memory.load is loaded as a new document. This approach is useful in cases where you already have a javascript representation of your store (as in the case of memory) and don't need to interact with a remote api as in the case of couchdb.

update()

update is used to modify existing resources by copying enumerable properties from the update object to the existing object (often called a "mixin" and implemented in javascript in resourceful.mixin and utile.mixin). Besides the mixin process (meaning your stored object won't lose existing properties), update is synonymous with put, and in fact uses put internally in the case of both the couchdb and memory engines.

Because the engines api was written with couchdb in mind, 'doc' should include an appropriate http status under doc.status. The expected status is '201', as with put. This pattern should be followed across all engines:

engine.put('key', { 'foo': 'bar' }, function (err, doc) {

  if (err) {
    throw err;
  }

  if (doc.status === 201) {
    console.log('Document updated!');
  }
  else {
    throw new Error('Document did not update.');
  }

  console.log(doc); // doc.foo should now be bar

});

get()

This pattern should be followed across all engines:

engine.get('key', function (err, doc) {

  if (err) {
    if (err.status === 404) {
      console.log('Document was not there!');
    }

    throw err;
  }

  console.log(doc);
});

destroy()

destroy is used to delete existing resources.

Because the engines api was written with couchdb in mind, 'doc' should include an appropriate http status under doc.status. The expected status is '204', which stands for 'successfully deleted'. This pattern should be followed across all engines:

engine.get('key', function (err, doc) {

  if (err) {
    throw err;
  }

  //
  // "status" should be the only property on `doc`.
  //
  if (doc.status !== 204) {
    throw new Error('Status: '+doc.status);
  }

  console.log('Successfully destroyed document.');
});

find()

find is a shorthand for finding resources which in some cases can be implemented as a special case of filter, as with memory here:

Memory.prototype.find = function (conditions, callback) {

  this.filter(function (obj) {
    return Object.keys(conditions).every(function (k) {
      return conditions[k] ===  obj[k];
    });
  }, callback);
};

This pattern should be followed across all engines:

engine.find({ 'foo': 'bar' }, function (err, docs) {

  if (err) {
    throw err;
  }

  //
  // docs[0].foo === 'bar'
  //

});

The couchdb version, however, uses special logic as couchdb uses temporary and stored views.


  IMPORTANT NOTE
  --------------

  `CouchDB.prototype.find` uses a temporary view. This is useful while testing but is slow and bad practice on a production couch. Please use `CouchDB.prototype.filter` instead.

filter()

The semantics of 'filter' vary slightly depending on the engine. The semantics of filter(), like those of request(), should reflect the particular idioms of the underlying transport.

//
// Example used with a Memory engine
//
engine.filter(filterfxn, function (err, docs) {

  if (err) {
    throw err;
  }

  //
  // returned docs filtered by "filter"
  //

});

The "memory" case simply applies a function against the store's documents. In contrast, the couchdb engine exposes an api for using stored mapreduce functions on the couch:

//
// Example used with a Couchdb engine
//
engine.filter("view", params, function (err, docs) {

  if (err) {
    throw err;
  }

  //
  // returned docs filtered using the "view" mapreduce function on couch.
  //

});

sync()

Engine.prototype.sync is used to sync "design document" information with the database if necessary. This is specific to couchdb; for the 'memory' transport there is no conception of (or parallel to) a design document.

engine.sync(factory, function (err) {

  if (err) {
    throw err;
  }
});

In the case where there is no doc or "stored procedures" of any kind to upload to the database, this step can be simplified to:

Engine.prototype.sync = function (factory, callback) {

  process.nextTick(function () { callback(); });
};