objection 3.5.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 (41) hide show
  1. package/README.md +3 -3
  2. package/lib/model/AjvValidator.js +33 -10
  3. package/lib/model/modelClone.js +0 -2
  4. package/lib/queryBuilder/InternalOptions.js +2 -0
  5. package/lib/queryBuilder/QueryBuilder.js +56 -10
  6. package/lib/queryBuilder/RelationExpression.js +6 -4
  7. package/lib/queryBuilder/graph/GraphOptions.js +141 -22
  8. package/lib/queryBuilder/graph/GraphUpsert.js +57 -0
  9. package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
  10. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -0
  11. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +6 -1
  12. package/lib/queryBuilder/operations/UpdateOperation.js +11 -0
  13. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
  14. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
  15. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +434 -364
  16. package/lib/queryBuilder/parsers/relationExpressionParser.js +513 -548
  17. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +1 -1
  18. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
  19. package/lib/relations/Relation.js +7 -0
  20. package/lib/relations/RelationFindOperation.js +27 -40
  21. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
  22. package/lib/relations/belongsToOne/BelongsToOneRelation.js +9 -0
  23. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +41 -13
  24. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
  25. package/lib/relations/manyToMany/ManyToManyRelation.js +15 -18
  26. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
  27. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +2 -3
  28. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
  29. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
  30. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
  31. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
  32. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +28 -2
  33. package/lib/utils/identifierMapping.js +8 -17
  34. package/lib/utils/objectUtils.js +109 -2
  35. package/lib/utils/parseFieldExpression.js +10 -3
  36. package/package.json +5 -2
  37. package/typings/objection/index.d.ts +56 -36
  38. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
  39. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
  40. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
  41. package/lib/utils/clone.js +0 -2329
