objection 3.2.0 → 3.4.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 (64) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +220 -13
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/modelId.js +10 -2
  5. package/lib/model/modelQueryProps.js +7 -0
  6. package/lib/queryBuilder/QueryBuilder.js +191 -8
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  8. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  9. package/lib/queryBuilder/RelationExpression.js +13 -1
  10. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  11. package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
  12. package/lib/queryBuilder/graph/GraphOptions.js +30 -1
  13. package/lib/queryBuilder/graph/GraphUpsert.js +38 -1
  14. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  15. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  16. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  17. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
  18. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  19. package/lib/queryBuilder/join/RelationJoiner.js +80 -27
  20. package/lib/queryBuilder/join/utils.js +20 -2
  21. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  22. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  23. package/lib/queryBuilder/operations/InsertOperation.js +129 -13
  24. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  25. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  26. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  27. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  28. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  29. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  30. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  31. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  32. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  33. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  34. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  35. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  36. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  37. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  38. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  39. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  40. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  41. package/lib/queryBuilder/whereGroups.js +125 -0
  42. package/lib/relations/Relation.js +5 -2
  43. package/lib/relations/RelationFindOperation.js +46 -2
  44. package/lib/relations/RelationProperty.js +3 -2
  45. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  46. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  47. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  48. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  49. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  50. package/lib/relations/manyToMany/ManyToManyRelation.js +104 -51
  51. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
  52. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  53. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  54. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  55. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +12 -2
  57. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  58. package/lib/utils/assert.js +9 -0
  59. package/lib/utils/deprecate.js +6 -0
  60. package/lib/utils/identifierMapping.js +15 -1
  61. package/lib/utils/parseFieldExpression.js +28 -0
  62. package/lib/utils/wrapDbError.js +36 -0
  63. package/package.json +8 -7
  64. package/typings/objection/index.d.ts +281 -29
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { normalizeIds } = require('../../utils/normalizeIds');
4
4
  const { RelateOperation } = require('../../queryBuilder/operations/RelateOperation');
5
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
5
6
 
