objection 3.9.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/graph/ModelGraphNode.js +0 -15
- package/lib/model/modelQueryProps.js +35 -7
- package/lib/model/modelToJson.js +3 -11
- package/lib/model/modelUtils.js +1 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +2 -4
- package/lib/queryBuilder/graph/GraphUpsert.js +9 -5
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
- package/lib/queryBuilder/graph/insert/GraphInsert.js +60 -21
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +13 -1
- package/lib/queryBuilder/operations/DeleteOperation.js +9 -2
- package/lib/queryBuilder/operations/InsertOperation.js +2 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +14 -0
- package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +40 -17
- package/lib/utils/parseFieldExpression.js +15 -0
- package/lib/utils/promiseUtils/map.js +24 -7
- package/lib/utils/wrapDbError.js +14 -0
- package/package.json +1 -1
|
@@ -143,21 +143,6 @@ class ModelGraphNode {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* Returns all nodes that are references to this node.
|
|
148
|
-
*/
|
|
149
|
-
get referencingNodes() {
|
|
150
|
-
const nodes = [];
|
|
151
|
-
|
|
152
|
-
for (const edge of this.refEdges) {
|
|
153
|
-
if (edge.refType === ModelGraphEdge.ReferenceType.Object && edge.isRelatedNode(this)) {
|
|
154
|
-
nodes.push(edge.ownerNode);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return nodes;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
146
|
get descendantRelationNodes() {
|
|
162
147
|
return this._collectDescendantRelationNodes([]);
|
|
163
148
|
}
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
const { isObject, isFunction, omit } = require('../utils/objectUtils');
|
|
4
4
|
const { defineNonEnumerableProperty } = require('./modelUtils');
|
|
5
5
|
const { isKnexRaw, isKnexQueryBuilder } = require('../utils/knexUtils');
|
|
6
|
-
const {
|
|
6
|
+
const { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
|
|
7
7
|
|
|
8
8
|
const QUERY_PROPS_PROPERTY = '$$queryProps';
|
|
9
|
+
// The keys of the field expression query properties like `'data:a'` that were
|
|
10
|
+
// given while their json column `data` had a value:
|
|
11
|
+
const COMBINED_QUERY_PROPS_PROPERTY = '$$combinedQueryProps';
|
|
9
12
|
|
|
10
13
|
// Moves the query properties of `json` into a hidden property of `model` so
|
|
11
14
|
// that they can be later merged back to its database json. A key holds either
|
|
@@ -17,13 +20,24 @@ function splitQueryProps(model, json) {
|
|
|
17
20
|
|
|
18
21
|
if (!json.$isObjectionModel && hasQueryProps(json, keys)) {
|
|
19
22
|
const queryProps = Object.assign({}, oldQueryProps);
|
|
23
|
+
const combinedKeys = new Set(model[COMBINED_QUERY_PROPS_PROPERTY]);
|
|
20
24
|
const modelProps = {};
|
|
25
|
+
const jsonSchema = model.constructor.getJsonSchema();
|
|
26
|
+
const properties = jsonSchema && jsonSchema.properties;
|
|
21
27
|
|
|
22
28
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
23
29
|
const key = keys[i];
|
|
24
30
|
const value = json[key];
|
|
25
31
|
|
|
26
32
|
if (isQueryProp(value)) {
|
|
33
|
+
const column = getFieldExpressionColumn(key, properties);
|
|
34
|
+
|
|
35
|
+
if (column !== null && (json[column] !== undefined || model[column] !== undefined)) {
|
|
36
|
+
combinedKeys.add(key);
|
|
37
|
+
} else {
|
|
38
|
+
combinedKeys.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
queryProps[key] = value;
|
|
28
42
|
delete model[key];
|
|
29
43
|
} else {
|
|
@@ -33,6 +47,7 @@ function splitQueryProps(model, json) {
|
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, queryProps);
|
|
50
|
+
defineNonEnumerableProperty(model, COMBINED_QUERY_PROPS_PROPERTY, [...combinedKeys]);
|
|
36
51
|
|
|
37
52
|
return modelProps;
|
|
38
53
|
} else {
|
|
@@ -61,13 +76,16 @@ function removeOverriddenQueryProps(model) {
|
|
|
61
76
|
if (queryProps) {
|
|
62
77
|
const jsonSchema = model.constructor.getJsonSchema();
|
|
63
78
|
const properties = jsonSchema && jsonSchema.properties;
|
|
64
|
-
|
|
65
|
-
const keys = Object.keys(queryProps).filter(
|
|
66
|
-
(key)
|
|
79
|
+
const combinedKeys = model[COMBINED_QUERY_PROPS_PROPERTY] || [];
|
|
80
|
+
const keys = Object.keys(queryProps).filter((key) => {
|
|
81
|
+
const column = getFieldExpressionColumn(key, properties);
|
|
82
|
+
return (
|
|
67
83
|
model[key] !== undefined ||
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
// The json column of a field expression key like `data:a` overrides it
|
|
85
|
+
// too, unless it was given while the column had a value (#206):
|
|
86
|
+
(column !== null && !combinedKeys.includes(key) && model[column] !== undefined)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
71
89
|
|
|
72
90
|
if (keys.length > 0) {
|
|
73
91
|
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(queryProps, keys));
|
|
@@ -75,6 +93,15 @@ function removeOverriddenQueryProps(model) {
|
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
// Removes the values that the hooks of a write set for the keys that `model`
|
|
97
|
+
// holds query properties for. The query properties are written instead, so the
|
|
98
|
+
// values must not count as given in later writes.
|
|
99
|
+
function removeOverriddenValues(model) {
|
|
100
|
+
for (const key of getQueryPropKeys(model)) {
|
|
101
|
+
delete model[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
function hasQueryProps(json, keys) {
|
|
79
106
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
80
107
|
if (isQueryProp(json[keys[i]])) {
|
|
@@ -192,6 +219,7 @@ module.exports = {
|
|
|
192
219
|
splitQueryProps,
|
|
193
220
|
omitQueryProps,
|
|
194
221
|
removeOverriddenQueryProps,
|
|
222
|
+
removeOverriddenValues,
|
|
195
223
|
mergeQueryProps,
|
|
196
224
|
getQueryPropKeys,
|
|
197
225
|
isQueryProp,
|
package/lib/model/modelToJson.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { isInternalProp } = require('../utils/internalPropUtils');
|
|
4
4
|
const { mergeQueryProps } = require('./modelQueryProps');
|
|
5
5
|
const { isObject, hasOwn, cloneDeep, isFunction } = require('../utils/objectUtils');
|
|
6
|
-
const {
|
|
6
|
+
const { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
|
|
7
7
|
const EMPTY_ARRAY = [];
|
|
8
8
|
|
|
9
9
|
function toJson(model, optIn) {
|
|
@@ -140,16 +140,8 @@ function shouldPick(opt, key) {
|
|
|
140
140
|
|
|
141
141
|
// Pick field expressions like `'meta:a'` by their json column.
|
|
142
142
|
function isPickedFieldExpression(pick, key) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return hasOwn(pick, parseFieldExpression(key).column);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
// Not a valid field expression.
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
143
|
+
const column = getFieldExpressionColumn(key, pick);
|
|
144
|
+
return column !== null && hasOwn(pick, column);
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
function assignVirtualAttributes(json, model, vAttr, opt) {
|
package/lib/model/modelUtils.js
CHANGED
|
@@ -5,7 +5,7 @@ const { ModelGraph } = require('../../model/graph/ModelGraph');
|
|
|
5
5
|
const { FetchStrategy } = require('./GraphOptions');
|
|
6
6
|
const { RelationExpression } = require('../RelationExpression');
|
|
7
7
|
const { getDeclaredColumns } = require('../operations/UpdateOperation');
|
|
8
|
-
const {
|
|
8
|
+
const { getFieldExpressionColumn } = require('../../utils/parseFieldExpression');
|
|
9
9
|
|
|
10
10
|
class GraphFetcher {
|
|
11
11
|
/**
|
|
@@ -112,9 +112,7 @@ function createInputColumnSelector(nodes) {
|
|
|
112
112
|
|
|
113
113
|
for (const key of Object.keys(databaseJson)) {
|
|
114
114
|
// Select the json column of field expressions like `'meta:a'`:
|
|
115
|
-
const column =
|
|
116
|
-
? parseFieldExpression(key).column
|
|
117
|
-
: key;
|
|
115
|
+
const column = getFieldExpressionColumn(key, declaredColumns) ?? key;
|
|
118
116
|
|
|
119
117
|
if (!shouldSelectColumn(column, selects, node)) {
|
|
120
118
|
continue;
|
|
@@ -472,12 +472,16 @@ function executeOperations(builder) {
|
|
|
472
472
|
GraphRecursiveUpsert,
|
|
473
473
|
];
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
// Create all actions before chaining the first ones, which would run even
|
|
476
|
+
// if creating the actions of a later operation throws, e.g. for a cycle.
|
|
477
|
+
const operationActions = operations.map((Operation) =>
|
|
478
|
+
new Operation(graphData).createActions(),
|
|
479
|
+
);
|
|
478
480
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
+
return operationActions.reduce(
|
|
482
|
+
(promise, actions) => promise.then(() => executeActions(builder, actions)),
|
|
483
|
+
Promise.resolve(),
|
|
484
|
+
);
|
|
481
485
|
};
|
|
482
486
|
}
|
|
483
487
|
|
|
@@ -7,9 +7,8 @@ const promiseUtils = require('../../../utils/promiseUtils');
|
|
|
7
7
|
/**
|
|
8
8
|
* Patches in the foreign keys of `BelongsToOneRelation` edges that `GraphInsert`
|
|
9
9
|
* had to defer to break a dependency cycle. The owner nodes of these edges were
|
|
10
|
-
* inserted without the foreign keys
|
|
11
|
-
*
|
|
12
|
-
* do is one patch query per owner.
|
|
10
|
+
* inserted without the foreign keys, which are copied from the related nodes
|
|
11
|
+
* and patched in with one query per owner.
|
|
13
12
|
*/
|
|
14
13
|
class DeferredRelationGraphInsertAction extends GraphAction {
|
|
15
14
|
constructor(graphData, { edges }) {
|
|
@@ -36,11 +35,13 @@ class DeferredRelationGraphInsertAction extends GraphAction {
|
|
|
36
35
|
const ownerIdProp = modelClass.getIdRelationProperty();
|
|
37
36
|
const patch = {};
|
|
38
37
|
|
|
39
|
-
for (const { relation } of edges) {
|
|
40
|
-
const { ownerProp } = relation;
|
|
38
|
+
for (const { relation, relatedNode } of edges) {
|
|
39
|
+
const { ownerProp, relatedProp } = relation;
|
|
41
40
|
|
|
42
41
|
ownerProp.forEach((i) => {
|
|
43
|
-
|
|
42
|
+
const value = relatedProp.getProp(relatedNode.obj, i);
|
|
43
|
+
ownerProp.setProp(ownerNode.obj, i, value);
|
|
44
|
+
ownerProp.patch(patch, i, value);
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -83,10 +83,23 @@ class GraphInsert extends GraphOperation {
|
|
|
83
83
|
// new ones. We need to pass all nodes to `GraphInsertActions` so that we
|
|
84
84
|
// can resolve all dependencies.
|
|
85
85
|
const nodesToInsert = this.graph.nodes.filter((node) => {
|
|
86
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
!node.isReference && !handledNodes.has(node) && !this._hasDependencies(node, handledNodes)
|
|
88
|
+
);
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
// `#ref` nodes aren't inserted, so they can be resolved in the same
|
|
92
|
+
// action as the nodes they depend on, once these are inserted.
|
|
93
|
+
const insertedNodes = new Set([...handledNodes, ...nodesToInsert]);
|
|
94
|
+
const refNodes = this.graph.nodes.filter((node) => {
|
|
95
|
+
return (
|
|
96
|
+
node.isReference && !handledNodes.has(node) && !this._hasDependencies(node, insertedNodes)
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const nodes = [...nodesToInsert, ...refNodes];
|
|
101
|
+
|
|
102
|
+
if (nodes.length === 0) {
|
|
90
103
|
if (handledNodes.size !== this.graph.nodes.length && this._breakCycle(handledNodes)) {
|
|
91
104
|
continue;
|
|
92
105
|
}
|
|
@@ -97,13 +110,14 @@ class GraphInsert extends GraphOperation {
|
|
|
97
110
|
actions.push(
|
|
98
111
|
new GraphInsertAction(this.graphData, {
|
|
99
112
|
nodes: nodesToInsert,
|
|
113
|
+
refNodes,
|
|
100
114
|
dependencies: this.dependencies,
|
|
101
115
|
deferredEdges: this.deferredEdges,
|
|
102
116
|
}),
|
|
103
117
|
);
|
|
104
118
|
|
|
105
|
-
for (const node of
|
|
106
|
-
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
handledNodes.add(node);
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -121,7 +135,7 @@ class GraphInsert extends GraphOperation {
|
|
|
121
135
|
// `#ref` nodes, since the graph is a tree otherwise.
|
|
122
136
|
_breakCycle(handledNodes) {
|
|
123
137
|
for (const node of this.graph.nodes) {
|
|
124
|
-
if (
|
|
138
|
+
if (handledNodes.has(node)) {
|
|
125
139
|
continue;
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -200,16 +214,12 @@ class GraphInsert extends GraphOperation {
|
|
|
200
214
|
];
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
_isHandled(node, handledNodes) {
|
|
204
|
-
return handledNodes.has(node);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
217
|
_hasDependencies(node, handledNodes) {
|
|
208
218
|
return this._pendingDependencyEdges(node, handledNodes).length > 0;
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
// Returns the dependency edges of `node` whose other node still needs to be
|
|
212
|
-
//
|
|
222
|
+
// handled first. Deferred edges are ignored.
|
|
213
223
|
_pendingDependencyEdges(node, handledNodes) {
|
|
214
224
|
const edges = this.dependencies.get(node) || [];
|
|
215
225
|
|
|
@@ -219,22 +229,51 @@ class GraphInsert extends GraphOperation {
|
|
|
219
229
|
return (
|
|
220
230
|
!this.deferredEdges.has(edge) &&
|
|
221
231
|
!handledNodes.has(dependencyNode) &&
|
|
222
|
-
!this.currentGraph.nodeForNode(dependencyNode)
|
|
232
|
+
(!this.currentGraph.nodeForNode(dependencyNode) ||
|
|
233
|
+
this._readsFilledProps(edge, dependencyNode))
|
|
223
234
|
);
|
|
224
235
|
});
|
|
225
236
|
}
|
|
226
237
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// also mark those nodes as handled as there is nothing to actually
|
|
234
|
-
// insert.
|
|
235
|
-
for (const refNode of node.referencingNodes) {
|
|
236
|
-
this._markHandled(refNode, handledNodes);
|
|
238
|
+
// Returns true if the reference `edge` reads props that the graph fills into
|
|
239
|
+
// `node`. The keys of existing models are known right away, but these props
|
|
240
|
+
// only once they're filled in (#356).
|
|
241
|
+
_readsFilledProps(edge, node) {
|
|
242
|
+
if (edge.type === ModelGraphEdge.Type.Relation) {
|
|
243
|
+
return false;
|
|
237
244
|
}
|
|
245
|
+
|
|
246
|
+
const filledProps = this._getFilledProps(node);
|
|
247
|
+
// A `#ref` node stands in for `node` in its relations, which read only
|
|
248
|
+
// their keys from it:
|
|
249
|
+
const readProps =
|
|
250
|
+
edge.refType === ModelGraphEdge.ReferenceType.Property
|
|
251
|
+
? [edge.refRelatedDataPath[0]]
|
|
252
|
+
: this._getRelationProps(edge.ownerNode.edges, edge.ownerNode);
|
|
253
|
+
|
|
254
|
+
return readProps.some((prop) => filledProps.includes(prop));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Returns the props of `node` that the graph fills in from other models,
|
|
258
|
+
// through its relations and its property references.
|
|
259
|
+
_getFilledProps(node) {
|
|
260
|
+
const edges = this.dependencies.get(node) || [];
|
|
261
|
+
|
|
262
|
+
return [
|
|
263
|
+
...this._getRelationProps(edges, node),
|
|
264
|
+
...edges
|
|
265
|
+
.filter((edge) => edge.refType === ModelGraphEdge.ReferenceType.Property)
|
|
266
|
+
.map((edge) => edge.refOwnerDataPath[0]),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Returns the props of `node` that the relation edges among `edges` join on.
|
|
271
|
+
_getRelationProps(edges, node) {
|
|
272
|
+
return edges
|
|
273
|
+
.filter((edge) => edge.type === ModelGraphEdge.Type.Relation)
|
|
274
|
+
.flatMap((edge) => {
|
|
275
|
+
return (edge.isOwnerNode(node) ? edge.relation.ownerProp : edge.relation.relatedProp).props;
|
|
276
|
+
});
|
|
238
277
|
}
|
|
239
278
|
|
|
240
279
|
_createJoinRowActions() {
|
|
@@ -14,22 +14,24 @@ const promiseUtils = require('../../../utils/promiseUtils');
|
|
|
14
14
|
* when needed.
|
|
15
15
|
*/
|
|
16
16
|
class GraphInsertAction extends GraphAction {
|
|
17
|
-
constructor(graphData, { nodes, dependencies, deferredEdges }) {
|
|
17
|
+
constructor(graphData, { nodes, refNodes, dependencies, deferredEdges }) {
|
|
18
18
|
super(graphData);
|
|
19
19
|
|
|
20
|
-
// Nodes to insert.
|
|
20
|
+
// Nodes to insert, and `#ref` nodes to resolve once they are inserted.
|
|
21
21
|
this.nodes = nodes;
|
|
22
|
+
this.refNodes = refNodes;
|
|
22
23
|
this.dependencies = dependencies;
|
|
23
24
|
// Dependency edges that are resolved once the dependency node has been
|
|
24
25
|
// inserted, and are patched in by `DeferredRelationGraphInsertAction`.
|
|
25
26
|
this.deferredEdges = deferredEdges;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
run(builder) {
|
|
29
|
+
async run(builder) {
|
|
29
30
|
const batches = this._createInsertBatches(builder);
|
|
30
31
|
const concurrency = this._getConcurrency(builder, this.nodes);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
await promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
|
|
34
|
+
this._resolveDependencies(this.refNodes);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
_createInsertBatches(builder) {
|
|
@@ -49,7 +51,6 @@ class GraphInsertAction extends GraphAction {
|
|
|
49
51
|
async _insertBatch(parentBuilder, nodes) {
|
|
50
52
|
await this._beforeInsert(nodes);
|
|
51
53
|
await this._insert(parentBuilder, nodes);
|
|
52
|
-
await this._afterInsert(nodes);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
_beforeInsert(nodes) {
|
|
@@ -82,6 +83,8 @@ class GraphInsertAction extends GraphAction {
|
|
|
82
83
|
_resolveDependency(dependencyNode, edge) {
|
|
83
84
|
if (edge.type === ModelGraphEdge.Type.Relation && !edge.relation.joinTable) {
|
|
84
85
|
this._resolveRelationDependency(dependencyNode, edge);
|
|
86
|
+
} else if (edge.refType === ModelGraphEdge.ReferenceType.Object) {
|
|
87
|
+
this._resolveReferences(edge.getOtherNode(dependencyNode));
|
|
85
88
|
} else if (edge.refType === ModelGraphEdge.ReferenceType.Property) {
|
|
86
89
|
this._resolvePropertyReferenceNode(dependencyNode, edge);
|
|
87
90
|
}
|
|
@@ -219,14 +222,6 @@ class GraphInsertAction extends GraphAction {
|
|
|
219
222
|
}),
|
|
220
223
|
);
|
|
221
224
|
}
|
|
222
|
-
|
|
223
|
-
_afterInsert(nodes) {
|
|
224
|
-
for (const node of nodes) {
|
|
225
|
-
for (const refNode of node.referencingNodes) {
|
|
226
|
-
this._resolveDependency(refNode, refNode.parentEdge);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
225
|
}
|
|
231
226
|
|
|
232
227
|
function getModelClass(node) {
|
|
@@ -31,6 +31,14 @@ class GraphPatchAction extends GraphAction {
|
|
|
31
31
|
// properties. That's why we handle them here.
|
|
32
32
|
const changePropsBecauseOfBelongsToOneDelete = this._handleBelongsToOneDeletes(node);
|
|
33
33
|
|
|
34
|
+
// HasManyRelation and HasOneRelation relates write the related object's
|
|
35
|
+
// foreign keys, also with `noUpdate`.
|
|
36
|
+
const changedPropsBecauseOfRelate =
|
|
37
|
+
this.graphOptions.shouldRelate(node, this.graphData) &&
|
|
38
|
+
node.parentEdge.relation.isObjectionHasManyRelation
|
|
39
|
+
? node.parentEdge.relation.relatedProp.props
|
|
40
|
+
: [];
|
|
41
|
+
|
|
34
42
|
const handleUpdate = () => {
|
|
35
43
|
const { changedProps, unchangedProps } = this._findChanges(node);
|
|
36
44
|
const allProps = [...changedProps, ...unchangedProps];
|
|
@@ -38,7 +46,11 @@ class GraphPatchAction extends GraphAction {
|
|
|
38
46
|
const propsToUpdate = difference(
|
|
39
47
|
shouldPatch || shouldUpdate
|
|
40
48
|
? changedProps
|
|
41
|
-
: [
|
|
49
|
+
: [
|
|
50
|
+
...changedPropsBecauseOfBelongsToOneInsert,
|
|
51
|
+
...changePropsBecauseOfBelongsToOneDelete,
|
|
52
|
+
...changedPropsBecauseOfRelate,
|
|
53
|
+
],
|
|
42
54
|
|
|
43
55
|
// Remove id properties from the props to update. With upsertGraph
|
|
44
56
|
// it never makes sense to change the id.
|
|
@@ -11,9 +11,16 @@ class DeleteOperation extends QueryBuilderOperation {
|
|
|
11
11
|
return result;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// knex leaves the
|
|
15
|
-
// the order doesn't pick the deleted rows. Warn
|
|
14
|
+
// knex leaves the offset out of deletes. On MySQL, it leaves out the order as
|
|
15
|
+
// well, but keeps the limit, so the order doesn't pick the deleted rows. Warn
|
|
16
|
+
// when the query is executed.
|
|
16
17
|
onAfterBuild(builder) {
|
|
18
|
+
if (builder.has('offset')) {
|
|
19
|
+
warnOnce(
|
|
20
|
+
'offset() has no effect in delete queries, so no rows are skipped. ' +
|
|
21
|
+
'This will throw an error in objection 4.0.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
17
24
|
if (isMySql(builder.knex()) && builder.has('limit') && builder.has(/^orderBy/)) {
|
|
18
25
|
warnOnce(
|
|
19
26
|
'orderBy() has no effect in delete queries with limit() on MySQL, so the limit ' +
|
|
@@ -5,6 +5,7 @@ const { StaticHookArguments } = require('../StaticHookArguments');
|
|
|
5
5
|
const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
|
|
6
6
|
const { isSqlite, isMySql, hasBatchInsert } = require('../../utils/knexUtils');
|
|
7
7
|
const { isObject, isString } = require('../../utils/objectUtils');
|
|
8
|
+
const { removeOverriddenValues } = require('../../model/modelQueryProps');
|
|
8
9
|
|
|
9
10
|
// Base class for all insert operations.
|
|
10
11
|
class InsertOperation extends QueryBuilderOperation {
|
|
@@ -31,6 +32,7 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
31
32
|
throw new Error('batch insert only works with Postgresql and SQL Server');
|
|
32
33
|
} else {
|
|
33
34
|
await callBeforeInsert(builder, this.models);
|
|
35
|
+
this.models.forEach(removeOverriddenValues);
|
|
34
36
|
return result;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -11,6 +11,8 @@ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
|
11
11
|
const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
|
|
12
12
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
13
13
|
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
14
|
+
const { removeOverriddenValues } = require('../../model/modelQueryProps');
|
|
15
|
+
const { warnOnce } = require('../../utils/warnings');
|
|
14
16
|
|
|
15
17
|
class UpdateOperation extends QueryBuilderOperation {
|
|
16
18
|
constructor(name, opt) {
|
|
@@ -31,6 +33,7 @@ class UpdateOperation extends QueryBuilderOperation {
|
|
|
31
33
|
|
|
32
34
|
async onBefore2(builder, result) {
|
|
33
35
|
await callBeforeUpdate(builder, this.model, this.modelOptions);
|
|
36
|
+
removeOverriddenValues(this.model);
|
|
34
37
|
return result;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -52,6 +55,17 @@ class UpdateOperation extends QueryBuilderOperation {
|
|
|
52
55
|
return builder.isExplicitlyResolvedOrRejected() && !this.hasNothingToUpdate;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
// knex leaves the offset out of updates, and on SQL Server the limit as well.
|
|
59
|
+
// Warn when the query is executed.
|
|
60
|
+
onAfterBuild(builder) {
|
|
61
|
+
if (builder.has('offset')) {
|
|
62
|
+
warnOnce(
|
|
63
|
+
'offset() has no effect in update queries, so no rows are skipped, and on SQL Server ' +
|
|
64
|
+
'limit() is ignored too. This will throw an error in objection 4.0.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
onBuildKnex(knexBuilder, builder) {
|
|
56
70
|
const json = this.model.$toDatabaseJson(builder);
|
|
57
71
|
const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { RelationInsertOperation } = require('../RelationInsertOperation');
|
|
4
|
+
const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
|
|
4
5
|
|
|
5
6
|
class HasManyInsertOperation extends RelationInsertOperation {
|
|
6
7
|
onAdd(builder, args) {
|
|
@@ -14,6 +15,9 @@ class HasManyInsertOperation extends RelationInsertOperation {
|
|
|
14
15
|
for (let j = 0, lp = relatedProp.size; j < lp; ++j) {
|
|
15
16
|
relatedProp.setProp(model, j, ownerValues[0][j]);
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
// The foreign keys set here beat query properties given for them:
|
|
20
|
+
removeOverriddenQueryProps(model);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
return retVal;
|
|
@@ -10,6 +10,8 @@ const { isMySql, isMsSql } = require('../../../utils/knexUtils');
|
|
|
10
10
|
const maxBatchValues = 500;
|
|
11
11
|
|
|
12
12
|
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
13
|
+
// Without underscores, so that column name mappers don't change it.
|
|
14
|
+
const relatedPropColumn = 'objectiontmprelated';
|
|
13
15
|
|
|
14
16
|
class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
15
17
|
constructor(name, opt) {
|
|
@@ -85,14 +87,23 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
85
87
|
// Selects the ids of the related rows to update. Fetch raw rows, so that no
|
|
86
88
|
// hooks run for them.
|
|
87
89
|
createRelatedIdsQuery(builder, hasLimit) {
|
|
88
|
-
const { relatedModelClass } = this.relation;
|
|
90
|
+
const { relatedModelClass, relatedProp } = this.relation;
|
|
89
91
|
|
|
90
92
|
if (hasLimit) {
|
|
91
93
|
// The limit picks related rows, not join rows, so filter the related
|
|
92
|
-
// table by the ids of the matching join rows
|
|
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:
|
|
93
98
|
const relatedIdsQuery = relatedModelClass.query().childQueryOf(builder);
|
|
94
99
|
const idRefs = relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
95
|
-
|
|
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
|
+
]);
|
|
96
107
|
} else {
|
|
97
108
|
// Select the ids from the filter subquery itself. Filtering the related
|
|
98
109
|
// table by them again lets Postgres pick plans that run the subquery
|
|
@@ -107,20 +118,30 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
107
118
|
// match them (#161). A query resolved without updating anything, e.g. with
|
|
108
119
|
// `none()`, leaves the join rows untouched too.
|
|
109
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);
|
|
110
124
|
if (this.hasExtraProps && !this.isResolvedWithoutUpdate(builder)) {
|
|
125
|
+
let relatedValues = null;
|
|
111
126
|
if (this.relatedIdsQuery) {
|
|
112
127
|
// The filters can also reference the extras that the join rows change,
|
|
113
128
|
// so identify the related rows to modify first.
|
|
114
129
|
const rows = await this.relatedIdsQuery.toKnexQuery();
|
|
115
|
-
|
|
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
|
+
}
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
let numJoinRowsUpdated;
|
|
119
|
-
if (
|
|
139
|
+
if (relatedValues) {
|
|
120
140
|
// The filters match the join rows of more related rows than the limit
|
|
121
|
-
// picks, so only update the join rows of the picked ones
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
124
145
|
}
|
|
125
146
|
} else {
|
|
126
147
|
numJoinRowsUpdated = await this.updateJoinRows(builder);
|
|
@@ -136,7 +157,7 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
|
|
139
|
-
updateJoinRows(builder,
|
|
160
|
+
updateJoinRows(builder, relatedValues = null) {
|
|
140
161
|
const joinTableUpdateQuery = this.relation
|
|
141
162
|
.getJoinModelClass(builder.knex())
|
|
142
163
|
.query()
|
|
@@ -148,11 +169,11 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
148
169
|
? this.applyIdFilterForJoinTable(joinTableUpdateQuery)
|
|
149
170
|
: this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
|
|
150
171
|
);
|
|
151
|
-
if (
|
|
172
|
+
if (relatedValues) {
|
|
152
173
|
addWhereGroup(joinTableUpdateQuery, () =>
|
|
153
174
|
joinTableUpdateQuery.whereInComposite(
|
|
154
175
|
this.relation.joinTableRelatedProp.refs(joinTableUpdateQuery),
|
|
155
|
-
|
|
176
|
+
relatedValues,
|
|
156
177
|
),
|
|
157
178
|
);
|
|
158
179
|
}
|
|
@@ -167,7 +188,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
167
188
|
// number of values in one statement.
|
|
168
189
|
queryBatches(builder) {
|
|
169
190
|
if (this.relatedIds) {
|
|
170
|
-
const batches =
|
|
191
|
+
const batches = chunkKeys(
|
|
192
|
+
this.relatedIds,
|
|
193
|
+
this.relation.relatedModelClass.getIdRelationProperty().size,
|
|
194
|
+
);
|
|
171
195
|
// Without ids, still execute the query once for the dialect's result:
|
|
172
196
|
return batches.length > 0 ? batches : [[]];
|
|
173
197
|
} else {
|
|
@@ -175,11 +199,6 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
relatedIdBatches() {
|
|
179
|
-
const idSize = this.relation.relatedModelClass.getIdRelationProperty().size;
|
|
180
|
-
return chunk(this.relatedIds, Math.floor(maxBatchValues / idSize));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
202
|
// Filters the join rows by the ids of the owner and the updated model.
|
|
184
203
|
applyIdFilterForJoinTable(builder) {
|
|
185
204
|
return builder
|
|
@@ -216,6 +235,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
216
235
|
}
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
function chunkKeys(keys, size) {
|
|
239
|
+
return chunk(keys, Math.floor(maxBatchValues / size));
|
|
240
|
+
}
|
|
241
|
+
|
|
219
242
|
module.exports = {
|
|
220
243
|
ManyToManyUpdateOperationBase,
|
|
221
244
|
};
|
|
@@ -54,6 +54,20 @@ function isFieldExpressionKey(key, declared) {
|
|
|
54
54
|
return key.includes(':') && !(isObject(declared) && hasOwn(declared, key));
|
|
55
55
|
}
|
|
56
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
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
// Formats the json path of a parsed field expression as a Postgres text array
|
|
58
72
|
// literal, e.g. `{a,"",b}`. Keys that would otherwise be misread (empty keys,
|
|
59
73
|
// keys with whitespace, delimiters, quotes or backslashes and `NULL`) are
|
|
@@ -90,6 +104,7 @@ function toPostgresJsonPathLiteral(access) {
|
|
|
90
104
|
module.exports = {
|
|
91
105
|
parseFieldExpression,
|
|
92
106
|
isFieldExpressionKey,
|
|
107
|
+
getFieldExpressionColumn,
|
|
93
108
|
toPostgresJsonPath,
|
|
94
109
|
toPostgresJsonPathLiteral,
|
|
95
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
|
};
|