planner.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ ██████╗ ██╗ █████╗ ███╗ ██╗███╗ ██╗███████╗██████╗
  2. // ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ ██╔══██╗██║ ██╔══██╗████╗ ██║████╗ ██║██╔════╝██╔══██╗
  3. // ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ ██████╔╝██║ ███████║██╔██╗ ██║██╔██╗ ██║█████╗ ██████╔╝
  4. // ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ██╔══██║██║╚██╗██║██║╚██╗██║██╔══╝ ██╔══██╗
  5. // ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ ██║ ███████╗██║ ██║██║ ╚████║██║ ╚████║███████╗██║ ██║
  6. // ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
  7. //
  8. // Takes a Waterline Criteria object and determines which types of associations
  9. // to plan out. For each association being populated, it will determine a specific
  10. // strategy to use for the instruction set.
  11. //
  12. // The strategies are used when building up statements based on a join criteria.
  13. // They represent the various ways a join could be constructed.
  14. //
  15. // HAS_FK Used when populating a model attribute where the foreign key
  16. // Type 1 exist on the parent record. Sometimes referred to as a belongsTo
  17. // association.
  18. //
  19. // VIA_FK Used when populating a collection attribute where the foreign
  20. // Type 2 key exist on the child record. Sometimes referred to as a
  21. // hasMany association because the parent can have many child records.
  22. //
  23. // VIA_JUNCTOR This is the most complicated type of join. It requires the use
  24. // Type 3 of an intermediate table to hold the values the connect the
  25. // two sets of records. Sometimes referred to as a manyToMany
  26. // association.
  27. //
  28. var util = require('util');
  29. var _ = require('@sailshq/lodash');
  30. // A set of named strategies to use
  31. var strategies = {
  32. HAS_FK: 1,
  33. VIA_FK: 2,
  34. VIA_JUNCTOR: 3
  35. };
  36. module.exports = function planner(options) {
  37. // Validate the options dictionary argument to ensure it has everything it needs
  38. if (!options || !_.isPlainObject(options)) {
  39. throw new Error('Planner is missing a required options input.');
  40. }
  41. if (_.isUndefined(options.joins) || !_.isArray(options.joins)) {
  42. throw new Error('Options must contain a joins array.');
  43. }
  44. if (_.isUndefined(options.getPk) || !_.isFunction(options.getPk)) {
  45. throw new Error('Options must contain a getPk function that accepts a single argument - modelName.');
  46. }
  47. // Grab the values from the options dictionary for local use.
  48. var joins = options.joins;
  49. var getPk = options.getPk;
  50. // Group the associations by alias
  51. var groupedAssociations = _.groupBy(joins, 'alias');
  52. // ╔╦╗╔═╗╔╦╗╔═╗╦═╗╔╦╗╦╔╗╔╔═╗ ┌─┐┌┬┐┬─┐┌─┐┌┬┐┌─┐┌─┐┬ ┬
  53. // ║║║╣ ║ ║╣ ╠╦╝║║║║║║║║╣ └─┐ │ ├┬┘├─┤ │ ├┤ │ ┬└┬┘
  54. // ═╩╝╚═╝ ╩ ╚═╝╩╚═╩ ╩╩╝╚╝╚═╝ └─┘ ┴ ┴└─┴ ┴ ┴ └─┘└─┘ ┴
  55. // Given an association's instructions, figure out which strategy to use
  56. // in order to correctly build the query.
  57. var determineStrategy = function determineStrategy(instructions) {
  58. if (!instructions) {
  59. throw new Error('Missing options when planning the query');
  60. }
  61. // Grab the parent and the child. In the case of a belongsTo or hasMany
  62. // there will only ever be a single instruction. However on the case of a
  63. // manyToMany there will be two items in the instructions array - one join
  64. // for the join table and one join to get the child records. To account for
  65. // this the parent is always the first and the child is always the last.
  66. var parentTableName = _.first(instructions).parent;
  67. var childTableName = _.last(instructions).child;
  68. // Ensure we found parent and child identities
  69. if (!parentTableName) {
  70. throw new Error('Unable to find a parentTableName in ' + util.inspect(instructions, false, 3));
  71. }
  72. if (!childTableName) {
  73. throw new Error('Unable to find a childTableName in ' + util.inspect(instructions, false, 3));
  74. }
  75. // Calculate the parent and child primary keys
  76. var parentPk;
  77. try {
  78. parentPk = getPk(parentTableName);
  79. } catch (e) {
  80. throw new Error('Error finding a primary key attribute for ' + parentTableName + '\n\n' + e.stack);
  81. }
  82. // Determine the type of association rule (i.e. "strategy") we'll be using.
  83. var strategy;
  84. // If there are more than one join instruction set, there must be an
  85. // intermediate (junctor) collection involved
  86. if (instructions.length === 2) {
  87. strategy = strategies.VIA_JUNCTOR;
  88. // If the parent's primary key IS the foreign key we know to use the `viaFK`
  89. // strategy. This means that the parent query will have many of the join
  90. // items - i.e. populating a collection.
  91. } else if (_.first(instructions).parentKey === parentPk) {
  92. strategy = strategies.VIA_FK;
  93. // Otherwise the parent query must have the foreign key. i.e. populating a
  94. // model.
  95. } else {
  96. strategy = strategies.HAS_FK;
  97. }
  98. // Build an object to hold any meta-data for the strategy
  99. var meta = {};
  100. // Now lookup strategy-specific association metadata.
  101. // `parentFk` will only be meaningful if this is the `HAS_FK` strategy. This
  102. // shows which field on the parent contains the id of the association to join.
  103. // It's used when populating a model.
  104. if (strategy === strategies.HAS_FK) {
  105. meta.parentFk = _.first(instructions).parentKey;
  106. }
  107. // `childFK` will only be meaningful if this is the `VIA_FK` strategy. This
  108. // shows which field on the child contains the value to use for the assocation.
  109. if (strategy === strategies.VIA_FK) {
  110. meta.childFk = _.first(instructions).childKey;
  111. }
  112. // `junctorIdentity`, `junctorFkToParent`, `junctorFkToChild`, and `junctorPk`
  113. // will only be meaningful if this is the `VIA_JUNCTOR` strategy. i.e. a
  114. // manyToMany join where an intermediate table is used.
  115. if (strategy === strategies.VIA_JUNCTOR) {
  116. meta.junctorIdentity = _.first(instructions).childCollectionIdentity;
  117. // Find the primary key of the join table.
  118. var junctorPk;
  119. try {
  120. junctorPk = getPk(_.first(instructions).child);
  121. } catch (e) {
  122. throw new Error('Error finding a primary key attribute for junction table: ' + _.first(instructions).child + '\n\n' + e.stack);
  123. }
  124. meta.junctorPk = junctorPk;
  125. meta.junctorFkToParent = _.first(instructions).childKey;
  126. meta.junctorFkToChild = _.last(instructions).parentKey;
  127. }
  128. return {
  129. strategy: strategy,
  130. meta: meta
  131. };
  132. };
  133. // ╔╗ ╦ ╦╦╦ ╔╦╗ ┌─┐┌┬┐┬─┐┌─┐┌┬┐┌─┐┌─┐┬┌─┐┌─┐
  134. // ╠╩╗║ ║║║ ║║ └─┐ │ ├┬┘├─┤ │ ├┤ │ ┬│├┤ └─┐
  135. // ╚═╝╚═╝╩╩═╝═╩╝ └─┘ ┴ ┴└─┴ ┴ ┴ └─┘└─┘┴└─┘└─┘
  136. // Go through all the associations being used and determine a strategy for
  137. // each one. Update the instructions to include the strategy metadata.
  138. _.each(groupedAssociations, function buildStrategy(val, key) {
  139. var strategy = determineStrategy(val);
  140. // Overwrite the grouped associations and insert the strategy and
  141. // original instructions.
  142. groupedAssociations[key] = {
  143. strategy: strategy,
  144. instructions: val
  145. };
  146. });
  147. return groupedAssociations;
  148. };