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.
- package/lib/model/AjvValidator.js +22 -6
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -1
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +49 -5
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +17 -2
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +17 -7
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
- package/lib/queryBuilder/graph/GraphOptions.js +66 -12
- package/lib/queryBuilder/graph/GraphUpsert.js +82 -4
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +3 -2
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
- package/lib/queryBuilder/operations/InsertOperation.js +2 -2
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
- package/lib/queryBuilder/operations/UpdateOperation.js +26 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
- package/lib/queryBuilder/operations/select/Selection.js +25 -3
- package/lib/relations/RelationFindOperation.js +7 -4
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +43 -7
- package/lib/utils/knexUtils.js +7 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +2 -5
- package/package.json +1 -1
- 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 =
|
|
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
|
|
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 (
|
|
281
|
-
return
|
|
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
|
-
: [
|
|
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(
|
|
11
|
-
|
|
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 =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
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
|
-
//
|
|
132
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}/${
|
|
138
|
+
return `${parentKey}/${escapedId}`;
|
|
134
139
|
} else {
|
|
135
|
-
return
|
|
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 === '*' &&
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
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 {
|
|
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 && !
|
|
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;
|