objection 3.8.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/model/AjvValidator.js +22 -6
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -16
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +78 -6
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +9 -2
- package/lib/model/modelUtils.js +1 -1
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +17 -7
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
- package/lib/queryBuilder/graph/GraphOptions.js +66 -12
- package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
- package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +3 -2
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
- package/lib/queryBuilder/operations/InsertOperation.js +4 -2
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
- package/lib/queryBuilder/operations/select/Selection.js +25 -3
- package/lib/relations/RelationFindOperation.js +7 -4
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
- package/lib/utils/knexUtils.js +7 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +17 -5
- package/lib/utils/promiseUtils/map.js +24 -7
- package/lib/utils/wrapDbError.js +14 -0
- package/package.json +1 -1
- package/typings/objection/index.d.ts +21 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { hasBatchInsert } = require('../../../utils/knexUtils');
|
|
3
4
|
const { RelationInsertOperation } = require('../../RelationInsertOperation');
|
|
4
5
|
const { ManyToManyRelateOperation } = require('../relate/ManyToManyRelateOperation');
|
|
5
6
|
|
|
@@ -19,13 +20,6 @@ class ManyToManyInsertOperation extends RelationInsertOperation {
|
|
|
19
20
|
async onAfter1(builder, ret) {
|
|
20
21
|
const inserted = await super.onAfter1(builder, ret);
|
|
21
22
|
|
|
22
|
-
const relateOp = new ManyToManyRelateOperation('relate', {
|
|
23
|
-
dontCopyReturning: true,
|
|
24
|
-
dontCopyOnConflict: true,
|
|
25
|
-
relation: this.relation,
|
|
26
|
-
owner: this.owner,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
23
|
const modelsToRelate = inserted.filter((it) => {
|
|
30
24
|
return this.relation.relatedProp.hasProps(it);
|
|
31
25
|
});
|
|
@@ -36,13 +30,26 @@ class ManyToManyInsertOperation extends RelationInsertOperation {
|
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
if (modelsToRelate.length
|
|
40
|
-
|
|
33
|
+
if (modelsToRelate.length > 0) {
|
|
34
|
+
// Without batch inserts, `insertGraph()` inserts the related models one at
|
|
35
|
+
// a time, and so we need to do the same with their join rows:
|
|
36
|
+
const batches = hasBatchInsert(builder.knex())
|
|
37
|
+
? [modelsToRelate]
|
|
38
|
+
: modelsToRelate.map((model) => [model]);
|
|
39
|
+
|
|
40
|
+
for (const batch of batches) {
|
|
41
|
+
const relateOp = new ManyToManyRelateOperation('relate', {
|
|
42
|
+
dontCopyReturning: true,
|
|
43
|
+
dontCopyOnConflict: true,
|
|
44
|
+
relation: this.relation,
|
|
45
|
+
owner: this.owner,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
relateOp.onAdd(builder, [batch]);
|
|
49
|
+
await relateOp.queryExecutor(builder);
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
relateOp.onAdd(builder, [modelsToRelate]);
|
|
44
|
-
await relateOp.queryExecutor(builder);
|
|
45
|
-
|
|
46
53
|
return inserted;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
@@ -3,24 +3,9 @@
|
|
|
3
3
|
const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
|
|
4
4
|
const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
|
|
5
5
|
|
|
6
|
-
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
7
|
-
|
|
8
6
|
class ManyToManyUpdateMySqlOperation extends ManyToManyMySqlModifyMixin(
|
|
9
7
|
ManyToManyUpdateOperationBase,
|
|
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
|
-
}
|
|
8
|
+
) {}
|
|
24
9
|
|
|
25
10
|
module.exports = {
|
|
26
11
|
ManyToManyUpdateMySqlOperation,
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
const { UpdateOperation } = require('../../../queryBuilder/operations/UpdateOperation');
|
|
4
4
|
const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
|
|
5
5
|
const { chunk } = require('../../../utils/objectUtils');
|
|
6
|
+
const { isMySql, isMsSql } = require('../../../utils/knexUtils');
|
|
6
7
|
|
|
7
8
|
// Stay below the limits of bound values per statement of SQLite before 3.32
|
|
8
9
|
// (999) and SQL Server (2100), leaving room for the patched values:
|
|
9
10
|
const maxBatchValues = 500;
|
|
10
11
|
|
|
12
|
+
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
13
|
+
// Without underscores, so that column name mappers don't change it.
|
|
14
|
+
const relatedPropColumn = 'objectiontmprelated';
|
|
15
|
+
|
|
11
16
|
class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
12
17
|
constructor(name, opt) {
|
|
13
18
|
super(name, opt);
|
|
@@ -59,9 +64,21 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
59
64
|
|
|
60
65
|
onBuild(builder) {
|
|
61
66
|
if (this.modifiesJoinTable && this.modifyFilterSubquery) {
|
|
67
|
+
// MySQL and SQL Server are the only databases that support a limit in
|
|
68
|
+
// updates. As the related rows are updated by their ids, the limit and
|
|
69
|
+
// the order pick these ids instead of limiting each batch of them.
|
|
70
|
+
// Without a limit or an offset, the order picks nothing and the query
|
|
71
|
+
// stays as it is.
|
|
72
|
+
const knex = builder.knex();
|
|
73
|
+
const hasLimit = (isMySql(knex) || isMsSql(knex)) && builder.has(/^(limit|offset)$/);
|
|
74
|
+
|
|
62
75
|
// The related rows to update are identified by their ids first, see
|
|
63
76
|
// `onAfterBuild()`.
|
|
64
|
-
this.relatedIdsQuery = this.createRelatedIdsQuery(builder);
|
|
77
|
+
this.relatedIdsQuery = this.createRelatedIdsQuery(builder, hasLimit);
|
|
78
|
+
if (hasLimit) {
|
|
79
|
+
this.relatedIdsQuery.copyFrom(builder, LimitSelector);
|
|
80
|
+
builder.clear(LimitSelector);
|
|
81
|
+
}
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
return super.onBuild(builder);
|
|
@@ -69,38 +86,78 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
69
86
|
|
|
70
87
|
// Selects the ids of the related rows to update. Fetch raw rows, so that no
|
|
71
88
|
// hooks run for them.
|
|
72
|
-
createRelatedIdsQuery(builder) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
createRelatedIdsQuery(builder, hasLimit) {
|
|
90
|
+
const { relatedModelClass, relatedProp } = this.relation;
|
|
91
|
+
|
|
92
|
+
if (hasLimit) {
|
|
93
|
+
// The limit picks related rows, not join rows, so filter the related
|
|
94
|
+
// table by the ids of the matching join rows. The join rows refer to the
|
|
95
|
+
// related rows by their `relatedProp`, which isn't necessarily their id
|
|
96
|
+
// (#360). We select it after the ids, aliased as it can be the same
|
|
97
|
+
// columns:
|
|
98
|
+
const relatedIdsQuery = relatedModelClass.query().childQueryOf(builder);
|
|
99
|
+
const idRefs = relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
100
|
+
const relatedPropRefs = relatedProp
|
|
101
|
+
.refs(relatedIdsQuery)
|
|
102
|
+
.map((ref, index) => ref.as(`${relatedPropColumn}${index}`));
|
|
103
|
+
return this.applyModifyFilterForRelatedTable(relatedIdsQuery).select([
|
|
104
|
+
...idRefs,
|
|
105
|
+
...relatedPropRefs,
|
|
106
|
+
]);
|
|
107
|
+
} else {
|
|
108
|
+
// Select the ids from the filter subquery itself. Filtering the related
|
|
109
|
+
// table by them again lets Postgres pick plans that run the subquery
|
|
110
|
+
// once per related row, which takes minutes for a few thousand (#161).
|
|
111
|
+
const relatedIdsQuery = this.modifyFilterSubquery.clone();
|
|
112
|
+
const idRefs = relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
113
|
+
return relatedIdsQuery.distinct(idRefs);
|
|
114
|
+
}
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
// Update the join rows before the related rows, while the filters still
|
|
80
118
|
// match them (#161). A query resolved without updating anything, e.g. with
|
|
81
119
|
// `none()`, leaves the join rows untouched too.
|
|
82
120
|
async onAfterBuild(builder) {
|
|
121
|
+
// An offset that `onBuild()` moved to the ids query is honoured, and
|
|
122
|
+
// doesn't warn.
|
|
123
|
+
super.onAfterBuild(builder);
|
|
83
124
|
if (this.hasExtraProps && !this.isResolvedWithoutUpdate(builder)) {
|
|
125
|
+
let relatedValues = null;
|
|
84
126
|
if (this.relatedIdsQuery) {
|
|
85
127
|
// The filters can also reference the extras that the join rows change,
|
|
86
128
|
// so identify the related rows to modify first.
|
|
87
129
|
const rows = await this.relatedIdsQuery.toKnexQuery();
|
|
88
|
-
|
|
130
|
+
const values = rows.map((row) => Object.values(row));
|
|
131
|
+
const { size } = this.relation.relatedModelClass.getIdRelationProperty();
|
|
132
|
+
this.relatedIds = values.map((row) => row.slice(0, size));
|
|
133
|
+
if (this.relatedIdsQuery.has(/^(limit|offset)$/)) {
|
|
134
|
+
relatedValues = values.map((row) => row.slice(size));
|
|
135
|
+
}
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
|
|
138
|
+
let numJoinRowsUpdated;
|
|
139
|
+
if (relatedValues) {
|
|
92
140
|
// 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
|
-
|
|
95
|
-
|
|
141
|
+
// picks, so only update the join rows of the picked ones, by the
|
|
142
|
+
// `relatedProp` values that follow their ids.
|
|
143
|
+
for (const values of chunkKeys(relatedValues, this.relation.relatedProp.size)) {
|
|
144
|
+
await this.updateJoinRows(builder, values);
|
|
96
145
|
}
|
|
97
146
|
} else {
|
|
98
|
-
await this.updateJoinRows(builder);
|
|
147
|
+
numJoinRowsUpdated = await this.updateJoinRows(builder);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.hasNothingToUpdate) {
|
|
151
|
+
// Only the join rows change, so count the related rows that they
|
|
152
|
+
// belong to, as the update of the related rows does. Without their
|
|
153
|
+
// ids, the join rows all belong to the updated model, see
|
|
154
|
+
// `applyIdFilterForJoinTable()`.
|
|
155
|
+
builder.resolve(this.relatedIds?.length ?? Math.min(numJoinRowsUpdated, 1));
|
|
99
156
|
}
|
|
100
157
|
}
|
|
101
158
|
}
|
|
102
159
|
|
|
103
|
-
updateJoinRows(builder,
|
|
160
|
+
updateJoinRows(builder, relatedValues = null) {
|
|
104
161
|
const joinTableUpdateQuery = this.relation
|
|
105
162
|
.getJoinModelClass(builder.knex())
|
|
106
163
|
.query()
|
|
@@ -112,11 +169,11 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
112
169
|
? this.applyIdFilterForJoinTable(joinTableUpdateQuery)
|
|
113
170
|
: this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
|
|
114
171
|
);
|
|
115
|
-
if (
|
|
172
|
+
if (relatedValues) {
|
|
116
173
|
addWhereGroup(joinTableUpdateQuery, () =>
|
|
117
174
|
joinTableUpdateQuery.whereInComposite(
|
|
118
175
|
this.relation.joinTableRelatedProp.refs(joinTableUpdateQuery),
|
|
119
|
-
|
|
176
|
+
relatedValues,
|
|
120
177
|
),
|
|
121
178
|
);
|
|
122
179
|
}
|
|
@@ -131,7 +188,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
131
188
|
// number of values in one statement.
|
|
132
189
|
queryBatches(builder) {
|
|
133
190
|
if (this.relatedIds) {
|
|
134
|
-
const batches =
|
|
191
|
+
const batches = chunkKeys(
|
|
192
|
+
this.relatedIds,
|
|
193
|
+
this.relation.relatedModelClass.getIdRelationProperty().size,
|
|
194
|
+
);
|
|
135
195
|
// Without ids, still execute the query once for the dialect's result:
|
|
136
196
|
return batches.length > 0 ? batches : [[]];
|
|
137
197
|
} else {
|
|
@@ -139,11 +199,6 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
139
199
|
}
|
|
140
200
|
}
|
|
141
201
|
|
|
142
|
-
relatedIdBatches() {
|
|
143
|
-
const idSize = this.relation.relatedModelClass.getIdRelationProperty().size;
|
|
144
|
-
return chunk(this.relatedIds, Math.floor(maxBatchValues / idSize));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
202
|
// Filters the join rows by the ids of the owner and the updated model.
|
|
148
203
|
applyIdFilterForJoinTable(builder) {
|
|
149
204
|
return builder
|
|
@@ -180,6 +235,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
180
235
|
}
|
|
181
236
|
}
|
|
182
237
|
|
|
238
|
+
function chunkKeys(keys, size) {
|
|
239
|
+
return chunk(keys, Math.floor(maxBatchValues / size));
|
|
240
|
+
}
|
|
241
|
+
|
|
183
242
|
module.exports = {
|
|
184
243
|
ManyToManyUpdateOperationBase,
|
|
185
244
|
};
|
package/lib/utils/knexUtils.js
CHANGED
|
@@ -36,6 +36,12 @@ function isMsSql(knex) {
|
|
|
36
36
|
return getDialect(knex) === 'mssql';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Only Postgres and SQL Server return the identifiers of all the rows inserted
|
|
40
|
+
// by one query.
|
|
41
|
+
function hasBatchInsert(knex) {
|
|
42
|
+
return isPostgres(knex) || isMsSql(knex);
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
// MySQL supports window functions since 8.0, MariaDB since 10.2. Without a
|
|
40
46
|
// version, knex assumes the latest one, and so do we.
|
|
41
47
|
function hasWindowFunctions(knex) {
|
|
@@ -82,6 +88,7 @@ module.exports = {
|
|
|
82
88
|
isSqlite,
|
|
83
89
|
isMsSql,
|
|
84
90
|
isOracle,
|
|
91
|
+
hasBatchInsert,
|
|
85
92
|
hasWindowFunctions,
|
|
86
93
|
isKnexQueryBuilder,
|
|
87
94
|
isKnexJoinBuilder,
|
package/lib/utils/objectUtils.js
CHANGED
|
@@ -16,6 +16,10 @@ function isObject(value) {
|
|
|
16
16
|
return value !== null && typeof value === 'object';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function hasOwn(obj, prop) {
|
|
20
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
// Quick and dirty check if an object is a plain object and not
|
|
20
24
|
// for example an instance of some class.
|
|
21
25
|
function isPlainObject(value) {
|
|
@@ -502,6 +506,7 @@ module.exports = {
|
|
|
502
506
|
isString,
|
|
503
507
|
isRegExp,
|
|
504
508
|
isObject,
|
|
509
|
+
hasOwn,
|
|
505
510
|
isNumber,
|
|
506
511
|
isFunction,
|
|
507
512
|
jsonEquals,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const jsonFieldExpressionParser = require('../queryBuilder/parsers/jsonFieldExpressionParser');
|
|
4
|
-
const { isObject } = require('./objectUtils');
|
|
4
|
+
const { isObject, hasOwn } = require('./objectUtils');
|
|
5
5
|
const cache = new Map();
|
|
6
6
|
|
|
7
7
|
function parseFieldExpression(expr) {
|
|
@@ -51,10 +51,21 @@ function freezeParsedExpr(parsedExpr) {
|
|
|
51
51
|
// `'meta:a.b'` that write into json columns, unless they are `declared`: the
|
|
52
52
|
// `properties` of the schema, or an object with their column names as keys.
|
|
53
53
|
function isFieldExpressionKey(key, declared) {
|
|
54
|
-
return (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
return key.includes(':') && !(isObject(declared) && hasOwn(declared, key));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Returns the json column of a field expression key like `'meta:a'`, or `null`
|
|
58
|
+
// if the key isn't a field expression or doesn't parse as one.
|
|
59
|
+
function getFieldExpressionColumn(key, declared) {
|
|
60
|
+
if (isFieldExpressionKey(key, declared)) {
|
|
61
|
+
try {
|
|
62
|
+
return parseFieldExpression(key).column;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
// Formats the json path of a parsed field expression as a Postgres text array
|
|
@@ -93,6 +104,7 @@ function toPostgresJsonPathLiteral(access) {
|
|
|
93
104
|
module.exports = {
|
|
94
105
|
parseFieldExpression,
|
|
95
106
|
isFieldExpressionKey,
|
|
107
|
+
getFieldExpressionColumn,
|
|
96
108
|
toPostgresJsonPath,
|
|
97
109
|
toPostgresJsonPathLiteral,
|
|
98
110
|
};
|
|
@@ -43,7 +43,8 @@ function mapMany(items, mapper, opt = {}) {
|
|
|
43
43
|
const ctx = {
|
|
44
44
|
reject,
|
|
45
45
|
resolve,
|
|
46
|
-
|
|
46
|
+
failed: false,
|
|
47
|
+
error: null,
|
|
47
48
|
index: 0,
|
|
48
49
|
numFinished: 0,
|
|
49
50
|
results: new Array(items.length),
|
|
@@ -51,7 +52,7 @@ function mapMany(items, mapper, opt = {}) {
|
|
|
51
52
|
mapper,
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
while (ctx.index < concurrency && ctx.index < items.length && !ctx.
|
|
55
|
+
while (ctx.index < concurrency && ctx.index < items.length && !ctx.failed) {
|
|
55
56
|
executeNext(ctx);
|
|
56
57
|
}
|
|
57
58
|
});
|
|
@@ -59,7 +60,7 @@ function mapMany(items, mapper, opt = {}) {
|
|
|
59
60
|
|
|
60
61
|
function executeNext(ctx) {
|
|
61
62
|
try {
|
|
62
|
-
if (ctx.
|
|
63
|
+
if (ctx.failed) {
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -82,12 +83,14 @@ function executeNext(ctx) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function afterExecute(ctx, result, index) {
|
|
85
|
-
|
|
86
|
+
ctx.numFinished++;
|
|
87
|
+
|
|
88
|
+
if (ctx.failed) {
|
|
89
|
+
rejectWhenFinished(ctx);
|
|
86
90
|
return null;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
ctx.results[index] = result;
|
|
90
|
-
ctx.numFinished++;
|
|
91
94
|
|
|
92
95
|
if (ctx.numFinished === ctx.items.length) {
|
|
93
96
|
ctx.resolve(ctx.results);
|
|
@@ -101,8 +104,22 @@ function afterExecute(ctx, result, index) {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
function onError(ctx, err) {
|
|
104
|
-
ctx.
|
|
105
|
-
|
|
107
|
+
ctx.numFinished++;
|
|
108
|
+
|
|
109
|
+
if (!ctx.failed) {
|
|
110
|
+
ctx.failed = true;
|
|
111
|
+
ctx.error = err;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rejectWhenFinished(ctx);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Wait for all started mappers before rejecting: They may run queries in a
|
|
118
|
+
// transaction that the caller rolls back on rejection.
|
|
119
|
+
function rejectWhenFinished(ctx) {
|
|
120
|
+
if (ctx.numFinished === ctx.index) {
|
|
121
|
+
ctx.reject(ctx.error);
|
|
122
|
+
}
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
module.exports = {
|
package/lib/utils/wrapDbError.js
CHANGED
|
@@ -8,6 +8,15 @@ const MSSQL_PRIMARY_KEY_REGEX =
|
|
|
8
8
|
/Violation of PRIMARY KEY constraint '(.+)'\. Cannot insert duplicate key in object '(.+)\.(.+)'\. The duplicate key value is \((.+)\)/;
|
|
9
9
|
|
|
10
10
|
function wrapDbError(err) {
|
|
11
|
+
// tedious throws an AggregateError with an empty message when SQL Server
|
|
12
|
+
// reports several errors for one statement, and knex only prepends the SQL to
|
|
13
|
+
// it. Add the messages of its errors, to give the reasons.
|
|
14
|
+
if (isTediousAggregateError(err)) {
|
|
15
|
+
const wrapped = new DBError({ nativeError: err, client: 'mssql' });
|
|
16
|
+
wrapped.message += err.errors.map((error) => error.message).join(' ');
|
|
17
|
+
return wrapped;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
if (!isTediousError(err)) {
|
|
12
21
|
return wrapError(err);
|
|
13
22
|
}
|
|
@@ -45,6 +54,11 @@ function isTediousError(err) {
|
|
|
45
54
|
return err?.code === 'EREQUEST' && Number.isInteger(err.number);
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
// Check the name, as AggregateError isn't defined on Node 14.
|
|
58
|
+
function isTediousAggregateError(err) {
|
|
59
|
+
return err?.name === 'AggregateError' && err.errors.every(isTediousError);
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
module.exports = {
|
|
49
63
|
wrapDbError,
|
|
50
64
|
};
|
package/package.json
CHANGED
|
@@ -374,20 +374,30 @@ declare namespace Objection {
|
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
376
|
* Converts the query builder NQB to the same result kind as QB:
|
|
377
|
-
* array,
|
|
377
|
+
* array, page, single or maybe-single. Any other result type, e.g. after
|
|
378
378
|
* `castTo()` or `patch()`, falls back to QB, without narrowing.
|
|
379
|
+
* QB's model type M and result type R are separate parameters so that QB
|
|
380
|
+
* only appears in the result, and TypeScript can relate the kinds of two
|
|
381
|
+
* query builders, e.g. of a custom query builder and its base class, through
|
|
382
|
+
* QB alone. The page and `object` checks come before the single ones, so
|
|
383
|
+
* that results like `number` or `Page<M>` are ruled out for a generic M too.
|
|
379
384
|
*/
|
|
380
|
-
type WithResultKind<
|
|
381
|
-
|
|
382
|
-
|
|
385
|
+
type WithResultKind<
|
|
386
|
+
QB extends AnyQueryBuilder,
|
|
387
|
+
NQB extends AnyQueryBuilder,
|
|
388
|
+
M extends Model = ModelType<QB>,
|
|
389
|
+
R = ResultType<QB>,
|
|
390
|
+
> = [R] extends [M[]]
|
|
383
391
|
? ArrayQueryBuilder<NQB>
|
|
384
|
-
: [
|
|
385
|
-
?
|
|
386
|
-
: [
|
|
387
|
-
?
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
: [R] extends [Page<M>]
|
|
393
|
+
? PageQueryBuilder<NQB>
|
|
394
|
+
: [R] extends [object | undefined]
|
|
395
|
+
? [R] extends [M]
|
|
396
|
+
? SingleQueryBuilder<NQB>
|
|
397
|
+
: [R] extends [M | undefined]
|
|
398
|
+
? MaybeSingleQueryBuilder<NQB>
|
|
399
|
+
: QB
|
|
400
|
+
: QB;
|
|
391
401
|
|
|
392
402
|
/**
|
|
393
403
|
* If T is an array, returns the item type, otherwise returns T.
|