New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

waterline

Package Overview
Dependencies
Maintainers
1
Versions
165
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

waterline - npm Package Compare versions

Comparing version 0.0.5 to 0.0.6

augmentAttributes.js

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) {});
});
});
// 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);
}
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc