objection 3.8.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 (41) hide show
  1. package/lib/model/AjvValidator.js +22 -6
  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 +17 -7
  11. package/lib/queryBuilder/graph/GraphData.js +39 -0
  12. package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
  13. package/lib/queryBuilder/graph/GraphOptions.js +66 -12
  14. package/lib/queryBuilder/graph/GraphUpsert.js +82 -4
  15. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  16. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  17. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
  18. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
  19. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  20. package/lib/queryBuilder/join/RelationJoiner.js +3 -2
  21. package/lib/queryBuilder/join/TableNode.js +6 -68
  22. package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
  23. package/lib/queryBuilder/operations/InsertOperation.js +2 -2
  24. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
  25. package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
  26. package/lib/queryBuilder/operations/UpdateOperation.js +26 -3
  27. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
  28. package/lib/queryBuilder/operations/select/Selection.js +25 -3
  29. package/lib/relations/RelationFindOperation.js +7 -4
  30. package/lib/relations/RelationOwner.js +2 -5
  31. package/lib/relations/RelationProperty.js +3 -14
  32. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  33. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
  34. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  35. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
  36. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +43 -7
  37. package/lib/utils/knexUtils.js +7 -0
  38. package/lib/utils/objectUtils.js +5 -0
  39. package/lib/utils/parseFieldExpression.js +2 -5
  40. package/package.json +1 -1
  41. package/typings/objection/index.d.ts +21 -11
@@ -161,8 +161,11 @@ class GraphOptions {
161
161
 
162
162
  return (
163
163
  !node &&
164
+ !hasIgnoredOtherModel(currentNode, graphData) &&
164
165
  !this._hasOption(currentNode, NO_UNRELATE) &&
165
- this.shouldUnrelateIgnoreDisable(currentNode)
166
+ (this.shouldUnrelateIgnoreDisable(currentNode) ||
167
+ // The unrelate replaces a delete, so `noDelete` disables it too.
168
+ (!this._hasOption(currentNode, NO_DELETE) && shouldUnrelateKeptRow(currentNode, graphData)))
166
169
  );
167
170
  }
168
171
 
@@ -173,8 +176,11 @@ class GraphOptions {
173
176
 
174
177
  return (
175
178
  !node &&
179
+ !hasIgnoredOtherModel(currentNode, graphData) &&
176
180
  !this._hasOption(currentNode, NO_DELETE) &&
177
- !this.shouldUnrelateIgnoreDisable(currentNode)
181
+ !this.shouldUnrelateIgnoreDisable(currentNode) &&
182
+ // Rows that the graph keeps in another place must not be deleted (#214).
183
+ !graphData.keptNodeForRow(currentNode)
178
184
  );
179
185
  }
180
186
 
@@ -182,6 +188,18 @@ class GraphOptions {
182
188
  return this.shouldInsert(node, graphData) || this.shouldRelate(node, graphData);
183
189
  }
184
190
 
191
+ // The graph ignores models that it neither inserts, relates nor patches, like
192
+ // models marked with `#unrelate` or `#delete` and models skipped by `noInsert`
193
+ // or `noRelate`. Inserted models have ids and can look like models to relate
194
+ // afterwards, so we check that they were inserted too.
195
+ shouldIgnore(node, graphData) {
196
+ return (
197
+ !node.userData.inserted &&
198
+ !this.shouldInsertOrRelate(node, graphData) &&
199
+ !this.shouldPatchOrUpdateIgnoreDisable(node, graphData)
200
+ );
201
+ }
202
+
185
203
  shouldDeleteOrUnrelate(currentNode, graphData) {
186
204
  return this.shouldDelete(currentNode, graphData) || this.shouldUnrelate(currentNode, graphData);
187
205
  }
@@ -264,27 +282,30 @@ function parseRelationExpression(optionName, expression) {
264
282
  }
265
283
 
266
284
  // Relation paths are matched by relation name, so aliases would never match.
267
- const alias = findAlias(parsedExpression.node);
285
+ const alias = findNode(parsedExpression.node, (node) => node.$name !== node.$relation);
268
286
 
269
287
  if (alias) {
270
288
  throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
271
289
  }
272
290
 