@@ -1,6 +1,6 @@
1
1
  {
2
2
  function assertDuplicateRelation(node, expr) {
3
- if (expr.$name in node) {
3
+ if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
4
4
  console.warn(`Duplicate relation "${expr.$name}" in a relation expression. You should use "a.[b, c]" instead of "[a.b, a.c]". This will cause an error in objection 2.0`);
5
5
 
6
6
  // TODO: enable for v2.0.
@@ -2,8 +2,6 @@
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);
7
5
 
8
6
  /**
9
7
  * Mysql doesn't allow queries like this:
@@ -32,11 +30,10 @@ class WrapMysqlModifySubqueryTransformation extends QueryTransformation {
32
30
  return query;
33
31
  }
34
32
 
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;
33
+ // Subqueries in where groups and join clauses are converted with a
34
+ // partial builder as their parent. Use the query that they belong to.
35
+ while (parentQuery.isPartial() && parentQuery.parentQuery()) {
36
+ parentQuery = parentQuery.parentQuery();
40
37
  }
41
38
 
42
39
  // This transformation only applies to update and delete queries.
@@ -83,6 +83,12 @@ class Relation {
83
83
  return false;
84
84
  }
85
85
 
86
+ // Whether owner models may legitimately lack the owner join property at
87
+ // `index`, so that it isn't reported as missing (#132).
88
+ isOptionalOwnerProp(_index) {
89
+ return false;
90
+ }
91
+
86
92
  clone() {
87
93
  const relation = new this.constructor(this.name, this.ownerModelClass);
88
94
 
@@ -97,6 +103,7 @@ class Relation {
97
103
  relation.joinTableRelatedProp = this.joinTableRelatedProp;
98
104
  relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
99
105
  relation.joinTableExtras = this.joinTableExtras;
106
+ relation.joinTableModify = this.joinTableModify;
100
107
 
101
108
  return relation;
102
109
  }
@@ -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,14 +76,27 @@ 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
- // is reported, as `null` is a legit value for a missing relation.
89
+ // is reported, as `null` is a legit value for a missing relation, and
90
+ // owner props that may legitimately be unset are skipped (#132).
89
91
  // TODO: Consider throwing an error instead in objection 4.0.
90
92
  warnOnMissingOwnerProps() {
91
93
  const { ownerProp } = this.relation;
92
94
 
93
95
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
96
+ if (this.relation.isOptionalOwnerProp(c)) {
97
+ continue;
98
+ }
99
+
94
100
  if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
95
101
  deprecate(
96
102
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
@@ -106,7 +112,11 @@ class RelationFindOperation extends FindOperation {
106
112
  // assigned to their owners, so all owners silently get no related models
107
113
  // (#2667). `$relatedQuery()` without assigning the results isn't affected.
108
114
  warnOnArrayOwnerProps() {
109
- 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.';
110
120
 
111
121
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
112
122
  if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
@@ -114,7 +124,7 @@ class RelationFindOperation extends FindOperation {
114
124
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
115
125
  `the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
116
126
  'an array, which is not supported, so no related models are found for them. ' +
117
- 'Use a ManyToManyRelation through a join table instead.',
127
+ advice,
118
128
  );
119
129
  }
120
130
  }
@@ -149,7 +159,7 @@ class RelationFindOperation extends FindOperation {
149
159
  const col = relatedProp.cols[c];
150
160
 
151
161
  if (!builder.hasSelectionAs(fullCol, col) && addedSelects.indexOf(fullCol) === -1) {
152
- this.omitProps.push(prop);
162
+ this.omitFromResult(builder, prop);
153
163
  addedSelects.push(fullCol);
154
164
  }
155
165
  }
@@ -165,34 +175,12 @@ class RelationFindOperation extends FindOperation {
165
175
  }
166
176
  }
167
177
 
168
- omitImplicitJoinProps(related) {
169
- const relatedModelClass = this.relation.relatedModelClass;
170
-
171
- if (!this.omitProps.length || !related) {
172
- return related;
173
- }
174
-
175
- if (!Array.isArray(related)) {
176
- return this.omitImplicitJoinPropsFromOne(relatedModelClass, related);
177
- }
178
-
179
- if (!related.length) {
180
- return related;
181
- }
182
-
183
- for (let i = 0, l = related.length; i < l; ++i) {
184
- this.omitImplicitJoinPropsFromOne(relatedModelClass, related[i]);
185
- }
186
-
187
- return related;
188
- }
189
-
190
- omitImplicitJoinPropsFromOne(relatedModelClass, model) {
191
- for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
192
- 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);
193
183
  }
194
-
195
- return model;
196
184
  }
197
185
 
198
186
  clone() {
@@ -201,7 +189,6 @@ class RelationFindOperation extends FindOperation {
201
189
  clone.alwaysReturnArray = this.alwaysReturnArray;
202
190
  clone.assignResultToOwner = this.assignResultToOwner;
203
191
  clone.relationProperty = this.relationProperty;
204
- clone.omitProps = this.omitProps.slice();
205
192
  clone.alias = this.alias;
206
193
 
207
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) {
@@ -11,6 +11,15 @@ class BelongsToOneRelation extends Relation {
11
11
  return true;
12
12
  }
13
13
 
14
+ // The foreign key is unset when no related model was ever assigned, e.g. on
15
+ // models returned by `insert()` or created with `fromJson()`, where no
16
+ // related model is the right result. This can't be told apart from a
17
+ // foreign key left out of a partial `select()`. Foreign keys that are part
18
+ // of the id are always expected to be set (#132).
19
+ isOptionalOwnerProp(index) {
20
+ return !this.ownerModelClass.getIdPropertyArray().includes(this.ownerProp.props[index]);
21
+ }
22
+
14
23
  insert(_, owner) {
15
24
  return new BelongsToOneInsertOperation('insert', {
16
25
  relation: this,
@@ -16,18 +16,27 @@ const ManyToManyModifyMixin = (Operation) => {
16
16
  constructor(...args) {
17
17
  super(...args);
18
18
  this.modifyFilterSubquery = null;
19
+ this.joinRowFilterSubquery = null;
20
+ this.skipRelationFilter = false;
19
21
  }
20
22
 
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).
21
26
  get modifyMainQuery() {
22
- return true;
27
+ return !this.modifiesJoinTableOnly && !this.skipRelationFilter;
23
28
  }
24
29
 
25
30
  // At this point `builder` should only have the user's own wheres and joins. There can
26
31
  // be other operations (like orderBy) too, but those are meaningless with modify operations.
27
32
  onBuild(builder) {
28
- this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
33
+ if (this.modifiesJoinTable && !this.skipRelationFilter) {
34
+ this.joinRowFilterSubquery = this.createJoinRowFilterSubquery(builder);
35
+ }
29
36
 
30
37
  if (this.modifyMainQuery) {
38
+ this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
39
+
31
40
  // We can now remove the where and join statements from the main query.
32
41
  this.removeFiltersFromMainQuery(builder);
33
42
 
@@ -71,22 +80,39 @@ const ManyToManyModifyMixin = (Operation) => {
71
80
  return builder.whereInComposite(idRefs, subquery);
72
81
  }
73
82
 
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.
83
+ // Only modifies the join rows that match the filters (#1853).
78
84
  applyModifyFilterForJoinTable(builder) {
79
85
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
80
- const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
81
-
82
- const relatedRefs = this.relation.relatedProp.refs(builder);
83
86
  const ownerValues = this.owner.getProps(this.relation);
84
87
 
85
- const subquery = this.modifyFilterSubquery.clone().select(relatedRefs);
86
-
87
88
  return builder
88
- .whereInComposite(joinTableRelatedRefs, subquery)
89
- .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);
90
116
  }
91
117
 
92
118
  toFindOperation() {
@@ -108,6 +134,8 @@ const ManyToManyModifyMixin = (Operation) => {
108
134
  clone() {
109
135
  const clone = super.clone();
110
136
  clone.modifyFilterSubquery = this.modifyFilterSubquery;
137
+ clone.joinRowFilterSubquery = this.joinRowFilterSubquery;
138
+ clone.skipRelationFilter = this.skipRelationFilter;
111
139
  return clone;
112
140
  }
113
141
  };
@@ -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');
@@ -93,7 +89,8 @@ class ManyToManyRelation extends Relation {
93
89
 
94
90
  // The modify operations can't tell the join rows apart from the related rows
95
91
  // if the join table is also the related table, so they would modify the
96
- // wrong rows (#1803).
92
+ // wrong rows (#1803). Inserting and relating would insert the join rows as
93
+ // new related rows (#133).
97
94
  assertCanModifyJoinTable(builder, operationName) {
98
95
  if (this.isJoinTableRelatedTable(builder)) {
99
96
  throw this.createError(
@@ -188,7 +185,9 @@ class ManyToManyRelation extends Relation {
188
185
  });
189
186
  }
190
187
 
191
- insert(_, owner) {
188
+ insert(builder, owner) {
189
+ this.assertCanModifyJoinTable(builder, 'insert');
190
+
192
191
  return new ManyToManyInsertOperation('insert', {
193
192
  relation: this,
194
193
  owner,
@@ -218,6 +217,8 @@ class ManyToManyRelation extends Relation {
218
217
  }
219
218
 
220
219
  relate(builder, owner) {
220
+ this.assertCanModifyJoinTable(builder, 'relate');
221
+
221
222
  return new ManyToManyRelateOperation('relate', {
222
223
  relation: this,
223
224
  owner,
@@ -498,26 +499,25 @@ function parseBeforeInsert(ctx) {
498
499
  }
499
500
 
500
501
  function finalizeJoinModelClass(ctx) {
501
- if (ctx.joinTableModelClass.getIdColumn() === null) {
502
- // We cannot know if the join table has a primary key. Therefore we set some
503
- // 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) {
504
506
  ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
505
507
  }
506
508
 
507
509
  return ctx;
508
510
  }
509
511
 
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 }) {
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 }) {
513
515
  const knex = builder.knex();
514
516
 
515
517
  if (isSqlite(knex)) {
516
518
  return sqlite;
517
519
  } else if (isMySql(knex)) {
518
520
  return mysql;
519
- } else if (isPostgres(knex)) {
520
- return postgres;
521
521
  } else {
522
522
  return other;
523
523
  }
@@ -527,7 +527,6 @@ function getUpdateOperationClass(builder) {
527
527
  return getOperationClass(builder, {
528
528
  sqlite: ManyToManyUpdateSqliteOperation,
529
529
  mysql: ManyToManyUpdateMySqlOperation,
530
- postgres: ManyToManyUpdatePostgresOperation,
531
530
  other: ManyToManyUpdateOperation,
532
531
  });
533
532
  }
@@ -536,7 +535,6 @@ function getUnrelateOperationClass(builder) {
536
535
  return getOperationClass(builder, {
537
536
  sqlite: ManyToManyUnrelateSqliteOperation,
538
537
  mysql: ManyToManyUnrelateMySqlOperation,
539
- postgres: ManyToManyUnrelatePostgresOperation,
540
538
  other: ManyToManyUnrelateOperation,
541
539
  });
542
540
  }
@@ -546,7 +544,6 @@ function getDeleteOperationClass(builder) {
546
544
  return getOperationClass(builder, {
547
545
  sqlite: ManyToManyDeleteSqliteOperation,
548
546
  mysql: ManyToManyDeleteOperation,
549
- postgres: ManyToManyDeleteOperation,
550
547
  other: ManyToManyDeleteOperation,
551
548
  });
552
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,