objection 3.1.4 → 3.2.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.
Files changed (34) hide show
  1. package/README.md +3 -3
  2. package/lib/model/AjvValidator.js +60 -25
  3. package/lib/model/Model.js +3 -4
  4. package/lib/model/graph/ModelGraphNode.js +12 -0
  5. package/lib/model/modelToJson.js +2 -1
  6. package/lib/queryBuilder/QueryBuilder.js +156 -45
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +1 -1
  8. package/lib/queryBuilder/RelationExpression.js +8 -8
  9. package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
  10. package/lib/queryBuilder/graph/GraphOptions.js +21 -2
  11. package/lib/queryBuilder/graph/GraphUpsert.js +30 -0
  12. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
  13. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -0
  14. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +2 -0
  15. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
  16. package/lib/queryBuilder/join/RelationJoiner.js +17 -0
  17. package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
  18. package/lib/queryBuilder/operations/NoneOperation.js +37 -0
  19. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
  20. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
  21. package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -5
  22. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +8 -2
  23. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +15 -3
  24. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +8 -2
  25. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
  26. package/lib/relations/RelationProperty.js +7 -3
  27. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
  28. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
  29. package/lib/relations/hasMany/HasManyRelateOperation.js +1 -0
  30. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
  31. package/lib/utils/identifierMapping.js +8 -2
  32. package/lib/utils/objectUtils.js +9 -5
  33. package/package.json +17 -13
  34. package/typings/objection/index.d.ts +77 -39
