index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // ███████╗ █████╗ ██████╗██╗ ██╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ██████╗
  2. // ██╔════╝██╔══██╗██╔════╝██║ ██║ ██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗
  3. // █████╗ ███████║██║ ███████║ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██║ ██║
  4. // ██╔══╝ ██╔══██║██║ ██╔══██║ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║
  5. // ███████╗██║ ██║╚██████╗██║ ██║ ██║ ██║███████╗╚██████╗╚██████╔╝██║ ██║██████╔╝
  6. // ╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝
  7. //
  8. // ██████╗ ███████╗███████╗██████╗
  9. // ██╔══██╗██╔════╝██╔════╝██╔══██╗
  10. // ██║ ██║█████╗ █████╗ ██████╔╝
  11. // ██║ ██║██╔══╝ ██╔══╝ ██╔═══╝
  12. // ██████╔╝███████╗███████╗██║
  13. // ╚═════╝ ╚══════╝╚══════╝╚═╝
  14. //
  15. /**
  16. * Module dependencies
  17. */
  18. var util = require('util');
  19. var _ = require('@sailshq/lodash');
  20. /*
  21. * eachRecordDeep()
  22. *
  23. * Iterate over an array of potentially-populated records, running the provided
  24. * iteratee function once per record, whether they are top-level (parent records)
  25. * or not (populated child records).
  26. *
  27. * Note that the iteratee always runs for any given parent record _before_ running
  28. * for any of the child records that it contains. This allows you to throw an error
  29. * or mutate the parent record before this iterator attempts to descend inside of it.
  30. *
  31. * Each parent record is assumed to be a dictionary, but beyond that, just about any
  32. * other sort of nonsense is completely ignored. The iteratee is only called for
  33. * singular associations if the value is at least a dictionary (e.g. if it is a number,
  34. * then this iterator turns a blind eye.)
  35. *
  36. * On the other hand, for _plural associations_, if the value is an array, the iteratee
  37. * is called once for each child record in the array- no matter WHAT data type those items
  38. * are. This is a deliberate choice for performance reasons, and it is up to whatever is
  39. * calling this utility to verify that array items are valid. (But note that this can easily
  40. * be done in the `iteratee`, when it runs for the containing parent record.)
  41. *
  42. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  43. *
  44. * @param {Array} records
  45. * An array of records.
  46. * > These might be normal logical records keyed by attribute name,
  47. * > or raw, physical-layer records ("pRecords") w/ column names
  48. * > instead of attribute names for its keys. Specify which kind of
  49. * > records these are using the `arePhysical` flag. If `arePhysical` is
  50. * > true, the child record of singular associations will be assumed to
  51. * > live under its column name instead of its attribute name. (And plural
  52. * > associations don't even have a "column name", so they're the same
  53. * > regardless.)
  54. *
  55. * @param {Function} iteratee
  56. * @param {Dictionary} record
  57. * @param {Ref} WLModel
  58. * @param {Number} depth
  59. * 1 - Parent record
  60. * 2 - Child record
  61. *
  62. * @param {Boolean} arePhysical
  63. * Whether or not these are physical-layer records keyed on column names.
  64. * For example, if using this utility in an adapter, pass in `true`.
  65. * Otherwise, use `false`. This is only relevant insofar as it affects
  66. * how singular associations are populated. If set to `true`, this indicates
  67. * that the populated child record dictionary for a singular association will
  68. * exist on the key for that association's `columnName` (vs. its attr name.)
  69. * > Regardless of what you put in here, be aware that the `tableName`
  70. * > should _never_ be relevant for the purposes of this utility. Any time
  71. * > `modelIdentity` is mentioned, that is exactly what is meant.
  72. *
  73. * @param {String} modelIdentity
  74. * The identity of the model these parent records came from (e.g. "pet" or "user")
  75. * > Useful for looking up the Waterline model and accessing its attribute definitions.
  76. * > Note that this is ALWAYS the model identity, and NEVER the table name.
  77. *
  78. * @param {Ref} orm
  79. * The Waterline ORM instance.
  80. * > Useful for accessing the model definitions.
  81. *
  82. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  83. */
  84. module.exports = function eachRecordDeep(records, iteratee, arePhysical, modelIdentity, orm) {
  85. if (!_.isArray(records)) {
  86. throw new Error('Consistency violation: Expected `records` to be an array. But instead, got: ' + util.inspect(records, { depth: 5 }) + '');
  87. }
  88. if (!_.isFunction(iteratee)) {
  89. throw new Error('Consistency violation: Expected `iteratee` to be a function. But instead, got: ' + util.inspect(iteratee, { depth: 5 }) + '');
  90. }
  91. if (!_.isString(modelIdentity) || modelIdentity === '') {
  92. throw new Error('Consistency violation: Expected `modelIdentity` to be a non-empty string. But instead, got: ' + util.inspect(modelIdentity, { depth: 5 }) + '');
  93. }
  94. // Grab dictionary of models (w/ backwards compatibility).
  95. var models = orm.collections !== undefined ? orm.collections : orm.models;
  96. // Look up the Waterline model for this query.
  97. // > This is so that we can reference the original model definition.
  98. // var ParentWLModel = getModel(modelIdentity, orm);
  99. var ParentWLModel = models[modelIdentity];
  100. // ┌─┐┌─┐┌─┐┬ ┬ ╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗ ┬┌┐┌ ┌─┐┬─┐┬─┐┌─┐┬ ┬
  101. // ├┤ ├─┤│ ├─┤ ╠╦╝║╣ ║ ║ ║╠╦╝ ║║ ││││ ├─┤├┬┘├┬┘├─┤└┬┘
  102. // └─┘┴ ┴└─┘┴ ┴ ╩╚═╚═╝╚═╝╚═╝╩╚══╩╝ ┴┘└┘ ┴ ┴┴└─┴└─┴ ┴ ┴
  103. // Loop over each parent record.
  104. _.each(records, function iterateRecords(record) {
  105. if (!_.isObject(record) || _.isArray(record) || _.isFunction(record)) {
  106. 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 }) + '');
  107. }
  108. // Call the iteratee for this parent record.
  109. iteratee(record, ParentWLModel, 1);
  110. // ┌─┐┌─┐┌─┐┬ ┬ ╔═╗╔╦╗╔╦╗╦═╗ ╔╦╗╔═╗╔═╗ ╦╔╗╔ ╔╦╗╔═╗╔╦╗╔═╗╦
  111. // ├┤ ├─┤│ ├─┤ ╠═╣ ║ ║ ╠╦╝ ║║║╣ ╠╣ ║║║║ ║║║║ ║ ║║║╣ ║
  112. // └─┘┴ ┴└─┘┴ ┴ ╩ ╩ ╩ ╩ ╩╚═ ═╩╝╚═╝╚ ╩╝╚╝ ╩ ╩╚═╝═╩╝╚═╝╩═╝
  113. // Loop over this model's defined attributes.
  114. _.each(ParentWLModel.attributes, function iterateAttributes(attrDef, attrName) {
  115. // If this attribute is SOMETHING OTHER THAN AN ASSOCIATION...
  116. if (!attrDef.model && !attrDef.collection) {
  117. // Then we just skip it.
  118. return;
  119. } // -•
  120. // But otherwise, we know we've got an association of some kind.
  121. // So we've got more work to do.
  122. // Look up the right-hand side value of this key in the parent record
  123. var valueInParentRecord = record[attrName];
  124. // Look up the live Waterline model referenced by this association.
  125. var childModelIdentity = attrDef.model || attrDef.collection;
  126. var ChildWLModel = models[childModelIdentity];
  127. // If this attribute is a singular association...
  128. if (attrDef.model) {
  129. // If `arePhysical` was specified, then use the value under this column name
  130. // (instead of whatever is under the attribute name)
  131. if (arePhysical) {
  132. valueInParentRecord = record[attrDef.columnName];
  133. }
  134. // If this singular association doesn't seem to be populated,
  135. // then simply ignore it and skip ahead.
  136. if (!_.isObject(valueInParentRecord) || _.isArray(valueInParentRecord) || _.isFunction(valueInParentRecord)) {
  137. return;
  138. }
  139. // But otherwise, it seems populated, so we'll assume it is
  140. // a child record and call our iteratee on it.
  141. var childRecord = valueInParentRecord;
  142. iteratee(childRecord, ChildWLModel, 2);
  143. // Otherwise, this attribute is a plural association...
  144. } else {
  145. // If this plural association doesn't seem to be populated,
  146. // then simply ignore it and skip ahead.
  147. if (!_.isArray(valueInParentRecord)) {
  148. return;
  149. }
  150. // But otherwise, it seems populated, so we'll assume it is
  151. // an array of child records and call our iteratee once for
  152. // each item in the array.
  153. var childRecords = valueInParentRecord;
  154. _.each(childRecords, function iterateChildRecords(thisChildRecord) {
  155. // Note that `thisChildRecord` is not guaranteed to be a dictionary!
  156. // (if you need this guarantee, use the `iteratee` to verify this when
  157. // it runs for parent records)
  158. iteratee(thisChildRecord, ChildWLModel, 2);
  159. });
  160. } // </ else (i.e. this is a plural assoc.) >
  161. }); // </ each defined attribute from model >
  162. }); // </ each parent record >
  163. // There is no return value.
  164. };
  165. /**
  166. * to demonstrate basic usage in the node REPL:
  167. */
  168. /* ```
  169. 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}));
  170. ```*/