objection 3.6.0 → 3.8.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 +132 -52
- package/lib/model/modelClone.js +0 -2
- package/lib/queryBuilder/QueryBuilder.js +128 -24
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/RelationExpression.js +6 -4
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +36 -79
- package/lib/queryBuilder/graph/GraphUpsert.js +35 -7
- package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
- package/lib/queryBuilder/join/RelationJoiner.js +104 -12
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +8 -2
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- 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 +2 -2
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +21 -20
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
- package/lib/queryBuilder/parsers/relationExpressionParser.js +6 -4
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +7 -3
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +4 -0
- package/lib/relations/RelationFindOperation.js +30 -43
- package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +65 -19
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
- package/lib/relations/manyToMany/ManyToManyRelation.js +15 -16
- package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +9 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +92 -16
- package/lib/utils/identifierMapping.js +10 -19
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/objectUtils.js +38 -6
- package/lib/utils/parseFieldExpression.js +22 -3
- 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 +54 -26
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
- package/lib/utils/deprecate.js +0 -21
|
@@ -4,13 +4,14 @@ const { getModel } = require('../../model/getModel');
|
|
|
4
4
|
const { Relation } = require('../Relation');
|
|
5
5
|
const { RelationProperty } = require('../RelationProperty');
|
|
6
6
|
|
|
7
|
-
const { isSqlite, isMySql
|
|
7
|
+
const { isSqlite, isMySql } = require('../../utils/knexUtils');
|
|
8
8
|
const { inheritModel } = require('../../model/inheritModel');
|
|
9
9
|
const { resolveModel } = require('../../utils/resolveModel');
|
|
10
10
|
const { mapAfterAllReturn } = require('../../utils/promiseUtils');
|
|
11
11
|
const { isFunction, isObject, isString } = require('../../utils/objectUtils');
|
|
12
12
|
const { createModifier } = require('../../utils/createModifier');
|
|
13
13
|
const { addWhereGroup } = require('../../queryBuilder/whereGroups');
|
|
14
|
+
const { withoutOrder } = require('../../queryBuilder/withoutOrder');
|
|
14
15
|
|
|
15
16
|
const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
|
|
16
17
|
const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
|
|
@@ -21,14 +22,10 @@ const {
|
|
|
21
22
|
ManyToManyUnrelateSqliteOperation,
|
|
22
23
|
} = require('./unrelate/ManyToManyUnrelateSqliteOperation');
|
|
23
24
|
const { ManyToManyUnrelateMySqlOperation } = require('./unrelate/ManyToManyUnrelateMySqlOperation');
|
|
24
|
-
const {
|
|
25
|
-
ManyToManyUnrelatePostgresOperation,
|
|
26
|
-
} = require('./unrelate/ManyToManyUnrelatePostgresOperation');
|
|
27
25
|
|
|
28
26
|
const { ManyToManyUpdateOperation } = require('./update/ManyToManyUpdateOperation');
|
|
29
27
|
const { ManyToManyUpdateSqliteOperation } = require('./update/ManyToManyUpdateSqliteOperation');
|
|
30
28
|
const { ManyToManyUpdateMySqlOperation } = require('./update/ManyToManyUpdateMySqlOperation');
|
|
31
|
-
const { ManyToManyUpdatePostgresOperation } = require('./update/ManyToManyUpdatePostgresOperation');
|
|
32
29
|
|
|
33
30
|
const { ManyToManyDeleteOperation } = require('./delete/ManyToManyDeleteOperation');
|
|
34
31
|
const { ManyToManyDeleteSqliteOperation } = require('./delete/ManyToManyDeleteSqliteOperation');
|
|
@@ -117,6 +114,8 @@ class ManyToManyRelation extends Relation {
|
|
|
117
114
|
.as(joinTableAlias);
|
|
118
115
|
if (joinTableSelect.isSelectAll()) {
|
|
119
116
|
joinTableSelect = aliasedTableName(joinTable, joinTableAlias);
|
|
117
|
+
} else {
|
|
118
|
+
withoutOrder(joinTableSelect);
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
builder.join(joinTableSelect, (join) => {
|
|
@@ -148,6 +147,8 @@ class ManyToManyRelation extends Relation {
|
|
|
148
147
|
if (relatedJoinSelect.isSelectAll()) {
|
|
149
148
|
// No need to join a subquery if the query is `select * from "RelatedTable"`.
|
|
150
149
|
relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
|
|
150
|
+
} else {
|
|
151
|
+
withoutOrder(relatedJoinSelect);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
let joinTableSelect = this.joinTableModelClass
|
|
@@ -157,6 +158,8 @@ class ManyToManyRelation extends Relation {
|
|
|
157
158
|
.as(joinTableAlias);
|
|
158
159
|
if (joinTableSelect.isSelectAll()) {
|
|
159
160
|
joinTableSelect = aliasedTableName(this.joinTable, joinTableAlias);
|
|
161
|
+
} else {
|
|
162
|
+
withoutOrder(joinTableSelect);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
return builder[joinOperation](joinTableSelect, (join) => {
|
|
@@ -503,26 +506,25 @@ function parseBeforeInsert(ctx) {
|
|
|
503
506
|
}
|
|
504
507
|
|
|
505
508
|
function finalizeJoinModelClass(ctx) {
|
|
506
|
-
if
|
|
507
|
-
|
|
508
|
-
|
|
509
|
+
// We cannot know if the join table has a primary key. Therefore we set some
|
|
510
|
+
// known column as the idColumn of the join model class that we created. A
|
|
511
|
+
// `through.modelClass` is left untouched, even if it has no `idColumn` (#2305).
|
|
512
|
+
if (!ctx.mapping.join.through.modelClass) {
|
|
509
513
|
ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
|
|
510
514
|
}
|
|
511
515
|
|
|
512
516
|
return ctx;
|
|
513
517
|
}
|
|
514
518
|
|
|
515
|
-
// The modify operations need dialect specific code
|
|
516
|
-
//
|
|
517
|
-
function getOperationClass(builder, { sqlite, mysql,
|
|
519
|
+
// The modify operations need dialect specific code on sqlite and mysql, see
|
|
520
|
+
// the `ManyToMany*ModifyMixin` classes.
|
|
521
|
+
function getOperationClass(builder, { sqlite, mysql, other }) {
|
|
518
522
|
const knex = builder.knex();
|
|
519
523
|
|
|
520
524
|
if (isSqlite(knex)) {
|
|
521
525
|
return sqlite;
|
|
522
526
|
} else if (isMySql(knex)) {
|
|
523
527
|
return mysql;
|
|
524
|
-
} else if (isPostgres(knex)) {
|
|
525
|
-
return postgres;
|
|
526
528
|
} else {
|
|
527
529
|
return other;
|
|
528
530
|
}
|
|
@@ -532,7 +534,6 @@ function getUpdateOperationClass(builder) {
|
|
|
532
534
|
return getOperationClass(builder, {
|
|
533
535
|
sqlite: ManyToManyUpdateSqliteOperation,
|
|
534
536
|
mysql: ManyToManyUpdateMySqlOperation,
|
|
535
|
-
postgres: ManyToManyUpdatePostgresOperation,
|
|
536
537
|
other: ManyToManyUpdateOperation,
|
|
537
538
|
});
|
|
538
539
|
}
|
|
@@ -541,7 +542,6 @@ function getUnrelateOperationClass(builder) {
|
|
|
541
542
|
return getOperationClass(builder, {
|
|
542
543
|
sqlite: ManyToManyUnrelateSqliteOperation,
|
|
543
544
|
mysql: ManyToManyUnrelateMySqlOperation,
|
|
544
|
-
postgres: ManyToManyUnrelatePostgresOperation,
|
|
545
545
|
other: ManyToManyUnrelateOperation,
|
|
546
546
|
});
|
|
547
547
|
}
|
|
@@ -551,7 +551,6 @@ function getDeleteOperationClass(builder) {
|
|
|
551
551
|
return getOperationClass(builder, {
|
|
552
552
|
sqlite: ManyToManyDeleteSqliteOperation,
|
|
553
553
|
mysql: ManyToManyDeleteOperation,
|
|
554
|
-
postgres: ManyToManyDeleteOperation,
|
|
555
554
|
other: ManyToManyDeleteOperation,
|
|
556
555
|
});
|
|
557
556
|
}
|
|
@@ -15,19 +15,6 @@ const ManyToManySqliteModifyMixin = (Operation) => {
|
|
|
15
15
|
|
|
16
16
|
return builder.whereInComposite(rowIdRef, subquery);
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
applyModifyFilterForJoinTable(builder) {
|
|
20
|
-
const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
|
|
21
|
-
const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
|
|
22
|
-
const rowIdRef = `${tableRef}.${SQLITE_BUILTIN_ROW_ID}`;
|
|
23
|
-
|
|
24
|
-
const ownerValues = this.owner.getProps(this.relation);
|
|
25
|
-
const subquery = this.modifyFilterSubquery.clone().select(rowIdRef);
|
|
26
|
-
|
|
27
|
-
return builder
|
|
28
|
-
.whereInComposite(rowIdRef, subquery)
|
|
29
|
-
.whereInComposite(joinTableOwnerRefs, ownerValues);
|
|
30
|
-
}
|
|
31
18
|
};
|
|
32
19
|
};
|
|
33
20
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { RelationFindOperation } = require('../../RelationFindOperation');
|
|
4
4
|
const { getTempColumn } = require('../../../utils/tmpColumnUtils');
|
|
5
|
-
const {
|
|
5
|
+
const { warnOnce } = require('../../../utils/warnings');
|
|
6
6
|
|
|
7
7
|
class ManyToManyFindOperation extends RelationFindOperation {
|
|
8
8
|
constructor(name, opt) {
|
|
@@ -36,7 +36,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (this.owner.isModels) {
|
|
39
|
-
this.
|
|
39
|
+
this.warnOnInvalidOwnerProps();
|
|
40
40
|
|
|
41
41
|
if (this.assignResultToOwner) {
|
|
42
42
|
this.selectMissingJoinColumns(builder);
|
|
@@ -83,6 +83,11 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
83
83
|
return related;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// @override
|
|
87
|
+
ownerKeyColumns() {
|
|
88
|
+
return this.ownerJoinColumnAlias;
|
|
89
|
+
}
|
|
90
|
+
|
|
86
91
|
// The related models are assigned to their owners by the owner join columns
|
|
87
92
|
// selected from the join table. If they get lost, e.g. in a custom
|
|
88
93
|
// `$parseDatabaseJson()`, the related models are silently dropped (#2258).
|
|
@@ -93,7 +98,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
93
98
|
const alias = this.ownerJoinColumnAlias[c];
|
|
94
99
|
|
|
95
100
|
if (related.some((rel) => rel[alias] === undefined)) {
|
|
96
|
-
|
|
101
|
+
warnOnce(
|
|
97
102
|
`Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
|
|
98
103
|
`some related models are missing the join table column ` +
|
|
99
104
|
`"${joinTableOwnerProp.propDescription(c)}" selected as "${alias}", so they are not ` +
|
|
@@ -122,8 +127,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
|
|
|
122
127
|
const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
|
|
123
128
|
|
|
124
129
|
builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
|
|
125
|
-
|
|
126
|
-
this.omitProps.push(propName);
|
|
130
|
+
this.omitFromResult(builder, propName);
|
|
127
131
|
});
|
|
128
132
|
|
|
129
133
|
super.selectMissingJoinColumns(builder);
|
|
@@ -5,11 +5,7 @@ const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
|
|
|
5
5
|
|
|
6
6
|
class ManyToManyUnrelateMySqlOperation extends ManyToManyMySqlModifyMixin(
|
|
7
7
|
ManyToManyUnrelateOperationBase,
|
|
8
|
-
) {
|
|
9
|
-
get modifyMainQuery() {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
8
|
+
) {}
|
|
13
9
|
|
|
14
10
|
module.exports = {
|
|
15
11
|
ManyToManyUnrelateMySqlOperation,
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
|
|
4
4
|
const { ManyToManyModifyMixin } = require('../ManyToManyModifyMixin');
|
|
5
5
|
|
|
6
|
-
class ManyToManyUnrelateOperation extends ManyToManyModifyMixin(ManyToManyUnrelateOperationBase) {
|
|
7
|
-
get modifyMainQuery() {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
6
|
+
class ManyToManyUnrelateOperation extends ManyToManyModifyMixin(ManyToManyUnrelateOperationBase) {}
|
|
11
7
|
|
|
12
8
|
module.exports = {
|
|
13
9
|
ManyToManyUnrelateOperation,
|
|
@@ -4,6 +4,15 @@ const { UnrelateOperation } = require('../../../queryBuilder/operations/Unrelate
|
|
|
4
4
|
const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
|
|
5
5
|
|
|
6
6
|
class ManyToManyUnrelateOperationBase extends UnrelateOperation {
|
|
7
|
+
get modifiesJoinTable() {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The main query is replaced by the query that `queryExecutor()` returns.
|
|
12
|
+
get modifiesJoinTableOnly() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
queryExecutor(builder) {
|
|
8
17
|
const unrelateQuery = this.relation
|
|
9
18
|
.getJoinModelClass(builder.knex())
|
|
@@ -5,11 +5,7 @@ const { ManyToManySqliteModifyMixin } = require('../ManyToManySqliteModifyMixin'
|
|
|
5
5
|
|
|
6
6
|
class ManyToManyUnrelateSqliteOperation extends ManyToManySqliteModifyMixin(
|
|
7
7
|
ManyToManyUnrelateOperationBase,
|
|
8
|
-
) {
|
|
9
|
-
get modifyMainQuery() {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
8
|
+
) {}
|
|
13
9
|
|
|
14
10
|
module.exports = {
|
|
15
11
|
ManyToManyUnrelateSqliteOperation,
|
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
|
|
4
4
|
const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
|
|
5
5
|
|
|
6
|
+
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
7
|
+
|
|
6
8
|
class ManyToManyUpdateMySqlOperation extends ManyToManyMySqlModifyMixin(
|
|
7
9
|
ManyToManyUpdateOperationBase,
|
|
8
|
-
) {
|
|
10
|
+
) {
|
|
11
|
+
// MySQL is the only database that supports a limit in updates. Updates that
|
|
12
|
+
// modify the join rows too update the related rows by their ids, so the limit
|
|
13
|
+
// and the order pick these ids instead of limiting each batch of them. Without
|
|
14
|
+
// a limit or an offset, the order picks nothing and the query stays as it is.
|
|
15
|
+
onBuild(builder) {
|
|
16
|
+
super.onBuild(builder);
|
|
17
|
+
|
|
18
|
+
if (this.relatedIdsQuery && builder.has(/^(limit|offset)$/)) {
|
|
19
|
+
this.relatedIdsQuery.copyFrom(builder, LimitSelector);
|
|
20
|
+
builder.clear(LimitSelector);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
module.exports = {
|
|
11
26
|
ManyToManyUpdateMySqlOperation,
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { UpdateOperation } = require('../../../queryBuilder/operations/UpdateOperation');
|
|
4
4
|
const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
|
|
5
|
+
const { chunk } = require('../../../utils/objectUtils');
|
|
6
|
+
|
|
7
|
+
// Stay below the limits of bound values per statement of SQLite before 3.32
|
|
8
|
+
// (999) and SQL Server (2100), leaving room for the patched values:
|
|
9
|
+
const maxBatchValues = 500;
|
|
5
10
|
|
|
6
11
|
class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
7
12
|
constructor(name, opt) {
|
|
@@ -11,10 +16,16 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
11
16
|
this.owner = opt.owner;
|
|
12
17
|
|
|
13
18
|
this.hasExtraProps = false;
|
|
19
|
+
this.relatedIdsQuery = null;
|
|
20
|
+
this.relatedIds = null;
|
|
14
21
|
this.joinTablePatch = {};
|
|
15
22
|
this.joinTablePatchFilterQuery = null;
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
get modifiesJoinTable() {
|
|
26
|
+
return this.hasExtraProps;
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
onAdd(builder, args) {
|
|
19
30
|
const obj = args[0];
|
|
20
31
|
|
|
@@ -46,30 +57,93 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
46
57
|
return res;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
if (this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.patch(this.joinTablePatch);
|
|
60
|
+
onBuild(builder) {
|
|
61
|
+
if (this.modifiesJoinTable && this.modifyFilterSubquery) {
|
|
62
|
+
// The related rows to update are identified by their ids first, see
|
|
63
|
+
// `onAfterBuild()`.
|
|
64
|
+
this.relatedIdsQuery = this.createRelatedIdsQuery(builder);
|
|
65
|
+
}
|
|
56
66
|
|
|
67
|
+
return super.onBuild(builder);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Selects the ids of the related rows to update. Fetch raw rows, so that no
|
|
71
|
+
// hooks run for them.
|
|
72
|
+
createRelatedIdsQuery(builder) {
|
|
73
|
+
const relatedIdsQuery = this.relation.relatedModelClass.query().childQueryOf(builder);
|
|
74
|
+
const idRefs = this.relation.relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
75
|
+
|
|
76
|
+
return this.applyModifyFilterForRelatedTable(relatedIdsQuery).select(idRefs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Update the join rows before the related rows, while the filters still
|
|
80
|
+
// match them (#161). A query resolved without updating anything, e.g. with
|
|
81
|
+
// `none()`, leaves the join rows untouched too.
|
|
82
|
+
async onAfterBuild(builder) {
|
|
83
|
+
if (this.hasExtraProps && !this.isResolvedWithoutUpdate(builder)) {
|
|
84
|
+
if (this.relatedIdsQuery) {
|
|
85
|
+
// The filters can also reference the extras that the join rows change,
|
|
86
|
+
// so identify the related rows to modify first.
|
|
87
|
+
const rows = await this.relatedIdsQuery.toKnexQuery();
|
|
88
|
+
this.relatedIds = rows.map((row) => Object.values(row));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.relatedIdsQuery?.has(/^(limit|offset)$/)) {
|
|
92
|
+
// The filters match the join rows of more related rows than the limit
|
|
93
|
+
// picks, so only update the join rows of the picked ones.
|
|
94
|
+
for (const ids of this.relatedIdBatches()) {
|
|
95
|
+
await this.updateJoinRows(builder, ids);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
await this.updateJoinRows(builder);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
updateJoinRows(builder, relatedIds = null) {
|
|
104
|
+
const joinTableUpdateQuery = this.relation
|
|
105
|
+
.getJoinModelClass(builder.knex())
|
|
106
|
+
.query()
|
|
107
|
+
.childQueryOf(builder)
|
|
108
|
+
.patch(this.joinTablePatch);
|
|
109
|
+
|
|
110
|
+
addWhereGroup(joinTableUpdateQuery, () =>
|
|
111
|
+
this.skipRelationFilter
|
|
112
|
+
? this.applyIdFilterForJoinTable(joinTableUpdateQuery)
|
|
113
|
+
: this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
|
|
114
|
+
);
|
|
115
|
+
if (relatedIds) {
|
|
57
116
|
addWhereGroup(joinTableUpdateQuery, () =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
addWhereGroup(joinTableUpdateQuery, () =>
|
|
63
|
-
joinTableUpdateQuery.modify(this.relation.joinTableModify),
|
|
117
|
+
joinTableUpdateQuery.whereInComposite(
|
|
118
|
+
this.relation.joinTableRelatedProp.refs(joinTableUpdateQuery),
|
|
119
|
+
relatedIds,
|
|
120
|
+
),
|
|
64
121
|
);
|
|
122
|
+
}
|
|
123
|
+
addWhereGroup(joinTableUpdateQuery, () =>
|
|
124
|
+
joinTableUpdateQuery.modify(this.relation.joinTableModify),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return joinTableUpdateQuery;
|
|
128
|
+
}
|
|
65
129
|
|
|
66
|
-
|
|
67
|
-
|
|
130
|
+
// Update the related rows in batches of their ids, as databases limit the
|
|
131
|
+
// number of values in one statement.
|
|
132
|
+
queryBatches(builder) {
|
|
133
|
+
if (this.relatedIds) {
|
|
134
|
+
const batches = this.relatedIdBatches();
|
|
135
|
+
// Without ids, still execute the query once for the dialect's result:
|
|
136
|
+
return batches.length > 0 ? batches : [[]];
|
|
68
137
|
} else {
|
|
69
|
-
return
|
|
138
|
+
return null;
|
|
70
139
|
}
|
|
71
140
|
}
|
|
72
141
|
|
|
142
|
+
relatedIdBatches() {
|
|
143
|
+
const idSize = this.relation.relatedModelClass.getIdRelationProperty().size;
|
|
144
|
+
return chunk(this.relatedIds, Math.floor(maxBatchValues / idSize));
|
|
145
|
+
}
|
|
146
|
+
|
|
73
147
|
// Filters the join rows by the ids of the owner and the updated model.
|
|
74
148
|
applyIdFilterForJoinTable(builder) {
|
|
75
149
|
return builder
|
|
@@ -97,6 +171,8 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
97
171
|
const clone = super.clone();
|
|
98
172
|
|
|
99
173
|
clone.hasExtraProps = this.hasExtraProps;
|
|
174
|
+
clone.relatedIdsQuery = this.relatedIdsQuery;
|
|
175
|
+
clone.relatedIds = this.relatedIds;
|
|
100
176
|
clone.joinTablePatch = this.joinTablePatch;
|
|
101
177
|
clone.joinTablePatchFilterQuery = this.joinTablePatchFilterQuery;
|
|
102
178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isObject
|
|
3
|
+
const { isObject } = require('./objectUtils');
|
|
4
4
|
|
|
5
5
|
// Super fast memoize for single argument functions.
|
|
6
6
|
function memoize(func) {
|
|
@@ -149,9 +149,8 @@ function mapLastPart(mapper, separator) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// Returns a function that takes an object as an input and maps the object's keys
|
|
152
|
-
// using `mapper
|
|
153
|
-
|
|
154
|
-
function keyMapper(mapper, valueMapper) {
|
|
152
|
+
// using `mapper`. If the input is not an object, the input is returned unchanged.
|
|
153
|
+
function keyMapper(mapper) {
|
|
155
154
|
return (obj) => {
|
|
156
155
|
if (!isObject(obj) || Array.isArray(obj)) {
|
|
157
156
|
return obj;
|
|
@@ -162,7 +161,7 @@ function keyMapper(mapper, valueMapper) {
|
|
|
162
161
|
|
|
163
162
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
164
163
|
const key = keys[i];
|
|
165
|
-
out[mapper(key)] =
|
|
164
|
+
out[mapper(key)] = obj[key];
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
return out;
|
|
@@ -172,8 +171,8 @@ function keyMapper(mapper, valueMapper) {
|
|
|
172
171
|
// Returns a function that only calls `mapper` for the part of the input string
|
|
173
172
|
// before the first `separator`, and leaves the rest untouched. For field
|
|
174
173
|
// expressions like `jsonColumn:some.jsonPath` this maps the column but keeps
|
|
175
|
-
// the json path as is.
|
|
176
|
-
// the boundary
|
|
174
|
+
// the json path as is. The mapper doesn't know the schema, so the first `:`
|
|
175
|
+
// marks the boundary also in a declared column name like `ns:name`.
|
|
177
176
|
function mapFirstPart(mapper, separator) {
|
|
178
177
|
return (str) => {
|
|
179
178
|
const idx = str.indexOf(separator);
|
|
@@ -190,16 +189,10 @@ function snakeCaseMappers(opt = {}) {
|
|
|
190
189
|
};
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
function knexIdentifierMappers({ parse, format, idSeparator = ':'
|
|
192
|
+
function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
|
|
194
193
|
const formatId = memoize(mapLastPart(format, idSeparator));
|
|
195
194
|
const parseId = memoize(mapLastPart(parse, idSeparator));
|
|
196
195
|
const parseKeys = keyMapper(parseId);
|
|
197
|
-
// With `mapNestedKeys`, also map the keys of plain objects one level down, as
|
|
198
|
-
// in the rows knex returns with its `nestTables: true` option on MySQL:
|
|
199
|
-
// `{ tableName: { columnName: value } }`.
|
|
200
|
-
const parseRow = mapNestedKeys
|
|
201
|
-
? keyMapper(parseId, (value) => (isPlainObject(value) ? parseKeys(value) : value))
|
|
202
|
-
: parseKeys;
|
|
203
196
|
|
|
204
197
|
return {
|
|
205
198
|
wrapIdentifier(identifier, origWrap) {
|
|
@@ -211,12 +204,12 @@ function knexIdentifierMappers({ parse, format, idSeparator = ':', mapNestedKeys
|
|
|
211
204
|
const output = new Array(result.length);
|
|
212
205
|
|
|
213
206
|
for (let i = 0, l = result.length; i < l; ++i) {
|
|
214
|
-
output[i] =
|
|
207
|
+
output[i] = parseKeys(result[i]);
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
return output;
|
|
218
211
|
} else {
|
|
219
|
-
return
|
|
212
|
+
return parseKeys(result);
|
|
220
213
|
}
|
|
221
214
|
},
|
|
222
215
|
};
|
|
@@ -226,11 +219,10 @@ function knexSnakeCaseMappers(opt = {}) {
|
|
|
226
219
|
return knexIdentifierMappers({
|
|
227
220
|
parse: (str) => camelCase(str, opt),
|
|
228
221
|
format: (str) => snakeCase(str, opt),
|
|
229
|
-
mapNestedKeys: opt.mapNestedKeys,
|
|
230
222
|
});
|
|
231
223
|
}
|
|
232
224
|
|
|
233
|
-
function knexIdentifierMapping(colToProp
|
|
225
|
+
function knexIdentifierMapping(colToProp) {
|
|
234
226
|
const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
|
|
235
227
|
propToCol[colToProp[column]] = column;
|
|
236
228
|
return propToCol;
|
|
@@ -239,7 +231,6 @@ function knexIdentifierMapping(colToProp, { mapNestedKeys } = {}) {
|
|
|
239
231
|
return knexIdentifierMappers({
|
|
240
232
|
parse: (column) => colToProp[column] || column,
|
|
241
233
|
format: (prop) => propToCol[prop] || prop,
|
|
242
|
-
mapNestedKeys,
|
|
243
234
|
});
|
|
244
235
|
}
|
|
245
236
|
|
package/lib/utils/knexUtils.js
CHANGED
|
@@ -36,6 +36,19 @@ function isMsSql(knex) {
|
|
|
36
36
|
return getDialect(knex) === 'mssql';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// MySQL supports window functions since 8.0, MariaDB since 10.2. Without a
|
|
40
|
+
// version, knex assumes the latest one, and so do we.
|
|
41
|
+
function hasWindowFunctions(knex) {
|
|
42
|
+
const { version, isMariaDB } = knex.client;
|
|
43
|
+
|
|
44
|
+
if (isMySql(knex) && version) {
|
|
45
|
+
const [major, minor] = version.split('.').map(Number);
|
|
46
|
+
return isMariaDB ? major > 10 || (major === 10 && minor >= 2) : major >= 8;
|
|
47
|
+
} else {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
function isKnexQueryBuilder(value) {
|
|
40
53
|
return (
|
|
41
54
|
hasConstructor(value) &&
|
|
@@ -69,6 +82,7 @@ module.exports = {
|
|
|
69
82
|
isSqlite,
|
|
70
83
|
isMsSql,
|
|
71
84
|
isOracle,
|
|
85
|
+
hasWindowFunctions,
|
|
72
86
|
isKnexQueryBuilder,
|
|
73
87
|
isKnexJoinBuilder,
|
|
74
88
|
isKnexRaw,
|
package/lib/utils/objectUtils.js
CHANGED
|
@@ -388,11 +388,43 @@ function mergeMaps(map1, map2) {
|
|
|
388
388
|
return map;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
391
|
+
// Returns the tag of `value` like `Object.prototype.toString()`, but ignores a
|
|
392
|
+
// `Symbol.toStringTag` that can be overwritten, like lodash's `baseGetTag()`.
|
|
393
|
+
// Objects that define a writable tag, like decimal.js numbers, are plain
|
|
394
|
+
// objects then, while built-ins like `URL` keep their own tag.
|
|
395
|
+
function getRawTag(value) {
|
|
396
|
+
const tag = Object.prototype.toString.call(value);
|
|
397
|
+
|
|
398
|
+
if (!(Symbol.toStringTag in value)) {
|
|
399
|
+
return tag;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const isOwn = Object.prototype.hasOwnProperty.call(value, Symbol.toStringTag);
|
|
403
|
+
const ownTag = value[Symbol.toStringTag];
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
value[Symbol.toStringTag] = undefined;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
return tag;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const rawTag = Object.prototype.toString.call(value);
|
|
412
|
+
|
|
413
|
+
if (isOwn) {
|
|
414
|
+
value[Symbol.toStringTag] = ownTag;
|
|
415
|
+
} else {
|
|
416
|
+
delete value[Symbol.toStringTag];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return rawTag;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Clones `value` deeply, like lodash's `cloneDeep()`: plain objects and class
|
|
423
|
+
// instances keep their prototype and get copies of their own enumerable
|
|
424
|
+
// properties, symbols included. Arrays, dates, regular expressions, maps,
|
|
425
|
+
// sets, buffers, typed arrays and array buffers are copied, circular
|
|
426
|
+
// references are kept. Anything else, like functions, errors and built-ins
|
|
427
|
+
// with internal state like `URL`, is kept as it is.
|
|
396
428
|
function cloneDeep(value, clones = new Map()) {
|
|
397
429
|
if (!isObject(value)) {
|
|
398
430
|
return value;
|
|
@@ -401,7 +433,7 @@ function cloneDeep(value, clones = new Map()) {
|
|
|
401
433
|
}
|
|
402
434
|
|
|
403
435
|
const Ctor = value.constructor;
|
|
404
|
-
const tag =
|
|
436
|
+
const tag = getRawTag(value);
|
|
405
437
|
let copy;
|
|
406
438
|
|
|
407
439
|
if (tag === '[object Object]') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const jsonFieldExpressionParser = require('../queryBuilder/parsers/jsonFieldExpressionParser');
|
|
4
|
+
const { isObject } = require('./objectUtils');
|
|
4
5
|
const cache = new Map();
|
|
5
6
|
|
|
6
7
|
function parseFieldExpression(expr) {
|
|
@@ -46,6 +47,16 @@ function freezeParsedExpr(parsedExpr) {
|
|
|
46
47
|
return parsedExpr;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Keys with a ':' in the data of patches and updates are field expressions like
|
|
51
|
+
// `'meta:a.b'` that write into json columns, unless they are `declared`: the
|
|
52
|
+
// `properties` of the schema, or an object with their column names as keys.
|
|
53
|
+
function isFieldExpressionKey(key, declared) {
|
|
54
|
+
return (
|
|
55
|
+
key.includes(':') &&
|
|
56
|
+
!(isObject(declared) && Object.prototype.hasOwnProperty.call(declared, key))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
// Formats the json path of a parsed field expression as a Postgres text array
|
|
50
61
|
// literal, e.g. `{a,"",b}`. Keys that would otherwise be misread (empty keys,
|
|
51
62
|
// keys with whitespace, delimiters, quotes or backslashes and `NULL`) are
|
|
@@ -66,14 +77,22 @@ function toPostgresJsonPath(access) {
|
|
|
66
77
|
// `whereIn(ref(...), values)`), so the keys need to be escaped instead: single
|
|
67
78
|
// quotes are doubled, and `?` is escaped so that knex doesn't read it as a
|
|
68
79
|
// binding placeholder. Backslashes are always doubled by `toPostgresJsonPath()`,
|
|
69
|
-
// so they can't escape the closing quote either.
|
|
80
|
+
// so they can't escape the closing quote either. But knex's Postgres dialect
|
|
81
|
+
// drops all backslashes in front of an escaped `?`, so if the path contains any,
|
|
82
|
+
// an escape string literal is used in which backslashes are written as `\134`.
|
|
70
83
|
function toPostgresJsonPathLiteral(access) {
|
|
71
|
-
const path = toPostgresJsonPath(access).replace(/'/g, "''")
|
|
72
|
-
|
|
84
|
+
const path = toPostgresJsonPath(access).replace(/'/g, "''");
|
|
85
|
+
|
|
86
|
+
if (path.includes('\\?')) {
|
|
87
|
+
return `E'${path.replace(/\\/g, '\\134').replace(/\?/g, '\\?')}'`;
|
|
88
|
+
} else {
|
|
89
|
+
return `'${path.replace(/\?/g, '\\?')}'`;
|
|
90
|
+
}
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
module.exports = {
|
|
76
94
|
parseFieldExpression,
|
|
95
|
+
isFieldExpressionKey,
|
|
77
96
|
toPostgresJsonPath,
|
|
78
97
|
toPostgresJsonPathLiteral,
|
|
79
98
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LOGGED_WARNINGS = new Set();
|
|
4
|
+
|
|
5
|
+
// Log each warning only once.
|
|
6
|
+
function warnOnce(message) {
|
|
7
|
+
if (!LOGGED_WARNINGS.has(message)) {
|
|
8
|
+
LOGGED_WARNINGS.add(message);
|
|
9
|
+
console.warn(`[objection] ${message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function deprecate(message) {
|
|
14
|
+
warnOnce(message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Only meant for tests that need to see a warning again.
|
|
18
|
+
function resetWarnings() {
|
|
19
|
+
LOGGED_WARNINGS.clear();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
warnOnce,
|
|
24
|
+
deprecate,
|
|
25
|
+
resetWarnings,
|
|
26
|
+
};
|