waterline
Advanced tools
Comparing version 0.0.5 to 0.0.51
419
adapter.js
var _ = require('underscore'); | ||
var parley = require('parley'); | ||
var uuid = require('node-uuid'); | ||
// Read global config | ||
var config = require('./config.js'); | ||
// Extend adapter definition | ||
var Adapter = module.exports = function (adapter) { | ||
module.exports = function(adapter) { | ||
var self = this; | ||
// Absorb configuration | ||
this.config = adapter.config || {}; | ||
this.config = adapter.config = _.extend({ | ||
// Default transaction collection name | ||
transactionCollection: _.clone(config.transactionCollection) | ||
this.initialize = function(cb) { | ||
var self = this; | ||
}, adapter.config || {}); | ||
// When process ends, close all open connections | ||
process.on('SIGINT', process.exit); | ||
process.on('SIGTERM', process.exit); | ||
process.on('exit', function () { self.teardown(); }); | ||
// Absorb identity | ||
this.identity = adapter.identity; | ||
// Set scheme based on `persistent` options | ||
this.config.scheme = this.config.persistent ? 'alter' : 'drop'; | ||
// Initialize is fired once-per-adapter | ||
this.initialize = function(cb) { | ||
adapter.initialize ? adapter.initialize(cb) : cb(); | ||
}; | ||
this.teardown = function (cb) { | ||
// Teardown is fired once-per-adapter | ||
// (i.e. tear down any remaining connections to the underlying data model) | ||
this.teardown = function(cb) { | ||
adapter.teardown ? adapter.teardown(cb) : (cb && cb()); | ||
}; | ||
// teardownCollection is fired once-per-collection | ||
// (i.e. flush data to disk before the adapter shuts down) | ||
this.teardownCollection = function(collectionName,cb) { | ||
adapter.teardownCollection ? adapter.teardownCollection(collectionName, cb) : (cb && cb()); | ||
}; | ||
@@ -36,25 +45,33 @@ | ||
////////////////////////////////////////////////////////////////////// | ||
this.define = function(collectionName, definition, cb) { | ||
this.define = function(collectionName, definition, cb) { | ||
// Grab attributes from definition | ||
var attributes = definition.attributes || {}; | ||
// If id is not defined, add it | ||
// TODO: Make this check for ANY primary key | ||
// TODO: Make this disableable in the config | ||
if (!definition.attributes.id) { | ||
definition.attributes.id = { | ||
if(this.config.defaultPK && !attributes.id) { | ||
attributes.id = { | ||
type: 'INTEGER', | ||
primaryKey: true, | ||
autoIncrement: true | ||
autoIncrement: true, | ||
'default': 'AUTO_INCREMENT', | ||
constraints: { | ||
unique: true, | ||
primaryKey: true | ||
} | ||
}; | ||
} | ||
// If the config allows it, and they aren't already specified, | ||
// If the adapter config allows it, and they aren't already specified, | ||
// extend definition with updatedAt and createdAt | ||
if(config.createdAt && !definition.createdAt) definition.createdAt = 'DATE'; | ||
if(config.updatedAt && !definition.updatedAt) definition.updatedAt = 'DATE'; | ||
var now = {type: 'DATE', 'default': 'NOW'}; | ||
if(this.config.createdAt && !attributes.createdAt) attributes.createdAt = now; | ||
if(this.config.updatedAt && !attributes.updatedAt) attributes.updatedAt = now; | ||
// Convert string-defined attributes into fully defined objects | ||
for (var attr in definition.attributes) { | ||
if(_.isString(definition[attr])) { | ||
definition[attr] = { | ||
type: definition[attr] | ||
for(var attr in attributes) { | ||
if(_.isString(attributes[attr])) { | ||
attributes[attr] = { | ||
type: attributes[attr] | ||
}; | ||
@@ -64,23 +81,20 @@ } | ||
// Grab schema from definition | ||
var schema = definition.attributes; | ||
// Verify that collection doesn't already exist | ||
// and then define it and trigger callback | ||
this.describe(collectionName,function (err,existingSchema) { | ||
if (err) return cb(err,schema); | ||
else if (existingSchema) return cb("Trying to define a collection ("+collectionName+") which already exists with schema:",existingSchema); | ||
else return ( adapter.define ? adapter.define(collectionName,schema,cb) : cb() ); | ||
this.describe(collectionName, function(err, existingAttributes) { | ||
if(err) return cb(err, attributes); | ||
else if(existingAttributes) return cb("Trying to define a collection (" + collectionName + ") which already exists."); | ||
else return(adapter.define ? adapter.define(collectionName, attributes, cb) : cb()); | ||
}); | ||
}; | ||
this.describe = function(collectionName, cb) { | ||
adapter.describe ? adapter.describe(collectionName,cb) : cb(); | ||
this.describe = function(collectionName, cb) { | ||
adapter.describe ? adapter.describe(collectionName, cb) : cb(); | ||
}; | ||
this.drop = function(collectionName, cb) { | ||
this.drop = function(collectionName, cb) { | ||
// TODO: foreach through and delete all of the models for this collection | ||
adapter.drop ? adapter.drop(collectionName,cb) : cb(); | ||
adapter.drop ? adapter.drop(collectionName, cb) : cb(); | ||
}; | ||
this.alter = function (collectionName,newAttrs,cb) { | ||
adapter.alter ? adapter.alter(collectionName,newAttrs,cb) : cb(); | ||
this.alter = function(collectionName, newAttrs, cb) { | ||
adapter.alter ? adapter.alter(collectionName, newAttrs, cb) : cb(); | ||
}; | ||
@@ -92,33 +106,23 @@ | ||
////////////////////////////////////////////////////////////////////// | ||
// TODO: ENSURE ATOMICITY | ||
this.create = function(collectionName, values, cb) { | ||
// Get status if specified | ||
if (adapter.status) adapter.status(collectionName,afterwards); | ||
else afterwards(); | ||
var self = this; | ||
if(!collectionName) return cb("No collectionName specified!"); | ||
if(!adapter.create) return cb("No create() method defined in adapter!"); | ||
// Modify values as necessary | ||
function afterwards(err,status){ | ||
if (err) throw err; | ||
// TODO: Populate default values | ||
// Auto increment fields that need it | ||
adapter.autoIncrement(collectionName,values,function (err,values) { | ||
if (err) return cb(err); | ||
// TODO: Validate constraints using Anchor | ||
// Automatically add updatedAt and createdAt (if enabled) | ||
if (self.config.createdAt) values.createdAt = new Date(); | ||
if (self.config.updatedAt) values.updatedAt = new Date(); | ||
// TODO: Verify constraints using Anchor | ||
adapter.create ? adapter.create(collectionName, values, cb) : cb(); | ||
// Add updatedAt and createdAt | ||
if (adapter.config.createdAt) values.createdAt = new Date(); | ||
if (adapter.config.updatedAt) values.updatedAt = new Date(); | ||
// Call create method in adapter | ||
return adapter.create ? adapter.create(collectionName,values,cb) : cb(); | ||
}); | ||
} | ||
// TODO: Return model instance Promise object for joins, etc. | ||
}; | ||
this.find = function(collectionName, options, cb) { | ||
if(!adapter.find) return cb("No find() method defined in adapter!"); | ||
options = normalizeCriteria(options); | ||
adapter.find ? adapter.find(collectionName,options,cb) : cb(); | ||
adapter.find ? adapter.find(collectionName, options, cb) : cb(); | ||
@@ -128,10 +132,17 @@ // TODO: Return model instance Promise object for joins, etc. | ||
this.update = function(collectionName, criteria, values, cb) { | ||
if(!adapter.update) return cb("No update() method defined in adapter!"); | ||
criteria = normalizeCriteria(criteria); | ||
adapter.update ? adapter.update(collectionName,criteria,values,cb) : cb(); | ||
// TODO: Validate constraints using Anchor | ||
// TODO: Automatically change updatedAt (if enabled) | ||
adapter.update ? adapter.update(collectionName, criteria, values, cb) : cb(); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
}; | ||
this.destroy = function(collectionName, criteria, cb) { | ||
if(!adapter.destroy) return cb("No destroy() method defined in adapter!"); | ||
criteria = normalizeCriteria(criteria); | ||
adapter.destroy ? adapter.destroy(collectionName,criteria,cb) : cb(); | ||
adapter.destroy ? adapter.destroy(collectionName, criteria, cb) : cb(); | ||
@@ -144,11 +155,11 @@ // TODO: Return model instance Promise object for joins, etc. | ||
////////////////////////////////////////////////////////////////////// | ||
this.findOrCreate = function (collectionName, criteria, values, cb) { | ||
var self = this; | ||
this.findOrCreate = function(collectionName, criteria, values, cb) { | ||
var self = this; | ||
criteria = normalizeCriteria(criteria); | ||
if (adapter.findOrCreate) adapter.findOrCreate(collectionName, criteria, values, cb); | ||
if(adapter.findOrCreate) adapter.findOrCreate(collectionName, criteria, values, cb); | ||
else { | ||
// TODO: ADD A TRANSACTION LOCK HERE!! | ||
self.find(collectionName,criteria,function (err,results) { | ||
if (err) cb(err); | ||
else if (results.length > 0) cb(null,results); | ||
self.find(collectionName, criteria, function(err, results) { | ||
if(err) cb(err); | ||
else if(results && results.length > 0) cb(null, results); | ||
else self.create(collectionName, values, cb); | ||
@@ -160,5 +171,5 @@ }); | ||
}; | ||
this.findAndUpdate = function (collectionName, criteria, values, cb) { | ||
this.findAndUpdate = function(collectionName, criteria, values, cb) { | ||
criteria = normalizeCriteria(criteria); | ||
if (adapter.findAndUpdate) adapter.findAndUpdate(collectionName, criteria, values, cb); | ||
if(adapter.findAndUpdate) adapter.findAndUpdate(collectionName, criteria, values, cb); | ||
else this.update(collectionName, criteria, values, cb); | ||
@@ -168,5 +179,5 @@ | ||
}; | ||
this.findAndDestroy = function (collectionName, criteria, cb) { | ||
this.findAndDestroy = function(collectionName, criteria, cb) { | ||
criteria = normalizeCriteria(criteria); | ||
if (adapter.findAndDestroy) adapter.findAndDestroy(collectionName, criteria, cb); | ||
if(adapter.findAndDestroy) adapter.findAndDestroy(collectionName, criteria, cb); | ||
else this.destroy(collectionName, criteria, cb); | ||
@@ -178,69 +189,111 @@ | ||
// App-level transaction | ||
this.transaction = function(transactionName, cb) { | ||
var self = this; | ||
// Generate unique lock | ||
var newLock = { | ||
uuid: uuid.v4(), | ||
name: transactionName, | ||
timestamp: epoch(), | ||
cb: cb | ||
}; | ||
// console.log("Generating lock "+newLock.uuid+" ("+transactionName+")"); | ||
// write new lock to commit log | ||
this.transactionCollection.create(newLock, function(err) { | ||
if(err) return cb(err, function() { | ||
throw err; | ||
}); | ||
// Begin an atomic transaction | ||
// lock models in collection which fit criteria (if criteria is null, lock all) | ||
this.lock = function (collectionName, criteria, cb) { | ||
// Check if lock was written, and is the oldest with the proper name | ||
self.transactionCollection.findAll(function(err, locks) { | ||
if(err) return cb(err, function() { | ||
throw err; | ||
}); | ||
// Allow criteria argument to be omitted | ||
if (_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
} | ||
var conflict = false; | ||
_.each(locks, function(entry) { | ||
// ************************************** | ||
// NAIVE SOLUTION | ||
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note) | ||
// If a conflict IS found, respect the oldest | ||
// (the conflict-causer is responsible for cleaning up his entry-- ignore it!) | ||
if(entry.name === newLock.name && entry.uuid !== newLock.uuid && true && //entry.timestamp <= newLock.timestamp && | ||
entry.id < newLock.id) conflict = entry; | ||
}); | ||
// No need to check the fridge! Just start writing your note. | ||
// If there are no conflicts, the lock is acquired! | ||
if(!conflict) { | ||
self.lock(newLock, cb); | ||
} else { | ||
// console.log("************ Conflict encountered:: lock already exists for that transaction!!"); | ||
// console.log("MY LOCK:: transaction: "+newLock.name," uuid: "+newLock.uuid, "timestamp: ",newLock.timestamp); | ||
// console.log("CONFLICTING LOCK:: transaction: "+conflict.name," uuid: "+conflict.uuid, "timestamp: ",conflict.timestamp); | ||
// console.log("***************"); | ||
} | ||
// TODO: Generate identifier for this transaction (use collection name to start with, | ||
// but better yet, boil down criteria to essentials to allow for more concurrent access) | ||
// TODO: Create entry in transaction DB (write a note on the fridge and check it) | ||
// TODO: Check the transaction db (CHECK THE DAMN FRIDGE IN CASE ONE OF YOUR ROOMMATES WROTE THE NOTE WHILE YOU WERE BUSY) | ||
// Otherwise, get in line | ||
// In other words, do nothing-- | ||
// unlock() will grant lock request in order it was received | ||
}); | ||
}); | ||
}; | ||
// TODO: If > 1 entry exists in the transaction db, subscribe to mutex queue to be notified later | ||
// (if you see a note already on the fridge, get in line to be notified when roommate gets home) | ||
this.lock = function(newLock, cb) { | ||
var self = this; | ||
// console.log("====> Lock acquired "+newLock.uuid+" ("+newLock.name+")"); | ||
var warningTimer = setTimeout(function() { | ||
console.error("Transaction :: " + newLock.name + " is taking an abnormally long time (> " + self.config.transactionWarningTimer + "ms)"); | ||
}, self.config.transactionWarningTimer); | ||
// TODO: Otherwise, trigger callback! QA immediately (you're good to go get the milk) | ||
cb(null, function unlock(cb) { | ||
clearTimeout(warningTimer); | ||
self.unlock(newLock.uuid, newLock.name, cb); | ||
}); | ||
}; | ||
// ************************************** | ||
// AGRESSIVE SOLUTION | ||
// (all roommates try to go get the milk, but the first person to get the milk prevents others from putting it in the fridge) | ||
// TODO: Ask locksmith for model clone | ||
// TODO: Pass model clone in callback | ||
this.unlock = function(uuid, transactionName, cb) { | ||
var self = this; | ||
// console.log("Releasing lock "+uuid+" ("+transactionName+")"); | ||
// Remove current lock | ||
self.transactionCollection.destroy({ | ||
uuid: uuid | ||
}, function(err) { | ||
if(err) return cb && cb(err); | ||
// console.log("<≠≠≠≠≠ Lock released :: "+uuid+" ("+transactionName+")"); | ||
self.transactionCollection.findAll(function(err, locks) { | ||
if(err) return cb && cb(err); | ||
adapter.lock ? adapter.lock(collectionName,criteria,cb) : cb(); | ||
}; | ||
// Determine the next user in line (oldest lock w/ the proper transactionName) | ||
var nextInLine = getNextLock(locks, transactionName); | ||
// nextInLine ? console.log("Preparing to hand off lock to "+nextInLine.uuid+" ("+nextInLine.name+")") : console.log("No locks remaining !!!"); | ||
// Trigger unlock's callback if specified | ||
cb && cb(); | ||
// Commit and end an atomic transaction | ||
// unlock models in collection which fit criteria (if criteria is null, unlock all) | ||
this.unlock = function (collectionName, criteria, cb) { | ||
// Now allow the nextInLine lock to be acquired | ||
// This marks the end of the previous transaction | ||
nextInLine && self.lock(nextInLine, nextInLine.cb); | ||
// Allow criteria argument to be omitted | ||
if (_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
} | ||
}); | ||
}); | ||
}; | ||
// ************************************** | ||
// NAIVE SOLUTION | ||
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note) | ||
// TODO: Remove entry from transaction db (Remove your note from fridge) | ||
// TODO: Callback can be triggered immediately, since you're sure the note will be removed | ||
// Find the oldest lock with the same transaction name | ||
// ************************************************************ | ||
// this function wouldn't be necessary if we could.... | ||
// TODO: call find() with the [currently unfinished] ORDER option | ||
// ************************************************************ | ||
adapter.unlock ? adapter.unlock(collectionName,criteria,cb) : cb(); | ||
}; | ||
function getNextLock(locks, transactionName) { | ||
var nextLock; | ||
_.each(locks, function(lock) { | ||
// Ignore locks with different names | ||
if(lock.name !== transactionName) return; | ||
this.status = function (collectionName, cb) { | ||
adapter.status ? adapter.status(collectionName,cb) : cb(); | ||
}; | ||
// If this is the first one, or this lock is older than the one we have, use it | ||
if(!nextLock || lock.timestamp < nextLock.timestamp) nextLock = lock; | ||
}); | ||
return nextLock; | ||
} | ||
this.autoIncrement = function (collectionName, values,cb) { | ||
adapter.autoIncrement ? adapter.autoIncrement(collectionName, values, cb) : cb(); | ||
}; | ||
// If @collectionName and @otherCollectionName are both using this adapter, do a more efficient remote join. | ||
@@ -253,3 +306,2 @@ // (By default, an inner join, but right and left outer joins are also supported.) | ||
// Sync given collection's schema with the underlying data model | ||
// Scheme can be 'drop' or 'alter' | ||
// Controls whether database is dropped and recreated when app starts, | ||
@@ -260,9 +312,10 @@ // or whether waterline will try and synchronize the schema with the app models. | ||
// Drop and recreate collection | ||
drop: function(collection,cb) { | ||
drop: function(collection, cb) { | ||
var self = this; | ||
this.drop(collection.identity,function (err,data) { | ||
self.define(collection.identity,collection,cb); | ||
this.drop(collection.identity, function(err, data) { | ||
if(err) cb(err); | ||
else self.define(collection.identity, collection, cb); | ||
}); | ||
}, | ||
// Alter schema | ||
@@ -273,34 +326,76 @@ alter: function(collection, cb) { | ||
// Check that collection exists-- if it doesn't go ahead and add it and get out | ||
this.describe(collection.identity,function (err,data) { | ||
if (err) throw err; | ||
else if (!data) return self.define(collection.identity,collection,cb); | ||
}); | ||
this.describe(collection.identity, function(err, data) { | ||
data = _.clone(data); | ||
// Iterate through each attribute on each model in your app | ||
_.each(collection.attributes, function checkAttribute(attribute) { | ||
// and make sure that a comparable field exists in the data store | ||
// TODO | ||
}); | ||
if(err) return cb(err); | ||
else if(!data) return self.define(collection.identity, collection, cb); | ||
// Check that the attribute exists in the data store | ||
// TODO | ||
// TODO: move all of this to the alter() call in the adapter | ||
// If not, alter the collection to include it | ||
// TODO | ||
// If it *DOES* exist, we'll try to guess what changes need to be made | ||
// Iterate through each attribute in this collection | ||
// and make sure that a comparable field exists in the model | ||
// TODO | ||
// Iterate through each attribute in this collection's schema | ||
_.each(collection.attributes, function checkAttribute(attribute,attrName) { | ||
// Make sure that a comparable field exists in the data store | ||
if (!data[attrName]) { | ||
data[attrName] = attribute; | ||
// If not, alter the collection and remove it | ||
// TODO | ||
cb(); | ||
// Add the default value for this new attribute to each row in the data model | ||
// TODO | ||
} | ||
// And that it matches completely | ||
else { | ||
data[attrName] = attribute; | ||
// Update the data belonging to this attribute to reflect the new properties | ||
// Realistically, this will mainly be about constraints, and primarily uniquness | ||
// It'd be good if waterline could enforce all constraints at this time, | ||
// but there's a trade-off with destroying people's data | ||
// TODO | ||
} | ||
}); | ||
// Now iterate through each attribute in the adapter's data store | ||
// and remove any that don't have an analog in the collection definition | ||
// Also prune the data belonging to removed attributes from rows | ||
// TODO: | ||
// Persist that | ||
// Check that the attribute exists in the data store | ||
// TODO | ||
// If not, alter the collection to include it | ||
// TODO | ||
// Iterate through each attribute in this collection | ||
// and make sure that a comparable field exists in the model | ||
// TODO | ||
// If not, alter the collection and remove it | ||
// TODO | ||
// cb(); | ||
}); | ||
}, | ||
// Do nothing to the underlying data model | ||
safe: function (collection,cb) { | ||
cb(); | ||
} | ||
}; | ||
// Always grant access to a few of Adapter's methods to the user adapter instance | ||
// (things that may or may not be defined by the user adapter) | ||
adapter.transaction = function(name, cb) { | ||
return self.transaction(name, cb); | ||
}; | ||
// adapter.teardown = adapter.teardown || self.teardown; | ||
// adapter.teardownCollection = adapter.teardownCollection || self.teardownCollection; | ||
// Bind adapter methods to self | ||
_.bindAll(adapter); | ||
_.bindAll(this); | ||
_.bind(this.sync.drop,this); | ||
_.bind(this.sync.alter,this); | ||
_.bind(this.sync.drop, this); | ||
_.bind(this.sync.alter, this); | ||
@@ -316,3 +411,4 @@ // Mark as valid adapter | ||
*/ | ||
function plural (collection, application) { | ||
function plural(collection, application) { | ||
if(_.isArray(collection)) { | ||
@@ -328,6 +424,11 @@ return _.map(collection, application); | ||
// Normalize the different ways of specifying criteria into a uniform object | ||
function normalizeCriteria (criteria) { | ||
function normalizeCriteria(criteria) { | ||
if(!criteria) return { | ||
where: null | ||
}; | ||
// Empty undefined values from criteria object | ||
_.each(criteria,function(val,key) { | ||
if (val === undefined) delete criteria[key]; | ||
_.each(criteria, function(val, key) { | ||
if(val === undefined) delete criteria[key]; | ||
}); | ||
@@ -343,11 +444,11 @@ | ||
} | ||
if (!criteria.where && !criteria.limit && | ||
!criteria.skip && !criteria.offset && | ||
!criteria.order) { | ||
criteria = { where: criteria }; | ||
if(!criteria.where && !criteria.limit && !criteria.skip && !criteria.offset && !criteria.order) { | ||
criteria = { | ||
where: criteria | ||
}; | ||
} | ||
// If any item in criteria is a parsable finite number, use that | ||
for (var attrName in criteria.where) { | ||
if (Math.pow(+criteria.where[attrName],2) > 0) { | ||
for(var attrName in criteria.where) { | ||
if(Math.pow(+criteria.where[attrName], 2) > 0) { | ||
criteria.where[attrName] = +criteria.where[attrName]; | ||
@@ -358,2 +459,8 @@ } | ||
return criteria; | ||
} | ||
// Number of miliseconds since the Unix epoch Jan 1st, 1970 | ||
function epoch() { | ||
return(new Date()).getTime(); | ||
} |
@@ -8,2 +8,11 @@ // Dependencies | ||
// ****************************************** | ||
// Poor man's auto-increment | ||
// ****************************************** | ||
// In production, the transaction database should be set to something else | ||
// with true, database-side auto-increment capabilities | ||
// This in-memory auto-increment will not scale to a multi-instance / cluster setup. | ||
// ****************************************** | ||
var statusDb = {}; | ||
/*--------------------- | ||
@@ -22,23 +31,13 @@ :: DirtyAdapter | ||
// Attributes are case insensitive by default | ||
// attributesCaseSensitive: false, | ||
// If inMemory is true, all data will be destroyed when the server stops | ||
inMemory: true, | ||
// What persistence scheme is being used? | ||
// Is the db dropped & recreated each time or persisted to disc? | ||
persistent: false, | ||
// File path for disk file output (in persistent mode) | ||
// File path for disk file output (when NOT in inMemory mode) | ||
dbFilePath: './.waterline/dirty.db', | ||
// String to precede key name for schema defininitions | ||
schemaPrefix: 'sails_schema_', | ||
schemaPrefix: 'waterline_schema_', | ||
// String to precede key name for actual data in collection | ||
dataPrefix: 'sails_data_', | ||
// String to precede key name for collection status data | ||
statusPrefix: 'sails_status_', | ||
// String to precede key name for transaction data | ||
lockPrefix: 'sails_transactions_' | ||
dataPrefix: 'waterline_data_' | ||
}, | ||
@@ -50,11 +49,12 @@ | ||
if(this.config.persistent) { | ||
if(! this.config.inMemory) { | ||
// Check that dbFilePath file exists and build tree as necessary | ||
require('fs-extra').touch(this.config.dbFilePath, function (err) { | ||
if (err) return cb(err); | ||
require('fs-extra').touch(this.config.dbFilePath, function(err) { | ||
if(err) return cb(err); | ||
my.db = new(dirty.Dirty)(my.config.dbFilePath); | ||
afterwards(); | ||
}); | ||
} | ||
else { | ||
} else { | ||
this.db = new(dirty.Dirty)(); | ||
@@ -65,2 +65,3 @@ afterwards(); | ||
function afterwards() { | ||
// Make logger easily accessible | ||
@@ -76,6 +77,16 @@ my.log = my.config.log; | ||
// Tear down any remaining connections to the underlying data model | ||
teardown: function(cb) { | ||
this.db = null; | ||
cb && cb(); | ||
// Logic to handle flushing collection data to disk before the adapter shuts down | ||
teardownCollection: function(collectionName, cb) { | ||
var my = this; | ||
// Always go ahead and write the new auto-increment to disc, even though it will be wrong sometimes | ||
// (this is done so that the auto-increment counter can be "ressurected" when the adapter is restarted from disk) | ||
// console.log("******** Wrote to "+collectionName+":: AI => ", statusDb[collectionName].autoIncrement); | ||
var schema = _.extend(this.db.get(this.config.schemaPrefix + collectionName),{ | ||
autoIncrement: statusDb[collectionName].autoIncrement | ||
}); | ||
this.db.set(this.config.schemaPrefix + collectionName, schema, function (err) { | ||
my.db = null; | ||
cb && cb(err); | ||
}); | ||
}, | ||
@@ -85,20 +96,27 @@ | ||
// Fetch the schema for a collection | ||
describe: function(collectionName, cb) { | ||
var schema, err; | ||
try { | ||
schema = this.db.get(this.config.schemaPrefix+collectionName); | ||
} | ||
catch (e) { | ||
err = e; | ||
} | ||
this.log(" DESCRIBING :: "+collectionName,{ | ||
err: err, | ||
schema: schema | ||
// (contains attributes and autoIncrement value) | ||
describe: function(collectionName, cb) { | ||
this.log(" DESCRIBING :: " + collectionName); | ||
var schema = this.db.get(this.config.schemaPrefix + collectionName); | ||
var attributes = schema && schema.attributes; | ||
return cb(null, attributes); | ||
}, | ||
// Fetch the current auto-increment value | ||
getAutoIncrement: function (collectionName,cb) { | ||
var schema = this.db.get(this.config.schemaPrefix + collectionName); | ||
return cb(err,schema.autoIncrement); | ||
}, | ||
// Persist the current auto-increment value | ||
setAutoIncrement: function (collectionName,cb) { | ||
this.db.set(this.config.schemaPrefix + collectionName, { | ||
}); | ||
return cb(err,schema); | ||
return cb(err,schema.autoIncrement); | ||
}, | ||
// Create a new collection | ||
define: function(collectionName, schema, cb) { | ||
this.log(" DEFINING "+collectionName, { | ||
define: function(collectionName, attributes, cb) { | ||
this.log(" DEFINING " + collectionName, { | ||
as: schema | ||
@@ -108,8 +126,14 @@ }); | ||
var schema = { | ||
attributes: _.clone(attributes), | ||
autoIncrement: 1 | ||
}; | ||
// Write schema and status objects | ||
return self.db.set(this.config.schemaPrefix+collectionName,schema,function (err) { | ||
if (err) return cb(err); | ||
return self.db.set(self.config.statusPrefix+collectionName,{ | ||
autoIncrement: 1 | ||
},cb); | ||
return self.db.set(this.config.schemaPrefix + collectionName, schema, function(err) { | ||
if(err) return cb(err); | ||
// Set in-memory schema for this collection | ||
statusDb[collectionName] = schema; | ||
cb(); | ||
}); | ||
@@ -121,6 +145,6 @@ }, | ||
var self = this; | ||
self.log(" DROPPING "+collectionName); | ||
return self.db.rm(self.config.dataPrefix+collectionName,function (err) { | ||
if (err) return cb("Could not drop collection!"); | ||
return self.db.rm(self.config.schemaPrefix+collectionName,cb); | ||
self.log(" DROPPING " + collectionName); | ||
return self.db.rm(self.config.dataPrefix + collectionName, function(err) { | ||
if(err) return cb("Could not drop collection!"); | ||
return self.db.rm(self.config.schemaPrefix + collectionName, cb); | ||
}); | ||
@@ -131,8 +155,6 @@ }, | ||
alter: function(collectionName, newAttrs, cb) { | ||
this.log(" ALTERING "+collectionName); | ||
this.db.describe(collectionName,function (e0,existingSchema) { | ||
if (err) return cb(collectionName+" does not exist!"); | ||
var schema = _.extend(existingSchema,newAttrs); | ||
return this.db.set(this.config.schemaPrefix+collectionName,schema,cb); | ||
}); | ||
this.log(" ALTERING " + collectionName); | ||
var schema = this.db.get(this.config.schemaPrefix + collectionName); | ||
schema = _.extend(schema.attributes, newAttrs); | ||
return this.db.set(this.config.schemaPrefix + collectionName, schema, cb); | ||
}, | ||
@@ -144,14 +166,30 @@ | ||
create: function(collectionName, values, cb) { | ||
this.log(" CREATING :: "+collectionName,values); | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
this.log(" CREATING :: " + collectionName, values); | ||
values = values || {}; | ||
var dataKey = this.config.dataPrefix + collectionName; | ||
var data = this.db.get(dataKey); | ||
var self = this; | ||
// Create new model | ||
// (if data collection doesn't exist yet, create it) | ||
data = data || []; | ||
data.push(values); | ||
// Replace data collection and go back | ||
this.db.set(dataKey,data,function (err) { | ||
cb(err,values); | ||
// Lookup schema & status so we know all of the attribute names and the current auto-increment value | ||
var schema = this.db.get(this.config.schemaPrefix + collectionName); | ||
// Auto increment fields that need it | ||
doAutoIncrement(collectionName, schema.attributes, values, this, function (err, values) { | ||
if (err) return cb(err); | ||
self.describe(collectionName, function(err, attributes) { | ||
if(err) return cb(err); | ||
// TODO: add other fields with default values | ||
// Create new model | ||
// (if data collection doesn't exist yet, create it) | ||
data = data || []; | ||
data.push(values); | ||
// Replace data collection and go back | ||
self.db.set(dataKey, data, function(err) { | ||
return cb(err, values); | ||
}); | ||
}); | ||
}); | ||
@@ -164,3 +202,3 @@ }, | ||
find: function(collectionName, options, cb) { | ||
var criteria = options.where; | ||
@@ -174,8 +212,7 @@ | ||
//////////////////////////////////////////////// | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var dataKey = this.config.dataPrefix + collectionName; | ||
var data = this.db.get(dataKey); | ||
// Query and return result set using criteria | ||
cb(null,applyFilter(data,criteria)); | ||
cb(null, applyFilter(data, criteria)); | ||
}, | ||
@@ -185,3 +222,3 @@ | ||
update: function(collectionName, options, values, cb) { | ||
this.log(" UPDATING :: "+collectionName,{ | ||
this.log(" UPDATING :: " + collectionName, { | ||
options: options, | ||
@@ -200,4 +237,3 @@ values: values | ||
//////////////////////////////////////////////// | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var dataKey = this.config.dataPrefix + collectionName; | ||
var data = this.db.get(dataKey); | ||
@@ -207,22 +243,22 @@ | ||
var resultIndices = []; | ||
_.each(data,function (row,index) { | ||
my.log('matching row/index',{ | ||
_.each(data, function(row, index) { | ||
my.log('matching row/index', { | ||
row: row, | ||
index: index | ||
}); | ||
my.log("against",criteria); | ||
my.log("with outcome",matchSet(row,criteria)); | ||
my.log("against", criteria); | ||
my.log("with outcome", matchSet(row, criteria)); | ||
if (matchSet(row,criteria)) resultIndices.push(index); | ||
if(matchSet(row, criteria)) resultIndices.push(index); | ||
}); | ||
this.log("filtered indices::",resultIndices,'criteria',criteria); | ||
this.log("filtered indices::", resultIndices, 'criteria', criteria); | ||
// Update value(s) | ||
_.each(resultIndices,function(index) { | ||
data[index] = _.extend(data[index],values); | ||
_.each(resultIndices, function(index) { | ||
data[index] = _.extend(data[index], values); | ||
}); | ||
// Replace data collection and go back | ||
this.db.set(dataKey,data,function (err) { | ||
cb(err,values); | ||
this.db.set(dataKey, data, function(err) { | ||
cb(err, values); | ||
}); | ||
@@ -233,3 +269,3 @@ }, | ||
destroy: function(collectionName, options, cb) { | ||
this.log(" DESTROYING :: "+collectionName,options); | ||
this.log(" DESTROYING :: " + collectionName, options); | ||
@@ -244,13 +280,12 @@ var criteria = options.where; | ||
//////////////////////////////////////////////// | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var dataKey = this.config.dataPrefix + collectionName; | ||
var data = this.db.get(dataKey); | ||
// Query result set using criteria | ||
data = _.reject(data,function (row,index) { | ||
return matchSet(row,criteria); | ||
data = _.reject(data, function(row, index) { | ||
return matchSet(row, criteria); | ||
}); | ||
// Replace data collection and go back | ||
this.db.set(dataKey,data,function (err) { | ||
this.db.set(dataKey, data, function(err) { | ||
cb(err); | ||
@@ -261,100 +296,3 @@ }); | ||
// Look for auto-increment fields, increment counters accordingly, and return refined values | ||
autoIncrement: function (collectionName, values, cb) { | ||
// Lookup schema & status so we know all of the attribute names and the current auto-increment value | ||
var schema = this.db.get(this.config.schemaPrefix+collectionName); | ||
var status = this.db.get(this.config.statusPrefix+collectionName); | ||
var self = this; | ||
// if this is an autoIncrement field, increment it in values set | ||
var keys = _.keys(_.extend({},schema,values)); | ||
async.forEach(keys, function (attrName,cb) { | ||
if (_.isObject(schema[attrName]) && schema[attrName].autoIncrement) { | ||
values[attrName] = status.autoIncrement; | ||
// Then, increment the status db persistently | ||
status.autoIncrement++; | ||
self.db.set(self.config.statusPrefix+collectionName,status,cb); | ||
} | ||
else cb(); | ||
}, function (err) { | ||
return cb(err,values); | ||
}); | ||
}, | ||
// 2PL | ||
// Lock access to this collection or a subset of it | ||
// (assume this is a 'write' lock) | ||
// TODO: Smarter locking (i.e. don't always lock the entire collection) | ||
lock: function (collectionName, criteria, cb) { | ||
var self = this; | ||
var commitLogKey = this.config.lockPrefix+collectionName; | ||
var locks = this.db.get(commitLogKey); | ||
locks = locks || []; | ||
// Generate unique lock and update commit log as if lock was acquired | ||
var newLock = { | ||
id: uuid.v4(), | ||
type: 'write', | ||
timestamp: epoch(), | ||
cb: cb | ||
}; | ||
locks.push(newLock); | ||
// Write commit log to disc | ||
this.db.set(commitLogKey,locks,function (err) { | ||
if (err) return cb(err); | ||
// Verify that lock was successfully acquired | ||
// (i.e. no other locks w/ overlapping criteria exist) | ||
var conflict = false; | ||
locks = self.db.get(commitLogKey); | ||
_.each(locks,function (entry) { | ||
// If a conflict IS found, respect the oldest | ||
// (the conflict-causer is responsible for cleaning up his entry) | ||
if (entry.id !== newLock.id && | ||
entry.timestamp <= newLock.timestamp) conflict = true; | ||
// Otherwise, other lock is older-- ignore it | ||
}); | ||
// Lock acquired! | ||
if (!conflict) { | ||
self.log("Acquired lock :: "+newLock.id); | ||
cb(); | ||
} | ||
// Otherwise, get in line | ||
// In other words, do nothing-- | ||
// unlock() will grant lock request in order it was received | ||
}); | ||
}, | ||
unlock: function (collectionName, criteria, cb) { | ||
var self = this; | ||
var commitLogKey = this.config.lockPrefix+collectionName; | ||
var locks = this.db.get(commitLogKey); | ||
locks = locks || []; | ||
// Guess current lock by grabbing the oldest | ||
// (this will only work if unlock() is used inside of a transaction) | ||
var currentLock = getOldest(locks); | ||
if (!currentLock) return cb('Trying to unlock, but no lock exists!'); | ||
// Remove currentLock | ||
locks = _.without(locks,currentLock); | ||
this.db.set(commitLogKey,locks,function (err) { | ||
// Trigger unlock's callback | ||
if (err) return cb(err); | ||
else cb(); | ||
// Now allow the next user in line to acquire the lock (trigger the NEW oldest lock's callback) | ||
// This marks the end of the previous transaction | ||
var nextInLine = getOldest(locks); | ||
nextInLine && nextInLine.cb(); | ||
}); | ||
}, | ||
// Identity is here to facilitate unit testing | ||
@@ -371,10 +309,30 @@ // (this is optional and normally automatically populated based on filename) | ||
// Look for auto-increment field, increment counter accordingly, and return refined value set | ||
function doAutoIncrement (collectionName, attributes, values, ctx, cb) { | ||
// Determine the attribute names which will be included in the created object | ||
var attrNames = _.keys(_.extend({}, attributes, values)); | ||
// increment AI fields in values set | ||
_.each(attrNames, function(attrName) { | ||
if(_.isObject(attributes[attrName]) && attributes[attrName].autoIncrement) { | ||
values[attrName] = statusDb[collectionName].autoIncrement; | ||
// Then, increment the auto-increment counter for this collection | ||
statusDb[collectionName].autoIncrement++; | ||
} | ||
}); | ||
// Return complete values set w/ auto-incremented data | ||
return cb(null,values); | ||
} | ||
// Run criteria query against data aset | ||
function applyFilter (data,criteria) { | ||
if (criteria) { | ||
return _.filter(data,function (model) { | ||
return matchSet(model,criteria); | ||
function applyFilter(data, criteria) { | ||
if(criteria && data) { | ||
return _.filter(data, function(model) { | ||
return matchSet(model, criteria); | ||
}); | ||
} | ||
else return data; | ||
} else return data; | ||
} | ||
@@ -384,6 +342,10 @@ | ||
// Match a model against each criterion in a criteria query | ||
function matchSet (model, criteria) { | ||
function matchSet(model, criteria) { | ||
// Null WHERE query always matches everything | ||
if(!criteria) return true; | ||
// By default, treat entries as AND | ||
for (var key in criteria) { | ||
if (! matchItem(model,key,criteria[key])) return false; | ||
for(var key in criteria) { | ||
if(!matchItem(model, key, criteria[key])) return false; | ||
} | ||
@@ -394,23 +356,25 @@ return true; | ||
function matchOr (model, disjuncts) { | ||
function matchOr(model, disjuncts) { | ||
var outcome = false; | ||
_.each(disjuncts,function (criteria) { | ||
if (matchSet(model,criteria)) outcome = true; | ||
_.each(disjuncts, function(criteria) { | ||
if(matchSet(model, criteria)) outcome = true; | ||
}); | ||
return outcome; | ||
} | ||
function matchAnd (model, conjuncts) { | ||
function matchAnd(model, conjuncts) { | ||
var outcome = true; | ||
_.each(conjuncts,function (criteria) { | ||
if (!matchSet(model,criteria)) outcome = false; | ||
_.each(conjuncts, function(criteria) { | ||
if(!matchSet(model, criteria)) outcome = false; | ||
}); | ||
return outcome; | ||
} | ||
function matchLike (model, criteria) { | ||
for (var key in criteria) { | ||
function matchLike(model, criteria) { | ||
for(var key in criteria) { | ||
// Make attribute names case insensitive unless overridden in config | ||
if (! adapter.config.attributesCaseSensitive) key = key.toLowerCase(); | ||
if(!adapter.config.attributesCaseSensitive) key = key.toLowerCase(); | ||
// Check that criterion attribute and is at least similar to the model's value for that attr | ||
if ( !model[key] || (!~model[key].indexOf(criteria[key]) ) ) { | ||
if(!model[key] || (!~model[key].indexOf(criteria[key]))) { | ||
return false; | ||
@@ -421,24 +385,22 @@ } | ||
} | ||
function matchNot (model,criteria) { | ||
return !matchSet(model,criteria); | ||
function matchNot(model, criteria) { | ||
return !matchSet(model, criteria); | ||
} | ||
function matchItem (model,key,criterion) { | ||
function matchItem(model, key, criterion) { | ||
// Make attribute names case insensitive unless overridden in config | ||
if (! adapter.config.attributesCaseSensitive) key = key.toLowerCase(); | ||
if(!adapter.config.attributesCaseSensitive) key = key.toLowerCase(); | ||
if (key.toLowerCase() === 'or') { | ||
return matchOr(model,criterion); | ||
if(key.toLowerCase() === 'or') { | ||
return matchOr(model, criterion); | ||
} else if(key.toLowerCase() === 'not') { | ||
return matchNot(model, criterion); | ||
} else if(key.toLowerCase() === 'and') { | ||
return matchAnd(model, criterion); | ||
} else if(key.toLowerCase() === 'like') { | ||
return matchLike(model, criterion); | ||
} | ||
else if (key.toLowerCase() === 'not') { | ||
return matchNot(model,criterion); | ||
} | ||
else if (key.toLowerCase() === 'and') { | ||
return matchAnd(model,criterion); | ||
} | ||
else if (key.toLowerCase() === 'like') { | ||
return matchLike(model,criterion); | ||
} | ||
// Otherwise this is an attribute name: ensure it exists and matches | ||
else if ( !model[key] || (model[key] !== criterion)) { | ||
else if(!model[key] || (model[key] !== criterion)) { | ||
return false; | ||
@@ -450,14 +412,16 @@ } | ||
// Number of miliseconds since the Unix epoch Jan 1st, 1970 | ||
function epoch () { | ||
return (new Date()).getTime(); | ||
function epoch() { | ||
return(new Date()).getTime(); | ||
} | ||
// Return the oldest lock in the collection | ||
function getOldest (locks) { | ||
function getOldest(locks) { | ||
var currentLock; | ||
_.each(locks,function (lock) { | ||
if (!currentLock) currentLock = lock; | ||
else if (lock.timestamp < currentLock.timestamp) currentLock = lock; | ||
_.each(locks, function(lock) { | ||
if(!currentLock) currentLock = lock; | ||
else if(lock.timestamp < currentLock.timestamp) currentLock = lock; | ||
}); | ||
return currentLock; | ||
} |
@@ -6,12 +6,22 @@ var _ = require('underscore'); | ||
var Collection = module.exports = function(definition) { | ||
var self = this; | ||
// Sync (depending on scheme) | ||
definition.scheme = definition.scheme || 'alter'; | ||
switch (definition.scheme) { | ||
case "drop" : definition.sync = _.bind(definition.adapter.sync.drop, definition.adapter, definition); break; | ||
case "alter": definition.sync = _.bind(definition.adapter.sync.alter, definition.adapter, definition); break; | ||
// Not having a scheme is not an error, just default to alter | ||
// default : throw new Error('Invalid scheme in '+definition.identity+' model!'); | ||
// ******************************************************************** | ||
// Configure collection-specific configuration | ||
// Copy over only the methods from the adapter that you need, and modify if necessary | ||
// ******************************************************************** | ||
// (defaults to 'alter') | ||
definition.migrate = !_.isUndefined(definition.migrate) ? definition.migrate : 'alter'; | ||
if (definition.migrate === 'drop') { | ||
definition.sync = _.bind(definition.adapter.sync.drop, definition.adapter, definition); | ||
} | ||
else if (definition.migrate === 'alter') { | ||
definition.sync = _.bind(definition.adapter.sync.alter, definition.adapter, definition); | ||
} | ||
else if (definition.migrate === 'safe') { | ||
definition.sync = _.bind(definition.adapter.sync.safe, definition.adapter, definition); | ||
} | ||
@@ -23,2 +33,6 @@ // Absorb definition methods | ||
this.create = function(values, cb) { | ||
if (_.isFunction(values)) { | ||
cb = values; | ||
values = null; | ||
} | ||
return this.adapter.create(this.identity,values,cb); | ||
@@ -28,4 +42,10 @@ }; | ||
this.find = function(options, cb) { | ||
if (_.isFunction(options) || !options) { | ||
throw new Error('find(criteria,callback) requires a criteria parameter. To get all models in a collection, use findAll()'); | ||
} | ||
return this.adapter.find(this.identity,options,cb); | ||
}; | ||
this.findAll = function (cb) { | ||
return this.adapter.find(this.identity,null,cb); | ||
}; | ||
// Call update method in adapter | ||
@@ -50,11 +70,5 @@ this.update = function(criteria, values, cb) { | ||
this.lock = function(criteria, cb) { | ||
return this.adapter.lock(this.identity,criteria,cb); | ||
this.transaction = function (name, cb) { | ||
return this.adapter.transaction(name, cb); | ||
}; | ||
this.unlock = function(criteria, cb) { | ||
return this.adapter.unlock(this.identity,criteria,cb); | ||
}; | ||
this.cancel = function(criteria, cb) { | ||
return this.adapter.cancel(this.identity,criteria,cb); | ||
}; | ||
@@ -61,0 +75,0 @@ ////////////////////////////////////////// |
@@ -0,3 +1,9 @@ | ||
// Global adapter defaults | ||
module.exports = { | ||
// This uses an auto-incrementing integer attirbute (id) as the primary key for the collection | ||
// This is a common pattern and best-practice in relational and non-relational databases, | ||
// since it eliminates confusion when more than one developer hops on the project | ||
defaultPK: true, | ||
// Automatically define updatedAt field in schema and refresh with the current timestamp when models are updated | ||
@@ -7,3 +13,21 @@ updatedAt: true, | ||
// Automatically define createdAt field in schema and populate with the current timestamp during model creation | ||
createdAt: true | ||
createdAt: true, | ||
// Attributes are case insensitive by default | ||
// attributesCaseSensitive: false, | ||
// Define a collection to use for app-level transactions | ||
// TODO: replace this with convention of the "Transaction.js" collection | ||
transactionCollection: require('./collections/Transaction.js'), | ||
// ms to wait before warning that a tranaction is taking too long | ||
// TODO: move this logic as a configuration option into the actual transaction collection | ||
transactionWarningTimer: 2000, | ||
// ms to wait before timing out a transaction and calling unlock() with an error | ||
// (App can then handle the logic to undo the transaction) | ||
// TODO: Make this work | ||
// TODO: move this logic as a configuration option into the actual transaction collection | ||
transactionTimeout: 15000 | ||
}; |
{ | ||
"name": "waterline", | ||
"version": "0.0.5", | ||
"version": "0.0.51", | ||
"description": "Adaptable data access layer for Node.js", | ||
@@ -38,4 +38,5 @@ "main": "waterline.js", | ||
"sails-util": "0.0.0", | ||
"parley": "0.0.2" | ||
"parley": "0.0.2", | ||
"microtime": "~0.3.3" | ||
} | ||
} |
@@ -0,7 +1,11 @@ | ||
// Keep a reference to waterline to use for teardown() | ||
var waterline = require("../waterline.js"); | ||
var adapters, collections; | ||
var _ = require('underscore'); | ||
var parley = require('parley'); | ||
var assert = require("assert"); | ||
var waterline = require('../waterline'); | ||
var collections = require('../buildDictionary.js')(__dirname + '/collections', /(.+)\.js$/); | ||
module.exports = { | ||
@@ -12,2 +16,4 @@ | ||
teardown: teardown, | ||
// Override every collection's adapter with the specified adapter | ||
@@ -26,11 +32,23 @@ // Then return the init() method | ||
// Initialize waterline | ||
function initialize (exit) { | ||
require("../waterline.js")({ | ||
function initialize (done) { | ||
waterline({ | ||
collections: collections, | ||
log: blackhole | ||
}, exit); | ||
}, function (err, waterlineData){ | ||
adapters = waterlineData.adapters; | ||
collections = waterlineData.collections; | ||
done(err); | ||
}); | ||
} | ||
// Tear down adapters and collections | ||
function teardown (done) { | ||
waterline.teardown({ | ||
adapters: adapters, | ||
collections: collections | ||
},done); | ||
} | ||
// Use silent logger for testing | ||
// (this prevents annoying output from cluttering up our nice clean console) | ||
var blackhole = function (){}; |
@@ -9,4 +9,10 @@ /*--------------------- | ||
// exports.scheme = 'alter'; | ||
// What synchronization scheme to use: default is 'alter' | ||
// | ||
// 'drop' => Delete the database and recreate it when the server starts | ||
// 'alter' => Do a best-guess automatic migration from the existing data model to the new one | ||
// 'safe' => Never automatically synchonize-- leave the underlying data alone | ||
exports.migrate = 'drop'; | ||
// Attributes for the user data model | ||
exports.attributes = { | ||
@@ -16,3 +22,4 @@ name : 'STRING', | ||
title : 'STRING', | ||
phone : 'STRING' | ||
phone : 'STRING', | ||
type : 'STRING' | ||
}; | ||
@@ -19,0 +26,0 @@ |
@@ -153,3 +153,6 @@ /** | ||
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts | ||
after(bootstrap.teardown); | ||
}); | ||
}; |
@@ -14,8 +14,8 @@ /** | ||
var buildDictionary = require('../buildDictionary.js'); | ||
var bootstrap = require('./bootstrap.test.js'); | ||
describe('waterline', function() { | ||
// Bootstrap waterline with default adapters and bundled test collections | ||
before(require('./bootstrap.test.js').init); | ||
before(bootstrap.init); | ||
@@ -27,2 +27,5 @@ describe('#initialize() and sync()', function() { | ||
}); | ||
}); | ||
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts | ||
after(bootstrap.teardown); | ||
}); |
@@ -12,2 +12,3 @@ /** | ||
var parley = require('parley'); | ||
var async = require('async'); | ||
var assert = require("assert"); | ||
@@ -29,36 +30,106 @@ | ||
describe('static semaphore (collection-wide lock)', function() { | ||
describe('app-level transaction', function() { | ||
it('should be able to acquire lock', function(done) { | ||
User.lock(null, done); | ||
User.transaction("test", function(err, unlock) { | ||
unlock(); | ||
done(err); | ||
}); | ||
}); | ||
it('should NOT allow another lock to be acquired until the first lock is released', function(done) { | ||
var orderingTest = []; | ||
// The callback should not fire until the lock is released | ||
User.lock(null, function (err) { | ||
if (err) throw err; | ||
orderingTest.push('lock'); | ||
if (orderingTest[0] === 'unlock' && | ||
orderingTest[1] === 'lock' && | ||
orderingTest.length === 2) { | ||
done(); | ||
} | ||
else throw "The lock was acquired by two users at once!"; | ||
User.transaction('test', function(err, unlock1) { | ||
if(err) return done(err); | ||
if(!unlock1) throw new Error("No unlock() method provided!"); | ||
testAppendLock(); | ||
User.transaction('test', function(err, unlock2) { | ||
if(err) return done(err); | ||
if(!unlock2) throw new Error("No unlock() method provided!"); | ||
testAppendLock(); | ||
// Release lock so other tests can use the 'test' transaction | ||
unlock2(); | ||
if(_.isEqual(orderingTest, ['lock', 'unlock', 'lock'])) done(); | ||
else { | ||
console.error(orderingTest); | ||
throw "The lock was acquired by two users at once!"; | ||
} | ||
}); | ||
// Set timeout to release the lock after a 1/20 of a second | ||
setTimeout(function() { | ||
unlock1(testAppendUnlock); | ||
}, 50); | ||
}); | ||
// Note that other code can still run while the semaphore remains gated | ||
// Appends "unlock" to the orderingTest array and handles any errors | ||
// Set timeout to release the lock after a 1/4 of a second | ||
setTimeout(function () { | ||
User.unlock(null,function(err){ | ||
orderingTest.push('unlock'); | ||
function testAppendUnlock(err) { | ||
if(err) throw err; | ||
orderingTest.push('unlock'); | ||
} | ||
// Appends "lock" to the orderingTest array and handles any errors | ||
function testAppendLock(err) { | ||
if(err) throw err; | ||
orderingTest.push('lock'); | ||
} | ||
}); | ||
it('should support 10 simultaneous dummy transactions', function(done) { | ||
var constellations = ['Andromeda', 'Antlia', 'Apus', 'Aquarius', 'Aquila', 'Ara', 'Aries', 'Auriga', 'Boötes', 'Caelum']; | ||
dummyTransactionTest(constellations,'constellation',done); | ||
}); | ||
it('should support 50 simultaneous dummy transactions', function(done) { | ||
dummyTransactionTest(_.range(50),'number test 1',done); | ||
}); | ||
it('should support 200 simultaneous dummy transactions', function(done) { | ||
dummyTransactionTest(_.range(200),'number test 2',done); | ||
}); | ||
function dummyTransactionTest(items,type,done) { | ||
async.forEach(items, function(constellation, cb) { | ||
User.transaction('test_create',function(err,unlock) { | ||
User.create({ | ||
name: constellation, | ||
type: type | ||
},function(err) { | ||
// Wait a short moment to introduce an element of choas | ||
setTimeout(function() { | ||
unlock(); | ||
cb(); | ||
},Math.round(Math.random())*5); | ||
}); | ||
}); | ||
},250); | ||
}, function(err) { | ||
User.find({ type: type },function (err,users) { | ||
if(users.length != items.length) { | ||
console.error("Users: "); | ||
console.error(users); | ||
return done('Proper users were not created!'); | ||
} | ||
done(); | ||
}); | ||
}); | ||
} | ||
}); | ||
// it ('should timeout if the transaction takes a long time', function (done) {}); | ||
// it('should not be able to release a lock more than once', function (done) {}); | ||
}); | ||
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts | ||
after(bootstrap.teardown); | ||
}); |
147
waterline.js
@@ -11,2 +11,5 @@ // Dependencies | ||
// Read global config | ||
var config = require('./config.js'); | ||
// Util | ||
@@ -18,2 +21,3 @@ var buildDictionary = require('./buildDictionary.js'); | ||
/** | ||
@@ -23,23 +27,93 @@ * Prepare waterline to interact with adapters | ||
module.exports = function (options,cb) { | ||
// Only tear down waterline once | ||
// (if teardown() is called explicitly, don't tear it down when the process exits) | ||
var tornDown = false; | ||
var adapters = options.adapters; | ||
var collections = options.collections; | ||
var adapters = options.adapters || {}; | ||
var collections = options.collections || {}; | ||
var log = options.log || console.log; | ||
var $$ = new parley(); | ||
// Merge passed-in adapters with default adapters | ||
adapters = _.extend(builtInAdapters,adapters || {}); | ||
adapters = _.extend(builtInAdapters,adapters); | ||
// Error aggregator obj | ||
var errs; | ||
// var errs; | ||
// initialize each adapter in series | ||
// TODO: parallelize this process (would decrease server startup time) | ||
for (var adapterName in adapters) { | ||
$$(async).forEach(_.keys(adapters),prepareAdapter); | ||
// Instantiate special collections | ||
var transactionsDb = $$(instantiateCollection)(config.transactionCollection); | ||
// Attach transaction collection to each adapter | ||
$$(function (err,transactionsDb,xcb) { | ||
_.each(adapters,function(adapter,adapterName){ | ||
adapters[adapterName].transactionCollection = transactionsDb; | ||
}); | ||
xcb(); | ||
})(transactionsDb); | ||
// TODO: in sails, in the same way ----> | ||
// set up session adapter | ||
// set up socket adapter | ||
// -------> | ||
// then associate each collection with its adapter and sync its schema | ||
$$(async).forEach(_.keys(collections),prepareCollection); | ||
// Now that the others are instantiated, add transaction collection to list | ||
// (this is so events like teardownCollection() fire properly) | ||
$$(function (err,transactionsDb,xcb) { | ||
collections[transactionsDb.identity] = transactionsDb; | ||
xcb(); | ||
})(transactionsDb); | ||
// Fire teardown() on process-end and make it public | ||
// (this logic lives here to avoid assigning multiple events in each adapter and collection) | ||
$$(function (xcb) { | ||
// Make teardown() public | ||
module.exports.teardown = function(options,cb) { | ||
teardown({ | ||
adapters: adapters, | ||
collections: collections | ||
},cb); | ||
}; | ||
// When process ends, fire teardown | ||
process.on('SIGINT', process.exit); | ||
process.on('SIGTERM', process.exit); | ||
process.on('exit', function() { | ||
teardown({ | ||
adapters: adapters, | ||
collections: collections | ||
}); | ||
}); | ||
xcb(); | ||
})(); | ||
// Pass instantiated adapters and models | ||
$$(function (xcb) { | ||
cb(null,{ | ||
adapters: adapters, | ||
collections: collections | ||
}); | ||
xcb(); | ||
})(); | ||
// Instantiate an adapter object | ||
function prepareAdapter (adapterName,cb) { | ||
// Pass waterline config down to adapters | ||
adapters[adapterName].config = _.extend({ | ||
log: log | ||
}, adapters[adapterName].config); | ||
}, config, adapters[adapterName].config); | ||
@@ -51,11 +125,17 @@ // Build actual adapter object from definition | ||
// Load adapter data source | ||
$$(adapters[adapterName]).initialize(); | ||
adapters[adapterName].initialize(cb); | ||
} | ||
// When all adapters are loaded, | ||
// associate each model with its adapter and sync its schema | ||
collections = collections || {}; | ||
for (var collectionName in collections) { | ||
// Instantiate a collection object and store it back in the dictionary | ||
function prepareCollection (collectionName, cb) { | ||
var collection = collections[collectionName]; | ||
instantiateCollection(collection,function (err,collection) { | ||
collections[collectionName] = collection; | ||
cb(err,collection); | ||
}); | ||
} | ||
// Instantiate a collection object | ||
function instantiateCollection (collection, cb) { | ||
// If no adapter is specified, default to 'dirty' | ||
@@ -76,14 +156,39 @@ if (!collection.adapter) collection.adapter = 'dirty'; | ||
// Build actual collection object from definition | ||
collections[collectionName] = new Collection(collection); | ||
collection = new Collection(collection); | ||
// Synchronize schema with data source | ||
var e = $$(collection).sync(); | ||
$$(function (e) {errs = errs || e;}).ifError(e); | ||
collection.sync(function (err){ | ||
if (err) throw err; | ||
cb(err,collection); | ||
}); | ||
} | ||
// Pass instantiated adapters and models | ||
$$(cb)(errs,{ | ||
adapters: adapters, | ||
collections: collections | ||
}); | ||
}; | ||
// Tear down all open waterline adapters and collections | ||
function teardown (options,cb) { | ||
cb = cb || function(){}; | ||
// Only tear down once | ||
if (tornDown) return cb(); | ||
tornDown = true; | ||
// console.log(options.adapters); | ||
async.auto({ | ||
// Fire each adapter's teardown event | ||
adapters: function (cb) { | ||
async.forEach(_.values(options.adapters),function (adapter,cb) { | ||
adapter.teardown(cb); | ||
},cb); | ||
}, | ||
// Fire each collection's teardown event | ||
collections: function (cb) { | ||
async.forEach(_.values(options.collections),function (collection,cb) { | ||
collection.adapter.teardownCollection(collection.identity,cb); | ||
},cb); | ||
} | ||
}, cb); | ||
} | ||
}; | ||
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
61523
21
1602
0
41
9
+ Addedmicrotime@~0.3.3
+ Addedbindings@1.0.0(transitive)
+ Addedmicrotime@0.3.3(transitive)