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
@@ -215,7 +215,22 @@ class AjvValidator extends Validator {
215
215
  this.setupAjv(this.ajvQueryProps);
216
216
  }
217
217
 
218
- return this.ajvQueryProps.compile(jsonSchema);
218
+ for (;;) {
219
+ try {
220
+ return this.ajvQueryProps.compile(jsonSchema);
221
+ } catch (err) {
222
+ // The schemas of other model classes that `$ref`s point to aren't added
223
+ // to this instance. Copy them over from the normal instance when needed.
224
+ const validator =
225
+ err instanceof Ajv.MissingRefError && this.ajv.getSchema(err.missingSchema);
226
+
227
+ if (!validator) {
228
+ throw err;
229
+ }
230
+
231
+ this.ajvQueryProps.addSchema(validator.schema, err.missingSchema);
232
+ }
233
+ }
219
234
  }
220
235
  }
221
236
 
@@ -357,7 +372,9 @@ function fieldExpressionPath(key) {
357
372
  return [key];
358
373
  }
359
374
 
360
- if (parsed.access.some((access) => access.type !== 'object')) {
375
+ // Numeric keys like the `0` in `tags:0` also access array elements when the
376
+ // json value is an array, so treat them like `[0]`.
377
+ if (parsed.access.some((access) => access.type !== 'object' || /^-?\d+$/.test(access.ref))) {
361
378
  return null;
362
379
  }
363
380
 
@@ -470,6 +487,11 @@ function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
470
487
  }
471
488
 
