objection 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/lib/model/AjvValidator.js +123 -50
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -1
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +49 -5
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +17 -2
  8. package/lib/model/modelValidate.js +2 -1
  9. package/lib/model/modelValues.js +43 -50
  10. package/lib/queryBuilder/QueryBuilder.js +106 -38
  11. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  12. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  13. package/lib/queryBuilder/graph/GraphData.js +39 -0
  14. package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
  15. package/lib/queryBuilder/graph/GraphOptions.js +77 -38
  16. package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
  17. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  18. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
  20. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
  21. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  22. package/lib/queryBuilder/join/RelationJoiner.js +105 -12
  23. package/lib/queryBuilder/join/TableNode.js +6 -68
  24. package/lib/queryBuilder/join/utils.js +2 -2
  25. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  26. package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
  27. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  28. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  29. package/lib/queryBuilder/operations/InsertOperation.js +2 -2
  30. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  31. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  32. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
  33. package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
  34. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  35. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  36. package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
  37. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  38. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  39. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  40. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +3 -3
  41. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  42. package/lib/queryBuilder/operations/select/Selection.js +39 -16
  43. package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
  44. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
  45. package/lib/queryBuilder/whereGroups.js +35 -40
  46. package/lib/queryBuilder/withoutOrder.js +33 -0
  47. package/lib/relations/Relation.js +3 -0
  48. package/lib/relations/RelationFindOperation.js +13 -5
  49. package/lib/relations/RelationOwner.js +2 -5
  50. package/lib/relations/RelationProperty.js +3 -14
  51. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  52. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
  53. package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
  54. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
  55. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
  57. package/lib/utils/identifierMapping.js +2 -2
  58. package/lib/utils/knexUtils.js +21 -0
  59. package/lib/utils/objectUtils.js +5 -0
  60. package/lib/utils/parseFieldExpression.js +9 -0
  61. package/lib/utils/warnings.js +26 -0
  62. package/lib/utils/wrapDbError.js +34 -20
  63. package/package.json +5 -4
  64. package/typings/objection/index.d.ts +31 -13
  65. package/lib/utils/deprecate.js +0 -21
@@ -6,7 +6,7 @@ const { createNotModelError } = require('../../model/graph/ModelGraphBuilder');
6
6
  const { GraphFetcher } = require('../graph/GraphFetcher');
7
7
  const { GraphInsert } = require('./insert/GraphInsert');
8
8
  const { GraphPatch } = require('./patch/GraphPatch');
9
- const { GraphDelete } = require('./delete/GraphDelete');
9
+ const { GraphDelete, GraphDereferencedBelongsToOneDelete } = require('./delete/GraphDelete');
10
10
  const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
11
11
  const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
12
12
  const { ValidationErrorType } = require('../../model/ValidationError');
@@ -14,16 +14,17 @@ const { RelationExpression } = require('../RelationExpression');
14
14
  const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
15
15
  const { GraphData } = require('./GraphData');
16
16
  const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
17
- const { deprecate } = require('../../utils/deprecate');
17
+ const { warnOnce } = require('../../utils/warnings');
18
18
 