291
+ // `*` and `^` only have a meaning at the end of a relation path. Anywhere else
292
+ // they are parsed as relation names that would never match.
293
+ const token = findNode(parsedExpression.node, (node) => /^(\*|\^\d*)$/.test(node.$relation));
294
+
295
+ if (token) {
296
+ throw createError(`"${token.$relation}" is only supported at the end of a relation path`);
297
+ }
298
+
273
299
  return parsedExpression;
274
300
  }
275
301
 
276
- function findAlias(node) {
302
+ function findNode(node, predicate) {
277
303
  for (const childName of node.$childNames) {
278
304
  const child = node[childName];
305
+ const found = predicate(child) ? child : findNode(child, predicate);
279
306
 
280
- if (child.$name !== child.$relation) {
281
- return child;
282
- }
283
-
284
- const alias = findAlias(child);
285
-
286
- if (alias) {
287
- return alias;
307
+ if (found) {
308
+ return found;
288
309
  }
289
310
  }
290
311
 
@@ -344,6 +365,39 @@ function hasRelateProp(node) {
344
365
  }
345
366
  }
346
367
 
368
+ // An ignored model that isn't the current model of a BelongsToOneRelation
369
+ // doesn't replace it, so we keep the current one instead of removing it as
370
+ // missing.
371
+ function hasIgnoredOtherModel(currentNode, graphData) {
372
+ const { relation } = currentNode.parentEdge;
373
+ const parentNode = getNode(currentNode.parentNode, graphData.graph);
374
+
375
+ return (
376
+ relation.isObjectionBelongsToOneRelation &&
377
+ !!parentNode &&
378
+ parentNode.edges.some(
379
+ (edge) =>
380
+ edge.relation === relation &&
381
+ edge.isOwnerNode(parentNode) &&
382
+ graphData.graphOptions.shouldIgnore(edge.relatedNode, graphData),
383
+ )
384
+ );
385
+ }
386
+
387
+ // A row that the graph keeps in another place is unrelated here instead of
388
+ // being deleted, unless it's a has-many row that the graph relates through the
389
+ // same relation: That relate moves it by overwriting the foreign key, which
390
+ // may not be nullable.
391
+ function shouldUnrelateKeptRow(currentNode, graphData) {
392
+ const keptNode = graphData.keptNodeForRow(currentNode);
393
+ const { relation } = currentNode.parentEdge;
394
+
395
+ return (
396
+ !!keptNode &&
397
+ !(relation.isObjectionHasManyRelation && keptNode.parentEdge?.relation === relation)
398
+ );
399
+ }
400
+
347
401
  function getNode(currentNode, graph) {
348
402
  if (!graph || !currentNode) {
349
403
  return null;
@@ -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');
@@ -37,7 +37,7 @@ class GraphUpsert {
37
37
  // by `ignore()` aren't returned, which leaves the root models without ids,
38
38
  // so that related rows would be inserted with missing or wrong foreign keys.
39
39
  // The clause is ignored, and a warning is logged once per method.
40
- // TODO: Throw an error instead in objection 4.0.
40
+ // TODO: Throw an error instead in objection 4.0 (#53).
41
41
  static warnOnConflict(builder, method) {
42
42
  if (builder.has(isOnConflictOperation)) {
43
43
  warnOnce(
@@ -60,13 +60,14 @@ class GraphUpsert {
60
60
  .then(pruneGraphs())
61
61
  .then(checkForJoinTableRelatedTableErrors(builder))
62
62
  .then(executeOperations(builder))
63
+ .then(removeReferenceProps(graph.nodes))
63
64
  .then(returnResult(this.objects, this.isArray));
64
65
  }
65
66
  }
66
67
 
67
68
  // Misspelled options are silently ignored, so the operation runs with the
68
69
  // default behaviour instead (#84). A warning is logged once per option.
69
- // TODO: Throw an error instead in objection 4.0.
70
+ // TODO: Throw an error instead in objection 4.0 (#53).
70
71
  function warnOnUnknownOptions(upsertOptions) {
71
72
  // Recursive upserts pass on the already checked options of their parent.
72
73
  if (upsertOptions instanceof GraphOptions) {
@@ -128,6 +129,7 @@ function fetchCurrentGraph(builder, graphOptions, graph) {
128
129
  function pruneGraphs() {
129
130
  return (graphData) => {
130
131
  pruneRelatedBranches(graphData);
132
+ pruneSkippedBranches(graphData);
131
133
 
132
134
  if (!graphData.graphOptions.isInsertOnly()) {
133
135
  pruneDeletedBranches(graphData);
@@ -149,6 +151,31 @@ function pruneRelatedBranches(graphData) {
149
151
  removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
150
152
  }
151
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
+
152
179
  function pruneDeletedBranches(graphData) {
153
180
  const { graph, currentGraph } = graphData;
154
181
 
@@ -253,6 +280,7 @@ function checkForErrors(builder) {
253
280
 
254
281
  if (graphData.graphOptions.isInsertOnly()) {
255
282
  checkForHasManyRelateErrors(graphData);
283
+ warnOnInsertBelowRelate(graphData);
256
284
  }
257
285
 
258
286
  return graphData;
@@ -290,10 +318,18 @@ function checkForJoinTableRelatedTableErrors(builder) {
290
318
 
291
319
  function checkForNotFoundErrors(graphData, builder) {
292
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
+ );
293
328
 
294
329
  for (const node of graph.nodes) {
295
330
  if (
296
331
  node.obj.$hasId() &&
332
+ !nestedNodes.has(node) &&
297
333
  !node.toBeUnrelatedOrDeleted &&
298
334
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
299
335
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
@@ -400,11 +436,41 @@ function checkForHasManyRelateErrors(graphData) {
400
436
  }
401
437
  }
402
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
+
403
463
  function executeOperations(builder) {
404
464
  return (graphData) => {
405
465
  const operations = graphData.graphOptions.isInsertOnly()
406
466
  ? [GraphInsert]
407
- : [GraphDelete, GraphInsert, GraphPatch, GraphRecursiveUpsert];
467
+ : [
468
+ GraphDelete,
469
+ GraphInsert,
470
+ GraphPatch,
471
+ GraphDereferencedBelongsToOneDelete,
472
+ GraphRecursiveUpsert,
473
+ ];
408
474
 
409
475
  return operations.reduce((promise, Operation) => {
410
476
  const operation = new Operation(graphData);
@@ -422,6 +488,18 @@ function executeActions(builder, actions) {
422
488
  );
423
489
  }
424
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
+
425
503
  function isOnConflictOperation(op) {
426
504
  return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
427
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, mapIdentifier } = 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');
@@ -487,7 +487,8 @@ function getIdSelectionsForNode({ tableNode }) {
487
487
  function removeRootSelectAll(builder) {
488
488
  const tableRef = builder.tableRef();
489
489
  const isRootSelectAll = (selection) =>
490
- selection.column === '*' && (!selection.table || selection.table === tableRef);
490
+ selection.column === '*' &&
491
+ (!selection.table || isSameTable(builder, selection.table, tableRef));
491
492
  const isSelect = (op) =>
492
493
  op.constructor === SelectOperation && !isAggregateOperation(op) && !op.isDistinctOn;
493
494
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { propKey, hasProps } = require('../../model/modelValues');
4
+
3
5
  class TableNode {
4
6
  constructor({ tableTree, modelClass, expression, parentNode = null, relation = null }) {
5
7
  this.tableTree = tableTree;
@@ -86,78 +88,14 @@ class TableNode {
86
88
  }
87
89
 
88
90
  _createIdGetter() {
89
- const idColumns = this.modelClass.getIdColumnArray();
90
- const columnAliases = idColumns.map((column) => this.getColumnAliasForColumn(column));
91
-
92
- if (idColumns.length === 1) {
93
- return createIdGetter(columnAliases);
94
- } else {
95
- return createCompositeIdGetter(columnAliases);
96
- }
97
- }
98
- }
99
-
100
- function createIdGetter(columnAliases) {
101
- const columnAlias = columnAliases[0];
91
+ const columnAliases = this.modelClass
92
+ .getIdColumnArray()
93
+ .map((column) => this.getColumnAliasForColumn(column));
102
94
 
103
- return (flatRow) => {
104
- const id = flatRow[columnAlias];
105
-
106
- if (id === null) {
107
- return null;
108
- }
109
-
110
- return `${id}`;
111
- };
112
- }
113
-
114
- function createCompositeIdGetter(columnAliases) {
115
- if (columnAliases.length === 2) {
116
- return createTwoIdGetter(columnAliases);
117
- } else {
118
- return createMultiIdGetter(columnAliases);
95
+ return (flatRow) => (hasProps(flatRow, columnAliases) ? propKey(flatRow, columnAliases) : null);
119
96
  }
120
97
  }
121
98
 
122
- function createTwoIdGetter(columnAliases) {
123
- const columnAlias1 = columnAliases[0];
124
- const columnAlias2 = columnAliases[1];
125
-
126
- return (flatRow) => {
127
- const id1 = flatRow[columnAlias1];
128
- const id2 = flatRow[columnAlias2];
129
-
130
- if (id1 === null || id2 === null) {
131
- return null;
132
- }
133
-
134
- return `${id1},${id2}`;
135
- };
136
- }
137
-
138
- function createMultiIdGetter(columnAliases) {
139
- return (flatRow) => {
140
- let idStr = '';
141
-
142
- for (let i = 0, l = columnAliases.length; i < l; ++i) {
143
- const columnAlias = columnAliases[i];
144
- const id = flatRow[columnAlias];
145
-
146
- if (id === null) {
147
- return null;
148
- }
149
-
150
- idStr += id;
151
-
152
- if (i !== l - 1) {
153
- idStr += ',';
154
- }
155
- }
156
-
157
- return idStr;
158
- };
159
- }
160
-
161
99
  module.exports = {
162
100
  TableNode,
163
101
  };
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
+ const { isMySql } = require('../../utils/knexUtils');
6
+ const { warnOnce } = require('../../utils/warnings');
5
7
 
6
8
  class DeleteOperation extends QueryBuilderOperation {
7
9
  async onBefore2(builder, result) {
@@ -9,6 +11,17 @@ class DeleteOperation extends QueryBuilderOperation {
9
11
  return result;
10
12
  }
11
13
 
14
+ // knex leaves the order out of deletes on MySQL, but keeps their limit, so
15
+ // the order doesn't pick the deleted rows. Warn when the query is executed.
16
+ onAfterBuild(builder) {
17
+ if (isMySql(builder.knex()) && builder.has('limit') && builder.has(/^orderBy/)) {
18
+ warnOnce(
19
+ 'orderBy() has no effect in delete queries with limit() on MySQL, so the limit ' +
20
+ 'deletes rows in any order. This will throw an error in objection 4.0.',
21
+ );
22
+ }
23
+ }
24
+
12
25
  onBuildKnex(knexBuilder) {
13
26
  return knexBuilder.delete();
14
27
  }
@@ -3,7 +3,7 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
5
  const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
6
- const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
6
+ const { isSqlite, isMySql, hasBatchInsert } = require('../../utils/knexUtils');
7
7
  const { isObject, isString } = require('../../utils/objectUtils');
8
8
 
9
9
  // Base class for all insert operations.
@@ -27,7 +27,7 @@ class InsertOperation extends QueryBuilderOperation {
27
27
  }
28
28
 
29
29
  async onBefore2(builder, result) {
30
- if (this.models.length > 1 && !isPostgres(builder.knex()) && !isMsSql(builder.knex())) {
30
+ if (this.models.length > 1 && !hasBatchInsert(builder.knex())) {
31
31
  throw new Error('batch insert only works with Postgresql and SQL Server');
32
32
  } else {
33
33
  await callBeforeInsert(builder, this.models);
@@ -5,6 +5,7 @@ const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
6
  const { isObject } = require('../../utils/objectUtils');
7
7
  const { addWhereGroup } = require('../whereGroups');
8
+ const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
8
9
 
9
10
  class InstanceUpdateOperation extends UpdateOperation {
10
11
  constructor(name, opt) {
@@ -19,6 +20,7 @@ class InstanceUpdateOperation extends UpdateOperation {
19
20
 
20
21
  if (!this.model) {
21
22
  this.model = this.instance;
23
+ removeOverriddenQueryProps(this.model);
22
24
  }
23
25
 
24
26
  return retVal;