waterline
Advanced tools
Comparing version 0.0.5 to 0.0.6
591
adapter.js
@@ -0,29 +1,51 @@ | ||
var async = require('async'); | ||
var _ = require('underscore'); | ||
var parley = require('parley'); | ||
var uuid = require('node-uuid'); | ||
// (for sorting) | ||
var MAX_INTEGER = 4294967295; | ||
// 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 = _.extend({}, adapter.config || {}); | ||
// Absorb identity | ||
this.identity = adapter.identity; | ||
// Initialize is fired once-per-adapter | ||
this.initialize = function(cb) { | ||
var self = this; | ||
if (adapter.initialize) adapter.initialize(cb); | ||
else cb(); | ||
}; | ||
// When process ends, close all open connections | ||
process.on('SIGINT', process.exit); | ||
process.on('SIGTERM', process.exit); | ||
process.on('exit', function () { self.teardown(); }); | ||
// Logic to handle the (re)instantiation of collections | ||
this.initializeCollection = function(collectionName, cb) { | ||
if (adapter.initializeCollection) { | ||
adapter.initializeCollection.apply(this,arguments); | ||
} | ||
else cb && cb(); | ||
}; | ||
// Set scheme based on `persistent` options | ||
this.config.scheme = this.config.persistent ? 'alter' : 'drop'; | ||
// Teardown is fired once-per-adapter | ||
// (i.e. tear down any remaining connections to the underlying data model) | ||
this.teardown = function(cb) { | ||
adapter.initialize ? adapter.initialize(cb) : cb(); | ||
}; | ||
if (adapter.teardown) adapter.teardown.apply(this,arguments); | ||
else cb && cb(); | ||
}; | ||
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) { | ||
if (adapter.teardownCollection) { | ||
adapter.teardownCollection.apply(this,arguments); | ||
} | ||
else cb && cb(); | ||
}; | ||
@@ -36,50 +58,101 @@ | ||
////////////////////////////////////////////////////////////////////// | ||
this.define = function(collectionName, definition, cb) { | ||
this.define = function(collectionName, definition, cb) { | ||
// 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 = { | ||
type: 'INTEGER', | ||
primaryKey: true, | ||
autoIncrement: true | ||
}; | ||
} | ||
// Grab attributes from definition | ||
var attributes = definition.attributes || {}; | ||
// If the 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'; | ||
// Marshal attributes to a standard format | ||
attributes = require('./augmentAttributes')(attributes,this.config); | ||
// Convert string-defined attributes into fully defined objects | ||
for (var attr in definition.attributes) { | ||
if(_.isString(definition[attr])) { | ||
definition[attr] = { | ||
type: definition[attr] | ||
}; | ||
} | ||
} | ||
// 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."); | ||
if (adapter.define) adapter.define(collectionName, attributes, cb); | ||
else cb(); | ||
}); | ||
}; | ||
this.describe = function(collectionName, cb) { | ||
adapter.describe ? adapter.describe(collectionName,cb) : cb(); | ||
this.describe = function(collectionName, cb) { | ||
if (adapter.describe) { | ||
adapter.describe.apply(this,arguments); | ||
} | ||
else 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(); | ||
this.drop = function(collectionName, cb) { | ||
if (adapter.drop) { | ||
adapter.drop.apply(this,arguments); | ||
} | ||
else cb(); | ||
}; | ||
this.alter = function (collectionName,newAttrs,cb) { | ||
adapter.alter ? adapter.alter(collectionName,newAttrs,cb) : cb(); | ||
this.alter = function(collectionName, attributes, cb) { | ||
var self = this; | ||
if (adapter.alter) { | ||
adapter.alter.apply(this,arguments); | ||
} | ||
else defaultAlter(cb); | ||
// Default behavior | ||
function defaultAlter(done) { | ||
// Alter the schema | ||
self.describe(collectionName, function afterDescribe (err, oldAttributes) { | ||
if (err) return done(err); | ||
// Keep track of previously undefined attributes | ||
// for use when updating the actual data | ||
var newAttributes = {}; | ||
// Iterate through each attribute in the new definition | ||
_.each(attributes, function checkAttribute(attribute,attrName) { | ||
// If the attribute doesn't exist, create it | ||
if (!oldAttributes[attrName]) { | ||
// console.log("new attr",attrName, attribute, "in ", collectionName); | ||
newAttributes[attrName] = attribute; | ||
} | ||
// If the old attribute is not exactly the same, or it doesn't exist, (re)create it | ||
if ( !oldAttributes[attrName] || !_.isEqual(oldAttributes[attrName],attribute) ) { | ||
oldAttributes[attrName] = attribute; | ||
} | ||
}); | ||
// Then alter the actual data as necessary | ||
self.findAll(collectionName,null, function afterFind (err,data) { | ||
if (err) return done(err); | ||
// 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: Figure this out | ||
// For new columns, just use the default value if one exists (otherwise use null) | ||
_.each(newAttributes, function checkAttribute(attribute,attrName) { | ||
if (attribute.defaultValue) { | ||
_.each(data,function (model) { | ||
model[attrName] = attribute.defaultValue; | ||
}); | ||
} | ||
}); | ||
// Create deferred object | ||
var $$ = new parley(); | ||
// Dumbly drop the table and redefine it | ||
$$(self).drop(collectionName); | ||
$$(self).define(collectionName, { attributes: attributes, identity: collectionName }); | ||
// Then dumbly add the data back in | ||
$$(self).createEach(collectionName,data); | ||
$$(function(xcb) { xcb(); done && done(); })(); | ||
}); | ||
}); | ||
} | ||
}; | ||
@@ -91,46 +164,77 @@ | ||
////////////////////////////////////////////////////////////////////// | ||
// TODO: ENSURE ATOMICITY | ||
this.create = function(collectionName, values, cb) { | ||
// Get status if specified | ||
if (adapter.status) adapter.status(collectionName,afterwards); | ||
else afterwards(); | ||
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(collectionName, values, cb); | ||
// Add updatedAt and createdAt | ||
if (adapter.config.createdAt) values.createdAt = new Date(); | ||
if (adapter.config.updatedAt) values.updatedAt = new Date(); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
}; | ||
// Call create method in adapter | ||
return adapter.create ? adapter.create(collectionName,values,cb) : cb(); | ||
}); | ||
} | ||
// Find a set of models | ||
this.findAll = function(collectionName, criteria, cb) { | ||
if(!adapter.find) return cb("No find() method defined in adapter!"); | ||
criteria = normalizeCriteria(criteria); | ||
if (_.isString(criteria)) return cb(criteria); | ||
adapter.find(collectionName, criteria, cb); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
}; | ||
this.find = function(collectionName, options, cb) { | ||
options = normalizeCriteria(options); | ||
adapter.find ? adapter.find(collectionName,options,cb) : cb(); | ||
// Find exactly one model | ||
this.find = function(collectionName, criteria, cb) { | ||
// If no criteria specified, use first model | ||
if (!criteria) criteria = {limit: 1}; | ||
// If no limit specified | ||
this.findAll(collectionName, criteria, function (err, models) { | ||
if (models.length < 1) return cb(); | ||
else if (models.length > 1) return cb("More than one "+collectionName+" returned!"); | ||
else return cb(null,models[0]); | ||
}); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
}; | ||
this.count = function(collectionName, criteria, cb) { | ||
var self = this; | ||
criteria = normalizeCriteria(criteria); | ||
if (!adapter.count) { | ||
self.findAll(collectionName, criteria, function (err,models){ | ||
cb(err,models.length); | ||
}); | ||
} | ||
else adapter.count(collectionName, criteria, cb); | ||
}; | ||
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(); | ||
if (_.isString(criteria)) return cb(criteria); | ||
// TODO: Validate constraints using Anchor | ||
// Automatically change updatedAt (if enabled) | ||
if (self.config.updatedAt) values.updatedAt = new Date(); | ||
adapter.update(collectionName, criteria, values, 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(); | ||
if (_.isString(criteria)) return cb(criteria); | ||
adapter.destroy(collectionName, criteria, cb); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
@@ -140,15 +244,28 @@ }; | ||
////////////////////////////////////////////////////////////////////// | ||
// Convenience methods (overwritable in adapters) | ||
// Compound methods (overwritable in adapters) | ||
////////////////////////////////////////////////////////////////////// | ||
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 (_.isString(criteria)) return cb(criteria); | ||
// If no values were specified, use criteria | ||
if (!values) values = criteria.where; | ||
if(adapter.findOrCreate) { | ||
adapter.findOrCreate(collectionName, criteria, values, cb); | ||
} | ||
// Default behavior | ||
// Warning: Inefficient! App-level tranactions should not be used for built-in compound queries. | ||
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); | ||
else self.create(collectionName, values, cb); | ||
}); | ||
// Create transaction name based on collection | ||
var transactionName = collectionName+'.waterline.default.create.findOrCreate'; | ||
self.transaction(transactionName, function (err,done) { | ||
self.find(collectionName, criteria, function(err, result) { | ||
if(err) done(err); | ||
else if(result) done(null, result); | ||
else self.create(collectionName, values, done); | ||
}); | ||
}, cb); | ||
} | ||
@@ -158,15 +275,41 @@ | ||
}; | ||
this.findAndUpdate = function (collectionName, criteria, values, cb) { | ||
criteria = normalizeCriteria(criteria); | ||
if (adapter.findAndUpdate) adapter.findAndUpdate(collectionName, criteria, values, cb); | ||
else this.update(collectionName, criteria, values, cb); | ||
// TODO: Return model instance Promise object for joins, etc. | ||
////////////////////////////////////////////////////////////////////// | ||
// Aggregate | ||
////////////////////////////////////////////////////////////////////// | ||
// If an optimized createEach exists, use it, otherwise use an asynchronous loop with create() | ||
this.createEach = function (collectionName, valuesList, cb) { | ||
var my = this; | ||
// Custom user adapter behavior | ||
if (adapter.createEach) adapter.createEach.apply(this,arguments); | ||
// Default behavior | ||
else { | ||
// Create transaction name based on collection | ||
my.transaction(collectionName+'..waterline.default.createEach', function (err,done) { | ||
async.forEach(valuesList, function (values,cb) { | ||
my.create(collectionName, values, cb); | ||
}, done); | ||
},cb); | ||
} | ||
}; | ||
this.findAndDestroy = function (collectionName, criteria, cb) { | ||
criteria = normalizeCriteria(criteria); | ||
if (adapter.findAndDestroy) adapter.findAndDestroy(collectionName, criteria, cb); | ||
else this.destroy(collectionName, criteria, cb); | ||
// If an optimized findOrCreateEach exists, use it, otherwise use an asynchronous loop with create() | ||
this.findOrCreateEach = function (collectionName, valuesList, cb) { | ||
var my = this; | ||
// TODO: Return model instance Promise object for joins, etc. | ||
// Custom user adapter behavior | ||
if (adapter.findOrCreateEach) adapter.findOrCreateEach(collectionName,valuesList,cb); | ||
// Default behavior | ||
else { | ||
// Create transaction name based on collection | ||
my.transaction(collectionName+'.waterline.default.createEach', function (err,done) { | ||
async.forEach(valuesList, function (values,cb) { | ||
my.findOrCreate(collectionName, values, null, cb); | ||
}, done); | ||
},cb); | ||
} | ||
}; | ||
@@ -176,68 +319,111 @@ | ||
////////////////////////////////////////////////////////////////////// | ||
// Concurrency | ||
////////////////////////////////////////////////////////////////////// | ||
/** | ||
* App-level transaction | ||
* @transactionName a unique identifier for this transaction | ||
* @atomicLogic the logic to be run atomically | ||
* @afterUnlock (optional) the function to trigger after unlock() is called | ||
*/ | ||
this.transaction = function(transactionName, atomicLogic, afterUnlock) { | ||
var self = this; | ||
// Begin an atomic transaction | ||
// lock models in collection which fit criteria (if criteria is null, lock all) | ||
this.lock = function (collectionName, criteria, cb) { | ||
// Generate unique lock | ||
var newLock = { | ||
uuid: uuid.v4(), | ||
name: transactionName, | ||
atomicLogic: atomicLogic, | ||
afterUnlock: afterUnlock | ||
}; | ||
// Allow criteria argument to be omitted | ||
if (_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
} | ||
// write new lock to commit log | ||
this.transactionCollection.create(newLock, function afterCreatingTransaction(err, newLock) { | ||
if(err) return atomicLogic(err, function() { | ||
throw err; | ||
}); | ||
// ************************************** | ||
// NAIVE SOLUTION | ||
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note) | ||
// Check if lock was written, and is the oldest with the proper name | ||
self.transactionCollection.findAll(function afterLookingUpTransactions(err, locks) { | ||
if(err) return atomicLogic(err, function() { | ||
throw err; | ||
}); | ||
// No need to check the fridge! Just start writing your note. | ||
var conflict = false; | ||
_.each(locks, function eachLock (entry) { | ||
// 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) | ||
// If a conflict IS found, respect the oldest | ||
if(entry.name === newLock.name && | ||
entry.uuid !== newLock.uuid && | ||
entry.id < newLock.id) conflict = entry; | ||
}); | ||
// 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) | ||
// If there are no conflicts, the lock is acquired! | ||
if(!conflict) acquireLock(newLock); | ||
// TODO: Otherwise, trigger callback! QA immediately (you're good to go get the milk) | ||
// Otherwise, get in line: a lock was acquired before mine, do nothing | ||
// ************************************** | ||
// 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 | ||
/** | ||
* acquireLock() is run after the lock is acquired, but before passing control to the atomic app logic | ||
* | ||
* @newLock the object representing the lock to acquire | ||
* @name name of the lock | ||
* @atomicLogic the transactional logic to be run atomically | ||
* @afterUnlock (optional) the function to run after the lock is subsequently released | ||
*/ | ||
var acquireLock = function(newLock) { | ||
adapter.lock ? adapter.lock(collectionName,criteria,cb) : cb(); | ||
var warningTimer = setTimeout(function() { | ||
console.error("Transaction :: " + newLock.name + " is taking an abnormally long time (> " + self.config.transactionWarningTimer + "ms)"); | ||
}, self.config.transactionWarningTimer); | ||
newLock.atomicLogic(null, function unlock () { | ||
clearTimeout(warningTimer); | ||
releaseLock(newLock,arguments); | ||
}); | ||
}; | ||
// 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) { | ||
// Allow criteria argument to be omitted | ||
if (_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
} | ||
// releaseLock() will grant pending lock requests in the order they were received | ||
// | ||
// @currentLock the lock currently acquired | ||
// @afterUnlockArgs the arguments to pass to the afterUnlock function | ||
var releaseLock = function(currentLock, afterUnlockArgs) { | ||
// ************************************** | ||
// NAIVE SOLUTION | ||
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note) | ||
var cb = currentLock.afterUnlock; | ||
// 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 | ||
// Get all locks | ||
self.transactionCollection.findAll(function afterLookingUpTransactions(err, locks) { | ||
if(err) return cb && cb(err); | ||
// Determine the next user in line | ||
// (oldest lock that isnt THIS ONE w/ the proper transactionName) | ||
var nextInLine = getNextLock(locks, currentLock); | ||
adapter.unlock ? adapter.unlock(collectionName,criteria,cb) : cb(); | ||
}; | ||
// Remove current lock | ||
self.transactionCollection.destroy({ | ||
uuid: currentLock.uuid | ||
}, function afterLockReleased (err) { | ||
if(err) return cb && cb(err); | ||
this.status = function (collectionName, cb) { | ||
adapter.status ? adapter.status(collectionName,cb) : cb(); | ||
}; | ||
// Trigger unlock's callback if specified | ||
// > NOTE: do this before triggering the next queued transaction | ||
// to prevent transactions from monopolizing the event loop | ||
cb && cb.apply(null, afterUnlockArgs); | ||
this.autoIncrement = function (collectionName, values,cb) { | ||
adapter.autoIncrement ? adapter.autoIncrement(collectionName, values, cb) : cb(); | ||
// Now allow the nextInLine lock to be acquired | ||
// This marks the end of the previous transaction | ||
nextInLine && acquireLock(nextInLine); | ||
}); | ||
}); | ||
}; | ||
// If @collectionName and @otherCollectionName are both using this adapter, do a more efficient remote join. | ||
@@ -250,3 +436,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, | ||
@@ -257,9 +442,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 afterDrop (err, data) { | ||
if(err) cb(err); | ||
else self.define(collection.identity, collection, cb); | ||
}); | ||
}, | ||
// Alter schema | ||
@@ -270,26 +456,15 @@ 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 afterDescribe (err, attrs) { | ||
attrs = _.clone(attrs); | ||
if(err) return cb(err); | ||
else if(!attrs) return self.define(collection.identity, collection, cb); | ||
// 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 | ||
// Otherwise, if it *DOES* exist, we'll try and guess what changes need to be made | ||
else self.alter(collection.identity, collection.attributes, cb); | ||
}); | ||
}, | ||
// 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(); | ||
} | ||
@@ -301,4 +476,4 @@ }; | ||
_.bindAll(this); | ||
_.bind(this.sync.drop,this); | ||
_.bind(this.sync.alter,this); | ||
_.bind(this.sync.drop, this); | ||
_.bind(this.sync.alter, this); | ||
@@ -310,2 +485,24 @@ // Mark as valid adapter | ||
// 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 | ||
// ************************************************************ | ||
function getNextLock(locks, currentLock) { | ||
var nextLock; | ||
_.each(locks, function(lock) { | ||
// Ignore locks with different transaction names | ||
if (lock.name !== currentLock.name) return; | ||
// Ignore current lock | ||
if (lock.uuid === currentLock.uuid) return; | ||
// Find the lock with the smallest id | ||
var minId = nextLock ? nextLock.id : MAX_INTEGER; | ||
if (lock.id < minId) nextLock = lock; | ||
}); | ||
return nextLock; | ||
} | ||
/** | ||
@@ -315,3 +512,4 @@ * Run a method on an object -OR- each item in an array and return the result | ||
*/ | ||
function plural (collection, application) { | ||
function plural(collection, application) { | ||
if(_.isArray(collection)) { | ||
@@ -327,8 +525,14 @@ 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(_.isUndefined(val)) delete criteria[key]; | ||
}); | ||
// Convert id and id strings into a criteria | ||
if((_.isFinite(criteria) || _.isString(criteria)) && +criteria > 0) { | ||
@@ -339,14 +543,16 @@ criteria = { | ||
} | ||
if(!_.isObject(criteria)) { | ||
throw 'Invalid criteria, ' + criteria + ' in find()'; | ||
// Return string to indicate an error | ||
if(!_.isObject(criteria)) return ('Invalid options/criteria :: ' + criteria); | ||
// If criteria doesn't seem to contain operational keys, assume all the keys are criteria | ||
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]; | ||
@@ -356,3 +562,24 @@ } | ||
// Normalize sort criteria | ||
if (criteria.sort) { | ||
// Split string into attr and sortDirection parts (default to 'asc') | ||
if (_.isString(criteria.sort)) { | ||
var parts = _.str.words(criteria.sort); | ||
parts[1] = parts[1] ? parts[1].toLowerCase() : 'asc'; | ||
if (parts.length !== 2 || (parts[1] !== 'asc' && parts[1] !== 'desc')) { | ||
throw new Error ('Invalid sort criteria :: '+criteria.sort); | ||
} | ||
criteria.sort = {}; | ||
criteria.sort[parts[0]] = (parts[1] === 'asc') ? 1 : -1; | ||
} | ||
// Verify that user either specified a proper object | ||
// or provided explicit comparator function | ||
if (!_.isObject(criteria.sort) && !_.isFunction(criteria.sort)) { | ||
throw new Error ('Invalid sort criteria for '+attrName+' :: '+direction); | ||
} | ||
} | ||
return criteria; | ||
} |
@@ -1,440 +0,1 @@ | ||
// Dependencies | ||
var async = require('async'); | ||
var _ = require('underscore'); | ||
var dirty = require('dirty'); | ||
var parley = require('parley'); | ||
var uuid = require('node-uuid'); | ||
/*--------------------- | ||
:: DirtyAdapter | ||
-> adapter | ||
*this refers to the adapter | ||
---------------------*/ | ||
// This disk+memory adapter is for development only! | ||
// Learn more: https://github.com/felixge/node-dirty | ||
var adapter = module.exports = { | ||
config: { | ||
// Attributes are case insensitive by default | ||
// attributesCaseSensitive: false, | ||
// 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) | ||
dbFilePath: './.waterline/dirty.db', | ||
// String to precede key name for schema defininitions | ||
schemaPrefix: 'sails_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_' | ||
}, | ||
// Initialize the underlying data model | ||
initialize: function(cb) { | ||
var my = this; | ||
if(this.config.persistent) { | ||
// Check that dbFilePath file exists and build tree as necessary | ||
require('fs-extra').touch(this.config.dbFilePath, function (err) { | ||
if (err) return cb(err); | ||
my.db = new(dirty.Dirty)(my.config.dbFilePath); | ||
afterwards(); | ||
}); | ||
} | ||
else { | ||
this.db = new(dirty.Dirty)(); | ||
afterwards(); | ||
} | ||
function afterwards() { | ||
// Make logger easily accessible | ||
my.log = my.config.log; | ||
// Trigger callback with no error | ||
my.db.on('load', function() { | ||
cb(); | ||
}); | ||
} | ||
}, | ||
// Tear down any remaining connections to the underlying data model | ||
teardown: function(cb) { | ||
this.db = null; | ||
cb && cb(); | ||
}, | ||
// 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 | ||
}); | ||
return cb(err,schema); | ||
}, | ||
// Create a new collection | ||
define: function(collectionName, schema, cb) { | ||
this.log(" DEFINING "+collectionName, { | ||
as: schema | ||
}); | ||
var self = this; | ||
// 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); | ||
}); | ||
}, | ||
// Drop an existing collection | ||
drop: function(collectionName, cb) { | ||
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); | ||
}); | ||
}, | ||
// Extend the schema for an existing collection | ||
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); | ||
}); | ||
}, | ||
// Create one or more new models in the collection | ||
create: function(collectionName, values, cb) { | ||
this.log(" CREATING :: "+collectionName,values); | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var data = this.db.get(dataKey); | ||
// 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); | ||
}); | ||
}, | ||
// Find one or more models from the collection | ||
// using where, limit, skip, and order | ||
// In where: handle `or`, `and`, and `like` queries | ||
find: function(collectionName, options, cb) { | ||
var criteria = options.where; | ||
//////////////////////////////////////////////// | ||
// TODO: Make this shit actually work | ||
var limit = options.limit; | ||
var skip = options.skip; | ||
var order = options.order; | ||
//////////////////////////////////////////////// | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var data = this.db.get(dataKey); | ||
// Query and return result set using criteria | ||
cb(null,applyFilter(data,criteria)); | ||
}, | ||
// Update one or more models in the collection | ||
update: function(collectionName, options, values, cb) { | ||
this.log(" UPDATING :: "+collectionName,{ | ||
options: options, | ||
values: values | ||
}); | ||
var my = this; | ||
var criteria = options.where; | ||
//////////////////////////////////////////////// | ||
// TODO: Make this shit actually work | ||
var limit = options.limit; | ||
var skip = options.skip; | ||
var order = options.order; | ||
//////////////////////////////////////////////// | ||
var dataKey = this.config.dataPrefix+collectionName; | ||
var data = this.db.get(dataKey); | ||
// Query result set using criteria | ||
var resultIndices = []; | ||
_.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)); | ||
if (matchSet(row,criteria)) resultIndices.push(index); | ||
}); | ||
this.log("filtered indices::",resultIndices,'criteria',criteria); | ||
// Update value(s) | ||
_.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); | ||
}); | ||
}, | ||
// Delete one or more models from the collection | ||
destroy: function(collectionName, options, cb) { | ||
this.log(" DESTROYING :: "+collectionName,options); | ||
var criteria = options.where; | ||
//////////////////////////////////////////////// | ||
// TODO: Make this shit actually work | ||
var limit = options.limit; | ||
var skip = options.skip; | ||
var order = options.order; | ||
//////////////////////////////////////////////// | ||
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); | ||
}); | ||
// Replace data collection and go back | ||
this.db.set(dataKey,data,function (err) { | ||
cb(err); | ||
}); | ||
}, | ||
// 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 | ||
// (this is optional and normally automatically populated based on filename) | ||
identity: 'dirty' | ||
}; | ||
////////////// ////////////////////////////////////////// | ||
////////////// Private Methods ////////////////////////////////////////// | ||
////////////// ////////////////////////////////////////// | ||
// Run criteria query against data aset | ||
function applyFilter (data,criteria) { | ||
if (criteria) { | ||
return _.filter(data,function (model) { | ||
return matchSet(model,criteria); | ||
}); | ||
} | ||
else return data; | ||
} | ||
// Match a model against each criterion in a criteria query | ||
function matchSet (model, criteria) { | ||
// By default, treat entries as AND | ||
for (var key in criteria) { | ||
if (! matchItem(model,key,criteria[key])) return false; | ||
} | ||
return true; | ||
} | ||
function matchOr (model, disjuncts) { | ||
var outcome = false; | ||
_.each(disjuncts,function (criteria) { | ||
if (matchSet(model,criteria)) outcome = true; | ||
}); | ||
return outcome; | ||
} | ||
function matchAnd (model, conjuncts) { | ||
var outcome = true; | ||
_.each(conjuncts,function (criteria) { | ||
if (!matchSet(model,criteria)) outcome = false; | ||
}); | ||
return outcome; | ||
} | ||
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(); | ||
// 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]) ) ) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
function matchNot (model,criteria) { | ||
return !matchSet(model,criteria); | ||
} | ||
function matchItem (model,key,criterion) { | ||
// Make attribute names case insensitive unless overridden in config | ||
if (! adapter.config.attributesCaseSensitive) key = key.toLowerCase(); | ||
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); | ||
} | ||
// Otherwise this is an attribute name: ensure it exists and matches | ||
else if ( !model[key] || (model[key] !== criterion)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
// Number of miliseconds since the Unix epoch Jan 1st, 1970 | ||
function epoch () { | ||
return (new Date()).getTime(); | ||
} | ||
// Return the oldest lock in the collection | ||
function getOldest (locks) { | ||
var currentLock; | ||
_.each(locks,function (lock) { | ||
if (!currentLock) currentLock = lock; | ||
else if (lock.timestamp < currentLock.timestamp) currentLock = lock; | ||
}); | ||
return currentLock; | ||
} | ||
module.exports = require('waterline-dirty')(); |
@@ -13,3 +13,3 @@ // Dependencies | ||
---------------------*/ | ||
var adapter = module.exports = { | ||
var adapter = { | ||
@@ -16,0 +16,0 @@ config: { |
var _ = require('underscore'); | ||
var parley = require('parley'); | ||
var async = require('async'); | ||
var util = require('sails-util'); | ||
var config = require('./config'); | ||
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 | ||
// ******************************************************************** | ||
// Pass up options from adapter (defaults to global options) | ||
definition.migrate = definition.migrate || definition.adapter.config.migrate || config.migrate; | ||
definition.globalize = !_.isUndefined(definition.globalize) ? definition.globalize : !_.isUndefined(definition.adapter.config.globalize) ? definition.adapter.config.globalize : config.globalize; | ||
// Pass down appropriate configuration items to adapter | ||
_.each(['defaultPK', 'updatedAt', 'createdAt'], function(key) { | ||
if(!_.isUndefined(definition[key])) { | ||
definition.adapter.config[key] = definition[key]; | ||
} | ||
}); | ||
// Set behavior in adapter depending on migrate option | ||
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); | ||
} | ||
// Absorb definition methods | ||
_.extend(this, definition); | ||
// if configured as such, make each collection globally accessible | ||
if(definition.globalize) { | ||
var globalName = _.str.capitalize(this.identity); | ||
global[globalName] = this; | ||
} | ||
////////////////////////////////////////// | ||
// Dynamic finders | ||
////////////////////////////////////////// | ||
// Query the collection using the name of the attribute directly | ||
this.generateDynamicFinder = function(attrName, method) { | ||
// Figure out actual dynamic method name by injecting attribute name | ||
var actualMethodName = method.replace(/\*/g, _.str.capitalize(attrName)); | ||
// Assign this finder to the collection | ||
this[actualMethodName] = function dynamicMethod(value, options, cb) { | ||
if(_.isFunction(options)) { | ||
cb = options; | ||
options = null; | ||
} | ||
options = options || {}; | ||
var usage = _.str.capitalize(this.identity) + '.' + actualMethodName + '(someValue,[options],callback)'; | ||
if(_.isUndefined(value)) usageError('No value specified!', usage); | ||
if(options.where) usageError('Cannot specify `where` option in a dynamic ' + method + '*() query!', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
// Build criteria query and submit it | ||
options.where = {}; | ||
options.where[attrName] = value; | ||
// Make modifications based on method as necessary | ||
if (method === 'findBy*' || method === 'findBy*In') { | ||
return self.find(options, cb); | ||
} else if(method === 'findBy*Like') { | ||
return self.find(_.extend(options, { | ||
where: { | ||
like: options.where | ||
} | ||
}), cb); | ||
} | ||
// Aggregate finders | ||
else if(method === 'findAllBy*' || method === 'findAllBy*In') { | ||
return self.findAll(options, cb); | ||
} else if(method === 'findAllBy*Like') { | ||
return self.findAll(_.extend(options, { | ||
where: { | ||
like: options.where | ||
} | ||
}), cb); | ||
} | ||
// Count finders | ||
else if(method === 'countBy*' || method === 'countBy*In') { | ||
return self.count(options, cb); | ||
} else if(method === 'countBy*Like') { | ||
return self.count(_.extend(options, { | ||
where: { | ||
like: options.where | ||
} | ||
}), cb); | ||
} | ||
}; | ||
}; | ||
// Clone attributes and augment with id, createdAt, updatedAt, etc. if necessary | ||
var attributes = _.clone(this.attributes) || {}; | ||
attributes = require('./augmentAttributes')(attributes, _.extend({}, config, this.config)); | ||
// For each defined attribute, create a dynamic finder function | ||
_.each(attributes, function(attrDef, attrName) { | ||
this.generateDynamicFinder(attrName, 'findBy*'); | ||
this.generateDynamicFinder(attrName, 'findBy*In'); | ||
this.generateDynamicFinder(attrName, 'findBy*Like'); | ||
this.generateDynamicFinder(attrName, 'findAllBy*'); | ||
this.generateDynamicFinder(attrName, 'findAllBy*In'); | ||
this.generateDynamicFinder(attrName, 'findAllBy*Like'); | ||
this.generateDynamicFinder(attrName, 'countBy*'); | ||
this.generateDynamicFinder(attrName, 'countBy*In'); | ||
this.generateDynamicFinder(attrName, 'countBy*Like'); | ||
}, this); | ||
////////////////////////////////////////// | ||
// Promises / Deferred Objects | ||
////////////////////////////////////////// | ||
// ============================= | ||
// TODO: (for a later release) | ||
// ============================= | ||
/* | ||
// when done() is called (or some comparably-named terminator) | ||
// run every operation in the queue and trigger the callback | ||
this.done = function (cb) { | ||
// A callback is always required here | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!',usage); | ||
}; | ||
// Join with another collection | ||
// (use optimized join in adapter if one was provided) | ||
this.join = function (anotherOne, cb) { | ||
// Absorb definition methods | ||
_.extend(this, definition); | ||
} | ||
*/ | ||
// ============================= | ||
////////////////////////////////////////// | ||
// Core CRUD | ||
////////////////////////////////////////// | ||
this.create = function(values, cb) { | ||
// ============================= | ||
// TODO: (for a later release) | ||
// ============================= | ||
/* | ||
if (this._isDeferredObject) { | ||
if (this.terminated) { | ||
throw new Error("The callback was already triggered!"); | ||
} | ||
// Define core methods | ||
this.create = function(values, cb) { | ||
return this.adapter.create(this.identity,values,cb); | ||
}; | ||
// Call find method in adapter | ||
this.find = function(options, cb) { | ||
return this.adapter.find(this.identity,options,cb); | ||
}; | ||
// Call update method in adapter | ||
this.update = function(criteria, values, cb) { | ||
return this.adapter.update(this.identity,criteria,values,cb); | ||
}; | ||
// Call destroy method in adapter | ||
this.destroy = function(criteria, cb) { | ||
return this.adapter.destroy(this.identity,criteria,cb); | ||
}; | ||
// If this was called from a deferred object, | ||
// instead of doing the normal operation, pop it on a queue for later | ||
this.findOrCreate = function (criteria, values, cb) { | ||
return this.adapter.findOrCreate(this.identity,criteria,values,cb); | ||
}; | ||
this.findAndUpdate = function (criteria, values, cb) { | ||
return this.adapter.findAndUpdate(this.identity, criteria, values, cb); | ||
}; | ||
this.findAndDestroy = function (criteria, cb) { | ||
return this.adapter.findAndDestroy(this.identity, criteria, cb); | ||
}; | ||
if (cb) { | ||
// If a callback is specified, terminate the deferred object | ||
} | ||
} | ||
else { | ||
// Do the normal stuff | ||
} | ||
this.lock = function(criteria, cb) { | ||
return this.adapter.lock(this.identity,criteria,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); | ||
}; | ||
if (!cb) { | ||
// No callback specified | ||
// Initialize and return a deferred object | ||
} | ||
*/ | ||
// ============================= | ||
if(_.isFunction(values)) { | ||
cb = values; | ||
values = null; | ||
} | ||
var usage = _.str.capitalize(this.identity) + '.create({someAttr: "someValue"},callback)'; | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
////////////////////////////////////////// | ||
// Utility methods | ||
////////////////////////////////////////// | ||
return this.adapter.create(this.identity, values, cb); | ||
}; | ||
// Return a trimmed set of the specified attributes | ||
// with only the attributes which actually exist in the server-side model | ||
this.filter = function(params) { | ||
var trimmedParams = util.objFilter(params, function(value, name) { | ||
return _.contains(_.keys(this.attributes), name); | ||
}, this); | ||
return trimmedParams; | ||
// Call find method in adapter | ||
this.find = function(criteria, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.find([criteria],callback)'; | ||
if (_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
} | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.find(this.identity, criteria, cb); | ||
}; | ||
this.findAll = function(criteria, options, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.findAll([criteria],[options],callback)'; | ||
if(_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
options = null; | ||
} else if(_.isFunction(options)) { | ||
cb = options; | ||
options = null; | ||
} else if(_.isObject(options)) { | ||
criteria = _.extend({}, criteria, options); | ||
} else usageError('Invalid options specified!', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.findAll(this.identity, criteria, cb); | ||
}; | ||
this.where = this.findAll; | ||
this.select = this.findAll; | ||
this.findLike = function (criteria, options, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.findLike([criteria],[options],callback)'; | ||
if (criteria = normalizeLikeCriteria(criteria)) { | ||
return this.find(criteria, options, cb); | ||
} | ||
else usageError('Criteria must be string or object!',usage); | ||
}; | ||
this.findAllLike = function (criteria, options, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.findAllLike([criteria],[options],callback)'; | ||
if (criteria = normalizeLikeCriteria(criteria)) { | ||
return this.findAll(criteria, options, cb); | ||
} | ||
else usageError('Criteria must be string or object!',usage); | ||
}; | ||
this.count = function (criteria, options, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.count([criteria],[options],callback)'; | ||
if(_.isFunction(criteria)) { | ||
cb = criteria; | ||
criteria = null; | ||
options = null; | ||
} else if(_.isFunction(options)) { | ||
cb = options; | ||
options = null; | ||
} else if(_.isObject(options)) { | ||
criteria = _.extend({}, criteria, options); | ||
} else usageError('Invalid options specified!', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.count(this.identity, criteria, cb); | ||
}; | ||
this.countAll = this.count; | ||
// Call update method in adapter | ||
this.update = function(options, newValues, cb) { | ||
if(_.isFunction(options)) { | ||
cb = options; | ||
options = null; | ||
} | ||
var usage = _.str.capitalize(this.identity) + '.update(criteria, newValues, callback)'; | ||
if(!options) usageError('No criteria option specified! If you\'re trying to update everything, maybe try updateAll?', usage); | ||
if(!newValues) usageError('No updated values specified!', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.update(this.identity, options, newValues, cb); | ||
}; | ||
this.updateWhere = this.update; | ||
// Call destroy method in adapter | ||
this.destroy = function(options, cb) { | ||
if(_.isFunction(options)) { | ||
cb = options; | ||
options = null; | ||
} | ||
var usage = _.str.capitalize(this.identity) + '.destroy(options, callback)'; | ||
if(!options) usageError('No options specified! If you\'re trying to destroy everything, maybe try destroyAll?', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.destroy(this.identity, options, cb); | ||
}; | ||
this.destroyWhere = this.destroy; | ||
////////////////////////////////////////// | ||
// Composite | ||
////////////////////////////////////////// | ||
this.findOrCreate = function(criteria, values, cb) { | ||
if(_.isFunction(values)) { | ||
cb = values; | ||
values = null; | ||
} | ||
var usage = _.str.capitalize(this.identity) + '.findOrCreate(criteria, values, callback)'; | ||
if(!criteria) usageError('No criteria option specified!', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
return this.adapter.findOrCreate(this.identity, criteria, values, cb); | ||
}; | ||
////////////////////////////////////////// | ||
// Aggregate methods | ||
////////////////////////////////////////// | ||
this.createEach = function(valuesList, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.createEach(valuesList, callback)'; | ||
if(!valuesList) usageError('No valuesList specified!', usage); | ||
if(!_.isArray(valuesList)) usageError('Invalid valuesList specified (should be an array!)', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
this.adapter.createEach(this.identity, valuesList, cb); | ||
}; | ||
// Iterate through a list of objects, trying to find each one | ||
// For any that don't exist, create them | ||
this.findOrCreateEach = function(valuesList, cb) { | ||
var usage = _.str.capitalize(this.identity) + '.findOrCreateEach(valuesList, callback)'; | ||
if(!valuesList) usageError('No valuesList specified!', usage); | ||
if(!_.isArray(valuesList)) usageError('Invalid valuesList specified (should be an array!)', usage); | ||
if(!_.isFunction(cb)) usageError('Invalid callback specified!', usage); | ||
this.adapter.findOrCreateEach(this.identity, valuesList, cb); | ||
}; | ||
////////////////////////////////////////// | ||
// Special methods | ||
////////////////////////////////////////// | ||
this.transaction = function(transactionName, atomicLogic, afterUnlock) { | ||
var usage = _.str.capitalize(this.identity) + '.transaction(transactionName, atomicLogicFunction, afterUnlockFunction)'; | ||
if(!atomicLogic) { | ||
return usageError('Missing required parameter: atomicLogicFunction!', usage); | ||
} else if(!_.isFunction(atomicLogic)) { | ||
return usageError('Invalid atomicLogicFunction! Not a function: ' + atomicLogic, usage); | ||
} else if(afterUnlock && !_.isFunction(afterUnlock)) { | ||
return usageError('Invalid afterUnlockFunction! Not a function: ' + afterUnlock, usage); | ||
} else return this.adapter.transaction(this.identity + '.' + transactionName, atomicLogic, afterUnlock); | ||
}; | ||
////////////////////////////////////////// | ||
// Utility methods | ||
////////////////////////////////////////// | ||
// Return a trimmed set of the specified attributes | ||
// with only the attributes which actually exist in the server-side model | ||
this.filter = function(params) { | ||
var trimmedParams = util.objFilter(params, function(value, name) { | ||
return _.contains(_.keys(this.attributes), name); | ||
}, this); | ||
return trimmedParams; | ||
}; | ||
this.trimParams = this.filter; | ||
// Bind instance methods to collection | ||
_.bindAll(definition); | ||
_.bindAll(this); | ||
// Returns false if criteria is invalid, | ||
// otherwise returns normalized criteria obj. | ||
// (inside as a closure in order to get access to attributes object) | ||
function normalizeLikeCriteria (criteria) { | ||
if (_.isObject(criteria)) { | ||
if (!criteria.where) criteria = {where: criteria}; | ||
criteria.where = {like: criteria.where}; | ||
} | ||
// If string criteria is specified, check each attribute for a match | ||
else if (_.isString(criteria)) { | ||
var searchTerm = criteria; | ||
criteria = {where: {or: []}}; | ||
_.each(attributes,function (val, attrName) { | ||
var obj = {like: {}}; | ||
obj.like[attrName] = searchTerm; | ||
criteria.where.or.push(obj); | ||
}); | ||
} | ||
else return false; | ||
return criteria; | ||
} | ||
}; | ||
this.trimParams = this.filter; | ||
// Bind instance methods to collection | ||
_.bindAll(definition); | ||
_.bindAll(this); | ||
}; | ||
function usageError(err, usage) { | ||
console.error("\n\n"); | ||
throw new Error(err + '\n==============================================\nProper usage :: \n' + usage + '\n==============================================\n'); | ||
} |
@@ -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,20 @@ updatedAt: true, | ||
// Automatically define createdAt field in schema and populate with the current timestamp during model creation | ||
createdAt: true | ||
createdAt: true, | ||
// Default identity for transaction database | ||
transactionDbIdentity: '___transaction', | ||
// 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, | ||
migrate: 'alter', | ||
globalize: true | ||
}; |
{ | ||
"name": "waterline", | ||
"version": "0.0.5", | ||
"version": "0.0.6", | ||
"description": "Adaptable data access layer for Node.js", | ||
@@ -10,3 +10,3 @@ "main": "waterline.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "mocha --ignore-leaks -b" | ||
}, | ||
@@ -39,4 +39,7 @@ "repository": { | ||
"sails-util": "0.0.0", | ||
"parley": "0.0.2" | ||
"parley": "0.0.2", | ||
"microtime": "~0.3.3", | ||
"waterline-dirty": "0.1.0", | ||
"sails-moduleloader": "0.0.0" | ||
} | ||
} |
@@ -0,8 +1,7 @@ | ||
// Dependencies | ||
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 = { | ||
var bootstrap = { | ||
@@ -12,24 +11,63 @@ // Initialize waterline | ||
teardown: teardown, | ||
// Override every collection's adapter with the specified adapter | ||
// Then return the init() method | ||
initWithAdapter: function (adapter) { | ||
_.map(collections,function (collection) { | ||
_.map(bootstrap.collections,function (collection) { | ||
collection.adapter = adapter; | ||
}); | ||
return initialize; | ||
}, | ||
collections: collections | ||
} | ||
}; | ||
// Bootstrap waterline with default adapters and bundled test collections | ||
before(bootstrap.init); | ||
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts | ||
after(bootstrap.teardown); | ||
// Get User object ready to go before each test | ||
beforeEach(function() { | ||
return User = bootstrap.collections.user; | ||
}); | ||
// Initialize waterline | ||
function initialize (exit) { | ||
require("../waterline.js")({ | ||
function initialize (done) { | ||
// Keep a reference to waterline to use for teardown() | ||
bootstrap.waterline = require("../waterline.js"); | ||
var collections = require('sails-moduleloader').required({ | ||
dirname : __dirname + '/collections', | ||
filter : /(.+)\.js$/ | ||
}); | ||
bootstrap.waterline({ | ||
collections: collections, | ||
log: blackhole | ||
}, exit); | ||
}, function (err, waterlineData){ | ||
bootstrap.adapters = waterlineData.adapters; | ||
bootstrap.collections = waterlineData.collections; | ||
done(err); | ||
}); | ||
} | ||
// Tear down adapters and collections | ||
function teardown (done) { | ||
bootstrap.waterline.teardown({ | ||
adapters: bootstrap.adapters, | ||
collections: bootstrap.collections | ||
},done); | ||
} | ||
// Use silent logger for testing | ||
// (this prevents annoying output from cluttering up our nice clean console) | ||
var blackhole = function (){}; | ||
var blackhole = _.extend(function () {},{ | ||
verbose: function (){}, | ||
info: function (){}, | ||
debug: function (){}, | ||
warn: function (){}, | ||
error: 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 @@ |
@@ -14,18 +14,6 @@ /** | ||
// Bootstrap waterline and get access to User collection | ||
var bootstrap = require('./bootstrap.test.js'); | ||
var User; | ||
module.exports = function(adapter) { | ||
describe('adapter', function() { | ||
// Bootstrap waterline with default adapters and bundled test collections | ||
before(bootstrap.initWithAdapter(adapter)); | ||
// Get User object ready to go before each test | ||
beforeEach(function () { | ||
return User = bootstrap.collections.user; | ||
}); | ||
describe('#creating() users Johnny and Timmy', function() { | ||
@@ -54,6 +42,3 @@ | ||
name: "Johnny" | ||
}, function(err, users) { | ||
// Get first item from result set | ||
var user = users[0]; | ||
}, function(err, user) { | ||
if(err) throw err; | ||
@@ -81,5 +66,3 @@ else if(!user || !_.isObject(user) || !user.name || user.name !== "Johnny") throw "Invalid model returned."; | ||
name: "Richard" | ||
}, function(err, users) { | ||
// Get first item from result set | ||
var user = users[0]; | ||
}, function(err, user) { | ||
@@ -93,3 +76,3 @@ if(err) throw err; | ||
it('should only result in a single Richard existing', function(done) { | ||
User.find({ | ||
User.findAll({ | ||
name: "Richard" | ||
@@ -106,9 +89,7 @@ }, function(err, users) { | ||
name: "Richard" | ||
}, function(err, users) { | ||
// Get first item from result set | ||
var user = users[0]; | ||
}, function(err, user) { | ||
if(err) throw err; | ||
else if(!user.id) throw "Id missing!"; | ||
else done(err, users); | ||
else done(err, user); | ||
}); | ||
@@ -128,3 +109,3 @@ }); | ||
it('should mean trying to find Richard should return an empty array', function(done) { | ||
User.find({ | ||
User.findAll({ | ||
name: "Richard" | ||
@@ -149,3 +130,3 @@ }, function(err, users) { | ||
it('should mean trying to find Timmy should return an empty array', function(done) { | ||
User.find({ | ||
User.findAll({ | ||
name: "Timmy" | ||
@@ -152,0 +133,0 @@ }, function(err, users) { |
@@ -1,26 +0,32 @@ | ||
/** | ||
* init.test.js | ||
* | ||
* This module is just a basic sanity check to make sure everything kicks off properly | ||
* | ||
* | ||
*/ | ||
// /** | ||
// * init.test.js | ||
// * | ||
// * This module is just a basic sanity check to make sure everything kicks off properly | ||
// */ | ||
// Dependencies | ||
var _ = require('underscore'); | ||
var parley = require('parley'); | ||
var assert = require("assert"); | ||
var buildDictionary = require('../buildDictionary.js'); | ||
// // Dependencies | ||
// var _ = require('underscore'); | ||
// var parley = require('parley'); | ||
// var assert = require("assert"); | ||
// module.exports = function(bootstrap) { | ||
describe('waterline', function() { | ||
// var User; | ||
// Bootstrap waterline with default adapters and bundled test collections | ||
before(require('./bootstrap.test.js').init); | ||
// // Get User object ready to go before each test | ||
// beforeEach(function() { | ||
// return User = bootstrap.collections.user; | ||
// }); | ||
describe('#initialize() and sync()', function() { | ||
it('should work without firing an error', function(done) { | ||
done(); | ||
}); | ||
// describe('#initialize() and sync()', function() { | ||
// it('should work without firing an error', function(done) { | ||
// done(); | ||
// }); | ||
// }); | ||
// }; | ||
describe('#initialize() and sync()', function() { | ||
it('should work without firing an error', function(done) { | ||
done(); | ||
}); | ||
}); |
@@ -12,52 +12,107 @@ /** | ||
var parley = require('parley'); | ||
var async = require('async'); | ||
var assert = require("assert"); | ||
// Bootstrap waterline and get access to collections, especially User | ||
var bootstrap = require('./bootstrap.test.js'); | ||
var User; | ||
describe('transactions', function() { | ||
describe ('transactions',function () { | ||
// Bootstrap waterline with default adapters and bundled test collections | ||
before(bootstrap.init); | ||
// Get User object ready to go before each test | ||
beforeEach(function() { | ||
return User = bootstrap.collections.user; | ||
}); | ||
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, cb) { | ||
cb(); | ||
},done); | ||
}); | ||
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) throw new Error(err); | ||
if(!unlock1) throw new Error("No unlock() method provided!"); | ||
testAppendLock(); | ||
User.transaction('test', function(err, unlock2) { | ||
if(err) throw new Error(err); | ||
if(!unlock2) throw new Error("No unlock() method provided!"); | ||
testAppendLock(); | ||
// Release lock so other tests can use the 'test' transaction | ||
if(_.isEqual(orderingTest, ['lock', 'unlock', 'lock'])) unlock2(); | ||
else { | ||
console.error(orderingTest); | ||
throw new Error("The lock was acquired by two users at once!"); | ||
} | ||
}, done); | ||
// Set timeout to release the lock after a 1/20 of a second | ||
setTimeout(function() { | ||
testAppendUnlock(); | ||
unlock1(); | ||
}, 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) { | ||
if (err) throw new Error(err); | ||
User.create({ | ||
name: constellation, | ||
type: type | ||
},function(err) { | ||
if (err) throw new Error(err); | ||
// Wait a short moment to introduce an element of choas | ||
setTimeout(function() { | ||
unlock(); | ||
},Math.round(Math.random())*5); | ||
}); | ||
},cb); | ||
}, function(err) { | ||
if (err) throw new Error(err); | ||
User.findAll({ type: type },function (err,users) { | ||
if(users.length === items.length) return done(); | ||
else return done('Proper users were not created!'); | ||
}); | ||
},250); | ||
}); | ||
} | ||
}); | ||
// 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) {}); | ||
}); | ||
}); |
171
waterline.js
// Dependencies | ||
var async = require('async'); | ||
var _ = require('underscore'); | ||
_.str = require('underscore.string'); | ||
var parley = require('parley'); | ||
@@ -11,7 +12,18 @@ | ||
// Read global config | ||
var config = require('./config.js'); | ||
// Util | ||
var buildDictionary = require('./buildDictionary.js'); | ||
var modules = require('sails-moduleloader'); | ||
// Include built-in adapters | ||
var builtInAdapters = buildDictionary(__dirname + '/adapters', /(.+Adapter)\.js$/, /Adapter/); | ||
// Include built-in adapters and collections | ||
var builtInAdapters = modules.required({ | ||
dirname : __dirname + '/adapters', | ||
filter : /(.+Adapter)\.js$/, | ||
replaceExpr : /Adapter/ | ||
}); | ||
var builtInCollections = modules.required({ | ||
dirname : __dirname + '/collections', | ||
filter : /(.+)\.js$/ | ||
}); | ||
@@ -22,23 +34,77 @@ /** | ||
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 || {}); | ||
// Merge passed-in adapters + collections with defaults | ||
adapters = _.extend(builtInAdapters,adapters); | ||
collections = _.extend(builtInCollections,collections); | ||
// 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); | ||
// then associate each collection with its adapter and sync its schema | ||
$$(async).forEach(_.keys(collections),prepareCollection); | ||
// Now that everything is instantiated, augment the live collections with a transaction collection | ||
$$(async).forEach(_.values(collections),addTransactionCollection); | ||
// And sync them | ||
$$(async).forEach(_.values(collections),syncCollection); | ||
// 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); | ||
@@ -50,22 +116,28 @@ // 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 (definition, cb) { | ||
// If no adapter is specified, default to 'dirty' | ||
if (!collection.adapter) collection.adapter = 'dirty'; | ||
if (!definition.adapter) definition.adapter = 'dirty'; | ||
// Use adapter shortname in model def. to look up actual object | ||
if (_.isString(collection.adapter)) { | ||
if (! adapters[collection.adapter]) throw "Unknown adapter! ("+collection.adapter+") Maybe try installing it?"; | ||
else collection.adapter = adapters[collection.adapter]; | ||
if (_.isString(definition.adapter)) { | ||
if (! adapters[definition.adapter]) throw "Unknown adapter! ("+definition.adapter+") Did you include a valid adapter with this name?"; | ||
else definition.adapter = adapters[definition.adapter]; | ||
} | ||
// Then check that a valid adapter object was retrieved (or already existed) | ||
if (!(_.isObject(collection.adapter) && collection.adapter._isWaterlineAdapter)) { | ||
if (!(_.isObject(definition.adapter) && definition.adapter._isWaterlineAdapter)) { | ||
throw "Invalid adapter!"; | ||
@@ -75,14 +147,53 @@ } | ||
// Build actual collection object from definition | ||
collections[collectionName] = new Collection(collection); | ||
var collection = new Collection(definition); | ||
// Call initializeCollection() event on adapter | ||
collection.adapter.initializeCollection(collection.identity,function (err) { | ||
if (err) throw err; | ||
cb(err,collection); | ||
}); | ||
} | ||
// add transaction collection to each collection's adapter | ||
function addTransactionCollection (collection, cb) { | ||
collection.adapter.transactionCollection = collections[config.transactionDbIdentity]; | ||
cb(); | ||
} | ||
// Sync a collection w/ its adapter's data store | ||
function syncCollection (collection,cb) { | ||
// 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; | ||
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); | ||
} | ||
}; | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
82869
31
2200
2
0
41
0
11
1
+ Addedmicrotime@~0.3.3
+ Addedsails-moduleloader@0.0.0
+ Addedwaterline-dirty@0.1.0
+ Addedbindings@1.0.0(transitive)
+ Addedinclude-all@0.0.2(transitive)
+ Addedmicrotime@0.3.3(transitive)
+ Addedsails-moduleloader@0.0.0(transitive)
+ Addedwaterline-dirty@0.1.0(transitive)