Huge News!Announcing our $40M Series B led by Abstract Ventures.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.51

collections/Transaction.js

419

adapter.js
var _ = require('underscore');
var parley = require('parley');
var uuid = require('node-uuid');
// Read global config
var config = require('./config.js');
// Extend adapter definition
var Adapter = module.exports = function (adapter) {
module.exports = function(adapter) {
var self = this;
// Absorb configuration
this.config = adapter.config || {};
this.config = adapter.config = _.extend({
// Default transaction collection name
transactionCollection: _.clone(config.transactionCollection)
this.initialize = function(cb) {
var self = this;
}, adapter.config || {});
// When process ends, close all open connections
process.on('SIGINT', process.exit);
process.on('SIGTERM', process.exit);
process.on('exit', function () { self.teardown(); });
// Absorb identity
this.identity = adapter.identity;
// Set scheme based on `persistent` options
this.config.scheme = this.config.persistent ? 'alter' : 'drop';
// Initialize is fired once-per-adapter
this.initialize = function(cb) {
adapter.initialize ? adapter.initialize(cb) : cb();
};
this.teardown = function (cb) {
// Teardown is fired once-per-adapter
// (i.e. tear down any remaining connections to the underlying data model)
this.teardown = function(cb) {
adapter.teardown ? adapter.teardown(cb) : (cb && cb());
};
// teardownCollection is fired once-per-collection
// (i.e. flush data to disk before the adapter shuts down)
this.teardownCollection = function(collectionName,cb) {
adapter.teardownCollection ? adapter.teardownCollection(collectionName, cb) : (cb && cb());
};

@@ -36,25 +45,33 @@

//////////////////////////////////////////////////////////////////////
this.define = function(collectionName, definition, cb) {
this.define = function(collectionName, definition, cb) {
// Grab attributes from definition
var attributes = definition.attributes || {};
// If id is not defined, add it
// TODO: Make this check for ANY primary key
// TODO: Make this disableable in the config
if (!definition.attributes.id) {
definition.attributes.id = {
if(this.config.defaultPK && !attributes.id) {
attributes.id = {
type: 'INTEGER',
primaryKey: true,
autoIncrement: true
autoIncrement: true,
'default': 'AUTO_INCREMENT',
constraints: {
unique: true,
primaryKey: true
}
};
}
// If the config allows it, and they aren't already specified,
// If the adapter config allows it, and they aren't already specified,
// extend definition with updatedAt and createdAt
if(config.createdAt && !definition.createdAt) definition.createdAt = 'DATE';
if(config.updatedAt && !definition.updatedAt) definition.updatedAt = 'DATE';
var now = {type: 'DATE', 'default': 'NOW'};
if(this.config.createdAt && !attributes.createdAt) attributes.createdAt = now;
if(this.config.updatedAt && !attributes.updatedAt) attributes.updatedAt = now;
// Convert string-defined attributes into fully defined objects
for (var attr in definition.attributes) {
if(_.isString(definition[attr])) {
definition[attr] = {
type: definition[attr]
for(var attr in attributes) {
if(_.isString(attributes[attr])) {
attributes[attr] = {
type: attributes[attr]
};

@@ -64,23 +81,20 @@ }

// Grab schema from definition
var schema = definition.attributes;
// Verify that collection doesn't already exist
// and then define it and trigger callback
this.describe(collectionName,function (err,existingSchema) {
if (err) return cb(err,schema);
else if (existingSchema) return cb("Trying to define a collection ("+collectionName+") which already exists with schema:",existingSchema);
else return ( adapter.define ? adapter.define(collectionName,schema,cb) : cb() );
this.describe(collectionName, function(err, existingAttributes) {
if(err) return cb(err, attributes);
else if(existingAttributes) return cb("Trying to define a collection (" + collectionName + ") which already exists.");
else return(adapter.define ? adapter.define(collectionName, attributes, cb) : cb());
});
};
this.describe = function(collectionName, cb) {
adapter.describe ? adapter.describe(collectionName,cb) : cb();
this.describe = function(collectionName, cb) {
adapter.describe ? adapter.describe(collectionName, cb) : cb();
};
this.drop = function(collectionName, cb) {
this.drop = function(collectionName, cb) {
// TODO: foreach through and delete all of the models for this collection
adapter.drop ? adapter.drop(collectionName,cb) : cb();
adapter.drop ? adapter.drop(collectionName, cb) : cb();
};
this.alter = function (collectionName,newAttrs,cb) {
adapter.alter ? adapter.alter(collectionName,newAttrs,cb) : cb();
this.alter = function(collectionName, newAttrs, cb) {
adapter.alter ? adapter.alter(collectionName, newAttrs, cb) : cb();
};

@@ -92,33 +106,23 @@

//////////////////////////////////////////////////////////////////////
// TODO: ENSURE ATOMICITY
this.create = function(collectionName, values, cb) {
// Get status if specified
if (adapter.status) adapter.status(collectionName,afterwards);
else afterwards();
var self = this;
if(!collectionName) return cb("No collectionName specified!");
if(!adapter.create) return cb("No create() method defined in adapter!");
// Modify values as necessary
function afterwards(err,status){
if (err) throw err;
// TODO: Populate default values
// Auto increment fields that need it
adapter.autoIncrement(collectionName,values,function (err,values) {
if (err) return cb(err);
// TODO: Validate constraints using Anchor
// Automatically add updatedAt and createdAt (if enabled)
if (self.config.createdAt) values.createdAt = new Date();
if (self.config.updatedAt) values.updatedAt = new Date();
// TODO: Verify constraints using Anchor
adapter.create ? adapter.create(collectionName, values, cb) : cb();
// Add updatedAt and createdAt
if (adapter.config.createdAt) values.createdAt = new Date();
if (adapter.config.updatedAt) values.updatedAt = new Date();
// Call create method in adapter
return adapter.create ? adapter.create(collectionName,values,cb) : cb();
});
}
// TODO: Return model instance Promise object for joins, etc.
};
this.find = function(collectionName, options, cb) {
if(!adapter.find) return cb("No find() method defined in adapter!");
options = normalizeCriteria(options);
adapter.find ? adapter.find(collectionName,options,cb) : cb();
adapter.find ? adapter.find(collectionName, options, cb) : cb();

@@ -128,10 +132,17 @@ // TODO: Return model instance Promise object for joins, etc.

this.update = function(collectionName, criteria, values, cb) {
if(!adapter.update) return cb("No update() method defined in adapter!");
criteria = normalizeCriteria(criteria);
adapter.update ? adapter.update(collectionName,criteria,values,cb) : cb();
// TODO: Validate constraints using Anchor
// TODO: Automatically change updatedAt (if enabled)
adapter.update ? adapter.update(collectionName, criteria, values, cb) : cb();
// TODO: Return model instance Promise object for joins, etc.
};
this.destroy = function(collectionName, criteria, cb) {
if(!adapter.destroy) return cb("No destroy() method defined in adapter!");
criteria = normalizeCriteria(criteria);
adapter.destroy ? adapter.destroy(collectionName,criteria,cb) : cb();
adapter.destroy ? adapter.destroy(collectionName, criteria, cb) : cb();

@@ -144,11 +155,11 @@ // TODO: Return model instance Promise object for joins, etc.

//////////////////////////////////////////////////////////////////////
this.findOrCreate = function (collectionName, criteria, values, cb) {
var self = this;
this.findOrCreate = function(collectionName, criteria, values, cb) {
var self = this;
criteria = normalizeCriteria(criteria);
if (adapter.findOrCreate) adapter.findOrCreate(collectionName, criteria, values, cb);
if(adapter.findOrCreate) adapter.findOrCreate(collectionName, criteria, values, cb);
else {
// TODO: ADD A TRANSACTION LOCK HERE!!
self.find(collectionName,criteria,function (err,results) {
if (err) cb(err);
else if (results.length > 0) cb(null,results);
self.find(collectionName, criteria, function(err, results) {
if(err) cb(err);
else if(results && results.length > 0) cb(null, results);
else self.create(collectionName, values, cb);

@@ -160,5 +171,5 @@ });

};
this.findAndUpdate = function (collectionName, criteria, values, cb) {
this.findAndUpdate = function(collectionName, criteria, values, cb) {
criteria = normalizeCriteria(criteria);
if (adapter.findAndUpdate) adapter.findAndUpdate(collectionName, criteria, values, cb);
if(adapter.findAndUpdate) adapter.findAndUpdate(collectionName, criteria, values, cb);
else this.update(collectionName, criteria, values, cb);

@@ -168,5 +179,5 @@

};
this.findAndDestroy = function (collectionName, criteria, cb) {
this.findAndDestroy = function(collectionName, criteria, cb) {
criteria = normalizeCriteria(criteria);
if (adapter.findAndDestroy) adapter.findAndDestroy(collectionName, criteria, cb);
if(adapter.findAndDestroy) adapter.findAndDestroy(collectionName, criteria, cb);
else this.destroy(collectionName, criteria, cb);

@@ -178,69 +189,111 @@

// App-level transaction
this.transaction = function(transactionName, cb) {
var self = this;
// Generate unique lock
var newLock = {
uuid: uuid.v4(),
name: transactionName,
timestamp: epoch(),
cb: cb
};
// console.log("Generating lock "+newLock.uuid+" ("+transactionName+")");
// write new lock to commit log
this.transactionCollection.create(newLock, function(err) {
if(err) return cb(err, function() {
throw err;
});
// Begin an atomic transaction
// lock models in collection which fit criteria (if criteria is null, lock all)
this.lock = function (collectionName, criteria, cb) {
// Check if lock was written, and is the oldest with the proper name
self.transactionCollection.findAll(function(err, locks) {
if(err) return cb(err, function() {
throw err;
});
// Allow criteria argument to be omitted
if (_.isFunction(criteria)) {
cb = criteria;
criteria = null;
}
var conflict = false;
_.each(locks, function(entry) {
// **************************************
// NAIVE SOLUTION
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note)
// If a conflict IS found, respect the oldest
// (the conflict-causer is responsible for cleaning up his entry-- ignore it!)
if(entry.name === newLock.name && entry.uuid !== newLock.uuid && true && //entry.timestamp <= newLock.timestamp &&
entry.id < newLock.id) conflict = entry;
});
// No need to check the fridge! Just start writing your note.
// If there are no conflicts, the lock is acquired!
if(!conflict) {
self.lock(newLock, cb);
} else {
// console.log("************ Conflict encountered:: lock already exists for that transaction!!");
// console.log("MY LOCK:: transaction: "+newLock.name," uuid: "+newLock.uuid, "timestamp: ",newLock.timestamp);
// console.log("CONFLICTING LOCK:: transaction: "+conflict.name," uuid: "+conflict.uuid, "timestamp: ",conflict.timestamp);
// console.log("***************");
}
// TODO: Generate identifier for this transaction (use collection name to start with,
// but better yet, boil down criteria to essentials to allow for more concurrent access)
// TODO: Create entry in transaction DB (write a note on the fridge and check it)
// TODO: Check the transaction db (CHECK THE DAMN FRIDGE IN CASE ONE OF YOUR ROOMMATES WROTE THE NOTE WHILE YOU WERE BUSY)
// Otherwise, get in line
// In other words, do nothing--
// unlock() will grant lock request in order it was received
});
});
};
// TODO: If > 1 entry exists in the transaction db, subscribe to mutex queue to be notified later
// (if you see a note already on the fridge, get in line to be notified when roommate gets home)
this.lock = function(newLock, cb) {
var self = this;
// console.log("====> Lock acquired "+newLock.uuid+" ("+newLock.name+")");
var warningTimer = setTimeout(function() {
console.error("Transaction :: " + newLock.name + " is taking an abnormally long time (> " + self.config.transactionWarningTimer + "ms)");
}, self.config.transactionWarningTimer);
// TODO: Otherwise, trigger callback! QA immediately (you're good to go get the milk)
cb(null, function unlock(cb) {
clearTimeout(warningTimer);
self.unlock(newLock.uuid, newLock.name, cb);
});
};
// **************************************
// AGRESSIVE SOLUTION
// (all roommates try to go get the milk, but the first person to get the milk prevents others from putting it in the fridge)
// TODO: Ask locksmith for model clone
// TODO: Pass model clone in callback
this.unlock = function(uuid, transactionName, cb) {
var self = this;
// console.log("Releasing lock "+uuid+" ("+transactionName+")");
// Remove current lock
self.transactionCollection.destroy({
uuid: uuid
}, function(err) {
if(err) return cb && cb(err);
// console.log("<≠≠≠≠≠ Lock released :: "+uuid+" ("+transactionName+")");
self.transactionCollection.findAll(function(err, locks) {
if(err) return cb && cb(err);
adapter.lock ? adapter.lock(collectionName,criteria,cb) : cb();
};
// Determine the next user in line (oldest lock w/ the proper transactionName)
var nextInLine = getNextLock(locks, transactionName);
// nextInLine ? console.log("Preparing to hand off lock to "+nextInLine.uuid+" ("+nextInLine.name+")") : console.log("No locks remaining !!!");
// Trigger unlock's callback if specified
cb && cb();
// Commit and end an atomic transaction
// unlock models in collection which fit criteria (if criteria is null, unlock all)
this.unlock = function (collectionName, criteria, cb) {
// Now allow the nextInLine lock to be acquired
// This marks the end of the previous transaction
nextInLine && self.lock(nextInLine, nextInLine.cb);
// Allow criteria argument to be omitted
if (_.isFunction(criteria)) {
cb = criteria;
criteria = null;
}
});
});
};
// **************************************
// NAIVE SOLUTION
// (only the first roommate to notice gets the milk; the rest wait as soon as they see the note)
// TODO: Remove entry from transaction db (Remove your note from fridge)
// TODO: Callback can be triggered immediately, since you're sure the note will be removed
// Find the oldest lock with the same transaction name
// ************************************************************
// this function wouldn't be necessary if we could....
// TODO: call find() with the [currently unfinished] ORDER option
// ************************************************************
adapter.unlock ? adapter.unlock(collectionName,criteria,cb) : cb();
};
function getNextLock(locks, transactionName) {
var nextLock;
_.each(locks, function(lock) {
// Ignore locks with different names
if(lock.name !== transactionName) return;
this.status = function (collectionName, cb) {
adapter.status ? adapter.status(collectionName,cb) : cb();
};
// If this is the first one, or this lock is older than the one we have, use it
if(!nextLock || lock.timestamp < nextLock.timestamp) nextLock = lock;
});
return nextLock;
}
this.autoIncrement = function (collectionName, values,cb) {
adapter.autoIncrement ? adapter.autoIncrement(collectionName, values, cb) : cb();
};
// If @collectionName and @otherCollectionName are both using this adapter, do a more efficient remote join.

@@ -253,3 +306,2 @@ // (By default, an inner join, but right and left outer joins are also supported.)

// Sync given collection's schema with the underlying data model
// Scheme can be 'drop' or 'alter'
// Controls whether database is dropped and recreated when app starts,

@@ -260,9 +312,10 @@ // or whether waterline will try and synchronize the schema with the app models.

// Drop and recreate collection
drop: function(collection,cb) {
drop: function(collection, cb) {
var self = this;
this.drop(collection.identity,function (err,data) {
self.define(collection.identity,collection,cb);
this.drop(collection.identity, function(err, data) {
if(err) cb(err);
else self.define(collection.identity, collection, cb);
});
},
// Alter schema

@@ -273,34 +326,76 @@ alter: function(collection, cb) {

// Check that collection exists-- if it doesn't go ahead and add it and get out
this.describe(collection.identity,function (err,data) {
if (err) throw err;
else if (!data) return self.define(collection.identity,collection,cb);
});
this.describe(collection.identity, function(err, data) {
data = _.clone(data);
// Iterate through each attribute on each model in your app
_.each(collection.attributes, function checkAttribute(attribute) {
// and make sure that a comparable field exists in the data store
// TODO
});
if(err) return cb(err);
else if(!data) return self.define(collection.identity, collection, cb);
// Check that the attribute exists in the data store
// TODO
// TODO: move all of this to the alter() call in the adapter
// If not, alter the collection to include it
// TODO
// If it *DOES* exist, we'll try to guess what changes need to be made
// Iterate through each attribute in this collection
// and make sure that a comparable field exists in the model
// TODO
// Iterate through each attribute in this collection's schema
_.each(collection.attributes, function checkAttribute(attribute,attrName) {
// Make sure that a comparable field exists in the data store
if (!data[attrName]) {
data[attrName] = attribute;
// If not, alter the collection and remove it
// TODO
cb();
// Add the default value for this new attribute to each row in the data model
// TODO
}
// And that it matches completely
else {
data[attrName] = attribute;
// Update the data belonging to this attribute to reflect the new properties
// Realistically, this will mainly be about constraints, and primarily uniquness
// It'd be good if waterline could enforce all constraints at this time,
// but there's a trade-off with destroying people's data
// TODO
}
});
// Now iterate through each attribute in the adapter's data store
// and remove any that don't have an analog in the collection definition
// Also prune the data belonging to removed attributes from rows
// TODO:
// Persist that
// Check that the attribute exists in the data store
// TODO
// If not, alter the collection to include it
// TODO
// Iterate through each attribute in this collection
// and make sure that a comparable field exists in the model
// TODO
// If not, alter the collection and remove it
// TODO
// cb();
});
},
// Do nothing to the underlying data model
safe: function (collection,cb) {
cb();
}
};
// Always grant access to a few of Adapter's methods to the user adapter instance
// (things that may or may not be defined by the user adapter)
adapter.transaction = function(name, cb) {
return self.transaction(name, cb);
};
// adapter.teardown = adapter.teardown || self.teardown;
// adapter.teardownCollection = adapter.teardownCollection || self.teardownCollection;
// Bind adapter methods to self
_.bindAll(adapter);
_.bindAll(this);
_.bind(this.sync.drop,this);
_.bind(this.sync.alter,this);
_.bind(this.sync.drop, this);
_.bind(this.sync.alter, this);

@@ -316,3 +411,4 @@ // Mark as valid adapter

*/
function plural (collection, application) {
function plural(collection, application) {
if(_.isArray(collection)) {

@@ -328,6 +424,11 @@ return _.map(collection, application);

// Normalize the different ways of specifying criteria into a uniform object
function normalizeCriteria (criteria) {
function normalizeCriteria(criteria) {
if(!criteria) return {
where: null
};
// Empty undefined values from criteria object
_.each(criteria,function(val,key) {
if (val === undefined) delete criteria[key];
_.each(criteria, function(val, key) {
if(val === undefined) delete criteria[key];
});

@@ -343,11 +444,11 @@

}
if (!criteria.where && !criteria.limit &&
!criteria.skip && !criteria.offset &&
!criteria.order) {
criteria = { where: criteria };
if(!criteria.where && !criteria.limit && !criteria.skip && !criteria.offset && !criteria.order) {
criteria = {
where: criteria
};
}
// If any item in criteria is a parsable finite number, use that
for (var attrName in criteria.where) {
if (Math.pow(+criteria.where[attrName],2) > 0) {
for(var attrName in criteria.where) {
if(Math.pow(+criteria.where[attrName], 2) > 0) {
criteria.where[attrName] = +criteria.where[attrName];

@@ -358,2 +459,8 @@ }

return criteria;
}
// Number of miliseconds since the Unix epoch Jan 1st, 1970
function epoch() {
return(new Date()).getTime();
}

@@ -8,2 +8,11 @@ // Dependencies

// ******************************************
// Poor man's auto-increment
// ******************************************
// In production, the transaction database should be set to something else
// with true, database-side auto-increment capabilities
// This in-memory auto-increment will not scale to a multi-instance / cluster setup.
// ******************************************
var statusDb = {};
/*---------------------

@@ -22,23 +31,13 @@ :: DirtyAdapter

// Attributes are case insensitive by default
// attributesCaseSensitive: false,
// If inMemory is true, all data will be destroyed when the server stops
inMemory: true,
// What persistence scheme is being used?
// Is the db dropped & recreated each time or persisted to disc?
persistent: false,
// File path for disk file output (in persistent mode)
// File path for disk file output (when NOT in inMemory mode)
dbFilePath: './.waterline/dirty.db',
// String to precede key name for schema defininitions
schemaPrefix: 'sails_schema_',
schemaPrefix: 'waterline_schema_',
// String to precede key name for actual data in collection
dataPrefix: 'sails_data_',
// String to precede key name for collection status data
statusPrefix: 'sails_status_',
// String to precede key name for transaction data
lockPrefix: 'sails_transactions_'
dataPrefix: 'waterline_data_'
},

@@ -50,11 +49,12 @@

if(this.config.persistent) {
if(! this.config.inMemory) {
// Check that dbFilePath file exists and build tree as necessary
require('fs-extra').touch(this.config.dbFilePath, function (err) {
if (err) return cb(err);
require('fs-extra').touch(this.config.dbFilePath, function(err) {
if(err) return cb(err);
my.db = new(dirty.Dirty)(my.config.dbFilePath);
afterwards();
});
}
else {
} else {
this.db = new(dirty.Dirty)();

@@ -65,2 +65,3 @@ afterwards();

function afterwards() {
// Make logger easily accessible

@@ -76,6 +77,16 @@ my.log = my.config.log;

// Tear down any remaining connections to the underlying data model
teardown: function(cb) {
this.db = null;
cb && cb();
// Logic to handle flushing collection data to disk before the adapter shuts down
teardownCollection: function(collectionName, cb) {
var my = this;
// Always go ahead and write the new auto-increment to disc, even though it will be wrong sometimes
// (this is done so that the auto-increment counter can be "ressurected" when the adapter is restarted from disk)
// console.log("******** Wrote to "+collectionName+":: AI => ", statusDb[collectionName].autoIncrement);
var schema = _.extend(this.db.get(this.config.schemaPrefix + collectionName),{
autoIncrement: statusDb[collectionName].autoIncrement
});
this.db.set(this.config.schemaPrefix + collectionName, schema, function (err) {
my.db = null;
cb && cb(err);
});
},

@@ -85,20 +96,27 @@

// Fetch the schema for a collection
describe: function(collectionName, cb) {
var schema, err;
try {
schema = this.db.get(this.config.schemaPrefix+collectionName);
}
catch (e) {
err = e;
}
this.log(" DESCRIBING :: "+collectionName,{
err: err,
schema: schema
// (contains attributes and autoIncrement value)
describe: function(collectionName, cb) {
this.log(" DESCRIBING :: " + collectionName);
var schema = this.db.get(this.config.schemaPrefix + collectionName);
var attributes = schema && schema.attributes;
return cb(null, attributes);
},
// Fetch the current auto-increment value
getAutoIncrement: function (collectionName,cb) {
var schema = this.db.get(this.config.schemaPrefix + collectionName);
return cb(err,schema.autoIncrement);
},
// Persist the current auto-increment value
setAutoIncrement: function (collectionName,cb) {
this.db.set(this.config.schemaPrefix + collectionName, {
});
return cb(err,schema);
return cb(err,schema.autoIncrement);
},
// Create a new collection
define: function(collectionName, schema, cb) {
this.log(" DEFINING "+collectionName, {
define: function(collectionName, attributes, cb) {
this.log(" DEFINING " + collectionName, {
as: schema

@@ -108,8 +126,14 @@ });

var schema = {
attributes: _.clone(attributes),
autoIncrement: 1
};
// Write schema and status objects
return self.db.set(this.config.schemaPrefix+collectionName,schema,function (err) {
if (err) return cb(err);
return self.db.set(self.config.statusPrefix+collectionName,{
autoIncrement: 1
},cb);
return self.db.set(this.config.schemaPrefix + collectionName, schema, function(err) {
if(err) return cb(err);
// Set in-memory schema for this collection
statusDb[collectionName] = schema;
cb();
});

@@ -121,6 +145,6 @@ },

var self = this;
self.log(" DROPPING "+collectionName);
return self.db.rm(self.config.dataPrefix+collectionName,function (err) {
if (err) return cb("Could not drop collection!");
return self.db.rm(self.config.schemaPrefix+collectionName,cb);
self.log(" DROPPING " + collectionName);
return self.db.rm(self.config.dataPrefix + collectionName, function(err) {
if(err) return cb("Could not drop collection!");
return self.db.rm(self.config.schemaPrefix + collectionName, cb);
});

@@ -131,8 +155,6 @@ },

alter: function(collectionName, newAttrs, cb) {
this.log(" ALTERING "+collectionName);
this.db.describe(collectionName,function (e0,existingSchema) {
if (err) return cb(collectionName+" does not exist!");
var schema = _.extend(existingSchema,newAttrs);
return this.db.set(this.config.schemaPrefix+collectionName,schema,cb);
});
this.log(" ALTERING " + collectionName);
var schema = this.db.get(this.config.schemaPrefix + collectionName);
schema = _.extend(schema.attributes, newAttrs);
return this.db.set(this.config.schemaPrefix + collectionName, schema, cb);
},

@@ -144,14 +166,30 @@

create: function(collectionName, values, cb) {
this.log(" CREATING :: "+collectionName,values);
var dataKey = this.config.dataPrefix+collectionName;
this.log(" CREATING :: " + collectionName, values);
values = values || {};
var dataKey = this.config.dataPrefix + collectionName;
var data = this.db.get(dataKey);
var self = this;
// Create new model
// (if data collection doesn't exist yet, create it)
data = data || [];
data.push(values);
// Replace data collection and go back
this.db.set(dataKey,data,function (err) {
cb(err,values);
// Lookup schema & status so we know all of the attribute names and the current auto-increment value
var schema = this.db.get(this.config.schemaPrefix + collectionName);
// Auto increment fields that need it
doAutoIncrement(collectionName, schema.attributes, values, this, function (err, values) {
if (err) return cb(err);
self.describe(collectionName, function(err, attributes) {
if(err) return cb(err);
// TODO: add other fields with default values
// Create new model
// (if data collection doesn't exist yet, create it)
data = data || [];
data.push(values);
// Replace data collection and go back
self.db.set(dataKey, data, function(err) {
return cb(err, values);
});
});
});

@@ -164,3 +202,3 @@ },

find: function(collectionName, options, cb) {
var criteria = options.where;

@@ -174,8 +212,7 @@

////////////////////////////////////////////////
var dataKey = this.config.dataPrefix+collectionName;
var dataKey = this.config.dataPrefix + collectionName;
var data = this.db.get(dataKey);
// Query and return result set using criteria
cb(null,applyFilter(data,criteria));
cb(null, applyFilter(data, criteria));
},

@@ -185,3 +222,3 @@

update: function(collectionName, options, values, cb) {
this.log(" UPDATING :: "+collectionName,{
this.log(" UPDATING :: " + collectionName, {
options: options,

@@ -200,4 +237,3 @@ values: values

////////////////////////////////////////////////
var dataKey = this.config.dataPrefix+collectionName;
var dataKey = this.config.dataPrefix + collectionName;
var data = this.db.get(dataKey);

@@ -207,22 +243,22 @@

var resultIndices = [];
_.each(data,function (row,index) {
my.log('matching row/index',{
_.each(data, function(row, index) {
my.log('matching row/index', {
row: row,
index: index
});
my.log("against",criteria);
my.log("with outcome",matchSet(row,criteria));
my.log("against", criteria);
my.log("with outcome", matchSet(row, criteria));
if (matchSet(row,criteria)) resultIndices.push(index);
if(matchSet(row, criteria)) resultIndices.push(index);
});
this.log("filtered indices::",resultIndices,'criteria',criteria);
this.log("filtered indices::", resultIndices, 'criteria', criteria);
// Update value(s)
_.each(resultIndices,function(index) {
data[index] = _.extend(data[index],values);
_.each(resultIndices, function(index) {
data[index] = _.extend(data[index], values);
});
// Replace data collection and go back
this.db.set(dataKey,data,function (err) {
cb(err,values);
this.db.set(dataKey, data, function(err) {
cb(err, values);
});

@@ -233,3 +269,3 @@ },

destroy: function(collectionName, options, cb) {
this.log(" DESTROYING :: "+collectionName,options);
this.log(" DESTROYING :: " + collectionName, options);

@@ -244,13 +280,12 @@ var criteria = options.where;

////////////////////////////////////////////////
var dataKey = this.config.dataPrefix+collectionName;
var dataKey = this.config.dataPrefix + collectionName;
var data = this.db.get(dataKey);
// Query result set using criteria
data = _.reject(data,function (row,index) {
return matchSet(row,criteria);
data = _.reject(data, function(row, index) {
return matchSet(row, criteria);
});
// Replace data collection and go back
this.db.set(dataKey,data,function (err) {
this.db.set(dataKey, data, function(err) {
cb(err);

@@ -261,100 +296,3 @@ });

// Look for auto-increment fields, increment counters accordingly, and return refined values
autoIncrement: function (collectionName, values, cb) {
// Lookup schema & status so we know all of the attribute names and the current auto-increment value
var schema = this.db.get(this.config.schemaPrefix+collectionName);
var status = this.db.get(this.config.statusPrefix+collectionName);
var self = this;
// if this is an autoIncrement field, increment it in values set
var keys = _.keys(_.extend({},schema,values));
async.forEach(keys, function (attrName,cb) {
if (_.isObject(schema[attrName]) && schema[attrName].autoIncrement) {
values[attrName] = status.autoIncrement;
// Then, increment the status db persistently
status.autoIncrement++;
self.db.set(self.config.statusPrefix+collectionName,status,cb);
}
else cb();
}, function (err) {
return cb(err,values);
});
},
// 2PL
// Lock access to this collection or a subset of it
// (assume this is a 'write' lock)
// TODO: Smarter locking (i.e. don't always lock the entire collection)
lock: function (collectionName, criteria, cb) {
var self = this;
var commitLogKey = this.config.lockPrefix+collectionName;
var locks = this.db.get(commitLogKey);
locks = locks || [];
// Generate unique lock and update commit log as if lock was acquired
var newLock = {
id: uuid.v4(),
type: 'write',
timestamp: epoch(),
cb: cb
};
locks.push(newLock);
// Write commit log to disc
this.db.set(commitLogKey,locks,function (err) {
if (err) return cb(err);
// Verify that lock was successfully acquired
// (i.e. no other locks w/ overlapping criteria exist)
var conflict = false;
locks = self.db.get(commitLogKey);
_.each(locks,function (entry) {
// If a conflict IS found, respect the oldest
// (the conflict-causer is responsible for cleaning up his entry)
if (entry.id !== newLock.id &&
entry.timestamp <= newLock.timestamp) conflict = true;
// Otherwise, other lock is older-- ignore it
});
// Lock acquired!
if (!conflict) {
self.log("Acquired lock :: "+newLock.id);
cb();
}
// Otherwise, get in line
// In other words, do nothing--
// unlock() will grant lock request in order it was received
});
},
unlock: function (collectionName, criteria, cb) {
var self = this;
var commitLogKey = this.config.lockPrefix+collectionName;
var locks = this.db.get(commitLogKey);
locks = locks || [];
// Guess current lock by grabbing the oldest
// (this will only work if unlock() is used inside of a transaction)
var currentLock = getOldest(locks);
if (!currentLock) return cb('Trying to unlock, but no lock exists!');
// Remove currentLock
locks = _.without(locks,currentLock);
this.db.set(commitLogKey,locks,function (err) {
// Trigger unlock's callback
if (err) return cb(err);
else cb();
// Now allow the next user in line to acquire the lock (trigger the NEW oldest lock's callback)
// This marks the end of the previous transaction
var nextInLine = getOldest(locks);
nextInLine && nextInLine.cb();
});
},
// Identity is here to facilitate unit testing

@@ -371,10 +309,30 @@ // (this is optional and normally automatically populated based on filename)

// Look for auto-increment field, increment counter accordingly, and return refined value set
function doAutoIncrement (collectionName, attributes, values, ctx, cb) {
// Determine the attribute names which will be included in the created object
var attrNames = _.keys(_.extend({}, attributes, values));
// increment AI fields in values set
_.each(attrNames, function(attrName) {
if(_.isObject(attributes[attrName]) && attributes[attrName].autoIncrement) {
values[attrName] = statusDb[collectionName].autoIncrement;
// Then, increment the auto-increment counter for this collection
statusDb[collectionName].autoIncrement++;
}
});
// Return complete values set w/ auto-incremented data
return cb(null,values);
}
// Run criteria query against data aset
function applyFilter (data,criteria) {
if (criteria) {
return _.filter(data,function (model) {
return matchSet(model,criteria);
function applyFilter(data, criteria) {
if(criteria && data) {
return _.filter(data, function(model) {
return matchSet(model, criteria);
});
}
else return data;
} else return data;
}

@@ -384,6 +342,10 @@

// Match a model against each criterion in a criteria query
function matchSet (model, criteria) {
function matchSet(model, criteria) {
// Null WHERE query always matches everything
if(!criteria) return true;
// By default, treat entries as AND
for (var key in criteria) {
if (! matchItem(model,key,criteria[key])) return false;
for(var key in criteria) {
if(!matchItem(model, key, criteria[key])) return false;
}

@@ -394,23 +356,25 @@ return true;

function matchOr (model, disjuncts) {
function matchOr(model, disjuncts) {
var outcome = false;
_.each(disjuncts,function (criteria) {
if (matchSet(model,criteria)) outcome = true;
_.each(disjuncts, function(criteria) {
if(matchSet(model, criteria)) outcome = true;
});
return outcome;
}
function matchAnd (model, conjuncts) {
function matchAnd(model, conjuncts) {
var outcome = true;
_.each(conjuncts,function (criteria) {
if (!matchSet(model,criteria)) outcome = false;
_.each(conjuncts, function(criteria) {
if(!matchSet(model, criteria)) outcome = false;
});
return outcome;
}
function matchLike (model, criteria) {
for (var key in criteria) {
function matchLike(model, criteria) {
for(var key in criteria) {
// Make attribute names case insensitive unless overridden in config
if (! adapter.config.attributesCaseSensitive) key = key.toLowerCase();
if(!adapter.config.attributesCaseSensitive) key = key.toLowerCase();
// Check that criterion attribute and is at least similar to the model's value for that attr
if ( !model[key] || (!~model[key].indexOf(criteria[key]) ) ) {
if(!model[key] || (!~model[key].indexOf(criteria[key]))) {
return false;

@@ -421,24 +385,22 @@ }

}
function matchNot (model,criteria) {
return !matchSet(model,criteria);
function matchNot(model, criteria) {
return !matchSet(model, criteria);
}
function matchItem (model,key,criterion) {
function matchItem(model, key, criterion) {
// Make attribute names case insensitive unless overridden in config
if (! adapter.config.attributesCaseSensitive) key = key.toLowerCase();
if(!adapter.config.attributesCaseSensitive) key = key.toLowerCase();
if (key.toLowerCase() === 'or') {
return matchOr(model,criterion);
if(key.toLowerCase() === 'or') {
return matchOr(model, criterion);
} else if(key.toLowerCase() === 'not') {
return matchNot(model, criterion);
} else if(key.toLowerCase() === 'and') {
return matchAnd(model, criterion);
} else if(key.toLowerCase() === 'like') {
return matchLike(model, criterion);
}
else if (key.toLowerCase() === 'not') {
return matchNot(model,criterion);
}
else if (key.toLowerCase() === 'and') {
return matchAnd(model,criterion);
}
else if (key.toLowerCase() === 'like') {
return matchLike(model,criterion);
}
// Otherwise this is an attribute name: ensure it exists and matches
else if ( !model[key] || (model[key] !== criterion)) {
else if(!model[key] || (model[key] !== criterion)) {
return false;

@@ -450,14 +412,16 @@ }

// Number of miliseconds since the Unix epoch Jan 1st, 1970
function epoch () {
return (new Date()).getTime();
function epoch() {
return(new Date()).getTime();
}
// Return the oldest lock in the collection
function getOldest (locks) {
function getOldest(locks) {
var currentLock;
_.each(locks,function (lock) {
if (!currentLock) currentLock = lock;
else if (lock.timestamp < currentLock.timestamp) currentLock = lock;
_.each(locks, function(lock) {
if(!currentLock) currentLock = lock;
else if(lock.timestamp < currentLock.timestamp) currentLock = lock;
});
return currentLock;
}

@@ -6,12 +6,22 @@ var _ = require('underscore');

var Collection = module.exports = function(definition) {
var self = this;
// Sync (depending on scheme)
definition.scheme = definition.scheme || 'alter';
switch (definition.scheme) {
case "drop" : definition.sync = _.bind(definition.adapter.sync.drop, definition.adapter, definition); break;
case "alter": definition.sync = _.bind(definition.adapter.sync.alter, definition.adapter, definition); break;
// Not having a scheme is not an error, just default to alter
// default : throw new Error('Invalid scheme in '+definition.identity+' model!');
// ********************************************************************
// Configure collection-specific configuration
// Copy over only the methods from the adapter that you need, and modify if necessary
// ********************************************************************
// (defaults to 'alter')
definition.migrate = !_.isUndefined(definition.migrate) ? definition.migrate : 'alter';
if (definition.migrate === 'drop') {
definition.sync = _.bind(definition.adapter.sync.drop, definition.adapter, definition);
}
else if (definition.migrate === 'alter') {
definition.sync = _.bind(definition.adapter.sync.alter, definition.adapter, definition);
}
else if (definition.migrate === 'safe') {
definition.sync = _.bind(definition.adapter.sync.safe, definition.adapter, definition);
}

@@ -23,2 +33,6 @@ // Absorb definition methods

this.create = function(values, cb) {
if (_.isFunction(values)) {
cb = values;
values = null;
}
return this.adapter.create(this.identity,values,cb);

@@ -28,4 +42,10 @@ };

this.find = function(options, cb) {
if (_.isFunction(options) || !options) {
throw new Error('find(criteria,callback) requires a criteria parameter. To get all models in a collection, use findAll()');
}
return this.adapter.find(this.identity,options,cb);
};
this.findAll = function (cb) {
return this.adapter.find(this.identity,null,cb);
};
// Call update method in adapter

@@ -50,11 +70,5 @@ this.update = function(criteria, values, cb) {

this.lock = function(criteria, cb) {
return this.adapter.lock(this.identity,criteria,cb);
this.transaction = function (name, cb) {
return this.adapter.transaction(name, cb);
};
this.unlock = function(criteria, cb) {
return this.adapter.unlock(this.identity,criteria,cb);
};
this.cancel = function(criteria, cb) {
return this.adapter.cancel(this.identity,criteria,cb);
};

@@ -61,0 +75,0 @@ //////////////////////////////////////////

@@ -0,3 +1,9 @@

// Global adapter defaults
module.exports = {
// This uses an auto-incrementing integer attirbute (id) as the primary key for the collection
// This is a common pattern and best-practice in relational and non-relational databases,
// since it eliminates confusion when more than one developer hops on the project
defaultPK: true,
// Automatically define updatedAt field in schema and refresh with the current timestamp when models are updated

@@ -7,3 +13,21 @@ updatedAt: true,

// Automatically define createdAt field in schema and populate with the current timestamp during model creation
createdAt: true
createdAt: true,
// Attributes are case insensitive by default
// attributesCaseSensitive: false,
// Define a collection to use for app-level transactions
// TODO: replace this with convention of the "Transaction.js" collection
transactionCollection: require('./collections/Transaction.js'),
// ms to wait before warning that a tranaction is taking too long
// TODO: move this logic as a configuration option into the actual transaction collection
transactionWarningTimer: 2000,
// ms to wait before timing out a transaction and calling unlock() with an error
// (App can then handle the logic to undo the transaction)
// TODO: Make this work
// TODO: move this logic as a configuration option into the actual transaction collection
transactionTimeout: 15000
};
{
"name": "waterline",
"version": "0.0.5",
"version": "0.0.51",
"description": "Adaptable data access layer for Node.js",

@@ -38,4 +38,5 @@ "main": "waterline.js",

"sails-util": "0.0.0",
"parley": "0.0.2"
"parley": "0.0.2",
"microtime": "~0.3.3"
}
}

@@ -0,7 +1,11 @@

// Keep a reference to waterline to use for teardown()
var waterline = require("../waterline.js");
var adapters, collections;
var _ = require('underscore');
var parley = require('parley');
var assert = require("assert");
var waterline = require('../waterline');
var collections = require('../buildDictionary.js')(__dirname + '/collections', /(.+)\.js$/);
module.exports = {

@@ -12,2 +16,4 @@

teardown: teardown,
// Override every collection's adapter with the specified adapter

@@ -26,11 +32,23 @@ // Then return the init() method

// Initialize waterline
function initialize (exit) {
require("../waterline.js")({
function initialize (done) {
waterline({
collections: collections,
log: blackhole
}, exit);
}, function (err, waterlineData){
adapters = waterlineData.adapters;
collections = waterlineData.collections;
done(err);
});
}
// Tear down adapters and collections
function teardown (done) {
waterline.teardown({
adapters: adapters,
collections: collections
},done);
}
// Use silent logger for testing
// (this prevents annoying output from cluttering up our nice clean console)
var blackhole = function (){};

@@ -9,4 +9,10 @@ /*---------------------

// exports.scheme = 'alter';
// What synchronization scheme to use: default is 'alter'
//
// 'drop' => Delete the database and recreate it when the server starts
// 'alter' => Do a best-guess automatic migration from the existing data model to the new one
// 'safe' => Never automatically synchonize-- leave the underlying data alone
exports.migrate = 'drop';
// Attributes for the user data model
exports.attributes = {

@@ -16,3 +22,4 @@ name : 'STRING',

title : 'STRING',
phone : 'STRING'
phone : 'STRING',
type : 'STRING'
};

@@ -19,0 +26,0 @@

@@ -153,3 +153,6 @@ /**

// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts
after(bootstrap.teardown);
});
};

@@ -14,8 +14,8 @@ /**

var buildDictionary = require('../buildDictionary.js');
var bootstrap = require('./bootstrap.test.js');
describe('waterline', function() {
// Bootstrap waterline with default adapters and bundled test collections
before(require('./bootstrap.test.js').init);
before(bootstrap.init);

@@ -27,2 +27,5 @@ describe('#initialize() and sync()', function() {

});
});
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts
after(bootstrap.teardown);
});

@@ -12,2 +12,3 @@ /**

var parley = require('parley');
var async = require('async');
var assert = require("assert");

@@ -29,36 +30,106 @@

describe('static semaphore (collection-wide lock)', function() {
describe('app-level transaction', function() {
it('should be able to acquire lock', function(done) {
User.lock(null, done);
User.transaction("test", function(err, unlock) {
unlock();
done(err);
});
});
it('should NOT allow another lock to be acquired until the first lock is released', function(done) {
var orderingTest = [];
// The callback should not fire until the lock is released
User.lock(null, function (err) {
if (err) throw err;
orderingTest.push('lock');
if (orderingTest[0] === 'unlock' &&
orderingTest[1] === 'lock' &&
orderingTest.length === 2) {
done();
}
else throw "The lock was acquired by two users at once!";
User.transaction('test', function(err, unlock1) {
if(err) return done(err);
if(!unlock1) throw new Error("No unlock() method provided!");
testAppendLock();
User.transaction('test', function(err, unlock2) {
if(err) return done(err);
if(!unlock2) throw new Error("No unlock() method provided!");
testAppendLock();
// Release lock so other tests can use the 'test' transaction
unlock2();
if(_.isEqual(orderingTest, ['lock', 'unlock', 'lock'])) done();
else {
console.error(orderingTest);
throw "The lock was acquired by two users at once!";
}
});
// Set timeout to release the lock after a 1/20 of a second
setTimeout(function() {
unlock1(testAppendUnlock);
}, 50);
});
// Note that other code can still run while the semaphore remains gated
// Appends "unlock" to the orderingTest array and handles any errors
// Set timeout to release the lock after a 1/4 of a second
setTimeout(function () {
User.unlock(null,function(err){
orderingTest.push('unlock');
function testAppendUnlock(err) {
if(err) throw err;
orderingTest.push('unlock');
}
// Appends "lock" to the orderingTest array and handles any errors
function testAppendLock(err) {
if(err) throw err;
orderingTest.push('lock');
}
});
it('should support 10 simultaneous dummy transactions', function(done) {
var constellations = ['Andromeda', 'Antlia', 'Apus', 'Aquarius', 'Aquila', 'Ara', 'Aries', 'Auriga', 'Boötes', 'Caelum'];
dummyTransactionTest(constellations,'constellation',done);
});
it('should support 50 simultaneous dummy transactions', function(done) {
dummyTransactionTest(_.range(50),'number test 1',done);
});
it('should support 200 simultaneous dummy transactions', function(done) {
dummyTransactionTest(_.range(200),'number test 2',done);
});
function dummyTransactionTest(items,type,done) {
async.forEach(items, function(constellation, cb) {
User.transaction('test_create',function(err,unlock) {
User.create({
name: constellation,
type: type
},function(err) {
// Wait a short moment to introduce an element of choas
setTimeout(function() {
unlock();
cb();
},Math.round(Math.random())*5);
});
});
},250);
}, function(err) {
User.find({ type: type },function (err,users) {
if(users.length != items.length) {
console.error("Users: ");
console.error(users);
return done('Proper users were not created!');
}
done();
});
});
}
});
// it ('should timeout if the transaction takes a long time', function (done) {});
// it('should not be able to release a lock more than once', function (done) {});
});
// When this suite of tests is complete, shut down waterline to allow other tests to run without conflicts
after(bootstrap.teardown);
});

@@ -11,2 +11,5 @@ // Dependencies

// Read global config
var config = require('./config.js');
// Util

@@ -18,2 +21,3 @@ var buildDictionary = require('./buildDictionary.js');

/**

@@ -23,23 +27,93 @@ * Prepare waterline to interact with adapters

module.exports = function (options,cb) {
// Only tear down waterline once
// (if teardown() is called explicitly, don't tear it down when the process exits)
var tornDown = false;
var adapters = options.adapters;
var collections = options.collections;
var adapters = options.adapters || {};
var collections = options.collections || {};
var log = options.log || console.log;
var $$ = new parley();
// Merge passed-in adapters with default adapters
adapters = _.extend(builtInAdapters,adapters || {});
adapters = _.extend(builtInAdapters,adapters);
// Error aggregator obj
var errs;
// var errs;
// initialize each adapter in series
// TODO: parallelize this process (would decrease server startup time)
for (var adapterName in adapters) {
$$(async).forEach(_.keys(adapters),prepareAdapter);
// Instantiate special collections
var transactionsDb = $$(instantiateCollection)(config.transactionCollection);
// Attach transaction collection to each adapter
$$(function (err,transactionsDb,xcb) {
_.each(adapters,function(adapter,adapterName){
adapters[adapterName].transactionCollection = transactionsDb;
});
xcb();
})(transactionsDb);
// TODO: in sails, in the same way ---->
// set up session adapter
// set up socket adapter
// ------->
// then associate each collection with its adapter and sync its schema
$$(async).forEach(_.keys(collections),prepareCollection);
// Now that the others are instantiated, add transaction collection to list
// (this is so events like teardownCollection() fire properly)
$$(function (err,transactionsDb,xcb) {
collections[transactionsDb.identity] = transactionsDb;
xcb();
})(transactionsDb);
// Fire teardown() on process-end and make it public
// (this logic lives here to avoid assigning multiple events in each adapter and collection)
$$(function (xcb) {
// Make teardown() public
module.exports.teardown = function(options,cb) {
teardown({
adapters: adapters,
collections: collections
},cb);
};
// When process ends, fire teardown
process.on('SIGINT', process.exit);
process.on('SIGTERM', process.exit);
process.on('exit', function() {
teardown({
adapters: adapters,
collections: collections
});
});
xcb();
})();
// Pass instantiated adapters and models
$$(function (xcb) {
cb(null,{
adapters: adapters,
collections: collections
});
xcb();
})();
// Instantiate an adapter object
function prepareAdapter (adapterName,cb) {
// Pass waterline config down to adapters
adapters[adapterName].config = _.extend({
log: log
}, adapters[adapterName].config);
}, config, adapters[adapterName].config);

@@ -51,11 +125,17 @@ // Build actual adapter object from definition

// Load adapter data source
$$(adapters[adapterName]).initialize();
adapters[adapterName].initialize(cb);
}
// When all adapters are loaded,
// associate each model with its adapter and sync its schema
collections = collections || {};
for (var collectionName in collections) {
// Instantiate a collection object and store it back in the dictionary
function prepareCollection (collectionName, cb) {
var collection = collections[collectionName];
instantiateCollection(collection,function (err,collection) {
collections[collectionName] = collection;
cb(err,collection);
});
}
// Instantiate a collection object
function instantiateCollection (collection, cb) {
// If no adapter is specified, default to 'dirty'

@@ -76,14 +156,39 @@ if (!collection.adapter) collection.adapter = 'dirty';

// Build actual collection object from definition
collections[collectionName] = new Collection(collection);
collection = new Collection(collection);
// Synchronize schema with data source
var e = $$(collection).sync();
$$(function (e) {errs = errs || e;}).ifError(e);
collection.sync(function (err){
if (err) throw err;
cb(err,collection);
});
}
// Pass instantiated adapters and models
$$(cb)(errs,{
adapters: adapters,
collections: collections
});
};
// Tear down all open waterline adapters and collections
function teardown (options,cb) {
cb = cb || function(){};
// Only tear down once
if (tornDown) return cb();
tornDown = true;
// console.log(options.adapters);
async.auto({
// Fire each adapter's teardown event
adapters: function (cb) {
async.forEach(_.values(options.adapters),function (adapter,cb) {
adapter.teardown(cb);
},cb);
},
// Fire each collection's teardown event
collections: function (cb) {
async.forEach(_.values(options.collections),function (collection,cb) {
collection.adapter.teardownCollection(collection.identity,cb);
},cb);
}
}, cb);
}
};
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