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.
@@ -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 { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
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
- // The json column of a field expression key like `data:a` overrides it too:
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
- (isFieldExpressionKey(key, properties) &&
69
- model[parseFieldExpression(key).column] !== undefined),
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,
@@ -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 { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
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
- if (isFieldExpressionKey(key, pick)) {
144
- try {
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) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const hiddenProps = ['$$queryProps'];
3
+ const hiddenProps = ['$$queryProps', '$$combinedQueryProps'];
4
4
 
5
5
  const staticHiddenProps = [
6
6
  '$$knex',
@@ -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 { parseFieldExpression, isFieldExpressionKey } = require('../../utils/parseFieldExpression');
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 = isFieldExpressionKey(key, declaredColumns)
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
- return operations.reduce((promise, Operation) => {
476
- const operation = new Operation(graphData);
477
- const actions = operation.createActions();
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
- return promise.then(() => executeActions(builder, actions));
480
- }, Promise.resolve());
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. `GraphInsertAction` already copied them to
11
- * the owner objects once the related nodes were inserted, so all that's left to
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
- ownerProp.patch(patch, i, ownerProp.getProp(ownerNode.obj, i));
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 !this._isHandled(node, handledNodes) && !this._hasDependencies(node, handledNodes);
86
+ return (
87
+ !node.isReference && !handledNodes.has(node) && !this._hasDependencies(node, handledNodes)
88
+ );
87
89
  });
88
90
 
89
- if (nodesToInsert.length === 0) {
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 nodesToInsert) {
106
- this._markHandled(node, handledNodes);
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 (this._isHandled(node, handledNodes)) {
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
- // inserted first. Deferred edges are ignored.
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
- _markHandled(node, handledNodes) {
228
- handledNodes.add(node);
229
-
230
- // The referencing nodes are all references that don't
231
- // represent any real entity. They are simply intermediate nodes
232
- // that depend on this node. Once this node is handled, we can
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
- return promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
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
- : [...changedPropsBecauseOfBelongsToOneInsert, ...changePropsBecauseOfBelongsToOneDelete],
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 order out of deletes on MySQL, but keeps their limit, so
15
- // the order doesn't pick the deleted rows. Warn when the query is executed.
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
- return this.applyModifyFilterForRelatedTable(relatedIdsQuery).select(idRefs);
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
- this.relatedIds = rows.map((row) => Object.values(row));
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 (this.relatedIdsQuery?.has(/^(limit|offset)$/)) {
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
- for (const ids of this.relatedIdBatches()) {
123
- await this.updateJoinRows(builder, ids);
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, relatedIds = null) {
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 (relatedIds) {
172
+ if (relatedValues) {
152
173
  addWhereGroup(joinTableUpdateQuery, () =>
153
174
  joinTableUpdateQuery.whereInComposite(
154
175
  this.relation.joinTableRelatedProp.refs(joinTableUpdateQuery),
155
- relatedIds,
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 = this.relatedIdBatches();
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
- rejected: false,
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.rejected) {
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.rejected) {
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
- if (ctx.rejected) {
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.rejected = true;
105
- ctx.reject(err);
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 = {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objection",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "An SQL-friendly ORM for Node.js",
5
5
  "main": "lib/objection.js",
6
6
  "license": "MIT",