19
19
  class GraphUpsert {
20
- constructor({ rootModelClass, objects, upsertOptions }) {
20
+ constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
21
21
  checkCanBeConvertedToModels(rootModelClass, objects);
22
22
  warnOnUnknownOptions(upsertOptions);
23
23
 
24
24
  this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
25
25
  this.isArray = Array.isArray(objects);
26
26
  this.upsertOpt = upsertOptions;
27
+ this.insertOnly = insertOnly;
27
28
  }
28
29
 
29
30
  static get modelOptions() {
@@ -36,10 +37,10 @@ class GraphUpsert {
36
37
  // by `ignore()` aren't returned, which leaves the root models without ids,
37
38
  // so that related rows would be inserted with missing or wrong foreign keys.
38
39
  // The clause is ignored, and a warning is logged once per method.
39
- // TODO: Throw an error instead in objection 4.0.
40
+ // TODO: Throw an error instead in objection 4.0 (#53).
40
41
  static warnOnConflict(builder, method) {
41
42
  if (builder.has(isOnConflictOperation)) {
42
- deprecate(
43
+ warnOnce(
43
44
  `onConflict(), ignore() and merge() are not supported by ${method}(). ` +
44
45
  'Insert the conflicting rows with a separate insert() query instead. ' +
45
46
  'This will throw in objection 4.0.',
@@ -49,7 +50,7 @@ class GraphUpsert {
49
50
 
50
51
  run(builder) {
51
52
  const modelClass = builder.modelClass();
52
- const graphOptions = new GraphOptions(this.upsertOpt);
53
+ const graphOptions = new GraphOptions(this.upsertOpt, [], this.insertOnly);
53
54
 
54
55
  const graph = ModelGraph.create(modelClass, this.objects);
55
56
  assignDbRefsAsRelateProps(graph);
@@ -59,13 +60,14 @@ class GraphUpsert {
59
60
  .then(pruneGraphs())
60
61
  .then(checkForJoinTableRelatedTableErrors(builder))
61
62
  .then(executeOperations(builder))
63
+ .then(removeReferenceProps(graph.nodes))
62
64
  .then(returnResult(this.objects, this.isArray));
63
65
  }
64
66
  }
65
67
 
66
68
  // Misspelled options are silently ignored, so the operation runs with the
67
69
  // default behaviour instead (#84). A warning is logged once per option.
68
- // TODO: Throw an error instead in objection 4.0.
70
+ // TODO: Throw an error instead in objection 4.0 (#53).
69
71
  function warnOnUnknownOptions(upsertOptions) {
70
72
  // Recursive upserts pass on the already checked options of their parent.
71
73
  if (upsertOptions instanceof GraphOptions) {
@@ -74,7 +76,7 @@ function warnOnUnknownOptions(upsertOptions) {
74
76
 
75
77
  for (const name of Object.keys(upsertOptions)) {
76
78
  if (!OPTION_NAMES.includes(name)) {
77
- deprecate(
79
+ warnOnce(
78
80
  `Unknown graph option "${name}" is ignored. ` +
79
81
  `Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
80
82
  );
@@ -127,6 +129,7 @@ function fetchCurrentGraph(builder, graphOptions, graph) {
127
129
  function pruneGraphs() {
128
130
  return (graphData) => {
129
131
  pruneRelatedBranches(graphData);
132
+ pruneSkippedBranches(graphData);
130
133
 
131
134
  if (!graphData.graphOptions.isInsertOnly()) {
132
135
  pruneDeletedBranches(graphData);
@@ -148,6 +151,31 @@ function pruneRelatedBranches(graphData) {
148
151
  removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
149
152
  }
150
153
 
154
+ // Nothing is written below nodes that `noInsert` skips, as their descendants
155
+ // would be orphans (#215). Branches that references from outside point into
156
+ // are kept, since the references need their models.
157
+ function pruneSkippedBranches(graphData) {
158
+ const { graph, graphOptions } = graphData;
159
+
160
+ const skippedNodes = graph.nodes.filter((node) => {
161
+ return (
162
+ graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
163
+ !graphOptions.shouldInsert(node, graphData) &&
164
+ !isReferencedFromOutside(node.descendantRelationNodes)
165
+ );
166
+ });
167
+
168
+ removeBranchesFromGraph(findRoots(skippedNodes), graph);
169
+ }
170
+
171
+ function isReferencedFromOutside(nodes) {
172
+ const nodeSet = new Set(nodes);
173
+
174
+ return nodes.some((node) =>
175
+ node.refEdges.some((edge) => edge.isRelatedNode(node) && !nodeSet.has(edge.ownerNode)),
176
+ );
177
+ }
178
+
151
179
  function pruneDeletedBranches(graphData) {
152
180
  const { graph, currentGraph } = graphData;
153
181
 
@@ -245,11 +273,14 @@ function checkForErrors(builder) {
245
273
  checkForNotFoundErrors(graphData, builder);
246
274
  checkForUnallowedRelationErrors(graphData, builder);
247
275
  checkForUnallowedReferenceErrors(graphData, builder);
248
- checkForUnallowedMarkerErrors(graphData, builder);
276
+ // Root models can't be marked even with `allowMarkers`, so report that
277
+ // first instead of pointing to the option.
249
278
  checkForRootUnrelateOrDeleteErrors(graphData, builder);
279
+ checkForUnallowedMarkerErrors(graphData, builder);
250
280
 
251
281
  if (graphData.graphOptions.isInsertOnly()) {
252
282
  checkForHasManyRelateErrors(graphData);
283
+ warnOnInsertBelowRelate(graphData);
253
284
  }
254
285
 
255
286
  return graphData;
@@ -287,12 +318,19 @@ function checkForJoinTableRelatedTableErrors(builder) {
287
318
 
288
319
  function checkForNotFoundErrors(graphData, builder) {
289
320
  const { graphOptions, currentGraph, graph } = graphData;
321
+ // The branches below related nodes are upserted and checked by nested
322
+ // `upsertGraph` calls, against the current graphs of the related nodes:
323
+ const nestedNodes = new Set(
324
+ graph.nodes
325
+ .filter((node) => graphOptions.shouldRelate(node, graphData))
326
+ .flatMap((node) => node.descendantRelationNodes),
327
+ );
290
328
 
291
329
  for (const node of graph.nodes) {
292
330
  if (
293
331
  node.obj.$hasId() &&
294
- !node.toBeUnrelated &&
295
- !node.toBeDeleted &&
332
+ !nestedNodes.has(node) &&
333
+ !node.toBeUnrelatedOrDeleted &&
296
334
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
297
335
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
298
336
  !currentGraph.nodeForNode(node)
@@ -347,10 +385,12 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
347
385
 
348
386
  // `#unrelate` and `#delete` markers come from the graph data, which may be
349
387
  // untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
388
+ // An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
350
389
  function checkForUnallowedMarkerErrors(graphData, builder) {
351
390
  const { graph, graphOptions } = graphData;
391
+ const isInsertOnly = graphOptions.isInsertOnly();
352
392
 
353
- if (graphOptions.allowMarkers()) {
393
+ if (graphOptions.allowMarkers() && !isInsertOnly) {
354
394
  return;
355
395
  }
356
396
 
@@ -361,7 +401,9 @@ function checkForUnallowedMarkerErrors(graphData, builder) {
361
401
 
362
402
  throw builder.modelClass().createValidationError({
363
403
  type: ValidationErrorType.InvalidGraph,
364
- message: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers insert/upsert graph option`,
404
+ message: isInsertOnly
405
+ ? `${graphUnrelateProp} and ${graphDeleteProp} can only be used in upsertGraph, not in insertGraph`
406
+ : `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
365
407
  });
366
408
  }
367
409
  }
@@ -394,11 +436,41 @@ function checkForHasManyRelateErrors(graphData) {
394
436
  }
395
437
  }
396
438
 
439
+ // `upsertGraph` upserts the branches below related models recursively, but
440
+ // `insertGraph` prunes them, which drops the models that would be inserted
441
+ // there (#217). A warning is logged once.
442
+ // TODO: Throw an error instead in objection 4.0 (#53).
443
+ function warnOnInsertBelowRelate(graphData) {
444
+ const { graph, graphOptions } = graphData;
445
+
446
+ const hasInsertBelowRelate = graph.nodes.some(
447
+ (node) =>
448
+ graphOptions.shouldRelate(node, graphData) &&
449
+ node.descendantRelationNodes.some((descendantNode) =>
450
+ graphOptions.shouldInsert(descendantNode, graphData),
451
+ ),
452
+ );
453
+
454
+ if (hasInsertBelowRelate) {
455
+ warnOnce(
456
+ 'insertGraph() ignores new models below related models. ' +
457
+ 'Insert them with a separate query or use upsertGraph() instead. ' +
458
+ 'This will throw in objection 4.0.',
459
+ );
460
+ }
461
+ }
462
+
397
463
  function executeOperations(builder) {
398
464
  return (graphData) => {
399
465
  const operations = graphData.graphOptions.isInsertOnly()
400
466
  ? [GraphInsert]
401
- : [GraphDelete, GraphInsert, GraphPatch, GraphRecursiveUpsert];
467
+ : [
468
+ GraphDelete,
469
+ GraphInsert,
470
+ GraphPatch,
471
+ GraphDereferencedBelongsToOneDelete,
472
+ GraphRecursiveUpsert,
473
+ ];
402
474
 
403
475
  return operations.reduce((promise, Operation) => {
404
476
  const operation = new Operation(graphData);
@@ -416,6 +488,18 @@ function executeActions(builder, actions) {
416
488
  );
417
489
  }
418
490
 
491
+ // The reference markers only describe the input graph, don't return them.
492
+ // `nodes` are the nodes before pruning, since the results of pruned branches
493
+ // contain markers too.
494
+ function removeReferenceProps(nodes) {
495
+ return () => {
496
+ for (const node of nodes) {
497
+ delete node.obj[node.modelClass.uidRefProp];
498
+ delete node.obj[node.modelClass.dbRefProp];
499
+ }
500
+ };
501
+ }
502
+
419
503
  function isOnConflictOperation(op) {
420
504
  return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
421
505
  }
@@ -2,19 +2,72 @@
2
2
 
3
3
  const { GraphOperation } = require('../GraphOperation');
4
4
  const { GraphDeleteAction } = require('./GraphDeleteAction');
5
+ const { getPropertyTypes } = require('../../../model/modelJsonAttributes');
5
6
 
6
7
  class GraphDelete extends GraphOperation {
8
+ // The owner of a BelongsToOneRelation row references it until `GraphPatch`
9
+ // points the owner to a new row or to null. These rows are deleted
10
+ // afterwards by `GraphDereferencedBelongsToOneDelete`, all others before.
11
+ get deletesDereferencedRows() {
12
+ return false;
13
+ }
14
+
7
15
  createActions() {
8
16
  return [
9
17
  new GraphDeleteAction(this.graphData, {
10
- nodes: this.currentGraph.nodes.filter((currentNode) =>
11
- this.graphOptions.shouldDeleteOrUnrelate(currentNode, this.graphData),
18
+ nodes: this.currentGraph.nodes.filter(
19
+ (currentNode) =>
20
+ this.graphOptions.shouldDeleteOrUnrelate(currentNode, this.graphData) &&
21
+ isDereferencedBelongsToOneNode(currentNode, this.graphData) ===
22
+ this.deletesDereferencedRows,
12
23
  ),
24
+ areReferenced: !this.deletesDereferencedRows,
13
25
  }),
14
26
  ];
15
27
  }
16
28
  }
17
29
 
30
+ class GraphDereferencedBelongsToOneDelete extends GraphDelete {
31
+ get deletesDereferencedRows() {
32
+ return true;
33
+ }
34
+ }
35
+
36
+ function isDereferencedBelongsToOneNode(currentNode, graphData) {
37
+ const { graph, graphOptions } = graphData;
38
+ const relation = currentNode.parentEdge?.relation;
39
+ const parentNode = graph.nodeForNode(currentNode.parentNode);
40
+
41
+ if (relation?.isObjectionBelongsToOneRelation && parentNode) {
42
+ const node = graph.nodeForNode(currentNode);
43
+ const isReplaced = parentNode.edges.some(
44
+ (edge) =>
45
+ edge.relation === relation &&
46
+ edge.isOwnerNode(parentNode) &&
47
+ !graphOptions.shouldIgnore(edge.relatedNode, graphData),
48
+ );
49
+ const isRemoved =
50
+ parentNode.obj[relation.name] === null || (!!node && node.toBeUnrelatedOrDeleted);
51
+
52
+ // Removing the row sets the owner's foreign key to null, but only where
53
+ // its schema allows it. Otherwise we delete the row first and leave the
54
+ // owner's reference to the database, as the column may be `NOT NULL`.
55
+ return (
56
+ isReplaced ||
57
+ (isRemoved &&
58
+ relation.ownerProp.props.every((prop) => isNullable(relation.ownerModelClass, prop)))
59
+ );
60
+ } else {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function isNullable(modelClass, prop) {
66
+ const schema = modelClass.getJsonSchema()?.properties?.[prop];
67
+ return !!schema && (schema.nullable === true || getPropertyTypes(schema).includes('null'));
68
+ }
69
+
18
70
  module.exports = {
19
71
  GraphDelete,
72
+ GraphDereferencedBelongsToOneDelete,
20
73
  };
@@ -5,10 +5,14 @@ const { groupBy } = require('../../../utils/objectUtils');
5
5
  const promiseUtils = require('../../../utils/promiseUtils');
6
6
 
7
7
  class GraphDeleteAction extends GraphAction {
8
- constructor(graphData, { nodes }) {
8
+ constructor(graphData, { nodes, areReferenced = true }) {
9
9
  super(graphData);
10
10
  // Nodes to delete.
11
11
  this.nodes = nodes;
12
+ // Whether the parents still reference the nodes. Rows of
13
+ // BelongsToOneRelations can be deleted after their owners were patched to
14
+ // reference new rows or null, so they can't be found through the relation.
15
+ this.areReferenced = areReferenced;
12
16
  }
13
17
 
14
18
  run(builder) {
@@ -47,10 +51,14 @@ class GraphDeleteAction extends GraphAction {
47
51
  );
48
52
 
49
53
  nodesByShouldUnrelate.forEach((nodes, shouldUnrelate) => {
50
- const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
51
-
52
- if (!relation.isObjectionBelongsToOneRelation) {
53
- // This is useless in case of BelongsToOneRelation.
54
+ const builder = (
55
+ this.areReferenced
56
+ ? parentNode.obj.$relatedQuery(relation.name)
57
+ : relation.relatedModelClass.query()
58
+ ).childQueryOf(parentBuilder);
59
+
60
+ if (!this.areReferenced || !relation.isObjectionBelongsToOneRelation) {
61
+ // This is useless in case of a referenced BelongsToOneRelation.
54
62
  builder.findByIds(nodes.map((node) => node.obj.$id()));
55
63
  }
56
64
 
@@ -46,7 +46,12 @@ class GraphInsert extends GraphOperation {
46
46
  // In case of HasManyRelation the related node depends on the owner node
47
47
  // because the related node has the foreign key.
48
48
  this._addDependency(edge.relatedNode, edge, dependencies);
49
- } else if (edge.relation.isObjectionBelongsToOneRelation) {
49
+ } else if (
50
+ edge.relation.isObjectionBelongsToOneRelation &&
51
+ // Ignored models don't replace the current one, so their keys must not
52
+ // be copied to the owner either.
53
+ !this.graphOptions.shouldIgnore(edge.relatedNode, this.graphData)
54
+ ) {
50
55
  // In case of BelongsToOneRelation the owner node depends on the related
51
56
  // node because the owner node has the foreign key.
52
57
  this._addDependency(edge.ownerNode, edge, dependencies);
@@ -128,8 +128,9 @@ class GraphPatchAction extends GraphAction {
128
128
  edge.relation &&
129
129
  edge.relation.isObjectionBelongsToOneRelation &&
130
130
  edge.relation.relatedProp.hasProps(edge.relatedNode.obj) &&
131
- // Models marked with `#unrelate` or `#delete` are handled below.
132
- !edge.relatedNode.toBeUnrelatedOrDeleted
131
+ // Ignored models don't replace the current one, and models marked with
132
+ // `#unrelate` or `#delete` are handled below.
133
+ !this.graphOptions.shouldIgnore(edge.relatedNode, this.graphData)
133
134
  ) {
134
135
  const { relation } = edge;
135
136
 
@@ -9,7 +9,7 @@ class JoinResultParser {
9
9
  this.omitColumnAliases = new Set(omitColumnAliases);
10
10
 
11
11
  this.columnsByTableNode = null;
12
- this.parentMap = null;
12
+ this.modelsByTableNode = null;
13
13
  this.rootModels = null;
14
14
  }
15
15
 
@@ -25,7 +25,8 @@ class JoinResultParser {
25
25
  this._checkIdColumns(flatRows[0]);
26
26
 
27
27
  this.columnsByTableNode = this._createColumns(flatRows[0]);
28
- this.parentMap = new Map();
28
+ // Key the models of each table node by the ids on their path from the root:
29
+ this.modelsByTableNode = new Map(this.tableTree.nodes.map((node) => [node, new Map()]));
29
30
  this.rootModels = [];
30
31
 
31
32
  for (const flatRow of flatRows) {
@@ -42,14 +43,15 @@ class JoinResultParser {
42
43
  return;
43
44
  }
44
45
 
45
- const key = getKey(parentKey, id, tableNode);
46
- let model = this.parentMap.get(key);
46
+ const key = getKey(parentKey, id);
47
+ const models = this.modelsByTableNode.get(tableNode);
48
+ let model = models.get(key);
47
49
 
48
50
  if (!model) {
49
51
  model = this._createModel(tableNode, flatRow);
50
52
 
51
53
  this._addToParent(tableNode, model, parentModel);
52
- this.parentMap.set(key, model);
54
+ models.set(key, model);
53
55
  }
54
56
 
55
57
  for (const childNode of tableNode.childNodes) {
@@ -128,11 +130,14 @@ function getTableNode(column) {
128
130
  return column.tableNode;
129
131
  }
130
132
 
131
- function getKey(parentKey, id, tableNode) {
133
+ // Escape the separator in the ids, since string ids can contain it.
134
+ function getKey(parentKey, id) {
135
+ const escapedId = id.includes('/') || id.includes('\\') ? id.replace(/[/\\]/g, '\\$&') : id;
136
+
132
137
  if (parentKey !== null) {
133
- return `${parentKey}/${tableNode.relationProperty}/${id}`;
138
+ return `${parentKey}/${escapedId}`;
134
139
  } else {
135
- return `/${id}`;
140
+ return escapedId;
136
141
  }
137
142
  }
138
143
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { uniqBy } = require('../../utils/objectUtils');
4
- const { Selection } = require('../operations/select/Selection');
4
+ const { Selection, isSameTable, mapIdentifier } = require('../operations/select/Selection');
5
5
  const { SelectOperation } = require('../operations/select/SelectOperation');
6
6
  const { createModifier } = require('../../utils/createModifier');
7
7
  const { map: mapPromise } = require('../../utils/promiseUtils');
@@ -89,6 +89,13 @@ class RelationJoiner {
89
89
  }
90
90
  }
91
91
 
92
+ // Whether the joins can return more than one row per root model.
93
+ hasToManyJoins(builder) {
94
+ return this._getTableTree(builder).nodes.some(
95
+ (tableNode) => tableNode.relation && !tableNode.relation.isOneToOne(),
96
+ );
97
+ }
98
+
92
99
  parseResult(builder, flatRows) {
93
100
  const parser = JoinResultParser.create({
94
101
  tableTree: this._getTableTree(builder),
@@ -203,7 +210,60 @@ class RelationJoiner {
203
210
  }
204
211
  }
205
212
 
206
- builder.select(selectionsToStrings(selections));
213
+ // Each name is selected only once, as SQL Server returns duplicate names as
214
+ // arrays of values, and the other databases silently return one of them.
215
+ removeRootSelectAll(builder);
216
+ const tableTree = this._getTableTree(builder);
217
+ const userSelectionsByName = new Map();
218
+
219
+ for (const selection of findJoinSelections(builder)) {
220
+ // Only the columns of the joined tables are known for `table.*`:
221
+ const node = isSelectAll(selection) ? tableTree.nodesByAlias.get(selection.table) : null;
222
+ const names = node
223
+ ? getAllColumnSelectionsForNode({ builder, tableNode: node }).map((it) => it.column)
224
+ : [selection.name];
225
+
226
+ for (const name of names) {
227
+ userSelectionsByName.set(mapIdentifier(builder, name), { selection, name });
228
+ }
229
+ }
230
+
231
+ const idAliases = new Set(
232
+ tableTree.nodes.flatMap((node) =>
233
+ getIdSelectionsForNode({ tableNode: node }).map((it) => it.alias),
234
+ ),
235
+ );
236
+
237
+ builder.select(
238
+ selectionsToStrings(
239
+ selections.filter(({ table, column, alias }) => {
240
+ const { selection, name } = userSelectionsByName.get(mapIdentifier(builder, alias)) || {};
241
+
242
+ if (!selection) {
243
+ return true;
244
+ } else if (selection.column) {
245
+ if (!Selection.doesSelect(builder, selection, `${table}.${column}`)) {
246
+ throw new Error(
247
+ `withGraphJoined: the selection '${selectionToString(selection)}' selects '${name}', ` +
248
+ `the name under which the column '${table}.${column}' is selected. ` +
249
+ 'Select it under a different name.',
250
+ );
251
+ }
252
+ } else if (idAliases.has(alias)) {
253
+ // Raws, subqueries and aggregates select values that we can't see
254
+ // into. They replace our columns, but not the ids that we need to
255
+ // parse the result.
256
+ throw new Error(
257
+ `withGraphJoined: a raw, subquery or aggregate selection selects '${name}', ` +
258
+ `the name under which the id column '${table}.${column}' is selected. ` +
259
+ 'Select it under a different name, or select the id column itself.',
260
+ );
261
+ }
262
+
263
+ return false;
264
+ }),
265
+ ),
266
+ );
207
267
  }
208
268
  }
209
269
 
@@ -268,8 +328,9 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
268
328
  ),
269
329
  ];
270
330
 
331
+ const hasSelectionAs = createHasSelectionAs(builder);
271
332
  const selectStrings = uniqBy(cols)
272
- .filter((col) => !hasSelectionAs(builder, col))
333
+ .filter((col) => !hasSelectionAs(col))
273
334
  .map((col) => `${tableRef}.${col}`);
274
335
 
275
336
  return builder.select(selectStrings);
@@ -297,9 +358,10 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
297
358
  selections = allColumnSelections.concat(selections);
298
359
  } else {
299
360
  const idSelections = getIdSelectionsForNode({ tableNode });
361
+ const hasUserSelectionAs = createHasUserSelectionAs(userSelectQuery, tableNode);
300
362
 
301
363
  for (const idSelection of idSelections) {
302
- if (!hasUserSelectionAs(userSelectQuery, tableNode, idSelection.column)) {
364
+ if (!hasUserSelectionAs(idSelection.column)) {
303
365
  selections.push(idSelection);
304
366
  internalSelections.push(idSelection);
305
367
  }
@@ -334,14 +396,16 @@ function findJoinSelections(builder) {
334
396
  return selections;
335
397
  }
336
398
 
337
- // Like `builder.hasSelectionAs(column, column)`, but ignoring the columns
338
- // passed to aggregates, as they don't select them.
339
- function hasSelectionAs(builder, column) {
399
+ // Create a `(column) => builder.hasSelectionAs(column, column)` function that
400
+ // ignores the columns passed to aggregates, as they don't select them. Clone
401
+ // the builder once, not once per column.
402
+ function createHasSelectionAs(builder) {
340
403
  if (builder.has(isAggregateOperation)) {
404
+ const selectionQuery = builder.clone().clear(isAggregateOperation);
341
405
  // Explicit, as a query with only aggregates doesn't select all columns.
342
- return builder.clone().clear(isAggregateOperation).hasSelectionAs(column, column, true);
406
+ return (column) => selectionQuery.hasSelectionAs(column, column, true);
343
407
  } else {
344
- return builder.hasSelectionAs(column, column);
408
+ return (column) => builder.hasSelectionAs(column, column);
345
409
  }
346
410
  }
347
411
 
@@ -355,11 +419,12 @@ function findUserSelections(builder, tableNode) {
355
419
  }
356
420
  }
357
421
 
358
- function hasUserSelectionAs(builder, tableNode, column) {
422
+ function createHasUserSelectionAs(builder, tableNode) {
359
423
  if (tableNode.parentNode) {
360
- return hasSelectionAs(builder, column);
424
+ return createHasSelectionAs(builder);
361
425
  } else {
362
- return builder.hasSelectionAs(column, column);
426
+ // Qualified, as the root query can also select the columns of the joined tables.
427
+ return (column) => builder.hasSelectionAs(`${tableNode.alias}.${column}`, column);
363
428
  }
364
429
  }
365
430
 
@@ -416,6 +481,29 @@ function getIdSelectionsForNode({ tableNode }) {
416
481
  });
417
482
  }
418
483
 
484
+ // The selections of all columns of the root table replace the user's `*`, which
485
+ // would select them a second time, and a bare `*` also the columns of the
486
+ // joined tables under the same names.
487
+ function removeRootSelectAll(builder) {
488
+ const tableRef = builder.tableRef();
489
+ const isRootSelectAll = (selection) =>
490
+ selection.column === '*' &&
491
+ (!selection.table || isSameTable(builder, selection.table, tableRef));
492
+ const isSelect = (op) =>
493
+ op.constructor === SelectOperation && !isAggregateOperation(op) && !op.isDistinctOn;
494
+
495
+ builder.forEachOperation(isSelect, (op) => {
496
+ op.args = op.args.filter((arg) => {
497
+ const selection = Selection.create(arg);
498
+ return !selection || !isRootSelectAll(selection);
499
+ });
500
+ op.selections = op.selections.filter((selection) => !isRootSelectAll(selection));
501
+ });
502
+
503
+ // An empty `distinct()` still makes the whole select distinct.
504
+ builder.clear((op) => isSelect(op) && op.name !== 'distinct' && op.args.length === 0);
505
+ }
506
+
419
507
  function selectionsToStrings(selections) {
420
508
  return selections.map((selection) => {
421
509
  const selectStr = `${selection.table}.${selection.column}`;
@@ -423,6 +511,11 @@ function selectionsToStrings(selections) {
423
511
  });
424
512
  }
425
513
 
514
+ function selectionToString({ table, column, alias }) {
515
+ const selection = table ? `${table}.${column}` : column;
516
+ return alias ? `${selection} as ${alias}` : selection;
517
+ }
518
+
426
519
  function isSelectAll(selection) {
427
520
  return selection.column === '*';
428
521
  }