waterline-utils
Version:
Various utilities for working with Waterline queries and adapters.
195 lines (170 loc) • 11.6 kB
JavaScript
// ███████╗ █████╗ ██████╗██╗ ██╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ██████╗
// ██╔════╝██╔══██╗██╔════╝██║ ██║ ██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗
// █████╗ ███████║██║ ███████║ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██║ ██║
// ██╔══╝ ██╔══██║██║ ██╔══██║ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║
// ███████╗██║ ██║╚██████╗██║ ██║ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║██████╔╝
// ╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝
//
// ██████╗ ███████╗███████╗██████╗
// ██╔══██╗██╔════╝██╔════╝██╔══██╗
// ██║ ██║█████╗ █████╗ ██████╔╝
// ██║ ██║██╔══╝ ██╔══╝ ██╔═══╝
// ██████╔╝███████╗███████╗██║
// ╚═════╝ ╚══════╝╚══════╝╚═╝
//
/**
* 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);
});
} // </ else (i.e. this is a plural assoc.) >
}); // </ each defined attribute from model >
}); // </ each parent record >
// 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}));
```*/