@@ -51,6 +51,10 @@ class GraphOptions {
51
51
 
52
52
  // Like `shouldRelate` but ignores settings that explicitly disable relate operations.
53
53
  shouldRelateIgnoreDisable(node, graphData) {
54
+ if (node.toBeUnrelated || node.toBeDeleted) {
55
+ return false;
56
+ }
57
+
54
58
  if (node.isReference || node.isDbReference) {
55
59
  return true;
56
60
  }
@@ -71,6 +75,10 @@ class GraphOptions {
71
75
 
72
76
  // Like `shouldInsert` but ignores settings that explicitly disable insert operations.
73
77
  shouldInsertIgnoreDisable(node, graphData) {
78
+ if (node.toBeUnrelated || node.toBeDeleted) {
79
+ return false;
80
+ }
81
+
74
82
  return (
75
83
  !getCurrentNode(node, graphData) &&
76
84
  !this.shouldRelateIgnoreDisable(node, graphData) &&
@@ -89,6 +97,10 @@ class GraphOptions {
89
97
  // Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
90
98
  // update or patch operations.
91
99
  shouldPatchOrUpdateIgnoreDisable(node, graphData) {
100
+ if (node.toBeUnrelated || node.toBeDeleted) {
101
+ return false;
102
+ }
103
+
92
104
  if (this.shouldRelate(node, graphData)) {
93
105
  // We should update all nodes that are going to be related. Note that
94
106
  // we don't actually update anything unless there is something to update
@@ -121,16 +133,23 @@ class GraphOptions {
121
133
  }
122
134
 
123
135
  shouldUnrelate(currentNode, graphData) {
136
+ const node = getNode(currentNode, graphData.graph);
137
+ if (node && node.toBeUnrelated) return true;
138
+
124
139
  return (
125
- !getNode(currentNode, graphData.graph) &&
140
+ !node &&
126
141
  !this._hasOption(currentNode, NO_UNRELATE) &&
127
142
  this.shouldUnrelateIgnoreDisable(currentNode)
128
143
  );
129
144
  }
130
145
 
131
146
  shouldDelete(currentNode, graphData) {
147
+ const node = getNode(currentNode, graphData.graph);
148
+ // `#unrelate` takes precedence over `#delete` if both are set.
149
+ if (node && node.toBeDeleted && !node.toBeUnrelated) return true;
150
+
132
151
  return (
133
- !getNode(currentNode, graphData.graph) &&
152
+ !node &&
134
153
  !this._hasOption(currentNode, NO_DELETE) &&
135
154
  !this.shouldUnrelateIgnoreDisable(currentNode)
136
155
  );
@@ -91,6 +91,7 @@ function pruneGraphs() {
91
91
 
92
92
  if (!graphData.graphOptions.isInsertOnly()) {
93
93
  pruneDeletedBranches(graphData);
94
+ pruneUnrelatedOrDeletedBranches(graphData);
94
95
  }
95
96
 
96
97
  return graphData;
@@ -136,6 +137,19 @@ function pruneDeletedBranches(graphData) {
136
137
  removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
137
138
  }
138
139
 
140
+ // Models marked with `#unrelate` or `#delete` are unrelated or deleted
141
+ // as a whole. Their relations are ignored, both in the upserted graph
142
+ // and in the current graph.
143
+ function pruneUnrelatedOrDeletedBranches(graphData) {
144
+ const { graph, currentGraph } = graphData;
145
+
146
+ const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
147
+ const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
148
+
149
+ removeBranchesFromGraph(roots, graph);
150
+ removeBranchesFromGraph(currentRoots, currentGraph);
151
+ }
152
+
139
153
  function findRoots(nodes) {
140
154
  const nodeSet = new Set(nodes);
141
155
 
@@ -192,6 +206,7 @@ function checkForErrors(builder) {
192
206
  checkForNotFoundErrors(graphData, builder);
193
207
  checkForUnallowedRelationErrors(graphData, builder);
194
208
  checkForUnallowedReferenceErrors(graphData, builder);
209
+ checkForRootUnrelateOrDeleteErrors(graphData, builder);
195
210
 
196
211
  if (graphData.graphOptions.isInsertOnly()) {
197
212
  checkForHasManyRelateErrors(graphData);
@@ -207,6 +222,8 @@ function checkForNotFoundErrors(graphData, builder) {
207
222
  for (const node of graph.nodes) {
208
223
  if (
209
224
  node.obj.$hasId() &&
225
+ !node.toBeUnrelated &&
226
+ !node.toBeDeleted &&
210
227
  !graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
211
228
  !graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
212
229
  !currentGraph.nodeForNode(node)
@@ -259,6 +276,19 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
259
276
  }
260
277
  }
261
278
 
279
+ function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
280
+ for (const node of graphData.graph.nodes) {
281
+ if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
282
+ const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
283
+
284
+ throw builder.modelClass().createValidationError({
285
+ type: ValidationErrorType.InvalidGraph,
286
+ message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
287
+ });
288
+ }
289
+ }
290
+ }
291
+
262
292
  function checkForHasManyRelateErrors(graphData) {
263
293
  const { graph, graphOptions } = graphData;
264
294
 
@@ -40,20 +40,26 @@ class GraphDeleteAction extends GraphAction {
40
40
  const nodesByParent = groupBy(nodes, getParent);
41
41
 
42
42
  nodesByParent.forEach((nodes, parentNode) => {
43
- const shouldUnrelate = this.graphOptions.shouldUnrelate(nodes[0], this.graphData);
44
-
45
- const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
46
-
47
- if (!relation.isObjectionBelongsToOneRelation) {
48
- // This is useless in case of BelongsToOneRelation.
49
- builder.findByIds(nodes.map((node) => node.obj.$id()));
50
- }
51
-
52
- for (const node of nodes) {
53
- node.userData.deleted = true;
54
- }
55
-
56
- builders.push(shouldUnrelate ? builder.unrelate() : builder.delete());
43
+ // Nodes of the same relation and parent can still require different
44
+ // operations when they are marked with `#unrelate` or `#delete`.
45
+ const nodesByShouldUnrelate = groupBy(nodes, (node) =>
46
+ this.graphOptions.shouldUnrelate(node, this.graphData),
47
+ );
48
+
49
+ nodesByShouldUnrelate.forEach((nodes, shouldUnrelate) => {
50
+ const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
51
+
52
+ if (!relation.isObjectionBelongsToOneRelation) {
53
+ // This is useless in case of BelongsToOneRelation.
54
+ builder.findByIds(nodes.map((node) => node.obj.$id()));
55
+ }
56
+
57
+ for (const node of nodes) {
58
+ node.userData.deleted = true;
59
+ }
60
+
61
+ builders.push(shouldUnrelate ? builder.unrelate() : builder.delete());
62
+ });
57
63
  });
58
64
  });
59
65
 
@@ -30,6 +30,12 @@ class GraphInsert extends GraphOperation {
30
30
  }
31
31
 
32
32
  _createRelationDependency(edge, dependencies) {
33
+ if (edge.relatedNode.toBeUnrelatedOrDeleted) {
34
+ // Models marked with `#unrelate` or `#delete` are never inserted or
35
+ // related, so their keys must not be copied to the owner (or vice versa).
36
+ return;
37
+ }
38
+
33
39
  if (edge.relation.isObjectionHasManyRelation) {
34
40
  // In case of HasManyRelation the related node depends on the owner node
35
41
  // because the related node has the foreign key.
@@ -174,6 +174,8 @@ class GraphInsertAction extends GraphAction {
174
174
 
175
175
  for (const node of nodes) {
176
176
  delete node.obj[modelClass.uidProp];
177
+ delete node.obj[modelClass.graphUnrelateProp];
178
+ delete node.obj[modelClass.graphDeleteProp];
177
179
  node.obj.$validate(null, { dataPath: node.dataPathKey });
178
180
  }
179
181
 
@@ -75,6 +75,8 @@ class GraphPatchAction extends GraphAction {
75
75
  delete node.obj[node.modelClass.uidProp];
76
76
  delete node.obj[node.modelClass.uidRefProp];
77
77
  delete node.obj[node.modelClass.dbRefProp];
78
+ delete node.obj[node.modelClass.graphUnrelateProp];
79
+ delete node.obj[node.modelClass.graphDeleteProp];
78
80
 
79
81
  node.obj.$validate(null, {
80
82
  dataPath: node.dataPathKey,
@@ -122,7 +124,9 @@ class GraphPatchAction extends GraphAction {
122
124
  edge.isOwnerNode(node) &&
123
125
  edge.relation &&
124
126
  edge.relation.isObjectionBelongsToOneRelation &&
125
- edge.relation.relatedProp.hasProps(edge.relatedNode.obj)
127
+ edge.relation.relatedProp.hasProps(edge.relatedNode.obj) &&
128
+ // Models marked with `#unrelate` or `#delete` are handled below.
129
+ !edge.relatedNode.toBeUnrelatedOrDeleted
126
130
  ) {
127
131
  const { relation } = edge;
128
132
 
@@ -153,7 +157,8 @@ class GraphPatchAction extends GraphAction {
153
157
  if (
154
158
  edge.isOwnerNode(currentNode) &&
155
159
  edge.relation.isObjectionBelongsToOneRelation &&
156
- node.obj[edge.relation.name] === null &&
160
+ (node.obj[edge.relation.name] === null ||
161
+ isToBeUnrelatedOrDeleted(this.graph.nodeForNode(edge.relatedNode))) &&
157
162
  this.graphOptions.shouldDeleteOrUnrelate(edge.relatedNode, this.graphData)
158
163
  ) {
159
164
  const { relation } = edge;
@@ -231,7 +236,10 @@ class GraphPatchAction extends GraphAction {
231
236
  }
232
237
 
233
238
  _createRootBuilder(node) {
234
- return node.obj.$query();
239
+ const currentNode = this.currentGraph.nodeForNode(node);
240
+ const currentObj = currentNode && currentNode.obj;
241
+
242
+ return currentObj ? currentObj.$query() : node.obj.$query();
235
243
  }
236
244
  }
237
245
 
@@ -250,6 +258,10 @@ function nonStrictEquals(val1, val2) {
250
258
  return jsonEquals(val1, val2);
251
259
  }
252
260
 
261
+ function isToBeUnrelatedOrDeleted(node) {
262
+ return !!node && node.toBeUnrelatedOrDeleted;
263
+ }
264
+
253
265
  module.exports = {
254
266
  GraphPatchAction,
255
267
  };
@@ -81,6 +81,23 @@ class RelationJoiner {
81
81
  }
82
82
  }
83
83
 
84
+ releaseInternalRootSelection(column) {
85
+ const rootTableNode = this.tableTree && this.tableTree.rootNode;
86
+
87
+ if (!rootTableNode || !this.internalSelections) {
88
+ return false;
89
+ }
90
+
91
+ const alias = rootTableNode.getColumnAliasForColumn(column);
92
+ const count = this.internalSelections.length;
93
+
94
+ this.internalSelections = this.internalSelections.filter(
95
+ (it) => it.table !== rootTableNode.alias || it.alias !== alias,
96
+ );
97
+
98
+ return this.internalSelections.length !== count;
99
+ }
100
+
84
101
  parseResult(builder, flatRows) {
85
102
  const parser = JoinResultParser.create({
86
103
  tableTree: this._getTableTree(builder),
@@ -11,6 +11,10 @@ class DelegateOperation extends QueryBuilderOperation {
11
11
  this.delegate = opt.delegate;
12
12
  }
13
13
 
14
+ get modelOptions() {
15
+ return this.delegate.modelOptions;
16
+ }
17
+
14
18
  is(OperationClass) {
15
19
  return super.is(OperationClass) || this.delegate.is(OperationClass);
16
20
  }
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
+
5
+ const AggregateSelector =
6
+ /^(count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct|groupBy\w*)$/;
7
+
8
+ class NoneOperation extends QueryBuilderOperation {
9
+ onBefore1(builder, result) {
10
+ // Resolve the query without executing it, like `cancelQuery()` does.
11
+ if (builder.isFind()) {
12
+ // Aggregate queries are executed to get the result the database
13
+ // returns for no rows, e.g. `[{ count: 0 }]`.
14
+ if (!builder.has(AggregateSelector)) {
15
+ builder.resolve([]);
16
+ }
17
+ } else if (builder.isUpdate() || builder.isDelete()) {
18
+ builder.resolve(builder.has(/returning/) ? [] : 0);
19
+ } else {
20
+ throw new Error('none() can only be used with find, update and delete queries');
21
+ }
22
+
23
+ return result;
24
+ }
25
+
26
+ // Called by `buildKnexQuery()` after all other operations have been
27
+ // built, so that the query matches no rows when it does get executed,
28
+ // for example as a subquery or in `resultSize()`. The existing where
29
+ // clauses are removed so that `orWhere()` calls can't match any rows.
30
+ onAfterBuildKnex(knexBuilder) {
31
+ return knexBuilder.clearWhere().where(false);
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ NoneOperation,
37
+ };
@@ -106,6 +106,11 @@ function convertArray(arr, builder, i, opName, skipUndefined) {
106
106
  `undefined passed as an item in argument #${i} for '${opName}' operation. Call skipUndefined() method to ignore the undefined values.`,
107
107
  );
108
108
  }
109
+ } else if (opName === 'orderBy' && isObject(item)) {
110
+ // knex's `orderBy()` only reads `{ column, order, nulls }` objects from arrays,
111
+ // so wrap refs, raws and query builders in one and convert its properties.
112
+ const obj = isPlainObject(item) ? item : { column: item };
113
+ return convertPlainObject(obj, builder, i, opName, skipUndefined);
109
114
  } else if (hasToKnexRawMethod(item)) {
110
115
  return convertToKnexRaw(item, builder);
111
116
  } else if (isObjectionQueryBuilderBase(item)) {
@@ -14,6 +14,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
14
14
 
15
15
  this.id = null;
16
16
  this.skipIdWhere = false;
17
+ this.selectOperations = null;
17
18
  }
18
19
 
19
20
  get model() {
@@ -25,6 +26,19 @@ class UpdateAndFetchOperation extends DelegateOperation {
25
26
  return this.delegate.onAdd(builder, args.slice(1));
26
27
  }
27
28
 
29
+ onBefore3(builder, result) {
30
+ // Selects have no effect on the update query, so remember them here to
31
+ // apply them to the fetch query. Collect them before the query is built,
32
+ // so that selects added by `onBuild` hooks in the shared context aren't
33
+ // copied, as those hooks run again for the fetch query.
34
+ this.selectOperations = new Set();
35
+ builder.forEachOperation(builder.constructor.SelectSelector, (op) => {
36
+ this.selectOperations.add(op);
37
+ });
38
+
39
+ return super.onBefore3(builder, result);
40
+ }
41
+
28
42
  onBuild(builder) {
29
43
  if (!this.skipIdWhere) {
30
44
  builder.findById(this.id);
@@ -43,6 +57,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
43
57
  const fetched = await builder
44
58
  .emptyInstance()
45
59
  .childQueryOf(builder)
60
+ .copyFrom(builder, (op) => !!this.selectOperations?.has(op))
46
61
  .modify((builder) => {
47
62
  if (!this.skipIdWhere) {
48
63
  builder.findById(this.id);
@@ -52,6 +67,11 @@ class UpdateAndFetchOperation extends DelegateOperation {
52
67
 
53
68
  if (fetched) {
54
69
  this.model.$set(fetched);
70
+ // A custom `select` may not include the id. The row was fetched by
71
+ // id, so make sure the result still has it.
72
+ if (!this.model.$hasId()) {
73
+ this.model.$id(this.id);
74
+ }
55
75
  }
56
76
 
57
77
  await super.onAfter2(builder, numUpdated);
@@ -8,14 +8,13 @@ class EagerOperation extends QueryBuilderOperation {
8
8
  super(name, opt);
9
9
 
10
10
  this.expression = RelationExpression.create();
11
- this.modifiersAtPath = [];
12
11
  this.graphOptions = this.opt.defaultGraphOptions;
13
12
  }
14
13
 
15
- buildFinalExpression() {
14
+ buildFinalExpression(builder) {
16
15
  const expression = this.expression.clone();
17
16
 
18
- this.modifiersAtPath.forEach((modifier, i) => {
17
+ builder.graphModifiersAtPath().forEach((modifier, i) => {
19
18
  const modifierName = getModifierName(i);
20
19
 
21
20
  expression.expressionsAtPath(modifier.path).forEach((expr) => {
@@ -30,7 +29,7 @@ class EagerOperation extends QueryBuilderOperation {
30
29
  // `modifiers()` returns a clone so we can modify it.
31
30
  const modifiers = builder.modifiers();
32
31
 
33
- this.modifiersAtPath.forEach((modifier, i) => {
32
+ builder.graphModifiersAtPath().forEach((modifier, i) => {
34
33
  const modifierName = getModifierName(i);
35
34
 
36
35
  modifiers[modifierName] = modifier.modifier;
@@ -41,7 +40,6 @@ class EagerOperation extends QueryBuilderOperation {
41
40
 
42
41
  cloneFrom(eagerOp) {
43
42
  this.expression = eagerOp.expression.clone();
44
- this.modifiersAtPath = eagerOp.modifiersAtPath.slice();
45
43
  this.graphOptions = Object.assign({}, eagerOp.graphOptions);
46
44
  }
47
45
 
@@ -21,7 +21,7 @@ class JoinEagerOperation extends EagerOperation {
21
21
 
22
22
  onBefore3(builder) {
23
23
  return this.joiner
24
- .setExpression(this.buildFinalExpression())
24
+ .setExpression(this.buildFinalExpression(builder))
25
25
  .setModifiers(this.buildFinalModifiers(builder))
26
26
  .setOptions(this.graphOptions)
27
27
  .fetchColumnInfo(builder);
@@ -29,12 +29,18 @@ class JoinEagerOperation extends EagerOperation {
29
29
 
30
30
  onBuild(builder) {
31
31
  this.joiner
32
- .setExpression(this.buildFinalExpression())
32
+ .setExpression(this.buildFinalExpression(builder))
33
33
  .setModifiers(this.buildFinalModifiers(builder))
34
34
  .setOptions(this.graphOptions)
35
35
  .build(builder);
36
36
  }
37
37
 
38
+ // Stops the joiner from omitting an internally selected column of the root
39
+ // table from the result. Returns true if such a selection existed.
40
+ releaseInternalRootSelection(column) {
41
+ return !!this.joiner && this.joiner.releaseInternalRootSelection(column);
42
+ }
43
+
38
44
  onRawResult(builder, rows) {
39
45
  return this.joiner.parseResult(builder, rows);
40
46
  }
@@ -3,6 +3,7 @@
3
3
  const promiseUtils = require('../../../utils/promiseUtils');
4
4
 
5
5
  const { EagerOperation } = require('./EagerOperation');
6
+ const { JoinEagerOperation } = require('./JoinEagerOperation');
6
7
  const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
7
8
  const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
8
9
  const { ValidationErrorType } = require('../../../model/ValidationError');
@@ -41,7 +42,7 @@ class WhereInEagerOperation extends EagerOperation {
41
42
  onBuild(builder) {
42
43
  const relationsToFetch = findRelationsToFetch(
43
44
  builder.modelClass(),
44
- this.buildFinalExpression(),
45
+ this.buildFinalExpression(builder),
45
46
  );
46
47
 
47
48
  const { selectionsToAdd, selectedProps } = findRelationPropsToSelect(builder, relationsToFetch);
@@ -237,6 +238,11 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
237
238
  const selectionsToAdd = [];
238
239
  const selectedProps = [];
239
240
 
241
+ // When mixed with `withGraphJoined`, the join operation may have selected
242
+ // some of the needed columns internally, in which case it would omit them
243
+ // from the result models before we get to use them.
244
+ const joinOp = builder.findOperation(JoinEagerOperation);
245
+
240
246
  // Collect columns that need to be selected for the eager fetch
241
247
  // to work that are not currently selected.
242
248
  for (const { relation } of relationsToFetch) {
@@ -247,9 +253,15 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
247
253
  const prop = ownerProp.props[c];
248
254
  const col = ownerProp.cols[c];
249
255
 
250
- if (!builder.hasSelectionAs(fullCol, col) && selectionsToAdd.indexOf(fullCol) === -1) {
256
+ if (!builder.hasSelectionAs(fullCol, col)) {
257
+ if (selectionsToAdd.indexOf(fullCol) === -1) {
258
+ selectedProps.push(prop);
259
+ selectionsToAdd.push(fullCol);
260
+ }
261
+ } else if (joinOp && joinOp.releaseInternalRootSelection(col)) {
262
+ // The join operation no longer omits the column, so we need to
263
+ // omit the property once the relations have been fetched.
251
264
  selectedProps.push(prop);
252
- selectionsToAdd.push(fullCol);
253
265
  }
254
266
  }
255
267
  }
@@ -44,10 +44,16 @@ const { asArray, isObject, isString } = require('../../../utils/objectUtils');
44
44
 
45
45
  function parseFieldExpression(expression, extractAsText) {
46
46
  let parsed = parser.parseFieldExpression(expression);
47
+ let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
48
+ let column = `"${middleQuotedColumnName}"`;
49
+ // Reference the bare column when there is no json path, so that Postgres can
50
+ // use indexes on it. `#>> '{}'` is still needed to extract the value as text.
51
+ if (parsed.access.length === 0 && !extractAsText) {
52
+ return column;
53
+ }
47
54
  let jsonRefs = parsed.access.map((it) => it.ref).join(',');
48
55
  let extractor = extractAsText ? '#>>' : '#>';
49
- let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
50
- return `"${middleQuotedColumnName}"${extractor}'{${jsonRefs}}'`;
56
+ return `${column}${extractor}'{${jsonRefs}}'`;
51
57
  }
52
58
 
53
59
  function whereJsonbRefOnLeftJsonbValOrRefOnRight(
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { QueryTransformation } = require('./QueryTransformation');
4
4
  const { isMySql } = require('../../utils/knexUtils');
5
+ const { once } = require('../../utils/objectUtils');
6
+ const getJoinBuilder = once(() => require('../JoinBuilder').JoinBuilder);
5
7
 
6
8
  /**
7
9
  * Mysql doesn't allow queries like this:
@@ -30,6 +32,13 @@ class WrapMysqlModifySubqueryTransformation extends QueryTransformation {
30
32
  return query;
31
33
  }
32
34
 
35
+ // This transformation should not apply to join builder, otherwise it causes:
36
+ // "TypeError: parentQuery.isUpdate is not a function"
37
+ const JoinBuilder = getJoinBuilder();
38
+ if (parentQuery instanceof JoinBuilder) {
39
+ return query;
40
+ }
41
+
33
42
  // This transformation only applies to update and delete queries.
34
43
  if (!parentQuery.isUpdate() && !parentQuery.isDelete()) {
35
44
  return query;
@@ -184,14 +184,18 @@ function createRefs(refs) {
184
184
 
185
185
  function createPaths(refs, modelClassResolver) {
186
186
  return refs.map((ref) => {
187
- if (!ref.tableName) {
187
+ // References created with `SomeModel.ref(column)` have no explicit table
188
+ // name, so fall back to the table name of their model class.
189
+ const tableName = ref.tableName || (ref.modelClass && ref.modelClass.getTableName());
190
+
191
+ if (!tableName) {
188
192
  throw new InvalidReferenceError();
189
193
  }
190
194
 
191
- const modelClass = modelClassResolver(ref.tableName);
195
+ const modelClass = modelClassResolver(tableName);
192
196
 
193
197
  if (!modelClass) {
194
- throw new ModelNotFoundError(ref.tableName);
198
+ throw new ModelNotFoundError(tableName);
195
199
  }
196
200
 
197
201
  const prop = modelClass.columnNameToPropertyName(ref.column);
@@ -22,6 +22,7 @@ class BelongsToOneInsertOperation extends RelationInsertOperation {
22
22
  }
23
23
 
24
24
  const relateOp = new BelongsToOneRelateOperation('relate', {
25
+ dontCopyReturning: true,
25
26
  relation: this.relation,
26
27
  owner: this.owner,
27
28
  });
@@ -35,6 +35,11 @@ class BelongsToOneRelateOperation extends RelateOperation {
35
35
  .query()
36
36
  .childQueryOf(builder)
37
37
  .patch(patch)
38
+ .modify((query) => {
39
+ if (!this.opt.dontCopyReturning) {
40
+ query.copyFrom(builder, /returning/);
41
+ }
42
+ })
38
43
  .whereInComposite(ownerIdProp.refs(builder), ownerIds);
39
44
  }
40
45
  }
@@ -28,6 +28,7 @@ class HasManyRelateOperation extends RelateOperation {
28
28
  .query()
29
29
  .childQueryOf(builder)
30
30
  .patch(patch)
31
+ .copyFrom(builder, /returning/)
31
32
  .copyFrom(builder, builder.constructor.JoinSelector)
32
33
  .copyFrom(builder, builder.constructor.WhereSelector)
33
34
  .findByIds(this.ids)
@@ -17,6 +17,7 @@ class ManyToManyRelateOperation extends RelateOperation {
17
17
  const joinModelClass = this.relation.getJoinModelClass(builder.knex());
18
18
  const ownerValues = this.owner.getSplitProps(builder, this.relation);
19
19
  const joinModels = [];
20
+ const isReturning = !this.opt.dontCopyReturning && builder.has(/returning/);
20
21
 
21
22
  for (const ownerValue of ownerValues) {
22
23
  for (const relatedValue of this.ids) {
@@ -43,6 +44,11 @@ class ManyToManyRelateOperation extends RelateOperation {
43
44
  })
44
45
  .insert(joinModels)
45
46
  .runAfter((models) => {
47
+ if (isReturning) {
48
+ // Return the inserted join rows if `returning` was used.
49
+ return models;
50
+ }
51
+
46
52
  return Array.isArray(models) ? models.length : 1;
47
53
  });
48
54
  }
@@ -27,6 +27,7 @@ function snakeCase(
27
27
  upperCase = false,
28
28
  underscoreBeforeDigits = false,
29
29
  underscoreBetweenUppercaseLetters = false,
30
+ noDoubleUnderscores = false,
30
31
  } = {},
31
32
  ) {
32
33
  if (str.length === 0) {
@@ -48,11 +49,16 @@ function snakeCase(
48
49
  const lowerChar = lower[i];
49
50
  const prevLowerChar = lower[i - 1];
50
51
 
52
+ // If noDoubleUnderscores is true, never insert an underscore directly
53
+ // after an existing one (e.g. "foo_1" stays "foo_1" instead of becoming
54
+ // "foo__1" when underscoreBeforeDigits is true).
55
+ const canInsertUnderscore = !noDoubleUnderscores || prevChar !== '_';
56
+
51
57
  // If underScoreBeforeDigits is true then, well, insert an underscore
52
58
  // before digits :). Only the first digit gets an underscore if
53
59
  // there are multiple.
54
60
  if (underscoreBeforeDigits && isDigit(char) && !isDigit(prevChar)) {
55
- out += '_' + char;
61
+ out += canInsertUnderscore ? '_' + char : char;
56
62
  continue;
57
63
  }
58
64
 
@@ -65,7 +71,7 @@ function snakeCase(
65
71
  // If underscoreBetweenUppercaseLetters is true, we always place an underscore
66
72
  // before consecutive uppercase letters (e.g. "fooBAR" becomes "foo_b_a_r").
67
73
  // Otherwise, we don't (e.g. "fooBAR" becomes "foo_bar").
68
- if (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase) {
74
+ if (canInsertUnderscore && (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase)) {
69
75
  out += '_' + lowerChar;
70
76
  } else {
71
77
  out += lowerChar;
@@ -123,13 +123,17 @@ function union(arr1, arr2) {
123
123
  }
124
124
 
125
125
  function unionSmall(arr1, arr2) {
126
- const all = arr1.slice();
126
+ const all = [];
127
127
 
128
- for (let i = 0, l = arr2.length; i < l; ++i) {
129
- const item = arr2[i];
128
+ for (let i = 0, l = arr1.length; i < l; ++i) {
129
+ if (all.indexOf(arr1[i]) === -1) {
130
+ all.push(arr1[i]);
131
+ }
132
+ }
130
133
 
131
- if (all.indexOf(item) === -1) {
132
- all.push(item);
134
+ for (let i = 0, l = arr2.length; i < l; ++i) {
135
+ if (all.indexOf(arr2[i]) === -1) {
136
+ all.push(arr2[i]);
133
137
  }
134
138
  }
135
139