query-cache.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ ██████╗ █████╗ ██████╗██╗ ██╗███████╗
  2. // ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ ██╔════╝██╔══██╗██╔════╝██║ ██║██╔════╝
  3. // ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ ██║ ███████║██║ ███████║█████╗
  4. // ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ ██║ ██╔══██║██║ ██╔══██║██╔══╝
  5. // ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ ╚██████╗██║ ██║╚██████╗██║ ██║███████╗
  6. // ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝
  7. //
  8. // The query cache is used to hold the results of multiple queries. It's used in
  9. // adapters that need to perform multiple queries to fufill a request. This is
  10. // most commonly required when joins are performed with certain keys such as
  11. // skip, sort, or limit. It acts as a mini query heap and provides a way to
  12. // generate nested records that can be returned to the user.
  13. //
  14. // It provides a few methods for working with it:
  15. //
  16. // setParents - Sets the top level records. These will be returned as an array
  17. // whenever the results of the cache are generated.
  18. //
  19. // getParents - Returns an array of the current parent records.
  20. //
  21. // set - Adds an instruction set for a parent and children records to
  22. // the cache. Each item that is passed in should represent the logic
  23. // for connecting an array of children records to a single parent.
  24. //
  25. // extend - Extends a child cache to add more records. This is called when
  26. // multiple queries are run to add more values to an association.
  27. //
  28. // combineRecords - Responsible for creating a set of nested records where the
  29. // parents contain nested children records based on the join
  30. // logic set on it's cache item.
  31. //
  32. var _ = require('@sailshq/lodash');
  33. module.exports = function queryCache() {
  34. // Hold values used to keep track of records internally
  35. var store = [];
  36. var parents = [];
  37. // ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┬─┐┌─┐┌┐┌┌┬┐┌─┐
  38. // ╚═╗║╣ ║ ├─┘├─┤├┬┘├┤ │││ │ └─┐
  39. // ╚═╝╚═╝ ╩ ┴ ┴ ┴┴└─└─┘┘└┘ ┴ └─┘
  40. var setParents = function setParents(values) {
  41. // Normalize values to an array
  42. if (!_.isArray(values)) {
  43. values = [values];
  44. }
  45. parents = parents.concat(values);
  46. };
  47. // ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┬─┐┌─┐┌┐┌┌┬┐┌─┐
  48. // ║ ╦║╣ ║ ├─┘├─┤├┬┘├┤ │││ │ └─┐
  49. // ╚═╝╚═╝ ╩ ┴ ┴ ┴┴└─└─┘┘└┘ ┴ └─┘
  50. var getParents = function getParents() {
  51. return parents;
  52. };
  53. // ╔═╗╔═╗╔╦╗ ┌─┐ ┌─┐┬ ┬┬┬ ┌┬┐ ┌─┐┌─┐┌─┐┬ ┬┌─┐
  54. // ╚═╗║╣ ║ ├─┤ │ ├─┤││ ││ │ ├─┤│ ├─┤├┤
  55. // ╚═╝╚═╝ ╩ ┴ ┴ └─┘┴ ┴┴┴─┘─┴┘ └─┘┴ ┴└─┘┴ ┴└─┘
  56. var setChildCache = function setChildCache(values) {
  57. // Normalize values to an array
  58. if (!_.isArray(values)) {
  59. values = [values];
  60. }
  61. // Remove any records that are all null
  62. _.each(values, function cleanseRecords(val) {
  63. _.remove(val.records, function cleanseRecords(record) {
  64. var empty = true;
  65. _.each(_.keys(record), function checkRecordKeys(key) {
  66. if (!_.isNull(record[key])) {
  67. empty = false;
  68. }
  69. });
  70. return empty;
  71. });
  72. });
  73. _.each(values, function valueParser(val) {
  74. store.push({
  75. attrName: val.attrName,
  76. parentPkAttr: val.parentPkAttr,
  77. records: val.records || [],
  78. keyName: val.keyName,
  79. type: val.type,
  80. belongsToPkValue: val.belongsToPkValue,
  81. // Optional (only used if implementing a HAS_FK strategy)
  82. belongsToFkValue: val.belongsToFkValue
  83. });
  84. });
  85. };
  86. // ╔═╗═╗ ╦╔╦╗╔═╗╔╗╔╔╦╗ ┌─┐ ┌─┐┌─┐┌─┐┬ ┬┌─┐ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐
  87. // ║╣ ╔╩╦╝ ║ ║╣ ║║║ ║║ ├─┤ │ ├─┤│ ├─┤├┤ ├┬┘├┤ │ │ │├┬┘ ││
  88. // ╚═╝╩ ╚═ ╩ ╚═╝╝╚╝═╩╝ ┴ ┴ └─┘┴ ┴└─┘┴ ┴└─┘ ┴└─└─┘└─┘└─┘┴└──┴┘
  89. // Given a result set from a child query, parse the records and attach them to
  90. // the correct cache parent.
  91. var extend = function extend(records, instructions) {
  92. // Create a local cache to hold the grouped records.
  93. var localCache = {};
  94. // Grab the alias being used by the records
  95. var alias = _.isArray(instructions) ? _.first(instructions).alias : instructions.alias;
  96. // Process each record grouping them together as needed.
  97. _.each(records, function processRecord(record) {
  98. // Hold the child key used to group
  99. var childKey;
  100. // If this is not a many-to-many query then just group the records by
  101. // the child key defined in the instructions. This will be used to
  102. // determine which cache record they belong to.
  103. if (!_.isArray(instructions)) {
  104. childKey = instructions.childKey;
  105. // Ensure a value in the cache exists for the parent
  106. if (!_.has(localCache, record[childKey])) {
  107. localCache[record[childKey]] = [];
  108. }
  109. localCache[record[childKey]].push(record);
  110. }
  111. // If this IS a many-to-many then there is a bit more to do.
  112. if (_.isArray(instructions)) {
  113. // Grab the special "foreign key" we attach and make sure to remove it
  114. var fk = '_parent_fk';
  115. var fkValue = record[fk];
  116. // Ensure a value in the cache exists for the parent
  117. if (!_.has(localCache, fkValue)) {
  118. localCache[fkValue] = [];
  119. }
  120. // Delete the foreign key value that was added as a part of the join
  121. // process. It's not a value that the user is interested in.
  122. delete record[fk];
  123. // Ensure the record is valid and not made up of all `null` values
  124. var values = _.uniq(_.values(record));
  125. if (values.length < 2 && _.isNull(values[0])) {
  126. return;
  127. }
  128. // Add the record to the local cache
  129. localCache[fkValue].push(record);
  130. // Ensure there aren't duplicates in here
  131. localCache[fkValue] = _.uniq(localCache[fkValue], _.last(instructions).childKey);
  132. }
  133. });
  134. // Find the cached parents for this alias
  135. var cachedParents = _.filter(store, { attrName: alias });
  136. // Extend the parent cache with the child records related to them
  137. _.each(cachedParents, function extendCache(parent) {
  138. var childRecords = localCache[parent.belongsToPkValue];
  139. // If there are no child records, there is nothing to do
  140. if (!childRecords || !childRecords.length) {
  141. return;
  142. }
  143. if (!parent.records) {
  144. parent.records = [];
  145. }
  146. parent.records = parent.records.concat(childRecords);
  147. });
  148. };
  149. // ╔═╗╔═╗╔╦╗╔╗ ╦╔╗╔╔═╗ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐
  150. // ║ ║ ║║║║╠╩╗║║║║║╣ ├┬┘├┤ │ │ │├┬┘ ││└─┐
  151. // ╚═╝╚═╝╩ ╩╚═╝╩╝╚╝╚═╝ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘
  152. // Use the values in the query cache to output a set of nested dictionaries.
  153. // Each item in the set represents a parent and all the nested children records.
  154. var combineRecords = function combineRecords() {
  155. // If there are no parents then the results are always empty
  156. if (!parents.length) {
  157. return [];
  158. }
  159. // If there are no children records being populated, just return the parents.
  160. if (!store.length) {
  161. return parents;
  162. }
  163. // For each child in the cache, attach it to the parent
  164. _.each(store, function attachChildren(cache) {
  165. // Find the parent for this cache item
  166. var matchingParentRecord = _.find(parents, function match(parentRecord) {
  167. return parentRecord[cache.parentPkAttr] === cache.belongsToPkValue;
  168. });
  169. // This should always be true, but checking just in case.
  170. if (_.isObject(matchingParentRecord)) {
  171. // If the value in `attrName` for this record is not an array,
  172. // it is probably a foreign key value. Fortunately, at this point
  173. // we can go ahead and replace it safely since any logic relying on it
  174. // is complete.
  175. //
  176. // In fact, and for the same reason, we can safely override the value of
  177. // `buffer.attrName` for the parent record at this point, no matter what!
  178. // This is nice, because `buffer.records` is already sorted, limited, and
  179. // skipped, so we don't have to mess with that.
  180. //
  181. if (cache.records && cache.records.length) {
  182. matchingParentRecord[cache.keyName] = _.map(cache.records, _.clone);
  183. } else {
  184. matchingParentRecord[cache.keyName] = [];
  185. }
  186. // Check if the value should be an array or dictionary
  187. if (_.has(cache, 'type') && cache.type === 1) {
  188. matchingParentRecord[cache.keyName] = _.first(matchingParentRecord[cache.keyName]) || [];
  189. }
  190. }
  191. });
  192. // Collect all the aliases used by the query and ensure the nested objects
  193. // have a value for it.
  194. var aliases = _.uniq(_.map(store, 'keyName'));
  195. _.each(aliases, function normalizeAlias(alias) {
  196. _.each(parents, function setParentAlias(parentRecord) {
  197. parentRecord[alias] = parentRecord[alias] || [];
  198. });
  199. });
  200. return parents;
  201. };
  202. return {
  203. setParents: setParents,
  204. getParents: getParents,
  205. set: setChildCache,
  206. extend: extend,
  207. combineRecords: combineRecords
  208. };
  209. };