// ███████╗ █████╗ ██████╗██╗ ██╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ // ██╔════╝██╔══██╗██╔════╝██║ ██║ ██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗ // █████╗ ███████║██║ ███████║ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██║ ██║ // ██╔══╝ ██╔══██║██║ ██╔══██║ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ // ███████╗██║ ██║╚██████╗██║ ██║ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║██████╔╝ // ╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ // // ██████╗ ███████╗███████╗██████╗ // ██╔══██╗██╔════╝██╔════╝██╔══██╗ // ██║ ██║█████╗ █████╗ ██████╔╝ // ██║ ██║██╔══╝ ██╔══╝ ██╔═══╝ // ██████╔╝███████╗███████╗██║ // ╚═════╝ ╚══════╝╚══════╝╚═╝ // /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); /* * eachRecordDeep() * * Iterate over an array of potentially-populated records, running the provided * iteratee function once per record, whether they are top-level (parent records) * or not (populated child records). * * Note that the iteratee always runs for any given parent record _before_ running * for any of the child records that it contains. This allows you to throw an error * or mutate the parent record before this iterator attempts to descend inside of it. * * Each parent record is assumed to be a dictionary, but beyond that, just about any * other sort of nonsense is completely ignored. The iteratee is only called for * singular associations if the value is at least a dictionary (e.g. if it is a number, * then this iterator turns a blind eye.) * * On the other hand, for _plural associations_, if the value is an array, the iteratee * is called once for each child record in the array- no matter WHAT data type those items * are. This is a deliberate choice for performance reasons, and it is up to whatever is * calling this utility to verify that array items are valid. (But note that this can easily * be done in the `iteratee`, when it runs for the containing parent record.) * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Array} records * An array of records. * > These might be normal logical records keyed by attribute name, * > or raw, physical-layer records ("pRecords") w/ column names * > instead of attribute names for its keys. Specify which kind of * > records these are using the `arePhysical` flag. If `arePhysical` is * > true, the child record of singular associations will be assumed to * > live under its column name instead of its attribute name. (And plural * > associations don't even have a "column name", so they're the same * > regardless.) * * @param {Function} iteratee * @param {Dictionary} record * @param {Ref} WLModel * @param {Number} depth * 1 - Parent record * 2 - Child record * * @param {Boolean} arePhysical * Whether or not these are physical-layer records keyed on column names. * For example, if using this utility in an adapter, pass in `true`. * Otherwise, use `false`. This is only relevant insofar as it affects * how singular associations are populated. If set to `true`, this indicates * that the populated child record dictionary for a singular association will * exist on the key for that association's `columnName` (vs. its attr name.) * > Regardless of what you put in here, be aware that the `tableName` * > should _never_ be relevant for the purposes of this utility. Any time * > `modelIdentity` is mentioned, that is exactly what is meant. * * @param {String} modelIdentity * The identity of the model these parent records came from (e.g. "pet" or "user") * > Useful for looking up the Waterline model and accessing its attribute definitions. * > Note that this is ALWAYS the model identity, and NEVER the table name. * * @param {Ref} orm * The Waterline ORM instance. * > Useful for accessing the model definitions. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function eachRecordDeep(records, iteratee, arePhysical, modelIdentity, orm) { if (!_.isArray(records)) { throw new Error('Consistency violation: Expected `records` to be an array. But instead, got: ' + util.inspect(records, { depth: 5 }) + ''); } if (!_.isFunction(iteratee)) { throw new Error('Consistency violation: Expected `iteratee` to be a function. But instead, got: ' + util.inspect(iteratee, { depth: 5 }) + ''); } if (!_.isString(modelIdentity) || modelIdentity === '') { throw new Error('Consistency violation: Expected `modelIdentity` to be a non-empty string. But instead, got: ' + util.inspect(modelIdentity, { depth: 5 }) + ''); } // Grab dictionary of models (w/ backwards compatibility). var models = orm.collections !== undefined ? orm.collections : orm.models; // Look up the Waterline model for this query. // > This is so that we can reference the original model definition. // var ParentWLModel = getModel(modelIdentity, orm); var ParentWLModel = models[modelIdentity]; // ┌─┐┌─┐┌─┐┬ ┬ ╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗ ┬┌┐┌ ┌─┐┬─┐┬─┐┌─┐┬ ┬ // ├┤ ├─┤│ ├─┤ ╠╦╝║╣ ║ ║ ║╠╦╝ ║║ ││││ ├─┤├┬┘├┬┘├─┤└┬┘ // └─┘┴ ┴└─┘┴ ┴ ╩╚═╚═╝╚═╝╚═╝╩╚══╩╝ ┴┘└┘ ┴ ┴┴└─┴└─┴ ┴ ┴ // Loop over each parent record. _.each(records, function iterateRecords(record) { if (!_.isObject(record) || _.isArray(record) || _.isFunction(record)) { throw new Error('Consistency violation: Expected each item in the `records` array to be a record (a dictionary). But at least one of them is messed up. Record: ' + util.inspect(record, { depth: 5 }) + ''); } // Call the iteratee for this parent record. iteratee(record, ParentWLModel, 1); // ┌─┐┌─┐┌─┐┬ ┬ ╔═╗╔╦╗╔╦╗╦═╗ ╔╦╗╔═╗╔═╗ ╦╔╗╔ ╔╦╗╔═╗╔╦╗╔═╗╦ // ├┤ ├─┤│ ├─┤ ╠═╣ ║ ║ ╠╦╝ ║║║╣ ╠╣ ║║║║ ║║║║ ║ ║║║╣ ║ // └─┘┴ ┴└─┘┴ ┴ ╩ ╩ ╩ ╩ ╩╚═ ═╩╝╚═╝╚ ╩╝╚╝ ╩ ╩╚═╝═╩╝╚═╝╩═╝ // Loop over this model's defined attributes. _.each(ParentWLModel.attributes, function iterateAttributes(attrDef, attrName) { // If this attribute is SOMETHING OTHER THAN AN ASSOCIATION... if (!attrDef.model && !attrDef.collection) { // Then we just skip it. return; } // -• // But otherwise, we know we've got an association of some kind. // So we've got more work to do. // Look up the right-hand side value of this key in the parent record var valueInParentRecord = record[attrName]; // Look up the live Waterline model referenced by this association. var childModelIdentity = attrDef.model || attrDef.collection; var ChildWLModel = models[childModelIdentity]; // If this attribute is a singular association... if (attrDef.model) { // If `arePhysical` was specified, then use the value under this column name // (instead of whatever is under the attribute name) if (arePhysical) { valueInParentRecord = record[attrDef.columnName]; } // If this singular association doesn't seem to be populated, // then simply ignore it and skip ahead. if (!_.isObject(valueInParentRecord) || _.isArray(valueInParentRecord) || _.isFunction(valueInParentRecord)) { return; } // But otherwise, it seems populated, so we'll assume it is // a child record and call our iteratee on it. var childRecord = valueInParentRecord; iteratee(childRecord, ChildWLModel, 2); // Otherwise, this attribute is a plural association... } else { // If this plural association doesn't seem to be populated, // then simply ignore it and skip ahead. if (!_.isArray(valueInParentRecord)) { return; } // But otherwise, it seems populated, so we'll assume it is // an array of child records and call our iteratee once for // each item in the array. var childRecords = valueInParentRecord; _.each(childRecords, function iterateChildRecords(thisChildRecord) { // Note that `thisChildRecord` is not guaranteed to be a dictionary! // (if you need this guarantee, use the `iteratee` to verify this when // it runs for parent records) iteratee(thisChildRecord, ChildWLModel, 2); }); } // }); // }); // // There is no return value. }; /** * to demonstrate basic usage in the node REPL: */ /* ``` records = [{ _id: 'asdf', __pet: { _cool_id: 'asdf' }, pets: 'some crazy invalid value' }]; require('./lib/each-record-deep')(records, function(record, WLModel, depth){ console.log('\n• Ran iteratee for '+(depth===1?'parent':'child')+' record of model `'+WLModel.identity+'`:',util.inspect(record, {depth:5}),'\n'); }, false, 'user', { collections: { user: { identity: 'user', attributes: { id: { type: 'string', required: true, unique: true, columnName: '_id' }, age: { type: 'number', required: false, defaultsTo: 99 }, foo: { type: 'string', required: true }, favPet: { model: 'pet', columnName: '__pet' }, pets: { collection: 'pet' } }, primaryKey: 'id', hasSchema: true, fetchRecordsOnDestroy: true, cascadeOnDestroy: true}, pet: { identity: 'pet', attributes: { id: { type:'number', required: true, unique: true, columnName: '_cool_id' } }, primaryKey: 'id', hasSchema: true } } }); console.log('\n\nAll done.\n--\nRecords are now:\n'+util.inspect(records,{depth:5})); ```*/