offshore
Version:
An ORM for Node.js
318 lines (255 loc) • 10.3 kB
JavaScript
var _ = require('lodash');
var async = require('async');
var Schema = require('offshore-schema');
var Connections = require('./offshore/connections');
var CollectionLoader = require('./offshore/collection/loader');
var COLLECTION_DEFAULTS = require('./offshore/collection/defaults');
var hasOwnProperty = require('./offshore/utils/helpers').object.hasOwnProperty;
/**
* Offshore
*/
var Offshore = module.exports = function() {
if(!(this instanceof Offshore)) {
return new Offshore();
}
// Keep track of all the collections internally so we can build associations
// between them when needed.
this._collections = [];
// Keep track of all the active connections used by collections
this._connections = {};
return this;
};
/*
***********************************************************
* Modules that can be extended
***********************************************************/
// Collection to be extended in your application
Offshore.Collection = require('./offshore/collection');
// Model Instance, returned as query results
Offshore.Model = require('./offshore/model');
Offshore.Transaction = require('./offshore/query/transaction');
/*
***********************************************************
* Prototype Methods
***********************************************************/
/**
* loadCollection
*
* Loads a new Collection. It should be an extended Offshore.Collection
* that contains your attributes, instance methods and class methods.
*
* @param {Object} collection
* @return {Object} internal models dictionary
* @api public
*/
Offshore.prototype.loadCollection = function(collection) {
// Cache collection
this._collections.push(collection);
return this._collections;
};
/**
* initialize
*
* Creates an initialized version of each Collection and auto-migrates depending on
* the Collection configuration.
*
* @param {Object} config object containing adapters
* @param {Function} callback
* @return {Array} instantiated collections
* @api public
*/
Offshore.prototype.initialize = function(options, cb) {
var self = this;
// Ensure a config object is passed in containing adapters
if (!options) throw new Error('Usage Error: function(options, callback)');
if (!options.adapters) throw new Error('Options object must contain an adapters object');
if (!options.connections) throw new Error('Options object must contain a connections object');
// Allow collections to be passed in to the initialize method
if (options.collections) {
for (var collection in options.collections) {
this.loadCollection(options.collections[collection]);
}
// Remove collections from the options after they have been loaded
delete options.collections;
}
// Cache a reference to instantiated collections
this.collections = {};
// Build up all the connections used by the collections
this.connections = new Connections(options.adapters, options.connections);
// Grab config defaults or set them to empty
var defaults = _.merge({}, COLLECTION_DEFAULTS, options.defaults);
// Build a schema map
this.schema = new Schema(this._collections, this.connections, defaults);
// Load a Collection into memory
function loadCollection(item, next) {
var loader = new CollectionLoader(item, self.connections, defaults);
var collection = loader.initialize(self);
if (options.compat) {
collection[options.compat] = self;
}
// Store the instantiated collection so it can be used
// internally to create other records
self.collections[collection.identity.toLowerCase()] = collection;
next();
}
this.cache = require('./offshore/core/cache');
async.auto({
// initialize cache
initializeCache: function(next) {
self.cache.initialize(options.cache, next);
},
// Load all the collections into memory
loadCollections: function(next) {
async.each(self._collections, loadCollection, function(err) {
if (err) return next(err);
// Migrate Junction Tables
var junctionTables = [];
Object.keys(self.schema).forEach(function(table) {
if (!self.schema[table].junctionTable) return;
junctionTables.push(Offshore.Collection.extend(self.schema[table]));
});
async.each(junctionTables, loadCollection, function(err) {
if (err) return next(err);
next(null, self.collections);
});
});
},
// Build up Collection Schemas
buildCollectionSchemas: ['loadCollections', function(next, results) {
var collections = self.collections;
var schemas = {};
Object.keys(collections).forEach(function(key) {
var collection = collections[key];
// Remove hasMany association keys
var schema = _.clone(collection._schema.schema);
Object.keys(schema).forEach(function(key) {
if (hasOwnProperty(schema[key], 'type')) return;
delete schema[key];
});
// Grab JunctionTable flag
var meta = collection.meta || {};
meta.junctionTable = hasOwnProperty(collection.offshore.schema[collection.identity], 'junctionTable') ?
collection.offshore.schema[collection.identity].junctionTable : false;
schemas[collection.identity] = collection;
schemas[collection.identity].definition = schema;
schemas[collection.identity].attributes = collection._attributes;
schemas[collection.identity].meta = meta;
});
next(null, schemas);
}],
//initalize association criteria validator
initializeAssociationCriteria: ['buildCollectionSchemas', function(next) {
var collections = self.collections;
_.keys(collections).forEach(function(collectionName) {
var collection = collections[collectionName];
_.keys(collection._attributes).forEach(function(key) {
var attr = collection._attributes[key];
if (hasOwnProperty(attr, 'criteria')) {
if (hasOwnProperty(attr, 'collection')) {
collection._validator.associations[key].initialize(attr.collection, attr.criteria);
}
if (hasOwnProperty(attr, 'model')) {
collection._validator.associations[key].initialize(attr.model, attr.criteria);
}
}
});
});
next();
}],
// Register the Connections with an adapter
registerConnections: ['buildCollectionSchemas', function(next, results) {
async.each(Object.keys(self.connections), function(item, nextItem) {
var connection = self.connections[item];
var config = {};
var usedSchemas = {};
// Check if the connection's adapter has a register connection method
if (!hasOwnProperty(connection._adapter, 'registerConnection')) return nextItem();
// Copy all values over to a tempory object minus the adapter definition
Object.keys(connection.config).forEach(function(key) {
config[key] = connection.config[key];
});
// Set an identity on the connection
config.identity = item;
// register default connections
var collections = connection._collections;
if (connection.config && connection.config.default) {
collections = self.connections.default._collections;
}
// Grab the schemas used on this connection
collections.forEach(function(coll) {
var identity = coll;
if (hasOwnProperty(self.collections[coll].__proto__, 'tableName')) {
identity = self.collections[coll].__proto__.tableName;
}
var schema = results.buildCollectionSchemas[coll];
usedSchemas[identity] = schema;
});
// Call the registerConnection method
connection._adapter.registerConnection(config, usedSchemas, function(err) {
if (err) return nextItem(err);
nextItem();
});
}, next);
}]
}, function(err) {
if (err) return cb(err);
self.bootstrap(function(err) {
if (err) return cb(err);
cb(null, { collections: self.collections, connections: self.connections });
});
});
};
/**
* Teardown
*
* Calls the teardown method on each connection if available.
*/
Offshore.prototype.teardown = function teardown(cb) {
var self = this;
async.each(Object.keys(this.connections), function(item, next) {
var connection = self.connections[item];
// Check if the adapter has a teardown method implemented
if (!hasOwnProperty(connection._adapter, 'teardown')) return next();
connection._adapter.teardown(item, next);
}, cb);
};
/**
* Bootstrap
*
* Auto-migrate all collections
*/
Offshore.prototype.bootstrap = function bootstrap(cb) {
var self = this;
//
// TODO:
// Come back to this -- see https://github.com/balderdashy/offshore/issues/259
// (the stuff in this file works fine-- the work would be structural changes elsewhere)
//
// // Use the schema to get a list of junction tables idents
// // and then determine which are "logical" collections
// // (i.e. everything EXCEPT junction tables)
// var junctionTableIdents = _(this.schema).filter({junctionTable: true}).pluck('identity').value();
// var logicalCollections = _(this.collections).omit(junctionTableIdents).value();
// // Flatten logical collections obj into an array for convenience
// var toBeSynced = _.reduce(logicalCollections, function(logicals,coll,ident) {
// logicals.push(coll);
// return logicals;
// }, []);
// // console.log(junctionTableIdents);
// // console.log(Object.keys(logicalCollections));
// // console.log('\n',
// // 'Migrating collections ::',
// // _(toBeSynced).pluck('identity').value()
// // );
// For now:
var toBeSynced = _.reduce(this.collections, function(resources, collection, ident) {
resources.push(collection);
return resources;
}, []);
// Run auto-migration strategies on each collection
// async.each(toBeSynced, function(collection, next) {
async.eachSeries(toBeSynced, function(collection, next) {
// async.eachLimit(toBeSynced, 9, function(collection, next) {
collection.sync(next);
}, cb);
};