objection 3.7.0 → 3.9.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 (65) hide show
  1. package/lib/model/AjvValidator.js +123 -50
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -1
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +49 -5
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +17 -2
  8. package/lib/model/modelValidate.js +2 -1
  9. package/lib/model/modelValues.js +43 -50
  10. package/lib/queryBuilder/QueryBuilder.js +106 -38
  11. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  12. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  13. package/lib/queryBuilder/graph/GraphData.js +39 -0
  14. package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
  15. package/lib/queryBuilder/graph/GraphOptions.js +77 -38
  16. package/lib/queryBuilder/graph/GraphUpsert.js +98 -14
  17. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  18. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
  20. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
  21. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  22. package/lib/queryBuilder/join/RelationJoiner.js +105 -12
  23. package/lib/queryBuilder/join/TableNode.js +6 -68
  24. package/lib/queryBuilder/join/utils.js +2 -2
  25. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  26. package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
  27. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  28. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  29. package/lib/queryBuilder/operations/InsertOperation.js +2 -2
  30. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  31. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  32. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +4 -1
  33. package/lib/queryBuilder/operations/LimitPerParentOperation.js +152 -0
  34. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  35. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  36. package/lib/queryBuilder/operations/UpdateOperation.js +54 -5
  37. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  38. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  39. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  40. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +3 -3
  41. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  42. package/lib/queryBuilder/operations/select/Selection.js +39 -16
  43. package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
  44. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
  45. package/lib/queryBuilder/whereGroups.js +35 -40
  46. package/lib/queryBuilder/withoutOrder.js +33 -0
  47. package/lib/relations/Relation.js +3 -0
  48. package/lib/relations/RelationFindOperation.js +13 -5
  49. package/lib/relations/RelationOwner.js +2 -5
  50. package/lib/relations/RelationProperty.js +3 -14
  51. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  52. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
  53. package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
  54. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
  55. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +123 -17
  57. package/lib/utils/identifierMapping.js +2 -2
  58. package/lib/utils/knexUtils.js +21 -0
  59. package/lib/utils/objectUtils.js +5 -0
  60. package/lib/utils/parseFieldExpression.js +9 -0
  61. package/lib/utils/warnings.js +26 -0
  62. package/lib/utils/wrapDbError.js +34 -20
  63. package/package.json +5 -4
  64. package/typings/objection/index.d.ts +31 -13
  65. package/lib/utils/deprecate.js +0 -21
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { propKey, hasProps } = require('../../model/modelValues');
4
+
3
5
  class TableNode {
4
6
  constructor({ tableTree, modelClass, expression, parentNode = null, relation = null }) {
5
7
  this.tableTree = tableTree;
@@ -86,78 +88,14 @@ class TableNode {
86
88
  }
87
89
 
88
90
  _createIdGetter() {
89
- const idColumns = this.modelClass.getIdColumnArray();
90
- const columnAliases = idColumns.map((column) => this.getColumnAliasForColumn(column));
91
-
92
- if (idColumns.length === 1) {
93
- return createIdGetter(columnAliases);
94
- } else {
95
- return createCompositeIdGetter(columnAliases);
96
- }
97
- }
98
- }
99
-
100
- function createIdGetter(columnAliases) {
101
- const columnAlias = columnAliases[0];
91
+ const columnAliases = this.modelClass
92
+ .getIdColumnArray()
93
+ .map((column) => this.getColumnAliasForColumn(column));
102
94
 
103
- return (flatRow) => {
104
- const id = flatRow[columnAlias];
105
-
106
- if (id === null) {
107
- return null;
108
- }
109
-
110
- return `${id}`;
111
- };
112
- }
113
-
114
- function createCompositeIdGetter(columnAliases) {
115
- if (columnAliases.length === 2) {
116
- return createTwoIdGetter(columnAliases);
117
- } else {
118
- return createMultiIdGetter(columnAliases);
95
+ return (flatRow) => (hasProps(flatRow, columnAliases) ? propKey(flatRow, columnAliases) : null);
119
96
  }
120
97
  }
121
98
 
122
- function createTwoIdGetter(columnAliases) {
123
- const columnAlias1 = columnAliases[0];
124
- const columnAlias2 = columnAliases[1];
125
-
126
- return (flatRow) => {
127
- const id1 = flatRow[columnAlias1];
128
- const id2 = flatRow[columnAlias2];
129
-
130
- if (id1 === null || id2 === null) {
131
- return null;
132
- }
133
-
134
- return `${id1},${id2}`;
135
- };
136
- }
137
-
138
- function createMultiIdGetter(columnAliases) {
139
- return (flatRow) => {
140
- let idStr = '';
141
-
142
- for (let i = 0, l = columnAliases.length; i < l; ++i) {
143
- const columnAlias = columnAliases[i];
144
- const id = flatRow[columnAlias];
145
-
146
- if (id === null) {
147
- return null;
148
- }
149
-
150
- idStr += id;
151
-
152
- if (i !== l - 1) {
153
- idStr += ',';
154
- }
155
- }
156
-
157
- return idStr;
158
- };
159
- }
160
-
161
99
  module.exports = {
162
100
  TableNode,
163
101
  };
@@ -6,8 +6,8 @@ const { isPostgres, isMySql, isMsSql, isSqlite } = require('../../utils/knexUtil
6
6
  const RELATION_RECURSION_LIMIT = 64;
7
7
 
8
8
  // The maximum length of the column aliases of the joined relations, after
9
- // which the database would truncate them. Unknown databases use the strictest
10
- // of the known limits, the one of postgres.
9
+ // which the database would truncate them. Other databases keep the limit of
10
+ // postgres, which isn't the strictest one: Oracle before 12.2 only allows 30.
11
11
  function getIdentifierLengthLimit(knex) {
12
12
  if (isPostgres(knex)) {
13
13
  return 63;
@@ -55,6 +55,17 @@ class DelegateOperation extends QueryBuilderOperation {
55
55
  return this.onBuild !== DelegateOperation.prototype.onBuild || this.delegate.hasOnBuild();
56
56
  }
57
57
 
58
+ onAfterBuild(builder) {
59
+ return this.delegate.onAfterBuild(builder);
60
+ }
61
+
62
+ hasOnAfterBuild() {
63
+ return (
64
+ this.onAfterBuild !== DelegateOperation.prototype.onAfterBuild ||
65
+ this.delegate.hasOnAfterBuild()
66
+ );
67
+ }
68
+
58
69
  onBuildKnex(knexBuilder, builder) {
59
70
  return this.delegate.onBuildKnex(knexBuilder, builder);
60
71
  }
@@ -110,6 +121,17 @@ class DelegateOperation extends QueryBuilderOperation {
110
121
  );
111
122
  }
112
123
 
124
+ queryBatches(builder) {
125
+ return this.delegate.queryBatches(builder);
126
+ }
127
+
128
+ hasQueryBatches() {
129
+ return (
130
+ this.queryBatches !== DelegateOperation.prototype.queryBatches ||
131
+ this.delegate.hasQueryBatches()
132
+ );
133
+ }
134
+
113
135
  onError(builder, error) {
114
136
  return this.delegate.onError(builder, error);
115
137
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
+ const { isMySql } = require('../../utils/knexUtils');
6
+ const { warnOnce } = require('../../utils/warnings');
5
7
 
6
8
  class DeleteOperation extends QueryBuilderOperation {
7
9
  async onBefore2(builder, result) {
@@ -9,6 +11,17 @@ class DeleteOperation extends QueryBuilderOperation {
9
11
  return result;
10
12
  }
11
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.
16
+ onAfterBuild(builder) {
17
+ if (isMySql(builder.knex()) && builder.has('limit') && builder.has(/^orderBy/)) {
18
+ warnOnce(
19
+ 'orderBy() has no effect in delete queries with limit() on MySQL, so the limit ' +
20
+ 'deletes rows in any order. This will throw an error in objection 4.0.',
21
+ );
22
+ }
23
+ }
24
+
12
25
  onBuildKnex(knexBuilder) {
13
26
  return knexBuilder.delete();
14
27
  }
@@ -3,10 +3,12 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
 
5
5
  class FirstOperation extends QueryBuilderOperation {
6
- onBuildKnex(knexBuilder, builder) {
7
- const modelClass = builder.modelClass();
6
+ shouldLimit(builder) {
7
+ return builder.isFind() && builder.modelClass().useLimitInFirst;
8
+ }
8
9
 
9
- if (builder.isFind() && modelClass.useLimitInFirst) {
10
+ onBuildKnex(knexBuilder, builder) {
11
+ if (this.shouldLimit(builder)) {
10
12
  knexBuilder = knexBuilder.limit(1);
11
13
  }
12
14
 
@@ -42,6 +42,7 @@ class InsertGraphOperation extends DelegateOperation {
42
42
  noUnrelate: true,
43
43
  insertMissing: true,
44
44
  }),
45
+ insertOnly: true,
45
46
  });
46
47
 
47
48
  // We resolve this query here and will not execute it. This is because the root
@@ -3,7 +3,7 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
5
  const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
6
- const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
6
+ const { isSqlite, isMySql, hasBatchInsert } = require('../../utils/knexUtils');
7
7
  const { isObject, isString } = require('../../utils/objectUtils');
8
8
 
9
9
  // Base class for all insert operations.
@@ -27,7 +27,7 @@ class InsertOperation extends QueryBuilderOperation {
27
27
  }
28
28
 
29
29
  async onBefore2(builder, result) {
30
- if (this.models.length > 1 && !isPostgres(builder.knex()) && !isMsSql(builder.knex())) {
30
+ if (this.models.length > 1 && !hasBatchInsert(builder.knex())) {
31
31
  throw new Error('batch insert only works with Postgresql and SQL Server');
32
32
  } else {
33
33
  await callBeforeInsert(builder, this.models);
@@ -3,6 +3,7 @@
3
3
  const { DeleteOperation } = require('./DeleteOperation');
4
4
  const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
+ const { addWhereGroup } = require('../whereGroups');
6
7
 
7
8
  class InstanceDeleteOperation extends DeleteOperation {
8
9
  constructor(name, opt) {
@@ -20,7 +21,7 @@ class InstanceDeleteOperation extends DeleteOperation {
20
21
  super.onBuild(builder);
21
22
 
22
23
  assertHasId(this.instance);
23
- builder.findById(this.instance.$id());
24
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
24
25
  }
25
26
 
26
27
  async onAfter2(builder, result) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { FindOperation } = require('./FindOperation');
4
4
  const { assertHasId } = require('../../utils/assert');
5
+ const { addWhereGroup } = require('../whereGroups');
5
6
 
6
7
  class InstanceFindOperation extends FindOperation {
7
8
  constructor(name, opt) {
@@ -11,7 +12,7 @@ class InstanceFindOperation extends FindOperation {
11
12
 
12
13
  onBuild(builder) {
13
14
  assertHasId(this.instance);
14
- builder.findById(this.instance.$id());
15
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
15
16
  }
16
17
  }
17
18
 
@@ -4,6 +4,8 @@ const { UpdateOperation } = require('./UpdateOperation');
4
4
  const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
6
  const { isObject } = require('../../utils/objectUtils');
7
+ const { addWhereGroup } = require('../whereGroups');
8
+ const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
7
9
 
8
10
  class InstanceUpdateOperation extends UpdateOperation {
9
11
  constructor(name, opt) {
@@ -18,6 +20,7 @@ class InstanceUpdateOperation extends UpdateOperation {
18
20
 
19
21
  if (!this.model) {
20
22
  this.model = this.instance;
23
+ removeOverriddenQueryProps(this.model);
21
24
  }
22
25
 
23
26
  return retVal;
@@ -27,7 +30,7 @@ class InstanceUpdateOperation extends UpdateOperation {
27
30
  super.onBuild(builder);
28
31
 
29
32
  assertHasId(this.instance);
30
- builder.findById(this.instance.$id());
33
+ addWhereGroup(builder, () => builder.findById(this.instance.$id()));
31
34
  }
32
35
 
33
36
  async onAfter2(builder, result) {
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
+ const { RangeOperation } = require('./RangeOperation');
5
+ const { FirstOperation } = require('./FirstOperation');
6
+ const { isString, isNumber } = require('../../utils/objectUtils');
7
+ const { hasWindowFunctions, isKnexRaw } = require('../../utils/knexUtils');
8
+ const { Selection, isSameTable, withoutSchema } = require('./select/Selection');
9
+
10
+ // Without underscores, so that column name mappers don't change it.
11
+ const rowNumberColumn = 'objectiontmprownum';
12
+
13
+ // Applies the `limit()`, `offset()` and `first()` of a relation query that
14
+ // fetches the related models of many parents to each parent separately.
15
+ class LimitPerParentOperation extends QueryBuilderOperation {
16
+ constructor(name, opt) {
17
+ super(name, opt);
18
+ this.findOperation = opt.findOperation;
19
+ }
20
+
21
+ onBuild(builder) {
22
+ builder.addInternalSelection(rowNumberColumn);
23
+ }
24
+
25
+ // Return the limit and offset of the query, built into `knexBuilder` by any
26
+ // of its operations, or `null` if it has none:
27
+ getRange(knexBuilder, builder) {
28
+ // `page()` and `range()` keep limiting all parents, as their total does.
29
+ if (!builder.has(RangeOperation)) {
30
+ const { limit = null, offset = null } = knexBuilder._single;
31
+ // `first()` only limits the query with `useLimitInFirst`:
32
+ const isFirst = builder.has(FirstOperation);
33
+
34
+ if (limit !== null || offset !== null || isFirst) {
35
+ return { limit: isFirst ? 1 : limit, offset: offset ?? 0 };
36
+ }
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ // Called by `buildKnexQuery()` after the default table and select are set.
43
+ onAfterBuildKnex(knexBuilder, builder) {
44
+ const knex = builder.knex();
45
+ const range = this.getRange(knexBuilder, builder);
46
+
47
+ if (!range) {
48
+ return knexBuilder;
49
+ } else if (!hasWindowFunctions(knex)) {
50
+ // `WhereInEagerOperation` fetches the related models of each parent with
51
+ // a separate query then, so only `first()` still needs to be applied:
52
+ return range.limit === null ? knexBuilder : knexBuilder.limit(range.limit);
53
+ } else {
54
+ const { limit, offset } = range;
55
+ // Number the rows of each parent one level up, aliased as the related
56
+ // table, so that the order can refer to the selected columns and their
57
+ // aliases as in the query itself, e.g. to `id` when a joined table has
58
+ // an `id` too. The order moves into the window function, as SQL Server
59
+ // doesn't allow it in subqueries. An alias can't have a schema.
60
+ const table = withoutSchema(builder.tableRef());
61
+ // knex only accepts raw partitions wrapped in objects:
62
+ const partition = this.findOperation.ownerKeys(builder, table).map((column) => ({ column }));
63
+ const numberedQuery = knex
64
+ .queryBuilder()
65
+ .select('*')
66
+ .rowNumber(rowNumberColumn, (window) =>
67
+ window.orderBy(getWindowOrder(builder, knex, table)).partitionBy(partition),
68
+ )
69
+ .from(
70
+ knexBuilder.clone().clear('with').clear('limit').clear('offset').clear('order').as(table),
71
+ );
72
+
73
+ // Select the rows in range with the query itself, to keep its settings
74
+ // like `timeout()`, and its common table expressions.
75
+ knexBuilder
76
+ .clear('select')
77
+ .clear('where')
78
+ .clear('join')
79
+ .clear('union')
80
+ .clear('group')
81
+ .clear('having')
82
+ .clear('order')
83
+ .clear('limit')
84
+ .clear('offset')
85
+ .select('*')
86
+ .from(numberedQuery.as(rowNumberColumn))
87
+ .where(rowNumberColumn, '>', offset)
88
+ .orderBy(rowNumberColumn);
89
+
90
+ if (limit !== null) {
91
+ // knex also accepts raw offsets:
92
+ const end = isNumber(offset) ? offset + limit : knex.raw('? + ?', [offset, limit]);
93
+ knexBuilder.where(rowNumberColumn, '<=', end);
94
+ }
95
+
96
+ return knexBuilder;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
102
+ // of knex's window functions, which can only refer to the related table by its
103
+ // alias `table`.
104
+ function getWindowOrder(builder, knex, table) {
105
+ const order = [];
106
+ const tableRef = builder.tableRef();
107
+
108
+ const getAliasedColumn = (column) => {
109
+ const selection = isString(column) && Selection.create(column);
110
+ return selection?.table && isSameTable(builder, selection.table, tableRef)
111
+ ? `${table}.${selection.column}`
112
+ : column;
113
+ };
114
+
115
+ builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
116
+ const args = op.getKnexArgs(builder);
117
+
118
+ if (op.name === 'orderByRaw') {
119
+ const [sql, bindings] = args;
120
+ order.push({ column: isKnexRaw(sql) ? sql : knex.raw(sql, bindings) });
121
+ } else {
122
+ const [column, direction, nulls] = args;
123
+ const items = Array.isArray(column)
124
+ ? column.map((item) => (isString(item) ? { column: item } : item))
125
+ : [{ column, order: direction, nulls }];
126
+
127
+ for (const item of items) {
128
+ const aliasedColumn = getAliasedColumn(item.column);
129
+
130
+ if (item.nulls) {
131
+ // Not all databases support `nulls first` and `nulls last`:
132
+ order.push({
133
+ column: knex.raw('(case when ? is null then 0 else 1 end)', [
134
+ isString(aliasedColumn) ? knex.ref(aliasedColumn) : aliasedColumn,
135
+ ]),
136
+ order: item.nulls === 'first' ? 'asc' : 'desc',
137
+ });
138
+ }
139
+
140
+ // Unlike in `orderBy()`, knex adds the direction to the window as it is:
141
+ order.push({ column: aliasedColumn, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
142
+ }
143
+ }
144
+ });
145
+
146
+ // SQL Server requires an order in the window function:
147
+ return order.length ? order : [{ column: knex.raw('(select null)') }];
148
+ }
149
+
150
+ module.exports = {
151
+ LimitPerParentOperation,
152
+ };
@@ -6,9 +6,11 @@ const hookNameToHasMethodName = {
6
6
  onBefore2: 'hasOnBefore2',
7
7
  onBefore3: 'hasOnBefore3',
8
8
  onBuild: 'hasOnBuild',
9
+ onAfterBuild: 'hasOnAfterBuild',
9
10
  onBuildKnex: 'hasOnBuildKnex',
10
11
  onRawResult: 'hasOnRawResult',
11
12
  queryExecutor: 'hasQueryExecutor',
13
+ queryBatches: 'hasQueryBatches',
12
14
  onAfter1: 'hasOnAfter1',
13
15
  onAfter2: 'hasOnAfter2',
14
16
  onAfter3: 'hasOnAfter3',
@@ -107,6 +109,20 @@ class QueryBuilderOperation {
107
109
  return this.onBuild !== QueryBuilderOperation.prototype.onBuild;
108
110
  }
109
111
 
112
+ // This is called after `onBuild` when the query is executed, right before the
113
+ // knex query is built and executed, but not when the query is only built,
114
+ // e.g. with `toKnexQuery()` or `resultSize()`. Use it for work that needs the
115
+ // built query, like queries that have to run before it. It's also called for
116
+ // queries that are resolved or rejected without executing, e.g. with `none()`
117
+ // or `resolve()`, so check `builder.isExplicitlyResolvedOrRejected()` first.
118
+ //
119
+ // This method can be asynchronous.
120
+ // This method should never call any methods that add operations to the builder.
121
+ onAfterBuild(builder) {}
122
+ hasOnAfterBuild() {
123
+ return this.onAfterBuild !== QueryBuilderOperation.prototype.onAfterBuild;
124
+ }
125
+
110
126
  // This is called when the knex query is built. Here you should only call knex
111
127
  // methods. You may call getters and other immutable methods of the `builder`
112
128
  // but you should never call methods that add QueryBuilderOperations.
@@ -177,6 +193,21 @@ class QueryBuilderOperation {
177
193
  return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor;
178
194
  }
179
195
 
196
+ // This is called after `onAfterBuild` when the query is executed. It can
197
+ // return an array of batches to build and execute the knex query once per
198
+ // batch, e.g. when a filter has more values than the database accepts in one
199
+ // statement. The batch being built is in `builder._queryBatch`, and the
200
+ // results of all batches are merged before `onRawResult` is called. Return
201
+ // null to execute the query once.
202
+ //
203
+ // This method must be synchronous.
204
+ queryBatches(builder) {
205
+ return null;
206
+ }
207
+ hasQueryBatches() {
208
+ return this.queryBatches !== QueryBuilderOperation.prototype.queryBatches;
209
+ }
210
+
180
211
  // This is called if an error occurs in the query execution.
181
212
  //
182
213
  // This method must return a QueryBuilder instance.
@@ -3,6 +3,7 @@
3
3
  const { DelegateOperation } = require('./DelegateOperation');
4
4
  const { FindByIdOperation } = require('./FindByIdOperation');
5
5
  const { UpdateOperation } = require('./UpdateOperation');
6
+ const { addWhereGroup, assignWhereGroup } = require('../whereGroups');
6
7
 
7
8
  class UpdateAndFetchOperation extends DelegateOperation {
8
9
  constructor(name, opt) {
@@ -41,7 +42,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
41
42
 
42
43
  onBuild(builder) {
43
44
  if (!this.skipIdWhere) {
44
- builder.findById(this.id);
45
+ addWhereGroup(builder, () => builder.findById(this.id));
45
46
  }
46
47
 
47
48
  super.onBuild(builder);
@@ -78,10 +79,17 @@ class UpdateAndFetchOperation extends DelegateOperation {
78
79
  return fetched ? this.model : undefined;
79
80
  }
80
81
 
81
- toFindOperation() {
82
- return new FindByIdOperation('findById', {
83
- id: this.id,
84
- });
82
+ toFindOperation(builder) {
83
+ // The find operation of the delegate keeps its filters, e.g. the ones that
84
+ // many-to-many updates move to a subquery when the query is built.
85
+ const delegate = this.delegate.toFindOperation(builder);
86
+
87
+ // Keep the id condition in its own where group, like `onBuild()` does. The
88
+ // delegate isn't named `findById`, as that would put all the wheres that it
89
+ // adds in the user's group.
90
+ return delegate
91
+ ? new FindByIdDelegateOperation('find', { delegate, id: this.id })
92
+ : assignWhereGroup(new FindByIdOperation('findById', { id: this.id }));
85
93
  }
86
94
 
87
95
  clone() {
@@ -94,6 +102,15 @@ class UpdateAndFetchOperation extends DelegateOperation {
94
102
  }
95
103
  }
96
104
 
105
+ // Finds the rows of a delegate find operation by id, like `UpdateAndFetchOperation`
106
+ // updates them.
107
+ class FindByIdDelegateOperation extends DelegateOperation {
108
+ onBuild(builder) {
109
+ super.onBuild(builder);
110
+ addWhereGroup(builder, () => builder.findById(this.opt.id));
111
+ }
112
+ }
113
+
97
114
  module.exports = {
98
115
  UpdateAndFetchOperation,
99
116
  };
@@ -1,12 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const { ref } = require('../../queryBuilder/ReferenceBuilder');
4
- const { isEmpty } = require('../../utils/objectUtils');
5
- const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
4
+ const { isEmpty, isObject } = require('../../utils/objectUtils');
5
+ const {
6
+ isFieldExpressionKey,
7
+ toPostgresJsonPathLiteral,
8
+ } = require('../../utils/parseFieldExpression');
6
9
  const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
7
10
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
8
11
  const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
9
12
  const { StaticHookArguments } = require('../StaticHookArguments');
13
+ const { ValidationErrorType } = require('../../model/ValidationError');
10
14
 
11
15
  class UpdateOperation extends QueryBuilderOperation {
12
16
  constructor(name, opt) {
@@ -107,17 +111,39 @@ async function callStaticAfterUpdate(builder, result) {
107
111
 
108
112
  function convertFieldExpressionsToRaw(builder, model, json) {
109
113
  const knex = builder.knex();
114
+ const modelClass = builder.modelClass();
115
+ const declaredColumns = getDeclaredColumns(modelClass);
110
116
  const convertedJson = {};
111
117
 
112
118
  for (const key of Object.keys(json)) {
113
119
  let val = json[key];
114
120
 
115
- if (key.indexOf(':') > -1) {
121
+ if (isFieldExpressionKey(key, declaredColumns)) {
116
122
  // 'col:attr' : ref('other:lol') is transformed to
117
- // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
123
+ // "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
124
+ // with the column qualified, as it is ambiguous with `excluded` in merge().
118
125
  warnIfNotPostgres(knex);
119
126
 
120
127
  let parsed = ref(key);
128
+
129
+ // The field expression would be lost or produce invalid SQL next to a
130
+ // value for the whole json column. Report it with the property names:
131
+ if (Object.prototype.hasOwnProperty.call(json, parsed.column)) {
132
+ const property = modelClass.columnNameToPropertyName(parsed.column);
133
+ throw modelClass.createValidationError({
134
+ type: ValidationErrorType.ModelValidation,
135
+ data: {
136
+ [modelClass.columnNameToPropertyName(key)]: [
137
+ {
138
+ message: `can't be combined with a value for the json column '${property}'`,
139
+ keyword: null,
140
+ params: { property },
141
+ },
142
+ ],
143
+ },
144
+ });
145
+ }
146
+
121
147
  let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
122
148
  let valuePlaceholder = '?';
123
149
 
@@ -129,7 +155,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
129
155
 
130
156
  convertedJson[parsed.column] = knex.raw(
131
157
  `jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
132
- [convertedJson[parsed.column] || parsed.column, val],
158
+ [convertedJson[parsed.column] || `${builder.tableRef()}.${parsed.column}`, val],
133
159
  );
134
160
 
135
161
  delete model[key];
@@ -141,7 +167,30 @@ function convertFieldExpressionsToRaw(builder, model, json) {
141
167
  return convertedJson;
142
168
  }
143
169
 
170
+ const declaredColumnsByModelClass = new WeakMap();
171
+
172
+ // The keys of `json` are column names, so map the properties with a ':' that the
173
+ // schema declares to their columns, once per model class. Properties that only
174
+ // the schema returned by `$beforeValidate()` declares aren't known here.
175
+ function getDeclaredColumns(modelClass) {
176
+ let declaredColumns = declaredColumnsByModelClass.get(modelClass);
177
+
178
+ if (!declaredColumns) {
179
+ const jsonSchema = modelClass.getJsonSchema();
180
+ const properties = jsonSchema && jsonSchema.properties;
181
+ declaredColumns = Object.fromEntries(
182
+ (isObject(properties) ? Object.keys(properties) : [])
183
+ .filter((key) => key.includes(':'))
184
+ .map((key) => [modelClass.propertyNameToColumnName(key), true]),
185
+ );
186
+ declaredColumnsByModelClass.set(modelClass, declaredColumns);
187
+ }
188
+
189
+ return declaredColumns;
190
+ }
191
+
144
192
  module.exports = {
145
193
  UpdateOperation,
146
194
  convertFieldExpressionsToRaw,
195
+ getDeclaredColumns,
147
196
  };
@@ -3,11 +3,11 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
 
5
5
  // Builds a set of where operations inside a single knex `where(callback)`
6
- // group, i.e. wraps them in parentheses. See `wrapWhereGroups`.
6
+ // group, i.e. wraps them in parentheses. See `getWhereGroupOperations()`.
7
7
  class WhereGroupOperation extends QueryBuilderOperation {
8
8
  constructor(name, opt) {
9
9
  super(name, opt);
10
- this.operations = [];
10
+ this.operations = opt.operations;
11
11
  }
12
12
 
13
13
  onBuildKnex(knexBuilder, builder) {
@@ -28,12 +28,6 @@ class WhereGroupOperation extends QueryBuilderOperation {
28
28
  }
29
29
  });
30
30
  }
31
-
32
- clone() {
33
- const clone = super.clone();
34
- clone.operations = this.operations.map((op) => op.clone());
35
- return clone;
36
- }
37
31
  }
38
32
 
39
33
  module.exports = {
@@ -20,6 +20,11 @@ class JoinEagerOperation extends EagerOperation {
20
20
  }
21
21
 
22
22
  onBefore3(builder) {
23
+ return this.fetchTableMetadata(builder);
24
+ }
25
+
26
+ // Also called by `resultSize()`, which builds the query without executing it.
27
+ fetchTableMetadata(builder) {
23
28
  return this.joiner
24
29
  .setExpression(this.buildFinalExpression(builder))
25
30
  .setModifiers(this.buildFinalModifiers(builder))