-
Notifications
You must be signed in to change notification settings - Fork 56
Engine Constructor
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:
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.
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.
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');
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.
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:
-
this.request
should be asynchronous. - The callback should set 'this' to be the same context as outside the callback
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
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);
});
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'.
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
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
});
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
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
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.
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.
//
});
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(); });
};