objection 3.6.0 → 3.7.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 (35) hide show
  1. package/lib/model/AjvValidator.js +33 -10
  2. package/lib/model/modelClone.js +0 -2
  3. package/lib/queryBuilder/QueryBuilder.js +56 -10
  4. package/lib/queryBuilder/RelationExpression.js +6 -4
  5. package/lib/queryBuilder/graph/GraphOptions.js +27 -55
  6. package/lib/queryBuilder/graph/GraphUpsert.js +22 -0
  7. package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
  8. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +6 -1
  9. package/lib/queryBuilder/operations/UpdateOperation.js +11 -0
  10. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
  11. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
  12. package/lib/queryBuilder/parsers/relationExpressionParser.js +1 -1
  13. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +1 -1
  14. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
  15. package/lib/relations/Relation.js +1 -0
  16. package/lib/relations/RelationFindOperation.js +21 -39
  17. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
  18. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +39 -15
  19. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
  20. package/lib/relations/manyToMany/ManyToManyRelation.js +8 -16
  21. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
  22. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +2 -3
  23. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
  24. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
  25. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
  26. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
  27. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +7 -1
  28. package/lib/utils/identifierMapping.js +8 -17
  29. package/lib/utils/objectUtils.js +38 -6
  30. package/lib/utils/parseFieldExpression.js +10 -3
  31. package/package.json +1 -1
  32. package/typings/objection/index.d.ts +46 -26
  33. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
  34. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
  35. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
@@ -12,7 +12,6 @@ class RelationFindOperation extends FindOperation {
12
12
  this.alwaysReturnArray = false;
13
13
  this.assignResultToOwner = true;
14
14
  this.relationProperty = opt.relationProperty || this.relation.name;
15
- this.omitProps = [];
16
15
  this.alias = null;
17
16
  }
18
17
 
