UNPKG

water-orm

Version:

A monolith version of Standalone waterline ORM

557 lines (431 loc) 17.6 kB
/** * Module Dependencies */ var _ = require('lodash'); var async = require('async'); var hop = require('../helpers').object.hasOwnProperty; /** * Update nested associations. Will take a values object and perform updating and * creating of all the nested associations. It's the same as syncing so it will first * remove any associations related to the parent and then "sync" the new associations. * * @param {Array} parents * @param {Object} values * @param {Object} associations * @param {Function} cb */ module.exports = function(parents, values, associations, cb) { var self = this; // Combine model and collection associations associations = associations.collections.concat(associations.models); // Build up .add and .update operations for each association var operations = buildOperations.call(self, parents, associations, values); // Now that our operations are built, lets go through and run any updates. // Then for each parent, find all the current associations and remove them then add // all the new associations in using .add() sync.call(self, parents, operations, cb); }; /** * Build Up Operations (add and update) * * @param {Array} associations * @param {Object} values * @return {Object} */ function buildOperations(parents, associations, values) { var self = this; var operations = {}; // For each association, grab the primary key value and normalize into model.add methods associations.forEach(function(association) { var optValues = values[association]; // If values are being nulled out just return. This is used when removing foreign // keys on the parent model. if (optValues === null) return; // Pull out any association values that have primary keys, these will need to be updated. All // values can be added for each parent however. operations[association] = { add: [], update: [] }; // Normalize optValues to an array if (!Array.isArray(optValues)) optValues = [optValues]; queueOperations.call(self, parents, association, operations[association], optValues); }); return operations; } /** * Queue Up Operations. * * Takes the array normalized association values and queues up * operations for the specific association. * * @param {String} association * @param {Object} operation * @param {Array} values */ function queueOperations(parents, association, operation, values) { var self = this; var attribute = self.waterline.schema[self.identity].attributes[association]; var modelName; if (hop(attribute, 'collection')) modelName = attribute.collection; if (hop(attribute, 'foreignKey')) modelName = attribute.references; if (!modelName) return; var collection = self.waterline.collections[modelName]; // Grab the relation's PK var relatedPK = _.find(collection.attributes, { primaryKey: true }); var relatedPkName = collection.primaryKey; // If this is a join table, we can just queue up operations on the parent // for this association. if (collection.junctionTable) { // For each parent, queue up any .add() operations parents.forEach(function(parent) { values.forEach(function(val) { if (!hop(parent, association)) return; if (typeof parent[association].add !== 'function') return; parent[association].add(val); }); }); return; } values.forEach(function(val) { // Check the values and see if the model's primary key is given. If so look into // the schema attribute and check if this is a collection or model attribute. If it's // a collection attribute lets update the child record and if it's a model attribute, // update the child and set the parent's foreign key value to the new primary key. // // If a custom PK was used and it's not autoIncrementing add the record. This // allows nested creates to work when custom PK's are used. if (!relatedPK.autoIncrement && !collection.autoPK) { operation.add.push(val); return; } // If it's missing a PK queue up an add if (!hop(val, relatedPkName)) { operation.add.push(val); return; } // Build up the criteria that will be used to update the child record var criteria = {}; criteria[relatedPkName] = val[relatedPkName]; // Queue up the update operation operation.update.push({ model: modelName, criteria: criteria, values: val }); // Check if the parents foreign key needs to be updated if (!hop(attribute, 'foreignKey')) { operation.add.push(val[relatedPkName]); return; } // Set the new foreign key value for each parent parents.forEach(function(parent) { parent[association] = val[relatedPkName]; }); }); } /** * Sync Associated Data * * Using the operations, lets go through and run any updates on any nested object with * primary keys. This ensures that all the data passed up is persisted. Then for each parent, * find all the current associations and unlink them and then add all the new associations * in using .add(). This ensures that whatever is passed in to an update is what the value will * be when queried again. * * @param {Object} operations * @param {Function} cb */ function sync(parents, operations, cb) { var self = this; async.auto({ // Update any nested associations update: function(next) { updateRunner.call(self, parents, operations, next); }, // For each parent, unlink all the associations currently set unlink: ['update', function(next) { unlinkRunner.call(self, parents, operations, next); }], // For each parent found, link any associations passed in by either creating // the new record or linking an existing record link: ['unlink', function(next) { linkRunner.call(self, parents, operations, next); }] }, cb); } //////////////////////////////////////////////////////////////////////////////////////// // .sync() - Async Auto Runners //////////////////////////////////////////////////////////////////////////////////////// /** * Run Update Operations. * * Uses the information stored in an operation to perform a .update() on the * associated model using the new values. * * @param {Object} operation * @param {Function} cb */ function updateRunner(parents, operations, cb) { var self = this; // There will be an array of update operations inside of a namespace. Use this to run // an update on the model instance of the association. function associationLoop(association, next) { async.each(operations[association].update, update, next); } function update(operation, next) { var model = self.waterline.collections[operation.model]; model.update(operation.criteria, operation.values).exec(next); } // Operations are namespaced under an association key. So run each association's updates // in parallel for now. May need to be limited in the future but all adapters should // support connection pooling. async.each(Object.keys(operations), associationLoop, cb); } /** * Unlink Associated Records. * * For each association passed in to the update we are essentially replacing the * association's value. In order to do this we first need to clear out any associations * that currently exist. * * @param {Object} operations * @param {Function} cb */ function unlinkRunner(parents, operations, cb) { var self = this; // Given a parent, build up remove operations and run them. function unlinkParentAssociations(parent, next) { var opts = buildParentRemoveOperations.call(self, parent, operations); removeOperationRunner.call(self, opts, next); } async.each(parents, unlinkParentAssociations, cb); } /** * Link Associated Records * * Given a set of operations, associate the records with the parent records. This * can be done by either creating join table records or by setting foreign keys. * It defaults to a parent.add() method for most situations. * * @param {Object} operations * @param {Function} cb */ function linkRunner(parents, operations, cb) { var self = this; function linkChildRecords(parent, next) { // Queue up `.add()` operations on the parent model and figure out // which records need to be created. // // If an .add() method is available always use it. If this is a nested model an .add() // method won't be available so queue up a create operation. var recordsToCreate = buildParentLinkOperations.call(self, parent, operations); // Create the new records and update the parent with the new foreign key // values that may have been set when creating child records. createNewRecords.call(self, parent, recordsToCreate, function(err) { if (err) return next(err); updateParentRecord(parent, cb); }); } // Update the parent record one last time. This ensures a model attribute (single object) // on the parent can create a new record and then set the parent's foreign key value to // the newly created child record's primary key. // // Example: // Parent.update({ // name: 'foo', // nestedModel: { // name: 'bar' // } // }) // // The above query would create the new nested model and then set the parent's nestedModel // value to the newly created model's primary key. // // We then run a .save() to persist any .add() records that may have been used. The update and // .save() are used instead of a find and then save because it's the same amount of queries // and it's easier to take advantage of all that the .add() method gives us. // // // TO-DO: // Make this much smarter to reduce the amount of queries that need to be run. We should probably // be able to at least cut this in half! // function updateParentRecord(parent, next) { var criteria = {}; var model = self.waterline.collections[self.identity]; criteria[self.primaryKey] = parent[self.primaryKey]; var pValues = parent.toObject(); model.update(criteria, pValues).exec(function(err) { if (err) return next(err); // Call .save() to persist any .add() functions that may have been used. parent.save(next); }); } async.each(parents, linkChildRecords, cb); } //////////////////////////////////////////////////////////////////////////////////////// // .sync() - Helper Functions //////////////////////////////////////////////////////////////////////////////////////// /** * Build up operations for performing unlinks. * * Given a parent and a set of operations, queue up operations to either * remove join table records or null out any foreign keys on an child model. * * @param {Object} parent * @param {Object} operations * @return {Array} */ function buildParentRemoveOperations(parent, operations) { var self = this; var opts = []; // Inspect the association and see if this relationship has a joinTable. // If so create an operation criteria that clears all matching records from the // table. If it doesn't have a join table, build an operation criteria that // nulls out the foreign key on matching records. Object.keys(operations).forEach(function(association) { var criteria = {}; var searchCriteria = {}; var attribute = self.waterline.schema[self.identity].attributes[association]; ///////////////////////////////////////////////////////////////////////// // Parent Record: // If the foreign key is stored on the parent side, null it out ///////////////////////////////////////////////////////////////////////// if (hop(attribute, 'foreignKey')) { // Set search criteria where primary key is equal to the parents primary key searchCriteria[self.primaryKey] = parent[self.primaryKey]; // Store any information we may need to build up an operation. // Use the `nullify` key to show we want to perform an update and not a destroy. criteria = { model: self.identity, criteria: searchCriteria, keyName: association, nullify: true }; opts.push(criteria); return; } ///////////////////////////////////////////////////////////////////////// // Child Record: // Lookup the attribute on the other side of the association on in the // case of a m:m association the child table will be the join table. ///////////////////////////////////////////////////////////////////////// var child = self.waterline.schema[attribute.collection]; var childAttribute = child.attributes[attribute.onKey]; // Set the search criteria to use the collection's `via` key and the parent's primary key. searchCriteria[attribute.on] = parent[self.primaryKey]; // If the childAttribute stores the foreign key, find all children with the // foreignKey equal to the parent's primary key and null them out or in the case of // a `junctionTable` flag destroy them. if (hop(childAttribute, 'foreignKey')) { // Store any information needed to perform the query. Set nullify to false if // a `junctionTable` property is found. criteria = { model: child.identity, criteria: searchCriteria, keyName: attribute.on, nullify: !hop(child, 'junctionTable') }; opts.push(criteria); return; } }); return opts; } /** * Remove Operation Runner * * Given a criteria object matching a remove operation, perform the * operation using waterline collection instances. * * @param {Array} operations * @param {Function} callback */ function removeOperationRunner(operations, cb) { var self = this; function runner(operation, next) { var values = {}; // If nullify is false, run a destroy method using the criteria to destroy // the join table records. if (!operation.nullify) { self.waterline.collections[operation.model].destroy(operation.criteria).exec(next); return; } // Run an update operation to set the foreign key to null on all the // associated child records. values[operation.keyName] = null; self.waterline.collections[operation.model].update(operation.criteria, values).exec(next); } // Run the operations async.each(operations, runner, cb); } /** * Build up operations for performing links. * * Given a parent and a set of operations, queue up operations to associate two * records together. This could be using the parent's `.add()` method which handles * the logic for us or building up a `create` operation that we can run to create the * associated record with the correct foreign key set. * * @param {Object} parent * @param {Object} operations * @return {Object} */ function buildParentLinkOperations(parent, operations) { var recordsToCreate = {}; // Determine whether to use the parent association's `.add()` function // or whether to queue up a create operation. function determineOperation(association, opt) { // Check if the association has an `add` method, if so use it. if (hop(parent[association], 'add')) { parent[association].add(opt); return; } recordsToCreate[association] = recordsToCreate[association] || []; recordsToCreate[association].push(opt); } // For each operation look at all the .add operations and determine // what to do with them. Object.keys(operations).forEach(function(association) { operations[association].add.forEach(function(opt) { determineOperation(association, opt); }); }); return recordsToCreate; } /** * Create New Records. * * Given an object of association records to create, perform a create * on the child model and set the parent's foreign key to the newly * created record's primary key. * * @param {Object} parent * @param {Object} recordsToCreate * @param {Function} cb */ function createNewRecords(parent, recordsToCreate, cb) { var self = this; // For each association, run the createRecords function // in the model context. function mapAssociations(association, next) { // First, pull the model attribute's referenced (foreign) collection var attribute = self.waterline.schema[self.identity].attributes[association]; var referencedCollection = attribute.references; var model = self.waterline.collections[referencedCollection]; var records = recordsToCreate[association]; function createRunner(record, nextRecord) { var args = [parent, association, record, nextRecord]; createRecord.apply(model, args); } async.each(records, createRunner, next); } // Create a record and set the parent's foreign key to the // newly created record's primary key. function createRecord(parent, association, record, next) { var self = this; this.create(record).exec(function(err, val) { if (err) return next(err); parent[association] = val[self.primaryKey]; next(); }); } async.each(Object.keys(recordsToCreate), mapAssociations, cb); }