Source: data/undercomplicate/joins.js

  1. /**
  2. * Very simple client-side data joins. Useful for aligning records from two datasets based on a common key.
  3. */
  4. import { clone } from './util';
  5. /**
  6. * Simple grouping function, used to identify sets of records for joining.
  7. *
  8. * Used internally by join helpers, exported mainly for unit testing
  9. * @memberOf module:undercomplicate
  10. * @param {object[]} records
  11. * @param {string} group_key
  12. * @returns {Map<any, any>}
  13. */
  14. function groupBy(records, group_key) {
  15. const result = new Map();
  16. for (let item of records) {
  17. const item_group = item[group_key];
  18. if (typeof item_group === 'undefined') {
  19. throw new Error(`All records must specify a value for the field "${group_key}"`);
  20. }
  21. if (typeof item_group === 'object') {
  22. // If we can't group this item, then don't (exclude object, array, map, null, etc from grouping keys)
  23. throw new Error('Attempted to group on a field with non-primitive values');
  24. }
  25. let group = result.get(item_group);
  26. if (!group) {
  27. group = [];
  28. result.set(item_group, group);
  29. }
  30. group.push(item);
  31. }
  32. return result;
  33. }
  34. function _any_match(type, left, right, left_key, right_key) {
  35. // Helper that consolidates logic for all three join types
  36. const right_index = groupBy(right, right_key);
  37. const results = [];
  38. for (let item of left) {
  39. const left_match_value = item[left_key];
  40. const right_matches = right_index.get(left_match_value) || [];
  41. if (right_matches.length) {
  42. // Record appears on both left and right; equiv to an inner join
  43. results.push(...right_matches.map((right_item) => Object.assign({}, clone(right_item), clone(item))));
  44. } else if (type !== 'inner') {
  45. // Record appears on left but not right
  46. results.push(clone(item));
  47. }
  48. }
  49. if (type === 'outer') {
  50. // Outer join part! We've already added all left-only and left-right matches; all that's left is the items that only appear on right side
  51. const left_index = groupBy(left, left_key);
  52. for (let item of right) {
  53. const right_match_value = item[right_key];
  54. const left_matches = left_index.get(right_match_value) || [];
  55. if (!left_matches.length) {
  56. results.push(clone(item));
  57. }
  58. }
  59. }
  60. return results;
  61. }
  62. /**
  63. * Equivalent to LEFT OUTER JOIN in SQL. Return all left records, joined to matching right data where appropriate.
  64. * @memberOf module:undercomplicate
  65. * @param {Object[]} left The left side recordset
  66. * @param {Object[]} right The right side recordset
  67. * @param {string} left_key The join field in left records
  68. * @param {string} right_key The join field in right records
  69. * @returns {Object[]}
  70. */
  71. function left_match(left, right, left_key, right_key) {
  72. return _any_match('left', ...arguments);
  73. }
  74. /**
  75. * Equivalent to INNER JOIN in SQL. Only return record joins if the key field has a match on both left and right.
  76. * @memberOf module:undercomplicate
  77. * @param {object[]} left The left side recordset
  78. * @param {object[]} right The right side recordset
  79. * @param {string} left_key The join field in left records
  80. * @param {string} right_key The join field in right records
  81. * @returns {Object[]}
  82. */
  83. function inner_match(left, right, left_key, right_key) {
  84. return _any_match('inner', ...arguments);
  85. }
  86. /**
  87. * Equivalent to FULL OUTER JOIN in SQL. Return records in either recordset, joined where appropriate.
  88. * @memberOf module:undercomplicate
  89. * @param {object[]} left The left side recordset
  90. * @param {object[]} right The right side recordset
  91. * @param {string} left_key The join field in left records
  92. * @param {string} right_key The join field in right records
  93. * @returns {Object[]}
  94. */
  95. function full_outer_match(left, right, left_key, right_key) {
  96. return _any_match('outer', ...arguments);
  97. }
  98. export {left_match, inner_match, full_outer_match, groupBy};