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
|
@@ -8,9 +8,10 @@ const { ValidationErrorType } = require('../model/ValidationError');
|
|
|
8
8
|
const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
|
|
9
9
|
const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
|
|
10
10
|
const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
|
|
11
|
-
const {
|
|
11
|
+
const { warnOnce } = require('../utils/warnings');
|
|
12
|
+
const { isMsSql } = require('../utils/knexUtils');
|
|
12
13
|
|
|
13
|
-
const { Selection } = require('./operations/select/Selection');
|
|
14
|
+
const { Selection, isSameIdentifier } = require('./operations/select/Selection');
|
|
14
15
|
|
|
15
16
|
const { QueryBuilderContext } = require('./QueryBuilderContext');
|
|
16
17
|
const { QueryBuilderBase } = require('./QueryBuilderBase');
|
|
@@ -45,9 +46,11 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
|
|
|
45
46
|
const { RangeOperation } = require('./operations/RangeOperation');
|
|
46
47
|
const { FirstOperation } = require('./operations/FirstOperation');
|
|
47
48
|
const { NoneOperation } = require('./operations/NoneOperation');
|
|
49
|
+
const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
|
|
48
50
|
const { FromOperation } = require('./operations/FromOperation');
|
|
49
51
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
50
|
-
const {
|
|
52
|
+
const { clearUnsupportedOrder } = require('./withoutOrder');
|
|
53
|
+
const { addWhereGroup } = require('./whereGroups');
|
|
51
54
|
|
|
52
55
|
class QueryBuilder extends QueryBuilderBase {
|
|
53
56
|
static init(self, modelClass) {
|
|
@@ -56,6 +59,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
56
59
|
self._resultModelClass = null;
|
|
57
60
|
self._explicitRejectValue = null;
|
|
58
61
|
self._explicitResolveValue = null;
|
|
62
|
+
self._queryBatch = null;
|
|
59
63
|
self._modifiers = {};
|
|
60
64
|
|
|
61
65
|
self._allowedGraphExpression = null;
|
|
@@ -257,21 +261,29 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
257
261
|
// The `joinOperation` option only applies to the relations of this call,
|
|
258
262
|
// see `RelationJoiner`. All other options are shared by all calls.
|
|
259
263
|
const { joinOperation, ...graphOptions } = options;
|
|
264
|
+
const isJoined = eagerOp instanceof JoinEagerOperation;
|
|
260
265
|
|
|
261
266
|
eagerOp.expression = eagerOp.expression.merge(parsedExp);
|
|
262
267
|
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
|
|
263
268
|
|
|
264
269
|
if (joinOperation) {
|
|
265
|
-
if (
|
|
270
|
+
if (isJoined) {
|
|
266
271
|
eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
|
|
267
272
|
} else {
|
|
268
|
-
|
|
273
|
+
warnOnce(
|
|
269
274
|
'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
|
|
270
275
|
`fetched relations "${parsedExp.toString()}".`,
|
|
271
276
|
);
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
279
|
|
|
280
|
+
if (graphOptions.limitPerParent && isJoined) {
|
|
281
|
+
warnOnce(
|
|
282
|
+
'The limitPerParent option only applies to withGraphFetched() and is ignored for the ' +
|
|
283
|
+
`joined relations "${parsedExp.toString()}".`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
275
287
|
checkMixedEager(this);
|
|
276
288
|
checkEager(this);
|
|
277
289
|
return this;
|
|
@@ -387,7 +399,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
387
399
|
return this.everyOperation((op) => {
|
|
388
400
|
if (op.constructor === SelectOperation) {
|
|
389
401
|
// SelectOperations with zero selections are the ones that only have
|
|
390
|
-
// raw items or other non-trivial selections
|
|
402
|
+
// raw items or other non-trivial selections, or `distinctOn()`.
|
|
391
403
|
return (
|
|
392
404
|
op.selections.length > 0 &&
|
|
393
405
|
op.selections.every((select) => {
|
|
@@ -501,39 +513,50 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
501
513
|
|
|
502
514
|
async resultSize() {
|
|
503
515
|
const knex = this.knex();
|
|
504
|
-
|
|
516
|
+
// `distinctOn` picks the first row of each group in the order of the query,
|
|
517
|
+
// which decides which root models are counted with to-many joins.
|
|
518
|
+
const shouldKeepOrder = this.has('distinctOn');
|
|
519
|
+
const builder = this.clone().clear(shouldKeepOrder ? /offset|limit/ : /orderBy|offset|limit/);
|
|
505
520
|
const joinEagerOp = builder.findOperation(JoinEagerOperation);
|
|
506
521
|
|
|
507
522
|
if (joinEagerOp) {
|
|
508
523
|
// `withGraphJoined` needs the table metadata to build its selects.
|
|
509
|
-
await joinEagerOp.
|
|
524
|
+
await joinEagerOp.fetchTableMetadata(builder);
|
|
525
|
+
|
|
526
|
+
// To-many joins return one row per joined child, so the root models are
|
|
527
|
+
// counted by their distinct ids, see below.
|
|
528
|
+
if (joinEagerOp.joiner.hasToManyJoins(builder)) {
|
|
529
|
+
assertHasIdColumn(builder.modelClass());
|
|
530
|
+
}
|
|
510
531
|
}
|
|
511
532
|
|
|
533
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
534
|
+
|
|
512
535
|
// Also clear the order on the built knex query, since `orderBy` calls
|
|
513
536
|
// can be added at build time, e.g. by a relation's `modify`.
|
|
514
|
-
const buildQuery = (knexBuilder) =>
|
|
537
|
+
const buildQuery = (knexBuilder) => {
|
|
538
|
+
const query = builder.toKnexQuery(knexBuilder);
|
|
539
|
+
return shouldKeepOrder ? query : query.clearOrder();
|
|
540
|
+
};
|
|
515
541
|
|
|
516
542
|
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
517
|
-
if (!joinEagerOp) {
|
|
543
|
+
if (!joinEagerOp || idColumns.length === 0) {
|
|
544
|
+
// Without ids, the joins are all to-one and return one row per root
|
|
545
|
+
// model.
|
|
518
546
|
buildQuery(knexBuilder);
|
|
547
|
+
} else if (refersToSelections(builder) || builder.has('distinctOn')) {
|
|
548
|
+
// `having`, and `groupBy` with an alias, may refer to aliased
|
|
549
|
+
// selections, and clearing the selections would also clear
|
|
550
|
+
// `distinctOn`, so keep them and select the distinct ids from the
|
|
551
|
+
// result instead.
|
|
552
|
+
knexBuilder.distinct(idColumns).from((knexBuilder) => {
|
|
553
|
+
buildQuery(knexBuilder).as('inner');
|
|
554
|
+
});
|
|
519
555
|
} else {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (refersToSelections(builder)) {
|
|
525
|
-
// `having`, and `groupBy` with an alias, may refer to aliased
|
|
526
|
-
// selections, so keep them and select the distinct ids from the
|
|
527
|
-
// result instead.
|
|
528
|
-
knexBuilder.distinct(idColumns).from((knexBuilder) => {
|
|
529
|
-
buildQuery(knexBuilder).as('inner');
|
|
530
|
-
});
|
|
531
|
-
} else {
|
|
532
|
-
const tableRef = builder.tableRef();
|
|
533
|
-
buildQuery(knexBuilder)
|
|
534
|
-
.clearSelect()
|
|
535
|
-
.distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
536
|
-
}
|
|
556
|
+
const tableRef = builder.tableRef();
|
|
557
|
+
buildQuery(knexBuilder)
|
|
558
|
+
.clearSelect()
|
|
559
|
+
.distinct(idColumns.map((col) => `${tableRef}.${col}`));
|
|
537
560
|
}
|
|
538
561
|
|
|
539
562
|
knexBuilder.as('temp');
|
|
@@ -585,7 +608,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
585
608
|
let selectionInstance = null;
|
|
586
609
|
|
|
587
610
|
this.forEachOperation(true, (op) => {
|
|
588
|
-
if (op.constructor === SelectOperation) {
|
|
611
|
+
if (op.constructor === SelectOperation && !op.isDistinctOn) {
|
|
589
612
|
selectionInstance = op.findSelection(this, selection);
|
|
590
613
|
noSelectStatements = false;
|
|
591
614
|
|
|
@@ -637,9 +660,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
637
660
|
} else {
|
|
638
661
|
if (foundSelection.column === '*') {
|
|
639
662
|
// * selects the columns with their column names as aliases.
|
|
640
|
-
return selection.column
|
|
663
|
+
return isSameIdentifier(this, selection.column, alias);
|
|
641
664
|
} else {
|
|
642
|
-
return foundSelection.name
|
|
665
|
+
return isSameIdentifier(this, foundSelection.name, alias);
|
|
643
666
|
}
|
|
644
667
|
}
|
|
645
668
|
}
|
|
@@ -1000,7 +1023,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1000
1023
|
}
|
|
1001
1024
|
|
|
1002
1025
|
updateById(id, modelOrObject) {
|
|
1003
|
-
|
|
1026
|
+
// Keep the id condition in its own where group, so that a user's `orWhere()`
|
|
1027
|
+
// can't get around it. See `whereGroups.js`.
|
|
1028
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1004
1029
|
.update(modelOrObject)
|
|
1005
1030
|
.runBefore((_, builder) => {
|
|
1006
1031
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -1010,7 +1035,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1010
1035
|
}
|
|
1011
1036
|
|
|
1012
1037
|
patchById(id, modelOrObject) {
|
|
1013
|
-
return this.findById(id)
|
|
1038
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1014
1039
|
.patch(modelOrObject)
|
|
1015
1040
|
.runBefore((_, builder) => {
|
|
1016
1041
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -1020,7 +1045,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1020
1045
|
}
|
|
1021
1046
|
|
|
1022
1047
|
deleteById(id) {
|
|
1023
|
-
return this.findById(id)
|
|
1048
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1024
1049
|
.delete()
|
|
1025
1050
|
.runBefore((_, builder) => {
|
|
1026
1051
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -1325,14 +1350,14 @@ function checkMixedEager(builder) {
|
|
|
1325
1350
|
|
|
1326
1351
|
if (joinNode.$allRecursive || fetchNode.$allRecursive) {
|
|
1327
1352
|
throw new Error(
|
|
1328
|
-
'relation expression
|
|
1353
|
+
'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
|
|
1329
1354
|
);
|
|
1330
1355
|
}
|
|
1331
1356
|
|
|
1332
1357
|
for (const childName of joinNode.$childNames) {
|
|
1333
1358
|
if (fetchNode.$childNames.includes(childName)) {
|
|
1334
1359
|
throw new Error(
|
|
1335
|
-
`relation
|
|
1360
|
+
`relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
|
|
1336
1361
|
);
|
|
1337
1362
|
}
|
|
1338
1363
|
}
|
|
@@ -1382,10 +1407,13 @@ function beforeExecute(builder) {
|
|
|
1382
1407
|
return promise;
|
|
1383
1408
|
}
|
|
1384
1409
|
|
|
1385
|
-
function doExecute(builder) {
|
|
1410
|
+
async function doExecute(builder) {
|
|
1386
1411
|
let promise = Promise.resolve();
|
|
1387
1412
|
|
|
1388
1413
|
builder = callOnBuildHooks(builder);
|
|
1414
|
+
// Unlike `onBuild`, `onAfterBuild` can be async, as it only runs when the
|
|
1415
|
+
// query is executed, not when it's only built, e.g. with `toKnexQuery()`:
|
|
1416
|
+
await chainOperationHooks(promise, builder, 'onAfterBuild');
|
|
1389
1417
|
const queryExecutorOperation = findQueryExecutorOperation(builder);
|
|
1390
1418
|
|
|
1391
1419
|
const explicitRejectValue = builder._explicitRejectValue;
|
|
@@ -1398,7 +1426,7 @@ function doExecute(builder) {
|
|
|
1398
1426
|
} else if (queryExecutorOperation !== null) {
|
|
1399
1427
|
promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
|
|
1400
1428
|
} else {
|
|
1401
|
-
promise =
|
|
1429
|
+
promise = executeKnexQuery(builder);
|
|
1402
1430
|
|
|
1403
1431
|
promise = chainOperationHooks(promise, builder, 'onRawResult');
|
|
1404
1432
|
promise = promise.then((result) => createModels(result, builder));
|
|
@@ -1407,6 +1435,30 @@ function doExecute(builder) {
|
|
|
1407
1435
|
return promise;
|
|
1408
1436
|
}
|
|
1409
1437
|
|
|
1438
|
+
// An operation can split the query into batches, e.g. to stay below the number
|
|
1439
|
+
// of values a database accepts in one statement. Build and execute the knex
|
|
1440
|
+
// query once per batch then, with the operation reading the current batch from
|
|
1441
|
+
// `builder._queryBatch`, and merge the counts or rows of all batches.
|
|
1442
|
+
async function executeKnexQuery(builder) {
|
|
1443
|
+
const operation = builder.findOperation((op) => op.hasQueryBatches());
|
|
1444
|
+
const batches = operation?.queryBatches(builder);
|
|
1445
|
+
|
|
1446
|
+
if (batches) {
|
|
1447
|
+
const results = [];
|
|
1448
|
+
|
|
1449
|
+
for (const batch of batches) {
|
|
1450
|
+
builder._queryBatch = batch;
|
|
1451
|
+
results.push(await buildKnexQuery(builder));
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return results.reduce((merged, result) =>
|
|
1455
|
+
Array.isArray(merged) ? merged.concat(result) : merged + result,
|
|
1456
|
+
);
|
|
1457
|
+
} else {
|
|
1458
|
+
return buildKnexQuery(builder);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1410
1462
|
function afterExecute(builder, result) {
|
|
1411
1463
|
let promise = Promise.resolve(result);
|
|
1412
1464
|
|
|
@@ -1560,7 +1612,13 @@ function callOnBuildHooks(builder) {
|
|
|
1560
1612
|
callOnBuildFuncs(builder, builder.internalContext().onBuild);
|
|
1561
1613
|
|
|
1562
1614
|
builder.executeOnBuild();
|
|
1563
|
-
|
|
1615
|
+
|
|
1616
|
+
// Clear the order after all hooks, as any of them may add one.
|
|
1617
|
+
if (builder.isUpdate()) {
|
|
1618
|
+
clearUnsupportedOrder(builder);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
return builder;
|
|
1564
1622
|
}
|
|
1565
1623
|
|
|
1566
1624
|
function callOnBuildFuncs(builder, func) {
|
|
@@ -1595,6 +1653,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
|
|
|
1595
1653
|
}
|
|
1596
1654
|
}
|
|
1597
1655
|
|
|
1656
|
+
const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
|
|
1657
|
+
|
|
1658
|
+
if (limitPerParentOperation) {
|
|
1659
|
+
knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1598
1662
|
return knexBuilder;
|
|
1599
1663
|
}
|
|
1600
1664
|
|
|
@@ -1604,6 +1668,10 @@ function setDefaultTable(builder, knexBuilder) {
|
|
|
1604
1668
|
|
|
1605
1669
|
if (table === tableRef) {
|
|
1606
1670
|
return knexBuilder.table(table);
|
|
1671
|
+
} else if (isMsSql(builder.knex()) && (builder.isUpdate() || builder.isDelete())) {
|
|
1672
|
+
// SQL Server can't alias the table of updates and deletes, but it can modify
|
|
1673
|
+
// it through a common table expression named like the alias.
|
|
1674
|
+
return knexBuilder.with(tableRef, (query) => query.select('*').from(table)).table(tableRef);
|
|
1607
1675
|
} else {
|
|
1608
1676
|
return knexBuilder.table(`${table} as ${tableRef}`);
|
|
1609
1677
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
|
|
4
4
|
const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
|
|
5
5
|
const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
|
|
6
|
-
const { deprecate } = require('../utils/
|
|
6
|
+
const { deprecate } = require('../utils/warnings');
|
|
7
|
+
const { getWhereGroupOperations } = require('./whereGroups');
|
|
7
8
|
|
|
8
9
|
const AllSelector = () => true;
|
|
9
10
|
const SelectSelector =
|
|
@@ -489,8 +490,19 @@ class QueryBuilderOperationSupport {
|
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
executeOnBuildKnex(knexBuilder) {
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
const whereGroupOps = getWhereGroupOperations(this);
|
|
494
|
+
|
|
495
|
+
// Visit the operations while building, so that operations added by
|
|
496
|
+
// `onBuildKnex` hooks are built too. Build each where group in place of its
|
|
497
|
+
// first operation, and skip the operations that the groups build.
|
|
498
|
+
this.forEachOperation(true, (it) => {
|
|
499
|
+
const op = whereGroupOps.has(it)
|
|
500
|
+
? whereGroupOps.get(it)
|
|
501
|
+
: it.isAncestorInSet(whereGroupOps)
|
|
502
|
+
? null
|
|
503
|
+
: it;
|
|
504
|
+
|
|
505
|
+
if (op?.hasOnBuildKnex()) {
|
|
494
506
|
const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
|
|
495
507
|
// Default to the input knex builder for backwards compatibility
|
|
496
508
|
// with QueryBuilder.onBuildKnex hooks.
|
|
@@ -26,9 +26,12 @@ class StaticHookArguments {
|
|
|
26
26
|
return () => {
|
|
27
27
|
// The `runAfter()` callbacks of the query being executed (including the
|
|
28
28
|
// ones installed by `throwIfNotFound()` and `traverse()`) are meant for
|
|
29
|
-
// its result, not for the result of this find query.
|
|
29
|
+
// its result, not for the result of this find query. The same goes for
|
|
30
|
+
// the value it was resolved with, e.g. the 0 of an update that only
|
|
31
|
+
// changes the join rows of a many-to-many relation.
|
|
30
32
|
return this[BUILDER_SYMBOL].toFindQuery()
|
|
31
33
|
.clear('runAfter')
|
|
34
|
+
.clearResolve()
|
|
32
35
|
.clearWithGraphFetched()
|
|
33
36
|
.runAfter(toArray);
|
|
34
37
|
};
|
|
@@ -6,7 +6,46 @@ class GraphData {
|
|
|
6
6
|
this.currentGraph = currentGraph;
|
|
7
7
|
this.graphOptions = graphOptions;
|
|
8
8
|
this.nodeDbExistence = nodeDbExistence;
|
|
9
|
+
// Pruning replaces `graph.nodes`, but `keptNodeForRow()` also needs the
|
|
10
|
+
// branches below related nodes.
|
|
11
|
+
this._unprunedNodes = graph.nodes;
|
|
12
|
+
this._keptNodesByRowKey = null;
|
|
9
13
|
}
|
|
14
|
+
|
|
15
|
+
// Find the node that keeps the row of `currentNode` in another place of the
|
|
16
|
+
// graph, by relating or patching it there, or by upserting it below a related
|
|
17
|
+
// node. Only call this after pruning.
|
|
18
|
+
keptNodeForRow(currentNode) {
|
|
19
|
+
if (!this._keptNodesByRowKey) {
|
|
20
|
+
this._keptNodesByRowKey = new Map();
|
|
21
|
+
const nodes = new Set(this.graph.nodes);
|
|
22
|
+
|
|
23
|
+
// Nested upserts write the pruned branches below related nodes, except
|
|
24
|
+
// for models marked with `#unrelate` or `#delete` and their descendants:
|
|
25
|
+
const isUpsertedBelowRelatedNode = (node) =>
|
|
26
|
+
!node.toBeUnrelatedOrDeleted &&
|
|
27
|
+
(nodes.has(node.parentNode)
|
|
28
|
+
? this.graphOptions.shouldRelate(node.parentNode, this)
|
|
29
|
+
: isUpsertedBelowRelatedNode(node.parentNode));
|
|
30
|
+
|
|
31
|
+
for (const node of this._unprunedNodes) {
|
|
32
|
+
if (
|
|
33
|
+
node.obj.$hasId() &&
|
|
34
|
+
(nodes.has(node)
|
|
35
|
+
? this.graphOptions.shouldPatchOrUpdateIgnoreDisable(node, this)
|
|
36
|
+
: isUpsertedBelowRelatedNode(node))
|
|
37
|
+
) {
|
|
38
|
+
this._keptNodesByRowKey.set(getRowKey(node), node);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this._keptNodesByRowKey.get(getRowKey(currentNode)) || null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getRowKey(node) {
|
|
48
|
+
return `${node.modelClass.getTableName()}:${node.obj.$idKey()}`;
|
|
10
49
|
}
|
|
11
50
|
|
|
12
51
|
module.exports = {
|
|
@@ -4,6 +4,8 @@ const { asArray, groupBy } = require('../../utils/objectUtils');
|
|
|
4
4
|
const { ModelGraph } = require('../../model/graph/ModelGraph');
|
|
5
5
|
const { FetchStrategy } = require('./GraphOptions');
|
|
6
6
|
const { RelationExpression } = require('../RelationExpression');
|
|
7
|
+
const { getDeclaredColumns } = require('../operations/UpdateOperation');
|
|
8
|
+
const { parseFieldExpression, isFieldExpressionKey } = require('../../utils/parseFieldExpression');
|
|
7
9
|
|
|
8
10
|
class GraphFetcher {
|
|
9
11
|
/**
|
|
@@ -106,8 +108,14 @@ function createInputColumnSelector(nodes) {
|
|
|
106
108
|
|
|
107
109
|
for (const node of nodes) {
|
|
108
110
|
const databaseJson = node.obj.$toDatabaseJson(builder);
|
|
111
|
+
const declaredColumns = getDeclaredColumns(node.modelClass);
|
|
112
|
+
|
|
113
|
+
for (const key of Object.keys(databaseJson)) {
|
|
114
|
+
// Select the json column of field expressions like `'meta:a'`:
|
|
115
|
+
const column = isFieldExpressionKey(key, declaredColumns)
|
|
116
|
+
? parseFieldExpression(key).column
|
|
117
|
+
: key;
|
|
109
118
|
|
|
110
|
-
for (const column of Object.keys(databaseJson)) {
|
|
111
119
|
if (!shouldSelectColumn(column, selects, node)) {
|
|
112
120
|
continue;
|
|
113
121
|
}
|
|
@@ -17,21 +17,6 @@ const FETCH_STRATEGY = 'fetchStrategy';
|
|
|
17
17
|
const ALLOW_REFS = 'allowRefs';
|
|
18
18
|
const ALLOW_MARKERS = 'allowMarkers';
|
|
19
19
|
|
|
20
|
-
const OPTION_NAMES = [
|
|
21
|
-
NO_RELATE,
|
|
22
|
-
NO_UNRELATE,
|
|
23
|
-
NO_INSERT,
|
|
24
|
-
NO_UPDATE,
|
|
25
|
-
NO_DELETE,
|
|
26
|
-
UPDATE,
|
|
27
|
-
RELATE,
|
|
28
|
-
UNRELATE,
|
|
29
|
-
INSERT_MISSING,
|
|
30
|
-
FETCH_STRATEGY,
|
|
31
|
-
ALLOW_REFS,
|
|
32
|
-
ALLOW_MARKERS,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
20
|
// The options that are matched against the relation path of each node, see
|
|
36
21
|
// `_hasOption()`.
|
|
37
22
|
const NODE_OPTION_NAMES = [
|
|
@@ -46,6 +31,8 @@ const NODE_OPTION_NAMES = [
|
|
|
46
31
|
INSERT_MISSING,
|
|
47
32
|
];
|
|
48
33
|
|
|
34
|
+
const OPTION_NAMES = [...NODE_OPTION_NAMES, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
|
|
35
|
+
|
|
49
36
|
const FetchStrategy = {
|
|
50
37
|
OnlyIdentifiers: 'OnlyIdentifiers',
|
|
51
38
|
Everything: 'Everything',
|
|
@@ -55,14 +42,17 @@ const FetchStrategy = {
|
|
|
55
42
|
class GraphOptions {
|
|
56
43
|
// `rootRelationPath` is the relation path of the root node of a recursive
|
|
57
44
|
// upsert in the original graph, against which the relation paths and
|
|
58
|
-
// expressions of the options match.
|
|
59
|
-
|
|
45
|
+
// expressions of the options match. `insertOnly` is only set by
|
|
46
|
+
// `insertGraph`, which never fetches, updates or removes existing rows.
|
|
47
|
+
constructor(options, rootRelationPath = [], insertOnly = false) {
|
|
60
48
|
if (options instanceof GraphOptions) {
|
|
61
49
|
this.options = options.options;
|
|
62
50
|
this.rootRelationPath = options.rootRelationPath;
|
|
51
|
+
this.insertOnly = options.insertOnly;
|
|
63
52
|
} else {
|
|
64
53
|
this.options = parseRelationExpressions(options);
|
|
65
54
|
this.rootRelationPath = rootRelationPath;
|
|
55
|
+
this.insertOnly = insertOnly;
|
|
66
56
|
}
|
|
67
57
|
}
|
|
68
58
|
|
|
@@ -79,17 +69,12 @@ class GraphOptions {
|
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
isInsertOnly() {
|
|
82
|
-
|
|
83
|
-
// relate things that can be related using inserts.
|
|
84
|
-
// TODO: Use a special key for this.
|
|
85
|
-
return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
|
|
86
|
-
return this.options[opt] === true;
|
|
87
|
-
});
|
|
72
|
+
return this.insertOnly;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
75
|
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
|
|
91
76
|
shouldRelateIgnoreDisable(node, graphData) {
|
|
92
|
-
if (node.
|
|
77
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
93
78
|
return false;
|
|
94
79
|
}
|
|
95
80
|
|
|
@@ -113,7 +98,7 @@ class GraphOptions {
|
|
|
113
98
|
|
|
114
99
|
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
|
|
115
100
|
shouldInsertIgnoreDisable(node, graphData) {
|
|
116
|
-
if (node.
|
|
101
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
117
102
|
return false;
|
|
118
103
|
}
|
|
119
104
|
|
|
@@ -135,7 +120,7 @@ class GraphOptions {
|
|
|
135
120
|
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
|
|
136
121
|
// update or patch operations.
|
|
137
122
|
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
|
|
138
|
-
if (node.
|
|
123
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
139
124
|
return false;
|
|
140
125
|
}
|
|
141
126
|
|
|
@@ -176,8 +161,11 @@ class GraphOptions {
|
|
|
176
161
|
|
|
177
162
|
return (
|
|
178
163
|
!node &&
|
|
164
|
+
!hasIgnoredOtherModel(currentNode, graphData) &&
|
|
179
165
|
!this._hasOption(currentNode, NO_UNRELATE) &&
|
|
180
|
-
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)))
|
|
181
169
|
);
|
|
182
170
|
}
|
|
183
171
|
|
|
@@ -188,8 +176,11 @@ class GraphOptions {
|
|
|
188
176
|
|
|
189
177
|
return (
|
|
190
178
|
!node &&
|
|
179
|
+
!hasIgnoredOtherModel(currentNode, graphData) &&
|
|
191
180
|
!this._hasOption(currentNode, NO_DELETE) &&
|
|
192
|
-
!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)
|
|
193
184
|
);
|
|
194
185
|
}
|
|
195
186
|
|
|
@@ -197,6 +188,18 @@ class GraphOptions {
|
|
|
197
188
|
return this.shouldInsert(node, graphData) || this.shouldRelate(node, graphData);
|
|
198
189
|
}
|
|
199
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
|
+
|
|
200
203
|
shouldDeleteOrUnrelate(currentNode, graphData) {
|
|
201
204
|
return this.shouldDelete(currentNode, graphData) || this.shouldUnrelate(currentNode, graphData);
|
|
202
205
|
}
|
|
@@ -279,27 +282,30 @@ function parseRelationExpression(optionName, expression) {
|
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
// Relation paths are matched by relation name, so aliases would never match.
|
|
282
|
-
const alias =
|
|
285
|
+
const alias = findNode(parsedExpression.node, (node) => node.$name !== node.$relation);
|
|
283
286
|
|
|
284
287
|
if (alias) {
|
|
285
288
|
throw createError(`aliases like "${alias.$relation} as ${alias.$name}" are not supported`);
|
|
286
289
|
}
|
|
287
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
|
+
|
|
288
299
|
return parsedExpression;
|
|
289
300
|
}
|
|
290
301
|
|
|
291
|
-
function
|
|
302
|
+
function findNode(node, predicate) {
|
|
292
303
|
for (const childName of node.$childNames) {
|
|
293
304
|
const child = node[childName];
|
|
305
|
+
const found = predicate(child) ? child : findNode(child, predicate);
|
|
294
306
|
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const alias = findAlias(child);
|
|
300
|
-
|
|
301
|
-
if (alias) {
|
|
302
|
-
return alias;
|
|
307
|
+
if (found) {
|
|
308
|
+
return found;
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
@@ -359,6 +365,39 @@ function hasRelateProp(node) {
|
|
|
359
365
|
}
|
|
360
366
|
}
|
|
361
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
|
+
|
|
362
401
|
function getNode(currentNode, graph) {
|
|
363
402
|
if (!graph || !currentNode) {
|
|
364
403
|
return null;
|