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.
- package/lib/model/AjvValidator.js +123 -50
- 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 +106 -38
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
- package/lib/queryBuilder/graph/GraphOptions.js +77 -38
- package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
- 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 +105 -12
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InsertOperation.js +2 -2
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +3 -3
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +39 -16
- package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +3 -0
- package/lib/relations/RelationFindOperation.js +13 -5
- 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/ManyToManyModifyMixin.js +31 -9
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +21 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +9 -0
- package/lib/utils/warnings.js +26 -0
- package/lib/utils/wrapDbError.js +34 -20
- package/package.json +5 -4
- package/typings/objection/index.d.ts +31 -13
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
295
|
-
!node.
|
|
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:
|
|
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
|
-
: [
|
|
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(
|
|
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 } = 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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
338
|
-
// passed to aggregates, as they don't select them.
|
|
339
|
-
|
|
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
|
|
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
|
|
422
|
+
function createHasUserSelectionAs(builder, tableNode) {
|
|
359
423
|
if (tableNode.parentNode) {
|
|
360
|
-
return
|
|
424
|
+
return createHasSelectionAs(builder);
|
|
361
425
|
} else {
|
|
362
|
-
|
|
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
|
}
|