@@ -21,10 +20,9 @@ class RelationFindOperation extends FindOperation {
21
20
  this.relation.findQuery(builder, this.owner);
22
21
 
23
22
  if (this.owner.isModels) {
24
- this.warnOnMissingOwnerProps();
23
+ this.warnOnInvalidOwnerProps();
25
24
 
26
25
  if (this.assignResultToOwner) {
27
- this.warnOnArrayOwnerProps();
28
26
  this.selectMissingJoinColumns(builder);
29
27
  }
30
28
  }
@@ -70,11 +68,6 @@ class RelationFindOperation extends FindOperation {
70
68
 
71
69
  onAfter3(builder, related) {
72
70
  const isOneToOne = this.relation.isOneToOne();
73
- const intOpt = builder.internalOptions();
74
-
75
- if (!intOpt.keepImplicitJoinProps) {
76
- this.omitImplicitJoinProps(related);
77
- }
78
71
 
79
72
  if (!this.alwaysReturnArray && isOneToOne && related.length <= 1) {
80
73
  related = related[0] || undefined;
@@ -83,6 +76,14 @@ class RelationFindOperation extends FindOperation {
83
76
  return super.onAfter3(builder, related);
84
77
  }
85
78
 
79
+ warnOnInvalidOwnerProps() {
80
+ this.warnOnMissingOwnerProps();
81
+
82
+ if (this.assignResultToOwner) {
83
+ this.warnOnArrayOwnerProps();
84
+ }
85
+ }
86
+
86
87
  // Owner models without the join columns, e.g. loaded with a partial
87
88
  // `select()`, silently get no related models (#1832). Only `undefined`
88
89
  // is reported, as `null` is a legit value for a missing relation, and
@@ -111,7 +112,11 @@ class RelationFindOperation extends FindOperation {
111
112
  // assigned to their owners, so all owners silently get no related models
112
113
  // (#2667). `$relatedQuery()` without assigning the results isn't affected.
113
114
  warnOnArrayOwnerProps() {
114
- const { ownerProp } = this.relation;
115
+ const { ownerProp, joinTable } = this.relation;
116
+ // Relations through a join table already are the alternative to arrays.
117
+ const advice = joinTable
118
+ ? 'Map the relation from a column that holds a single key instead.'
119
+ : 'Use a ManyToManyRelation through a join table instead.';
115
120
 
116
121
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
117
122
  if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
@@ -119,7 +124,7 @@ class RelationFindOperation extends FindOperation {
119
124
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
120
125
  `the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
121
126
  'an array, which is not supported, so no related models are found for them. ' +
122
- 'Use a ManyToManyRelation through a join table instead.',
127
+ advice,
123
128
  );
124
129
  }
125
130
  }
@@ -154,7 +159,7 @@ class RelationFindOperation extends FindOperation {
154
159
  const col = relatedProp.cols[c];
155
160
 
156
161
  if (!builder.hasSelectionAs(fullCol, col) && addedSelects.indexOf(fullCol) === -1) {
157
- this.omitProps.push(prop);
162
+ this.omitFromResult(builder, prop);
158
163
  addedSelects.push(fullCol);
159
164
  }
160
165
  }
@@ -170,34 +175,12 @@ class RelationFindOperation extends FindOperation {
170
175
  }
171
176
  }
172
177
 
173
- omitImplicitJoinProps(related) {
174
- const relatedModelClass = this.relation.relatedModelClass;
175
-
176
- if (!this.omitProps.length || !related) {
177
- return related;
178
- }
179
-
180
- if (!Array.isArray(related)) {
181
- return this.omitImplicitJoinPropsFromOne(relatedModelClass, related);
182
- }
183
-
184
- if (!related.length) {
185
- return related;
186
- }
187
-
188
- for (let i = 0, l = related.length; i < l; ++i) {
189
- this.omitImplicitJoinPropsFromOne(relatedModelClass, related[i]);
190
- }
191
-
192
- return related;
193
- }
194
-
195
- omitImplicitJoinPropsFromOne(relatedModelClass, model) {
196
- for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
197
- relatedModelClass.omitImpl(model, this.omitProps[c]);
178
+ // Omits a property that was selected only to assign the related models to
179
+ // their owners from the result, before any `runAfter()` hooks are called.
180
+ omitFromResult(builder, prop) {
181
+ if (!builder.internalOptions().keepImplicitJoinProps) {
182
+ builder.addInternalSelection(prop);
198
183
  }
199
-
200
- return model;
201
184
  }
202
185
 
203
186
  clone() {
@@ -206,7 +189,6 @@ class RelationFindOperation extends FindOperation {
206
189
  clone.alwaysReturnArray = this.alwaysReturnArray;
207
190
  clone.assignResultToOwner = this.assignResultToOwner;
208
191
  clone.relationProperty = this.relationProperty;
209
- clone.omitProps = this.omitProps.slice();
210
192
  clone.alias = this.alias;
211
193
 
212
194
  return clone;
@@ -3,8 +3,10 @@
3
3
  const { RelationDeleteOperation } = require('../RelationDeleteOperation');
4
4
 
5
5
  class BelongsToOneDeleteOperation extends RelationDeleteOperation {
6
- onAfter1(_, result) {
7
- if (this.owner.isModels) {
6
+ onAfter1(builder, result) {
7
+ // A query resolved without executing it, e.g. with `none()`, deleted
8
+ // nothing, so the owners are left untouched.
9
+ if (this.owner.isModels && !builder.isExplicitlyResolvedOrRejected()) {
8
10
  const ownerProp = this.relation.ownerProp;
9
11
 
10
12
  for (const owner of this.owner.modelArray) {
@@ -16,21 +16,27 @@ const ManyToManyModifyMixin = (Operation) => {
16
16
  constructor(...args) {
17
17
  super(...args);
18
18
  this.modifyFilterSubquery = null;
19
+ this.joinRowFilterSubquery = null;
19
20
  this.skipRelationFilter = false;
20
21
  }
21
22
 
22
- // Updates of rows that are known to be related, like the patches of
23
- // `upsertGraph()`, keep their own filters (#135).
23
+ // Unrelate doesn't execute the main query, and updates of rows that are
24
+ // known to be related, like the patches of `upsertGraph()`, keep their own
25
+ // filters (#135).
24
26
  get modifyMainQuery() {
25
- return !this.skipRelationFilter;
27
+ return !this.modifiesJoinTableOnly && !this.skipRelationFilter;
26
28
  }
27
29
 
28
30
  // At this point `builder` should only have the user's own wheres and joins. There can
29
31
  // be other operations (like orderBy) too, but those are meaningless with modify operations.
30
32
  onBuild(builder) {
31
- this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
33
+ if (this.modifiesJoinTable && !this.skipRelationFilter) {
34
+ this.joinRowFilterSubquery = this.createJoinRowFilterSubquery(builder);
35
+ }
32
36
 
33
37
  if (this.modifyMainQuery) {
38
+ this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
39
+
34
40
  // We can now remove the where and join statements from the main query.
35
41
  this.removeFiltersFromMainQuery(builder);
36
42
 
@@ -74,22 +80,39 @@ const ManyToManyModifyMixin = (Operation) => {
74
80
  return builder.whereInComposite(idRefs, subquery);
75
81
  }
76
82
 
77
- // Modifies the join rows of all related rows that match the filters. If the filters
78
- // reference join table columns, this also modifies join rows that don't match them
79
- // but share a related row with one that does (#1853). The dialect specific mixins
80
- // override this to only modify the join rows that match the filters themselves.
83
+ // Only modifies the join rows that match the filters (#1853).
81
84
  applyModifyFilterForJoinTable(builder) {
82
85
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
83
- const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
84
-
85
- const relatedRefs = this.relation.relatedProp.refs(builder);
86
86
  const ownerValues = this.owner.getProps(this.relation);
87
87
 
88
- const subquery = this.modifyFilterSubquery.clone().select(relatedRefs);
89
-
90
88
  return builder
91
- .whereInComposite(joinTableRelatedRefs, subquery)
92
- .whereInComposite(joinTableOwnerRefs, ownerValues);
89
+ .whereInComposite(joinTableOwnerRefs, ownerValues)
90
+ .whereExists(this.joinRowFilterSubquery);
91
+ }
92
+
93
+ // Selects the related rows of a join row that match the filters. The join
94
+ // table isn't joined, so that the filters on its columns apply to the join
95
+ // row of the outer query.
96
+ createJoinRowFilterSubquery(builder) {
97
+ const { relation } = this;
98
+ const builderClass = builder.constructor;
99
+ const subquery = relation.relatedModelClass.query().childQueryOf(builder);
100
+ const joinTableRef = builder.tableRefFor(relation.joinTable);
101
+
102
+ addWhereGroup(subquery, () => {
103
+ relation.relatedProp.forEach((i) => {
104
+ const relatedRef = relation.relatedProp.ref(subquery, i);
105
+ const joinTableRelatedRef = relation.joinTableRelatedProp.ref(subquery, i);
106
+
107
+ subquery.where(relatedRef, joinTableRelatedRef.table(joinTableRef));
108
+ });
109
+ });
110
+
111
+ return relation
112
+ .applyModify(subquery)
113
+ .copyFrom(builder, builderClass.WhereSelector)
114
+ .copyFrom(builder, builderClass.JoinSelector)
115
+ .select(1);
93
116
  }
94
117
 
95
118
  toFindOperation() {
@@ -111,6 +134,7 @@ const ManyToManyModifyMixin = (Operation) => {
111
134
  clone() {
112
135
  const clone = super.clone();
113
136
  clone.modifyFilterSubquery = this.modifyFilterSubquery;
137
+ clone.joinRowFilterSubquery = this.joinRowFilterSubquery;
114
138
  clone.skipRelationFilter = this.skipRelationFilter;
115
139
  return clone;
116
140
  }
@@ -1,66 +1,55 @@
1
1
  'use strict';
2
2
 
3
3
  const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
- const { isObject } = require('../../utils/objectUtils');
5
4
 
6
5
  // 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:
6
+ // statement that modifies the join table to read the join table in a
7
+ // subquery (ER_UPDATE_TABLE_USED), e.g. through the user's filters, or the
8
+ // related table if a trigger on the join table modifies it
9
+ // (ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG).
9
10
  //
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.
11
+ // So the owner and related ids of the join rows that match the filters are
12
+ // fetched in a separate query before the statement, which then only modifies
13
+ // the join rows with these ids. Unlike the subquery, these can't tell apart
14
+ // join rows that only differ in their other columns (#1853).
16
15
  //
17
16
  // https://stackoverflow.com/a/2314264/3729316
18
17
  const ManyToManyMySqlModifyMixin = (Operation) => {
19
18
  return class extends ManyToManyModifyMixin(Operation) {
20
19
  applyModifyFilterForJoinTable(builder) {
21
- const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
22
- const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
23
-
20
+ const { joinTableOwnerProp, joinTableRelatedProp } = this.relation;
24
21
  const ownerValues = this.owner.getProps(this.relation);
25
- const subquery = this.modifyFilterSubquery.clone().select(`${tableRef}.*`);
22
+ const subquery = this.joinRowFilterSubquery;
26
23
 
27
24
  return builder
25
+ .whereInComposite(joinTableOwnerProp.refs(builder), ownerValues)
28
26
  .runBefore(async (_, builder) => {
27
+ const idRefs = [
28
+ ...joinTableOwnerProp.refs(builder),
29
+ ...joinTableRelatedProp.refs(builder),
30
+ ];
31
+
29
32
  // Fetch the raw rows: no models, so that no hooks run for them.
30
- const rows = await subquery.toKnexQuery();
33
+ const rows = await builder
34
+ .modelClass()
35
+ .query()
36
+ .childQueryOf(builder)
37
+ .copyFrom(builder, builder.constructor.WhereSelector)
38
+ .whereExists(subquery)
39
+ .select(idRefs)
40
+ .toKnexQuery();
31
41
 
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);
42
+ // An empty list still executes the query, so that it returns the
43
+ // expected result for the dialect, e.g. a count of 0 for deletes.
44
+ builder.whereInComposite(
45
+ idRefs,
46
+ rows.map((row) => Object.values(row)),
47
+ );
48
+ });
52
49
  }
53
50
  };
54
51
  };
55
52
 
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
53
  module.exports = {
65
54
  ManyToManyMySqlModifyMixin,
66
55
  };
@@ -4,7 +4,7 @@ const { getModel } = require('../../model/getModel');
4
4
  const { Relation } = require('../Relation');
5
5
  const { RelationProperty } = require('../RelationProperty');
6
6
 
7
- const { isSqlite, isMySql, isPostgres } = require('../../utils/knexUtils');
7
+ const { isSqlite, isMySql } = require('../../utils/knexUtils');
8
8
  const { inheritModel } = require('../../model/inheritModel');
9
9
  const { resolveModel } = require('../../utils/resolveModel');
10
10
  const { mapAfterAllReturn } = require('../../utils/promiseUtils');
@@ -21,14 +21,10 @@ const {
21
21
  ManyToManyUnrelateSqliteOperation,
22
22
  } = require('./unrelate/ManyToManyUnrelateSqliteOperation');
23
23
  const { ManyToManyUnrelateMySqlOperation } = require('./unrelate/ManyToManyUnrelateMySqlOperation');
24
- const {
25
- ManyToManyUnrelatePostgresOperation,
26
- } = require('./unrelate/ManyToManyUnrelatePostgresOperation');
27
24
 
28
25
  const { ManyToManyUpdateOperation } = require('./update/ManyToManyUpdateOperation');
29
26
  const { ManyToManyUpdateSqliteOperation } = require('./update/ManyToManyUpdateSqliteOperation');
30
27
  const { ManyToManyUpdateMySqlOperation } = require('./update/ManyToManyUpdateMySqlOperation');
31
- const { ManyToManyUpdatePostgresOperation } = require('./update/ManyToManyUpdatePostgresOperation');
32
28
 
33
29
  const { ManyToManyDeleteOperation } = require('./delete/ManyToManyDeleteOperation');
34
30
  const { ManyToManyDeleteSqliteOperation } = require('./delete/ManyToManyDeleteSqliteOperation');
@@ -503,26 +499,25 @@ function parseBeforeInsert(ctx) {
503
499
  }
504
500
 
505
501
  function finalizeJoinModelClass(ctx) {
506
- if (ctx.joinTableModelClass.getIdColumn() === null) {
507
- // We cannot know if the join table has a primary key. Therefore we set some
508
- // known column as the idColumn so that inserts will work.
502
+ // We cannot know if the join table has a primary key. Therefore we set some
503
+ // known column as the idColumn of the join model class that we created. A
504
+ // `through.modelClass` is left untouched, even if it has no `idColumn` (#2305).
505
+ if (!ctx.mapping.join.through.modelClass) {
509
506
  ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
510
507
  }
511
508
 
512
509
  return ctx;
513
510
  }
514
511
 
515
- // The modify operations need dialect specific code to only modify the join
516
- // rows that match the filters, see the `ManyToMany*ModifyMixin` classes.
517
- function getOperationClass(builder, { sqlite, mysql, postgres, other }) {
512
+ // The modify operations need dialect specific code on sqlite and mysql, see
513
+ // the `ManyToMany*ModifyMixin` classes.
514
+ function getOperationClass(builder, { sqlite, mysql, other }) {
518
515
  const knex = builder.knex();
519
516
 
520
517
  if (isSqlite(knex)) {
521
518
  return sqlite;
522
519
  } else if (isMySql(knex)) {
523
520
  return mysql;
524
- } else if (isPostgres(knex)) {
525
- return postgres;
526
521
  } else {
527
522
  return other;
528
523
  }
@@ -532,7 +527,6 @@ function getUpdateOperationClass(builder) {
532
527
  return getOperationClass(builder, {
533
528
  sqlite: ManyToManyUpdateSqliteOperation,
534
529
  mysql: ManyToManyUpdateMySqlOperation,
535
- postgres: ManyToManyUpdatePostgresOperation,
536
530
  other: ManyToManyUpdateOperation,
537
531
  });
538
532
  }
@@ -541,7 +535,6 @@ function getUnrelateOperationClass(builder) {
541
535
  return getOperationClass(builder, {
542
536
  sqlite: ManyToManyUnrelateSqliteOperation,
543
537
  mysql: ManyToManyUnrelateMySqlOperation,
544
- postgres: ManyToManyUnrelatePostgresOperation,
545
538
  other: ManyToManyUnrelateOperation,
546
539
  });
547
540
  }
@@ -551,7 +544,6 @@ function getDeleteOperationClass(builder) {
551
544
  return getOperationClass(builder, {
552
545
  sqlite: ManyToManyDeleteSqliteOperation,
553
546
  mysql: ManyToManyDeleteOperation,
554
- postgres: ManyToManyDeleteOperation,
555
547
  other: ManyToManyDeleteOperation,
556
548
  });
557
549
  }
@@ -15,19 +15,6 @@ const ManyToManySqliteModifyMixin = (Operation) => {
15
15
 
16
16
  return builder.whereInComposite(rowIdRef, subquery);
17
17
  }
18
-
19
- applyModifyFilterForJoinTable(builder) {
20
- const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
21
- const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
22
- const rowIdRef = `${tableRef}.${SQLITE_BUILTIN_ROW_ID}`;
23
-
24
- const ownerValues = this.owner.getProps(this.relation);
25
- const subquery = this.modifyFilterSubquery.clone().select(rowIdRef);
26
-
27
- return builder
28
- .whereInComposite(rowIdRef, subquery)
29
- .whereInComposite(joinTableOwnerRefs, ownerValues);
30
- }
31
18
  };
32
19
  };
33
20
 
@@ -36,7 +36,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
36
36
  }
37
37
 
38
38
  if (this.owner.isModels) {
39
- this.warnOnMissingOwnerProps();
39
+ this.warnOnInvalidOwnerProps();
40
40
 
41
41
  if (this.assignResultToOwner) {
42
42
  this.selectMissingJoinColumns(builder);
@@ -122,8 +122,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
122
122
  const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
123
123
 
124
124
  builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
125
- // Mark them to be omitted later.
126
- this.omitProps.push(propName);
125
+ this.omitFromResult(builder, propName);
127
126
  });
128
127
 
129
128
  super.selectMissingJoinColumns(builder);
@@ -5,11 +5,7 @@ const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
5
5
 
6
6
  class ManyToManyUnrelateMySqlOperation extends ManyToManyMySqlModifyMixin(
7
7
  ManyToManyUnrelateOperationBase,
8
- ) {
9
- get modifyMainQuery() {
10
- return false;
11
- }
12
- }
8
+ ) {}
13
9
 
14
10
  module.exports = {
15
11
  ManyToManyUnrelateMySqlOperation,
@@ -3,11 +3,7 @@
3
3
  const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
4
4
  const { ManyToManyModifyMixin } = require('../ManyToManyModifyMixin');
5
5
 
6
- class ManyToManyUnrelateOperation extends ManyToManyModifyMixin(ManyToManyUnrelateOperationBase) {
7
- get modifyMainQuery() {
8
- return false;
9
- }
10
- }
6
+ class ManyToManyUnrelateOperation extends ManyToManyModifyMixin(ManyToManyUnrelateOperationBase) {}
11
7
 
12
8
  module.exports = {
13
9
  ManyToManyUnrelateOperation,
@@ -4,6 +4,15 @@ const { UnrelateOperation } = require('../../../queryBuilder/operations/Unrelate
4
4
  const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
5
5
 
6
6
  class ManyToManyUnrelateOperationBase extends UnrelateOperation {
7
+ get modifiesJoinTable() {
8
+ return true;
9
+ }
10
+
11
+ // The main query is replaced by the query that `queryExecutor()` returns.
12
+ get modifiesJoinTableOnly() {
13
+ return true;
14
+ }
15
+
7
16
  queryExecutor(builder) {
8
17
  const unrelateQuery = this.relation
9
18
  .getJoinModelClass(builder.knex())
@@ -5,11 +5,7 @@ const { ManyToManySqliteModifyMixin } = require('../ManyToManySqliteModifyMixin'
5
5
 
6
6
  class ManyToManyUnrelateSqliteOperation extends ManyToManySqliteModifyMixin(
7
7
  ManyToManyUnrelateOperationBase,
8
- ) {
9
- get modifyMainQuery() {
10
- return false;
11
- }
12
- }
8
+ ) {}
13
9
 
14
10
  module.exports = {
15
11
  ManyToManyUnrelateSqliteOperation,
@@ -15,6 +15,10 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
15
15
  this.joinTablePatchFilterQuery = null;
16
16
  }
17
17
 
18
+ get modifiesJoinTable() {
19
+ return this.hasExtraProps;
20
+ }
21
+
18
22
  onAdd(builder, args) {
19
23
  const obj = args[0];
20
24
 
@@ -47,7 +51,9 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
47
51
  }
48
52
 
49
53
  async onAfter1(builder, result) {
50
- if (this.hasExtraProps) {
54
+ // A query resolved without updating anything, e.g. with `none()`, leaves
55
+ // the join rows untouched too.
56
+ if (this.hasExtraProps && !this.isResolvedWithoutUpdate(builder)) {
51
57
  const joinTableUpdateQuery = this.relation
52
58
  .getJoinModelClass(builder.knex())
53
59
  .query()
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { isObject, isPlainObject } = require('./objectUtils');
3
+ const { isObject } = require('./objectUtils');
4
4
 
5
5
  // Super fast memoize for single argument functions.
6
6
  function memoize(func) {
@@ -149,9 +149,8 @@ function mapLastPart(mapper, separator) {
149
149
  }
150
150
 
151
151
  // Returns a function that takes an object as an input and maps the object's keys
152
- // using `mapper`, and its values using the optional `valueMapper`. If the input
153
- // is not an object, the input is returned unchanged.
154
- function keyMapper(mapper, valueMapper) {
152
+ // using `mapper`. If the input is not an object, the input is returned unchanged.
153
+ function keyMapper(mapper) {
155
154
  return (obj) => {
156
155
  if (!isObject(obj) || Array.isArray(obj)) {
157
156
  return obj;
@@ -162,7 +161,7 @@ function keyMapper(mapper, valueMapper) {
162
161
 
163
162
  for (let i = 0, l = keys.length; i < l; ++i) {
164
163
  const key = keys[i];
165
- out[mapper(key)] = valueMapper ? valueMapper(obj[key]) : obj[key];
164
+ out[mapper(key)] = obj[key];
166
165
  }
167
166
 
168
167
  return out;
@@ -190,16 +189,10 @@ function snakeCaseMappers(opt = {}) {
190
189
  };
191
190
  }
192
191
 
193
- function knexIdentifierMappers({ parse, format, idSeparator = ':', mapNestedKeys = false } = {}) {
192
+ function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
194
193
  const formatId = memoize(mapLastPart(format, idSeparator));
195
194
  const parseId = memoize(mapLastPart(parse, idSeparator));
196
195
  const parseKeys = keyMapper(parseId);
197
- // With `mapNestedKeys`, also map the keys of plain objects one level down, as
198
- // in the rows knex returns with its `nestTables: true` option on MySQL:
199
- // `{ tableName: { columnName: value } }`.
200
- const parseRow = mapNestedKeys
201
- ? keyMapper(parseId, (value) => (isPlainObject(value) ? parseKeys(value) : value))
202
- : parseKeys;
203
196
 
204
197
  return {
205
198
  wrapIdentifier(identifier, origWrap) {
@@ -211,12 +204,12 @@ function knexIdentifierMappers({ parse, format, idSeparator = ':', mapNestedKeys
211
204
  const output = new Array(result.length);
212
205
 
213
206
  for (let i = 0, l = result.length; i < l; ++i) {
214
- output[i] = parseRow(result[i]);
207
+ output[i] = parseKeys(result[i]);
215
208
  }
216
209
 
217
210
  return output;
218
211
  } else {
219
- return parseRow(result);
212
+ return parseKeys(result);
220
213
  }
221
214
  },
222
215
  };
@@ -226,11 +219,10 @@ function knexSnakeCaseMappers(opt = {}) {
226
219
  return knexIdentifierMappers({
227
220
  parse: (str) => camelCase(str, opt),
228
221
  format: (str) => snakeCase(str, opt),
229
- mapNestedKeys: opt.mapNestedKeys,
230
222
  });
231
223
  }
232
224
 
233
- function knexIdentifierMapping(colToProp, { mapNestedKeys } = {}) {
225
+ function knexIdentifierMapping(colToProp) {
234
226
  const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
235
227
  propToCol[colToProp[column]] = column;
236
228
  return propToCol;
@@ -239,7 +231,6 @@ function knexIdentifierMapping(colToProp, { mapNestedKeys } = {}) {
239
231
  return knexIdentifierMappers({
240
232
  parse: (column) => colToProp[column] || column,
241
233
  format: (prop) => propToCol[prop] || prop,
242
- mapNestedKeys,
243
234
  });
244
235
  }
245
236