472
489
  function jsonSchemaWithoutRequired(jsonSchema) {
490
+ // Boolean schemas (`true` / `false`) have no `required` to strip.
491
+ if (!isObject(jsonSchema)) {
492
+ return jsonSchema;
493
+ }
494
+
473
495
  // `not` is intentionally left untouched: Stripping `required` inside it
474
496
  // would turn it into a schema that rejects everything (e.g.
475
497
  // `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
@@ -506,17 +528,18 @@ function definitionsWithoutRequired(jsonSchema, prop) {
506
528
  }
507
529
 
508
530
  function subSchemaWithoutRequired(jsonSchema, prop) {
509
- if (jsonSchema[prop]) {
531
+ if (jsonSchema[prop] !== undefined) {
510
532
  if (Array.isArray(jsonSchema[prop])) {
511
533
  const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
512
- const nonEmptySchemaArray = schemaArray.filter(isNotEmptyObject);
534
+ const nonEmptySchemaArray = schemaArray.filter((schema) => !matchesAnything(schema));
513
535
 
514
536
  // An `anyOf` or `oneOf` option that only consisted of `required` is
515
537
  // empty once `required` is stripped, and an empty schema matches any
516
- // patch, just like the stripped `required` keywords elsewhere. Drop the
517
- // whole keyword then: requiring the patch to match one of the remaining
518
- // options would reject valid patches, and with `oneOf` keeping the empty
519
- // option would fail every patch that also matches another option.
538
+ // patch, just like the stripped `required` keywords elsewhere and the
539
+ // `true` schema. Drop the whole keyword then: requiring the patch to
540
+ // match one of the remaining options would reject valid patches, and
541
+ // with `oneOf` keeping the empty option would fail every patch that also
542
+ // matches another option.
520
543
  if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
521
544
  return {};
522
545
  } else if (nonEmptySchemaArray.length !== 0) {
@@ -548,8 +571,8 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
548
571
  }
549
572
  }
550
573
 
551
- function isNotEmptyObject(obj) {
552
- return Object.keys(obj).length !== 0;
574
+ function matchesAnything(schema) {
575
+ return schema === true || (isObject(schema) && Object.keys(schema).length === 0);
553
576
  }
554
577
 
555
578
  module.exports = {
@@ -81,8 +81,6 @@ function cloneObject(value) {
81
81
  return cloneArray(value);
82
82
  } else if (value.$isObjectionModel) {
83
83
  return clone(value, false, false);
84
- } else if (Buffer.isBuffer(value)) {
85
- return new Buffer(value);
86
84
  } else {
87
85
  return cloneDeep(value);
88
86
  }
@@ -8,6 +8,7 @@ const { ValidationErrorType } = require('../model/ValidationError');
8
8
  const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
9
9
  const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
10
10
  const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
11
+ const { deprecate } = require('../utils/deprecate');
11
12
 
12
13
  const { Selection } = require('./operations/select/Selection');
13
14
 
@@ -176,7 +177,7 @@ class QueryBuilder extends QueryBuilderBase {
176
177
  }
177
178
 
178
179
  isExplicitlyResolvedOrRejected() {
179
- return !!(this._explicitRejectValue || this._explicitResolveValue);
180
+ return this._explicitRejectValue !== null || this._explicitResolveValue !== null;
180
181
  }
181
182
 
182
183
  isExecutable() {
@@ -261,7 +262,14 @@ class QueryBuilder extends QueryBuilderBase {
261
262
  eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
262
263
 
263
264
  if (joinOperation) {
264
- eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
265
+ if (eagerOp instanceof JoinEagerOperation) {
266
+ eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
267
+ } else {
268
+ deprecate(
269
+ 'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
270
+ `fetched relations "${parsedExp.toString()}".`,
271
+ );
272
+ }
265
273
  }
266
274
 
267
275
  checkMixedEager(this);
@@ -501,17 +509,31 @@ class QueryBuilder extends QueryBuilderBase {
501
509
  await joinEagerOp.onBefore3(builder);
502
510
  }
503
511
 
504
- const countQuery = knex.count('* as count').from((knexBuilder) => {
505
- // Also clear the order on the built knex query, since `orderBy` calls
506
- // can be added at build time, e.g. by a relation's `modify`.
507
- knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
512
+ // Also clear the order on the built knex query, since `orderBy` calls
513
+ // can be added at build time, e.g. by a relation's `modify`.
514
+ const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
508
515
 
509
- if (joinEagerOp) {
516
+ const countQuery = knex.count('* as count').from((knexBuilder) => {
517
+ if (!joinEagerOp) {
518
+ buildQuery(knexBuilder);
519
+ } else {
510
520
  // The joins of `withGraphJoined` return one row per joined child.
511
521
  // Count the distinct root models instead.
512
- const tableRef = builder.tableRef();
513
522
  const idColumns = builder.modelClass().getIdColumnArray();
514
- knexBuilder.clearSelect().distinct(idColumns.map((col) => `${tableRef}.${col}`));
523
+
524
+ if (refersToSelections(builder)) {
525
+ // `having`, and `groupBy` with an alias, may refer to aliased
526
+ // selections, so keep them and select the distinct ids from the
527
+ // result instead.
528
+ knexBuilder.distinct(idColumns).from((knexBuilder) => {
529
+ buildQuery(knexBuilder).as('inner');
530
+ });
531
+ } else {
532
+ const tableRef = builder.tableRef();
533
+ buildQuery(knexBuilder)
534
+ .clearSelect()
535
+ .distinct(idColumns.map((col) => `${tableRef}.${col}`));
536
+ }
515
537
  }
516
538
 
517
539
  knexBuilder.as('temp');
@@ -1171,6 +1193,29 @@ function getEagerAlgorithmByName(builder, name) {
1171
1193
  }
1172
1194
  }
1173
1195
 
1196
+ // Returns true if the `having` or `groupBy` clauses of `builder` may refer to
1197
+ // aliased selections (#2329).
1198
+ function refersToSelections(builder) {
1199
+ if (builder.has(/having/)) {
1200
+ return true;
1201
+ }
1202
+
1203
+ const aliases = builder
1204
+ .findAllSelections()
1205
+ .map((selection) => selection.alias)
1206
+ .filter((alias) => !!alias);
1207
+
1208
+ let refersToAlias = false;
1209
+
1210
+ builder.forEachOperation(/^groupBy$/, (op) => {
1211
+ if (flatten(op.args).some((arg) => aliases.includes(arg))) {
1212
+ refersToAlias = true;
1213
+ }
1214
+ });
1215
+
1216
+ return refersToAlias;
1217
+ }
1218
+
1174
1219
  function getEagerAlgorithmOfOperation(builder, eagerOp) {
1175
1220
  if (eagerOp instanceof JoinEagerOperation) {
1176
1221
  return getJoinEagerAlgorithm(builder);
@@ -1198,7 +1243,8 @@ function groupByEagerAlgorithm(builder, expression) {
1198
1243
 
1199
1244
  for (const childName of node.$childNames) {
1200
1245
  const eagerOp = findEagerOperations(builder).find(
1201
- (op) => op.expression.node.$allRecursive || op.expression.node[childName],
1246
+ (op) =>
1247
+ op.expression.node.$allRecursive || op.expression.node.$childNames.includes(childName),
1202
1248
  );
1203
1249
 
1204
1250
  const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
@@ -129,7 +129,8 @@ class RelationExpression {
129
129
  return new RelationExpression(this.node, this.recursionDepth + 1);
130
130
  }
131
131
 
132
- const child = this.node[childName];
132
+ // Don't mistake inherited properties like `constructor` for children.
133
+ const child = this.node.$childNames.includes(childName) ? this.node[childName] : null;
133
134
 
134
135
  if (child) {
135
136
  return new RelationExpression(child);
@@ -292,7 +293,7 @@ function modelToNode(model, node) {
292
293
  const relName = relationNames[r];
293
294
 
294
295
  if (model[relName] !== undefined) {
295
- let childNode = node[relName];
296
+ let childNode = node.$childNames.includes(relName) ? node[relName] : null;
296
297
 
297
298
  if (!childNode) {
298
299
  childNode = newNode(relName);
@@ -402,8 +403,9 @@ function mergeNodes(node1, node2) {
402
403
  node.$childNames = union(node1.$childNames, node2.$childNames);
403
404
 
404
405
  for (const childName of node.$childNames) {
405
- const child1 = node1[childName];
406
- const child2 = node2[childName];
406
+ // Don't mistake inherited properties like `constructor` for children.
407
+ const child1 = node1.$childNames.includes(childName) ? node1[childName] : null;
408
+ const child2 = node2.$childNames.includes(childName) ? node2[childName] : null;
407
409
 
408
410
  if (child1 && child2) {
409
411
  node[childName] = mergeNodes(child1, child2);
@@ -15,6 +15,7 @@ const UNRELATE = 'unrelate';
15
15
  const INSERT_MISSING = 'insertMissing';
16
16
  const FETCH_STRATEGY = 'fetchStrategy';
17
17
  const ALLOW_REFS = 'allowRefs';
18
+ const ALLOW_MARKERS = 'allowMarkers';
18
19
 
19
20
  const OPTION_NAMES = [
20
21
  NO_RELATE,
@@ -28,6 +29,7 @@ const OPTION_NAMES = [
28
29
  INSERT_MISSING,
29
30
  FETCH_STRATEGY,
30
31
  ALLOW_REFS,
32
+ ALLOW_MARKERS,
31
33
  ];
32
34
 
33
35
  // The options that are matched against the relation path of each node, see
@@ -52,7 +54,8 @@ const FetchStrategy = {
52
54
 
53
55
  class GraphOptions {
54
56
  // `rootRelationPath` is the relation path of the root node of a recursive
55
- // upsert in the original graph, against which relation expressions match.
57
+ // upsert in the original graph, against which the relation paths and
58
+ // expressions of the options match.
56
59
  constructor(options, rootRelationPath = []) {
57
60
  if (options instanceof GraphOptions) {
58
61
  this.options = options.options;
@@ -61,10 +64,6 @@ class GraphOptions {
61
64
  this.options = parseRelationExpressions(options);
62
65
  this.rootRelationPath = rootRelationPath;
63
66
  }
64
-
65
- // Caches the result of matching relation expressions against relation
66
- // paths, by option name and relation path key.
67
- this.expressionMatches = new Map();
68
67
  }
69
68
 
70
69
  isFetchStrategy(strategy) {
@@ -206,33 +205,23 @@ class GraphOptions {
206
205
  return !!this.options[ALLOW_REFS];
207
206
  }
208
207
 
209
- rebasedOptions(newRoot) {
210
- const newOpt = {};
211
- const newRootRelationPath = newRoot.relationPathKey;
212
-
213
- for (const name of Object.keys(this.options)) {
214
- const value = this.options[name];
215
-
216
- if (Array.isArray(value)) {
217
- newOpt[name] = value
218
- .filter((it) => it === newRootRelationPath || it.startsWith(`${newRootRelationPath}.`))
219
- .map((it) => it.slice(newRootRelationPath.length + 1))
220
- .filter((it) => !!it);
221
- } else {
222
- newOpt[name] = value;
223
- }
224
- }
208
+ allowMarkers() {
209
+ return !!this.options[ALLOW_MARKERS];
210
+ }
225
211
 
226
- return new GraphOptions(newOpt, [...this.rootRelationPath, ...newRoot.relationPath]);
212
+ // The options of recursive upserts keep matching against the relation paths
213
+ // of the original graph, which the relation paths of the new root lead to.
214
+ rebasedOptions(newRoot) {
215
+ return new GraphOptions(this.options, [...this.rootRelationPath, ...newRoot.relationPath]);
227
216
  }
228
217
 
229
218
  _hasOption(node, optionName) {
230
219
  const option = this.options[optionName];
231
220
 
232
221
  if (isRelationExpression(option)) {
233
- return this._matchesRelationExpression(node, optionName, option);
222
+ return matchesRelationExpression(option, this._relationPath(node));
234
223
  } else if (Array.isArray(option)) {
235
- return option.indexOf(node.relationPathKey) !== -1;
224
+ return option.includes(this._relationPath(node).join('.'));
236
225
  } else if (typeof option === 'boolean') {
237
226
  return option;
238
227
  } else if (option === undefined) {
@@ -244,36 +233,9 @@ class GraphOptions {
244
233
  }
245
234
  }
246
235
 
247
- // A node matches a relation expression when its relation path leads to a
248
- // leaf of the expression, like it would match the same relation paths in an
249
- // array: `'movies.reviews'` matches `movies.reviews` but not `movies`.
250
- // `'*'` matches all relations below it and recursive expressions like
251
- // `'parent.^'` match every level of the recursion. The root has no relation
252
- // path and never matches.
253
- _matchesRelationExpression(node, optionName, expression) {
254
- const relationPath = [...this.rootRelationPath, ...node.relationPath];
255
-
256
- if (relationPath.length === 0) {
257
- return false;
258
- }
259
-
260
- const relationPathKey = relationPath.join('.');
261
-
262
- let matches = this.expressionMatches.get(optionName);
263
-
264
- if (!matches) {
265
- matches = new Map();
266
- this.expressionMatches.set(optionName, matches);
267
- }
268
-
269
- let isMatch = matches.get(relationPathKey);
270
-
271
- if (isMatch === undefined) {
272
- isMatch = matchesRelationPath(expression, relationPath);
273
- matches.set(relationPathKey, isMatch);
274
- }
275
-
276
- return isMatch;
236
+ // The relation path of the node in the original graph, see `rebasedOptions()`.
237
+ _relationPath(node) {
238
+ return [...this.rootRelationPath, ...node.relationPath];
277
239
  }
278
240
  }
279
241
 
@@ -344,7 +306,17 @@ function findAlias(node) {
344
306
  return null;
345
307
  }
346
308
 
347
- function matchesRelationPath(expression, relationPath) {
309
+ // A relation path matches a relation expression when it leads to a leaf of
310
+ // the expression, like it would match the same relation paths in an array:
311
+ // `'movies.reviews'` matches `movies.reviews` but not `movies`. `'*'` matches
312
+ // all relations below it and recursive expressions like `'parent.^'` match
313
+ // every level of the recursion. The root has no relation path and never
314
+ // matches.
315
+ function matchesRelationExpression(expression, relationPath) {
316
+ if (relationPath.length === 0) {
317
+ return false;
318
+ }
319
+
348
320
  for (const relationName of relationPath) {
349
321
  expression = expression.childExpression(relationName);
350
322
 
@@ -245,6 +245,7 @@ function checkForErrors(builder) {
245
245
  checkForNotFoundErrors(graphData, builder);
246
246
  checkForUnallowedRelationErrors(graphData, builder);
247
247
  checkForUnallowedReferenceErrors(graphData, builder);
248
+ checkForUnallowedMarkerErrors(graphData, builder);
248
249
  checkForRootUnrelateOrDeleteErrors(graphData, builder);
249
250
 
250
251
  if (graphData.graphOptions.isInsertOnly()) {
@@ -344,6 +345,27 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
344
345
  }
345
346
  }
346
347
 
348
+ // `#unrelate` and `#delete` markers come from the graph data, which may be
349
+ // untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
350
+ function checkForUnallowedMarkerErrors(graphData, builder) {
351
+ const { graph, graphOptions } = graphData;
352
+
353
+ if (graphOptions.allowMarkers()) {
354
+ return;
355
+ }
356
+
357
+ const node = graph.nodes.find((node) => node.toBeUnrelatedOrDeleted);
358
+
359
+ if (node) {
360
+ const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
361
+
362
+ throw builder.modelClass().createValidationError({
363
+ type: ValidationErrorType.InvalidGraph,
364
+ message: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers insert/upsert graph option`,
365
+ });
366
+ }
367
+ }
368
+
347
369
  function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
348
370
  for (const node of graphData.graph.nodes) {
349
371
  if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
@@ -140,6 +140,8 @@ class GraphInsert extends GraphOperation {
140
140
  edge.relation.isObjectionBelongsToOneRelation &&
141
141
  edge.isOwnerNode(node) &&
142
142
  edge.relatedNode.isReference &&
143
+ // The owner is patched by its id, which models without one don't have (#2305).
144
+ node.modelClass.getIdColumnArray().length > 0 &&
143
145
  // Both ends need to be inserted for the foreign key to be patched in later.
144
146
  this.graphOptions.shouldInsert(node, this.graphData) &&
145
147
  this.graphOptions.shouldInsert(edge.relatedNode.referencedNode, this.graphData) &&
@@ -37,7 +37,12 @@ class InstanceUpdateOperation extends UpdateOperation {
37
37
  }
38
38
 
39
39
  result = await super.onAfter2(builder, result);
40
- this.instance.$set(this.model);
40
+
41
+ // A query resolved without updating anything, e.g. with `none()`, leaves
42
+ // the instance untouched.
43
+ if (!this.isResolvedWithoutUpdate(builder)) {
44
+ this.instance.$set(this.model);
45
+ }
41
46
 
42
47
  if (isObject(result)) {
43
48
  this.instance.$set(result);
@@ -14,6 +14,7 @@ class UpdateOperation extends QueryBuilderOperation {
14
14
 
15
15
  this.model = null;
16
16
  this.modelOptions = Object.assign({}, this.opt.modelOptions || {});
17
+ this.hasNothingToUpdate = false;
17
18
  }
18
19
 
19
20
  onAdd(builder, args) {
@@ -34,10 +35,19 @@ class UpdateOperation extends QueryBuilderOperation {
34
35
 
35
36
  if (isEmpty(row)) {
36
37
  // Resolve the query if there is nothing to update.
38
+ this.hasNothingToUpdate = !builder.isExplicitlyResolvedOrRejected();
37
39
  builder.resolve(0);
38
40
  }
39
41
  }
40
42
 
43
+ // Returns true if the query was resolved without updating anything, e.g.
44
+ // with `none()` or `resolve()` in a hook, but not if it was only resolved
45
+ // because there is nothing to update in the table, as the update still
46
+ // applies to everything else (#2184).
47
+ isResolvedWithoutUpdate(builder) {
48
+ return builder.isExplicitlyResolvedOrRejected() && !this.hasNothingToUpdate;
49
+ }
50
+
41
51
  onBuildKnex(knexBuilder, builder) {
42
52
  const json = this.model.$toDatabaseJson(builder);
43
53
  const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
@@ -56,6 +66,7 @@ class UpdateOperation extends QueryBuilderOperation {
56
66
  clone() {
57
67
  const clone = super.clone();
58
68
  clone.model = this.model;
69
+ clone.hasNothingToUpdate = this.hasNothingToUpdate;
59
70
  return clone;
60
71
  }
61
72
  }
@@ -42,7 +42,9 @@ class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation
42
42
  knexBuilder,
43
43
  columns,
44
44
  helperColumns,
45
- values.as(knex.raw(`V(${helperColumns.map((_) => '??')})`, helperColumns)),
45
+ // knex only accepts strings as aliases, so the subquery is aliased in
46
+ // a raw query.
47
+ knex.raw(`? AS V(${helperColumns.map((_) => '??')})`, [values, ...helperColumns]),
46
48
  );
47
49
  }
48
50
  }
@@ -48,18 +48,13 @@ function isCompositeKey(columns) {
48
48
  }
49
49
 
50
50
  function buildCompositeArgs(columns, values) {
51
- if (!Array.isArray(values)) {
52
- // If the `values` is not an array of values but a function or a subquery
53
- // we have no way to implement this method.
54
- throw new Error(`sqlite doesn't support multi-column where in clauses`);
55
- }
56
-
57
- if (values.length === 0) {
51
+ if (!Array.isArray(values) || values.length === 0) {
58
52
  // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
59
- // into `1 = 1`.
53
+ // into `1 = 1`. Subqueries are compared as row values, which sqlite
54
+ // supports since version 3.15.
60
55
  return {
61
56
  method: 'whereIn',
62
- args: [columns, []],
57
+ args: [columns, values],
63
58
  };
64
59
  }
65
60
 
@@ -943,7 +943,7 @@ function peg$parse(input, options) {
943
943
  }
944
944
 
945
945
  function assertDuplicateRelation(node, expr) {
946
- if (expr.$name in node) {
946
+ if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
947
947
  console.warn(
948
948
  `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`,
949
949
  );
@@ -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.
@@ -103,6 +103,7 @@ class Relation {
103
103
  relation.joinTableRelatedProp = this.joinTableRelatedProp;
104
104
  relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
105
105
  relation.joinTableExtras = this.joinTableExtras;
106
+ relation.joinTableModify = this.joinTableModify;
106
107
 
107
108
  return relation;
108
109
  }