6
7
  class HasManyRelateOperation extends RelateOperation {
7
8
  onAdd(_, args) {
@@ -31,8 +32,10 @@ class HasManyRelateOperation extends RelateOperation {
31
32
  .copyFrom(builder, /returning/)
32
33
  .copyFrom(builder, builder.constructor.JoinSelector)
33
34
  .copyFrom(builder, builder.constructor.WhereSelector)
34
- .findByIds(this.ids)
35
- .modify(this.relation.modify);
35
+ .modify((query) => {
36
+ addWhereGroup(query, () => query.findByIds(this.ids));
37
+ addWhereGroup(query, () => query.modify(this.relation.modify));
38
+ });
36
39
  }
37
40
  }
38
41
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { UnrelateOperation } = require('../../queryBuilder/operations/UnrelateOperation');
4
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
4
5
 
5
6
  class HasManyUnrelateOperation extends UnrelateOperation {
6
7
  queryExecutor(builder) {
@@ -19,8 +20,10 @@ class HasManyUnrelateOperation extends UnrelateOperation {
19
20
  .patch(patch)
20
21
  .copyFrom(builder, builder.constructor.JoinSelector)
21
22
  .copyFrom(builder, builder.constructor.WhereSelector)
22
- .whereInComposite(relatedRefs, ownerValues)
23
- .modify(this.relation.modify);
23
+ .modify((query) => {
24
+ addWhereGroup(query, () => query.whereInComposite(relatedRefs, ownerValues));
25
+ addWhereGroup(query, () => query.modify(this.relation.modify));
26
+ });
24
27
  }
25
28
  }
26
29
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
4
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
4
5
 
5
6
  // This mixin contains the shared code for all modify operations (update, delete, relate, unrelate)
6
7
  // for ManyToManyRelation operations.
@@ -31,7 +32,7 @@ const ManyToManyModifyMixin = (Operation) => {
31
32
  this.removeFiltersFromMainQuery(builder);
32
33
 
33
34
  // Add a single where clause that uses the created subquery.
34
- this.applyModifyFilterForRelatedTable(builder);
35
+ addWhereGroup(builder, () => this.applyModifyFilterForRelatedTable(builder));
35
36
  }
36
37
 
37
38
  return super.onBuild(builder);
@@ -70,6 +71,10 @@ const ManyToManyModifyMixin = (Operation) => {
70
71
  return builder.whereInComposite(idRefs, subquery);
71
72
  }
72
73
 
74
+ // Modifies the join rows of all related rows that match the filters. If the filters
75
+ // reference join table columns, this also modifies join rows that don't match them
76
+ // but share a related row with one that does (#1853). The dialect specific mixins
77
+ // override this to only modify the join rows that match the filters themselves.
73
78
  applyModifyFilterForJoinTable(builder) {
74
79
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
75
80
  const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
@@ -85,10 +90,19 @@ const ManyToManyModifyMixin = (Operation) => {
85
90
  }
86
91
 
87
92
  toFindOperation() {
88
- return new ManyToManyFindOperation('find', {
93
+ const opt = {
89
94
  relation: this.relation,
90
95
  owner: this.owner,
91
- });
96
+ };
97
+
98
+ if (this.modifyMainQuery && this.modifyFilterSubquery) {
99
+ // `onBuild` has already moved the main query's filters into the
100
+ // subquery and removed them. Apply them to the find query too.
101
+ // See issue #2266.
102
+ return new ManyToManyModifyFindOperation('find', { ...opt, modifyOperation: this });
103
+ } else {
104
+ return new ManyToManyFindOperation('find', opt);
105
+ }
92
106
  }
93
107
 
94
108
  clone() {
@@ -99,6 +113,15 @@ const ManyToManyModifyMixin = (Operation) => {
99
113
  };
100
114
  };
101
115
 
116
+ class ManyToManyModifyFindOperation extends ManyToManyFindOperation {
117
+ onBuild(builder) {
118
+ super.onBuild(builder);
119
+ addWhereGroup(builder, () =>
120
+ this.opt.modifyOperation.applyModifyFilterForRelatedTable(builder),
121
+ );
122
+ }
123
+ }
124
+
102
125
  module.exports = {
103
126
  ManyToManyModifyMixin,
104
127
  };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
+ const { isObject } = require('../../utils/objectUtils');
5
+
6
+ // We need to override this mixin for mysql because mysql doesn't allow a
7
+ // statement that modifies the join table to read the related table in a
8
+ // subquery, if a trigger on the join table modifies the related table:
9
+ //
10
+ // ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG
11
+ //
12
+ // So the join rows that match the filters are fetched in a separate query
13
+ // before the main query, which then only modifies these rows (#1853). Mysql
14
+ // has no row ids and the join table's primary key isn't known, so the rows are
15
+ // identified by the values of all their columns.
16
+ //
17
+ // https://stackoverflow.com/a/2314264/3729316
18
+ const ManyToManyMySqlModifyMixin = (Operation) => {
19
+ return class extends ManyToManyModifyMixin(Operation) {
20
+ applyModifyFilterForJoinTable(builder) {
21
+ const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
22
+ const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
23
+
24
+ const ownerValues = this.owner.getProps(this.relation);
25
+ const subquery = this.modifyFilterSubquery.clone().select(`${tableRef}.*`);
26
+
27
+ return builder
28
+ .runBefore(async (_, builder) => {
29
+ // Fetch the raw rows: no models, so that no hooks run for them.
30
+ const rows = await subquery.toKnexQuery();
31
+
32
+ if (rows.length) {
33
+ builder.where((builder) => {
34
+ for (const row of rows) {
35
+ builder.orWhere((builder) => {
36
+ for (const [column, value] of Object.entries(row)) {
37
+ builder.whereRaw('?? <=> ?', [
38
+ `${tableRef}.${column}`,
39
+ toComparableValue(value),
40
+ ]);
41
+ }
42
+ });
43
+ }
44
+ });
45
+ } else {
46
+ // Still execute the query so that it returns the expected
47
+ // result for the dialect, e.g. a count of 0 for deletes.
48
+ builder.where(false);
49
+ }
50
+ })
51
+ .whereInComposite(joinTableOwnerRefs, ownerValues);
52
+ }
53
+ };
54
+ };
55
+
56
+ // Some drivers return JSON columns parsed. Mysql parses strings that are
57
+ // compared to JSON columns, so the stringified value matches them again.
58
+ function toComparableValue(value) {
59
+ return isObject(value) && !(value instanceof Date) && !Buffer.isBuffer(value)
60
+ ? JSON.stringify(value)
61
+ : value;
62
+ }
63
+
64
+ module.exports = {
65
+ ManyToManyMySqlModifyMixin,
66
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
+
5
+ // Like `ManyToManySqliteModifyMixin`, this identifies the join rows to modify
6
+ // by their physical row id, so that only the join rows that match the filters
7
+ // are modified (#1853). `tableoid` makes `ctid` unique for partitioned tables.
8
+ const ManyToManyPostgresModifyMixin = (Operation) => {
9
+ return class extends ManyToManyModifyMixin(Operation) {
10
+ applyModifyFilterForJoinTable(builder) {
11
+ const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
12
+ const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
13
+ const rowIdRefs = [`${tableRef}.tableoid`, `${tableRef}.ctid`];
14
+
15
+ const ownerValues = this.owner.getProps(this.relation);
16
+ const subquery = this.modifyFilterSubquery.clone().select(rowIdRefs);
17
+
18
+ return builder
19
+ .whereInComposite(rowIdRefs, subquery)
20
+ .whereInComposite(joinTableOwnerRefs, ownerValues);
21
+ }
22
+ };
23
+ };
24
+
25
+ module.exports = {
26
+ ManyToManyPostgresModifyMixin,
27
+ };
@@ -4,12 +4,13 @@ const { getModel } = require('../../model/getModel');
4
4
  const { Relation } = require('../Relation');
5
5
  const { RelationProperty } = require('../RelationProperty');
6
6
 
7
- const { isSqlite } = require('../../utils/knexUtils');
7
+ const { isSqlite, isMySql, isPostgres } = require('../../utils/knexUtils');
8
8
  const { inheritModel } = require('../../model/inheritModel');
9
9
  const { resolveModel } = require('../../utils/resolveModel');
10
10
  const { mapAfterAllReturn } = require('../../utils/promiseUtils');
11
11
  const { isFunction, isObject, isString } = require('../../utils/objectUtils');
12
12
  const { createModifier } = require('../../utils/createModifier');
13
+ const { addWhereGroup } = require('../../queryBuilder/whereGroups');
13
14
 
14
15
  const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
15
16
  const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
@@ -19,9 +20,15 @@ const { ManyToManyUnrelateOperation } = require('./unrelate/ManyToManyUnrelateOp
19
20
  const {
20
21
  ManyToManyUnrelateSqliteOperation,
21
22
  } = require('./unrelate/ManyToManyUnrelateSqliteOperation');
23
+ const { ManyToManyUnrelateMySqlOperation } = require('./unrelate/ManyToManyUnrelateMySqlOperation');
24
+ const {
25
+ ManyToManyUnrelatePostgresOperation,
26
+ } = require('./unrelate/ManyToManyUnrelatePostgresOperation');
22
27
 
23
28
  const { ManyToManyUpdateOperation } = require('./update/ManyToManyUpdateOperation');
24
29
  const { ManyToManyUpdateSqliteOperation } = require('./update/ManyToManyUpdateSqliteOperation');
30
+ const { ManyToManyUpdateMySqlOperation } = require('./update/ManyToManyUpdateMySqlOperation');
31
+ const { ManyToManyUpdatePostgresOperation } = require('./update/ManyToManyUpdatePostgresOperation');
25
32
 
26
33
  const { ManyToManyDeleteOperation } = require('./delete/ManyToManyDeleteOperation');
27
34
  const { ManyToManyDeleteSqliteOperation } = require('./delete/ManyToManyDeleteSqliteOperation');
@@ -68,10 +75,39 @@ class ManyToManyRelation extends Relation {
68
75
  return [];
69
76
  }
70
77
 
78
+ isJoinTableRelatedTable(builder) {
79
+ return builder.tableNameFor(this.joinTable) === builder.tableNameFor(this.relatedModelClass);
80
+ }
81
+
82
+ // Returns the alias of the join table in `findQuery`. If the join table is
83
+ // also the related table, it needs an alias of its own (#1803).
84
+ joinTableAliasFor(builder) {
85
+ const joinTableRef = builder.tableRefFor(this.joinTable);
86
+
87
+ if (this.isJoinTableRelatedTable(builder)) {
88
+ return this.ownerModelClass.joinTableAlias(joinTableRef);
89
+ } else {
90
+ return joinTableRef;
91
+ }
92
+ }
93
+
94
+ // The modify operations can't tell the join rows apart from the related rows
95
+ // if the join table is also the related table, so they would modify the
96
+ // wrong rows (#1803).
97
+ assertCanModifyJoinTable(builder, operationName) {
98
+ if (this.isJoinTableRelatedTable(builder)) {
99
+ throw this.createError(
100
+ `${operationName} is not supported when the join table is also the related table`,
101
+ );
102
+ }
103
+ }
104
+
71
105
  findQuery(builder, owner) {
72
- const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
73
106
  const joinTable = builder.tableNameFor(this.joinTable);
74
- const joinTableAlias = builder.tableRefFor(this.joinTable);
107
+ const joinTableAlias = this.joinTableAliasFor(builder);
108
+ const joinTableOwnerRefs = this.joinTableOwnerProp
109
+ .refs(builder)
110
+ .map((ref) => ref.table(joinTableAlias));
75
111
 
76
112
  let joinTableSelect = this.joinTableModelClass
77
113
  .query()
@@ -85,13 +121,13 @@ class ManyToManyRelation extends Relation {
85
121
  builder.join(joinTableSelect, (join) => {
86
122
  this.relatedProp.forEach((i) => {
87
123
  const relatedRef = this.relatedProp.ref(builder, i);
88
- const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
124
+ const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
89
125
 
90
126
  join.on(relatedRef, joinTableRelatedRef);
91
127
  });
92
128
  });
93
129
 
94
- owner.buildFindQuery(builder, this, joinTableOwnerRefs);
130
+ addWhereGroup(builder, () => owner.buildFindQuery(builder, this, joinTableOwnerRefs));
95
131
  return this.applyModify(builder);
96
132
  }
97
133
 
@@ -160,47 +196,25 @@ class ManyToManyRelation extends Relation {
160
196
  }
161
197
 
162
198
  update(builder, owner) {
163
- if (isSqlite(builder.knex())) {
164
- return new ManyToManyUpdateSqliteOperation('update', {
165
- relation: this,
166
- owner,
167
- });
168
- } else {
169
- return new ManyToManyUpdateOperation('update', {
170
- relation: this,
171
- owner,
172
- });
173
- }
199
+ return new (getUpdateOperationClass(builder))('update', {
200
+ relation: this,
201
+ owner,
202
+ });
174
203
  }
175
204
 
176
205
  patch(builder, owner) {
177
- if (isSqlite(builder.knex())) {
178
- return new ManyToManyUpdateSqliteOperation('patch', {
179
- modelOptions: { patch: true },
180
- relation: this,
181
- owner,
182
- });
183
- } else {
184
- return new ManyToManyUpdateOperation('patch', {
185
- modelOptions: { patch: true },
186
- relation: this,
187
- owner,
188
- });
189
- }
206
+ return new (getUpdateOperationClass(builder))('patch', {
207
+ modelOptions: { patch: true },
208
+ relation: this,
209
+ owner,
210
+ });
190
211
  }
191
212
 
192
213
  delete(builder, owner) {
193
- if (isSqlite(builder.knex())) {
194
- return new ManyToManyDeleteSqliteOperation('delete', {
195
- relation: this,
196
- owner,
197
- });
198
- } else {
199
- return new ManyToManyDeleteOperation('delete', {
200
- relation: this,
201
- owner,
202
- });
203
- }
214
+ return new (getDeleteOperationClass(builder))('delete', {
215
+ relation: this,
216
+ owner,
217
+ });
204
218
  }
205
219
 
206
220
  relate(builder, owner) {
@@ -211,17 +225,12 @@ class ManyToManyRelation extends Relation {
211
225
  }
212
226
 
213
227
  unrelate(builder, owner) {
214
- if (isSqlite(builder.knex())) {
215
- return new ManyToManyUnrelateSqliteOperation('unrelate', {
216
- relation: this,
217
- owner,
218
- });
219
- } else {
220
- return new ManyToManyUnrelateOperation('unrelate', {
221
- relation: this,
222
- owner,
223
- });
224
- }
228
+ this.assertCanModifyJoinTable(builder, 'unrelate');
229
+
230
+ return new (getUnrelateOperationClass(builder))('unrelate', {
231
+ relation: this,
232
+ owner,
233
+ });
225
234
  }
226
235
 
227
236
  createJoinModels(ownerId, related) {
@@ -498,6 +507,50 @@ function finalizeJoinModelClass(ctx) {
498
507
  return ctx;
499
508
  }
500
509
 
510
+ // The modify operations need dialect specific code to only modify the join
511
+ // rows that match the filters, see the `ManyToMany*ModifyMixin` classes.
512
+ function getOperationClass(builder, { sqlite, mysql, postgres, other }) {
513
+ const knex = builder.knex();
514
+
515
+ if (isSqlite(knex)) {
516
+ return sqlite;
517
+ } else if (isMySql(knex)) {
518
+ return mysql;
519
+ } else if (isPostgres(knex)) {
520
+ return postgres;
521
+ } else {
522
+ return other;
523
+ }
524
+ }
525
+
526
+ function getUpdateOperationClass(builder) {
527
+ return getOperationClass(builder, {
528
+ sqlite: ManyToManyUpdateSqliteOperation,
529
+ mysql: ManyToManyUpdateMySqlOperation,
530
+ postgres: ManyToManyUpdatePostgresOperation,
531
+ other: ManyToManyUpdateOperation,
532
+ });
533
+ }
534
+
535
+ function getUnrelateOperationClass(builder) {
536
+ return getOperationClass(builder, {
537
+ sqlite: ManyToManyUnrelateSqliteOperation,
538
+ mysql: ManyToManyUnrelateMySqlOperation,
539
+ postgres: ManyToManyUnrelatePostgresOperation,
540
+ other: ManyToManyUnrelateOperation,
541
+ });
542
+ }
543
+
544
+ function getDeleteOperationClass(builder) {
545
+ // Deletes don't modify the join table, so only sqlite needs its own operation.
546
+ return getOperationClass(builder, {
547
+ sqlite: ManyToManyDeleteSqliteOperation,
548
+ mysql: ManyToManyDeleteOperation,
549
+ postgres: ManyToManyDeleteOperation,
550
+ other: ManyToManyDeleteOperation,
551
+ });
552
+ }
553
+
501
554
  module.exports = {
502
555
  ManyToManyRelation,
503
556
  };
@@ -29,7 +29,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
29
29
 
30
30
  // Also select all extra columns.
31
31
  for (const extra of this.relation.joinTableExtras) {
32
- const joinTable = builder.tableRefFor(this.relation.joinTable);
32
+ const joinTable = this.relation.joinTableAliasFor(builder);
33
33
  builder.select(`${joinTable}.${extra.joinTableCol} as ${extra.aliasCol}`);
34
34
  }
35
35
  }
@@ -88,7 +88,9 @@ class ManyToManyFindOperation extends RelationFindOperation {
88
88
  // We must select the owner join columns so that we know for which owner model the related
89
89
  // models belong to after the requests.
90
90
  joinTableOwnerProp.forEach((i) => {
91
- const joinTableOwnerRef = joinTableOwnerProp.ref(builder, i);
91
+ const joinTableOwnerRef = joinTableOwnerProp
92
+ .ref(builder, i)
93
+ .table(this.relation.joinTableAliasFor(builder));
92
94
  const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
93
95
 
94
96
  builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
4
+ const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
5
+
6
+ class ManyToManyUnrelateMySqlOperation extends ManyToManyMySqlModifyMixin(
7
+ ManyToManyUnrelateOperationBase,
8
+ ) {
9
+ get modifyMainQuery() {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ module.exports = {
15
+ ManyToManyUnrelateMySqlOperation,
16
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { UnrelateOperation } = require('../../../queryBuilder/operations/UnrelateOperation');
4
+ const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
4
5
 
5
6
  class ManyToManyUnrelateOperationBase extends UnrelateOperation {
6
7
  queryExecutor(builder) {
@@ -10,7 +11,10 @@ class ManyToManyUnrelateOperationBase extends UnrelateOperation {
10
11
  .childQueryOf(builder)
11
12
  .delete();
12
13
 
13
- return this.applyModifyFilterForJoinTable(unrelateQuery).modify(this.relation.joinTableModify);
14
+ addWhereGroup(unrelateQuery, () => this.applyModifyFilterForJoinTable(unrelateQuery));
15
+ addWhereGroup(unrelateQuery, () => unrelateQuery.modify(this.relation.joinTableModify));
16
+
17
+ return unrelateQuery;
14
18
  }
15
19
 
16
20
  /* istanbul ignore next */
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
4
+ const { ManyToManyPostgresModifyMixin } = require('../ManyToManyPostgresModifyMixin');
5
+
6
+ class ManyToManyUnrelatePostgresOperation extends ManyToManyPostgresModifyMixin(
7
+ ManyToManyUnrelateOperationBase,
8
+ ) {
9
+ get modifyMainQuery() {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ module.exports = {
15
+ ManyToManyUnrelatePostgresOperation,
16
+ };
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
4
+ const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
5
+
6
+ class ManyToManyUpdateMySqlOperation extends ManyToManyMySqlModifyMixin(
7
+ ManyToManyUpdateOperationBase,
8
+ ) {}
9
+
10
+ module.exports = {
11
+ ManyToManyUpdateMySqlOperation,
12
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { UpdateOperation } = require('../../../queryBuilder/operations/UpdateOperation');
4
+ const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
4
5
 
5
6
  class ManyToManyUpdateOperationBase extends UpdateOperation {
6
7
  constructor(name, opt) {
@@ -25,6 +26,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
25
26
  }
26
27
  }
27
28
 
29
+ if (this.hasExtraProps) {
30
+ this.relation.assertCanModifyJoinTable(builder, `${this.name} with extra properties`);
31
+ }
32
+
28
33
  const res = super.onAdd(builder, args);
29
34
 
30
35
  if (this.hasExtraProps) {
@@ -44,9 +49,14 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
44
49
  .childQueryOf(builder)
45
50
  .patch(this.joinTablePatch);
46
51
 
47
- await this.applyModifyFilterForJoinTable(joinTableUpdateQuery).modify(
48
- this.relation.joinTableModify,
52
+ addWhereGroup(joinTableUpdateQuery, () =>
53
+ this.applyModifyFilterForJoinTable(joinTableUpdateQuery),
49
54
  );
55
+ addWhereGroup(joinTableUpdateQuery, () =>
56
+ joinTableUpdateQuery.modify(this.relation.joinTableModify),
57
+ );
58
+
59
+ await joinTableUpdateQuery;
50
60
  return result;
51
61
  } else {
52
62
  return result;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
4
+ const { ManyToManyPostgresModifyMixin } = require('../ManyToManyPostgresModifyMixin');
5
+
6
+ class ManyToManyUpdatePostgresOperation extends ManyToManyPostgresModifyMixin(
7
+ ManyToManyUpdateOperationBase,
8
+ ) {}
9
+
10
+ module.exports = {
11
+ ManyToManyUpdatePostgresOperation,
12
+ };
@@ -1,6 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ function assertHasIdColumn(modelClass) {
4
+ if (modelClass.getIdColumnArray().length === 0) {
5
+ throw new Error(`model ${modelClass.name} has no idColumn`);
6
+ }
7
+ }
8
+
3
9
  function assertHasId(model) {
10
+ assertHasIdColumn(model.constructor);
11
+
4
12
  if (!model.$hasId()) {
5
13
  const modelClass = model.constructor;
6
14
  const ids = modelClass.getIdColumnArray().join(', ');
@@ -21,5 +29,6 @@ function assertIdNotUndefined(id, message) {
21
29
 
22
30
  module.exports = {
23
31
  assertHasId,
32
+ assertHasIdColumn,
24
33
  assertIdNotUndefined,
25
34
  };
@@ -10,6 +10,12 @@ function deprecate(message) {
10
10
  }
11
11
  }
12
12
 
13
+ // Only meant for tests that need to see a deprecation message again.
14
+ function resetDeprecations() {
15
+ LOGGED_DEPRECATIONS.clear();
16
+ }
17
+
13
18
  module.exports = {
14
19
  deprecate,
20
+ resetDeprecations,
15
21
  };
@@ -168,10 +168,24 @@ function keyMapper(mapper) {
168
168
  };
169
169
  }
170
170
 
171
+ // Returns a function that only calls `mapper` for the part of the input string
172
+ // before the first `separator`, and leaves the rest untouched. For field
173
+ // expressions like `jsonColumn:some.jsonPath` this maps the column but keeps
174
+ // the json path as is. Column names can't contain `:`, so the first one marks
175
+ // the boundary.
176
+ function mapFirstPart(mapper, separator) {
177
+ return (str) => {
178
+ const idx = str.indexOf(separator);
179
+ return idx === -1 ? mapper(str) : mapper(str.slice(0, idx)) + str.slice(idx);
180
+ };
181
+ }
182
+
171
183
  function snakeCaseMappers(opt = {}) {
184
+ const format = (str) => snakeCase(str, opt);
185
+
172
186
  return {
173
187
  parse: keyMapper(memoize((str) => camelCase(str, opt))),
174
- format: keyMapper(memoize((str) => snakeCase(str, opt))),
188
+ format: keyMapper(memoize(opt.preserveJsonKeys ? mapFirstPart(format, ':') : format)),
175
189
  };
176
190
  }
177
191
 
@@ -46,6 +46,34 @@ function freezeParsedExpr(parsedExpr) {
46
46
  return parsedExpr;
47
47
  }
48
48
 
49
+ // Formats the json path of a parsed field expression as a Postgres text array
50
+ // literal, e.g. `{a,"",b}`. Keys that would otherwise be misread (empty keys,
51
+ // keys with whitespace, delimiters, quotes or backslashes and `NULL`) are
52
+ // double-quoted, with quotes and backslashes escaped.
53
+ function toPostgresJsonPath(access) {
54
+ const keys = access.map(({ ref }) => {
55
+ const key = String(ref);
56
+ return key === '' || /[{}",\\\s]/.test(key) || key.toUpperCase() === 'NULL'
57
+ ? `"${key.replace(/["\\]/g, '\\$&')}"`
58
+ : key;
59
+ });
60
+ return `{${keys.join(',')}}`;
61
+ }
62
+
63
+ // Formats the json path of a parsed field expression as a quoted string literal
64
+ // to be used in raw knex SQL, e.g. `'{a,b}'`. The path can't be passed as a
65
+ // binding, as knex doesn't keep the order of bindings in raw columns (e.g. in
66
+ // `whereIn(ref(...), values)`), so the keys need to be escaped instead: single
67
+ // quotes are doubled, and `?` is escaped so that knex doesn't read it as a
68
+ // binding placeholder. Backslashes are always doubled by `toPostgresJsonPath()`,
69
+ // so they can't escape the closing quote either.
70
+ function toPostgresJsonPathLiteral(access) {
71
+ const path = toPostgresJsonPath(access).replace(/'/g, "''").replace(/\?/g, '\\?');
72
+ return `'${path}'`;
73
+ }
74
+
49
75
  module.exports = {
50
76
  parseFieldExpression,
77
+ toPostgresJsonPath,
78
+ toPostgresJsonPathLiteral,
51
79
  };