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
@@ -1,14 +1,21 @@
1
1
  'use strict';
2
2
 
3
- const { isString, isObject } = require('../../../utils/objectUtils');
3
+ const { isString, isObject, isPlainObject } = require('../../../utils/objectUtils');
4
+ const { isKnexQueryBuilder, isKnexRaw } = require('../../../utils/knexUtils');
4
5
 
5
6
  const ALIAS_REGEX = /\s+as\s+/i;
6
7
 
8
+ // Matches an alias at the end of a raw sql string: `as "alias"`, `as \`alias\``,
9
+ // `as [alias]`, `as ??`, `as :name:` or `as alias`.
10
+ const RAW_ALIAS_REGEX =
11
+ /\s+as\s+(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|(\?\?)|:(\w+):|([a-z_][\w$]*))\s*$/i;
12
+
7
13
  class Selection {
8
14
  constructor(table, column, alias) {
9
15
  this.table = table;
10
16
  this.column = column;
11
17
  this.alias = alias;
18
+ this.keepsSelectAll = false;
12
19
  }
13
20
 
14
21
  get name() {
@@ -23,6 +30,12 @@ class Selection {
23
30
  return createSelectionFromReference(selection);
24
31
  } else if (selection.isObjectionRawBuilder) {
25
32
  return createSelectionFromRaw(selection);
33
+ } else if (selection.isObjectionQueryBuilderBase) {
34
+ return createSelectionFromQueryBuilder(selection);
35
+ } else if (isKnexQueryBuilder(selection)) {
36
+ return createSelectionFromAlias(selection._single.as);
37
+ } else if (isKnexRaw(selection)) {
38
+ return createSelectionFromAlias(parseRawAlias(selection.sql, selection.bindings));
26
39
  } else {
27
40
  return null;
28
41
  }
@@ -50,10 +63,11 @@ class Selection {
50
63
  if (selectionInBuilder.column === '*') {
51
64
  if (selectionInBuilder.table) {
52
65
  if (selectionToTest.column === '*') {
53
- return selectionToTest.table === selectionInBuilder.table;
66
+ return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
54
67
  } else {
55
68
  return (
56
- selectionToTest.table === null || selectionToTest.table === selectionInBuilder.table
69
+ selectionToTest.table === null ||
70
+ isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
57
71
  );
58
72
  }
59
73
  } else {
@@ -67,7 +81,8 @@ class Selection {
67
81
  } else {
68
82
  return (
69
83
  selectionToTest.column === selectionInBuilder.column &&
70
- (selectionToTest.table === null || selectionToTest.table === selectionInBuilderTable)
84
+ (selectionToTest.table === null ||
85
+ isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
71
86
  );
72
87
  }
73
88
  }
@@ -82,6 +97,36 @@ Object.defineProperties(Selection.prototype, {
82
97
  },
83
98
  });
84
99
 
100
+ // Table names that only differ in a way that knex's `wrapIdentifier` mapping
101
+ // (for example `knexSnakeCaseMappers()`) cancels out refer to the same table:
102
+ // `dataExtension` and `data_extension` both end up as `data_extension`.
103
+ function isSameTable(builder, table1, table2) {
104
+ if (table1 === table2) {
105
+ return true;
106
+ } else if (!table1 || !table2) {
107
+ return false;
108
+ }
109
+
110
+ const knex = builder.unsafeKnex();
111
+ const wrapIdentifier = knex && knex.client && knex.client.config.wrapIdentifier;
112
+
113
+ if (!wrapIdentifier) {
114
+ return false;
115
+ }
116
+
117
+ const mapTable = (table) =>
118
+ table
119
+ .split('.')
120
+ .map((part) => wrapIdentifier(part, identity))
121
+ .join('.');
122
+
123
+ return mapTable(table1) === mapTable(table2);
124
+ }
125
+
126
+ function identity(value) {
127
+ return value;
128
+ }
129
+
85
130
  function createSelectionFromReference(ref) {
86
131
  return new Selection(ref.tableName, ref.column, ref.alias);
87
132
  }
@@ -89,6 +134,52 @@ function createSelectionFromReference(ref) {
89
134
  function createSelectionFromRaw(raw) {
90
135
  if (raw.alias) {
91
136
  return new Selection(null, null, raw.alias);
137
+ } else {
138
+ return createSelectionFromAlias(parseRawAlias(raw._sql, raw._args));
139
+ }
140
+ }
141
+
142
+ function parseRawAlias(sql, bindings) {
143
+ const match = isString(sql) && RAW_ALIAS_REGEX.exec(sql);
144
+
145
+ if (!match) {
146
+ return null;
147
+ }
148
+
149
+ const [, doubleQuoted, backQuoted, bracketQuoted, binding, namedBinding, unquoted] = match;
150
+
151
+ if (binding) {
152
+ return Array.isArray(bindings) ? bindings[bindings.length - 1] : null;
153
+ } else if (namedBinding) {
154
+ if (Array.isArray(bindings) && bindings.length === 1) {
155
+ // `raw(sql, { name: value })` of objection.
156
+ bindings = bindings[0];
157
+ }
158
+
159
+ return isPlainObject(bindings) ? bindings[namedBinding] : null;
160
+ } else if (unquoted) {
161
+ // Some databases, like postgres, fold unquoted identifiers to lower case
162
+ // and others don't, so only lower case ones can be used reliably.
163
+ return unquoted === unquoted.toLowerCase() ? unquoted : null;
164
+ } else {
165
+ return doubleQuoted || backQuoted || bracketQuoted;
166
+ }
167
+ }
168
+
169
+ // Subqueries aliased using `as()` select a column with the alias as name.
170
+ function createSelectionFromQueryBuilder(builder) {
171
+ const asOperation = builder.findLastOperation('as');
172
+ return createSelectionFromAlias(asOperation && asOperation.args[0]);
173
+ }
174
+
175
+ function createSelectionFromAlias(alias) {
176
+ if (isString(alias)) {
177
+ const selection = new Selection(null, null, alias);
178
+ // These selections weren't recognized in the past. To not change which
179
+ // other columns `withGraphJoined` selects, they don't count as explicit
180
+ // selections when deciding whether all columns are selected.
181
+ selection.keepsSelectAll = true;
182
+ return selection;
92
183
  } else {
93
184
  return null;
94
185
  }
@@ -26,6 +26,12 @@ class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation
26
26
  }
27
27
 
28
28
  buildComposite(knex, knexBuilder, columns, values) {
29
+ if (Array.isArray(values) && values.length === 0) {
30
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
31
+ // into `1 = 1`.
32
+ return this.whereIn(knexBuilder, columns, []);
33
+ }
34
+
29
35
  const helperColumns = columns.map((_, index) => getTempColumn(index));
30
36
 
31
37
  if (Array.isArray(values)) {
@@ -48,7 +48,11 @@ function buildCompositeArgs(knex, columns, values) {
48
48
  }
49
49
 
50
50
  function buildCompositeValueArgs(columns, values) {
51
- if (!Array.isArray(values[0])) {
51
+ if (values.length === 0) {
52
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
53
+ // into `1 = 1`.
54
+ return [columns, []];
55
+ } else if (!Array.isArray(values[0])) {
52
56
  return [columns, [values]];
53
57
  } else {
54
58
  return [columns, values];
@@ -54,6 +54,15 @@ function buildCompositeArgs(columns, values) {
54
54
  throw new Error(`sqlite doesn't support multi-column where in clauses`);
55
55
  }
56
56
 
57
+ if (values.length === 0) {
58
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
59
+ // into `1 = 1`.
60
+ return {
61
+ method: 'whereIn',
62
+ args: [columns, []],
63
+ };
64
+ }
65
+
57
66
  // Sqlite doesn't support the `where in` syntax for multiple columns but
58
67
  // we can emulate it using grouped `or` clauses.
59
68
  return {
@@ -166,10 +166,10 @@ function peg$parse(input, options) {
166
166
  peg$c4 = peg$literalExpectation('[', false),
167
167
  peg$c5 = '"',
168
168
  peg$c6 = peg$literalExpectation('"', false),
169
- peg$c7 = "'",
170
- peg$c8 = peg$literalExpectation("'", false),
171
- peg$c9 = ']',
172
- peg$c10 = peg$literalExpectation(']', false),
169
+ peg$c7 = ']',
170
+ peg$c8 = peg$literalExpectation(']', false),
171
+ peg$c9 = "'",
172
+ peg$c10 = peg$literalExpectation("'", false),
173
173
  peg$c11 = function (key) {
174
174
  return { type: 'object', ref: Array.isArray(key) ? key[1] : key };
175
175
  },
@@ -415,7 +415,7 @@ function peg$parse(input, options) {
415
415
  }
416
416
 
417
417
  function peg$parsebracketStringRef() {
418
- var s0, s1, s2, s3, s4, s5;
418
+ var s0, s1, s2, s3, s4, s5, s6, s7;
419
419
 
420
420
  s0 = peg$currPos;
421
421
  if (input.charCodeAt(peg$currPos) === 91) {
@@ -451,8 +451,31 @@ function peg$parse(input, options) {
451
451
  }
452
452
  }
453
453
  if (s5 !== peg$FAILED) {
454
- s3 = [s3, s4, s5];
455
- s2 = s3;
454
+ s6 = peg$currPos;
455
+ peg$silentFails++;
456
+ if (input.charCodeAt(peg$currPos) === 93) {
457
+ s7 = peg$c7;
458
+ peg$currPos++;
459
+ } else {
460
+ s7 = peg$FAILED;
461
+ if (peg$silentFails === 0) {
462
+ peg$fail(peg$c8);
463
+ }
464
+ }
465
+ peg$silentFails--;
466
+ if (s7 !== peg$FAILED) {
467
+ peg$currPos = s6;
468
+ s6 = void 0;
469
+ } else {
470
+ s6 = peg$FAILED;
471
+ }
472
+ if (s6 !== peg$FAILED) {
473
+ s3 = [s3, s4, s5, s6];
474
+ s2 = s3;
475
+ } else {
476
+ peg$currPos = s2;
477
+ s2 = peg$FAILED;
478
+ }
456
479
  } else {
457
480
  peg$currPos = s2;
458
481
  s2 = peg$FAILED;
@@ -468,29 +491,52 @@ function peg$parse(input, options) {
468
491
  if (s2 === peg$FAILED) {
469
492
  s2 = peg$currPos;
470
493
  if (input.charCodeAt(peg$currPos) === 39) {
471
- s3 = peg$c7;
494
+ s3 = peg$c9;
472
495
  peg$currPos++;
473
496
  } else {
474
497
  s3 = peg$FAILED;
475
498
  if (peg$silentFails === 0) {
476
- peg$fail(peg$c8);
499
+ peg$fail(peg$c10);
477
500
  }
478
501
  }
479
502
  if (s3 !== peg$FAILED) {
480
503
  s4 = peg$parsestringWithoutSingleQuotes();
481
504
  if (s4 !== peg$FAILED) {
482
505
  if (input.charCodeAt(peg$currPos) === 39) {
483
- s5 = peg$c7;
506
+ s5 = peg$c9;
484
507
  peg$currPos++;
485
508
  } else {
486
509
  s5 = peg$FAILED;
487
510
  if (peg$silentFails === 0) {
488
- peg$fail(peg$c8);
511
+ peg$fail(peg$c10);
489
512
  }
490
513
  }
491
514
  if (s5 !== peg$FAILED) {
492
- s3 = [s3, s4, s5];
493
- s2 = s3;
515
+ s6 = peg$currPos;
516
+ peg$silentFails++;
517
+ if (input.charCodeAt(peg$currPos) === 93) {
518
+ s7 = peg$c7;
519
+ peg$currPos++;
520
+ } else {
521
+ s7 = peg$FAILED;
522
+ if (peg$silentFails === 0) {
523
+ peg$fail(peg$c8);
524
+ }
525
+ }
526
+ peg$silentFails--;
527
+ if (s7 !== peg$FAILED) {
528
+ peg$currPos = s6;
529
+ s6 = void 0;
530
+ } else {
531
+ s6 = peg$FAILED;
532
+ }
533
+ if (s6 !== peg$FAILED) {
534
+ s3 = [s3, s4, s5, s6];
535
+ s2 = s3;
536
+ } else {
537
+ peg$currPos = s2;
538
+ s2 = peg$FAILED;
539
+ }
494
540
  } else {
495
541
  peg$currPos = s2;
496
542
  s2 = peg$FAILED;
@@ -509,12 +555,12 @@ function peg$parse(input, options) {
509
555
  }
510
556
  if (s2 !== peg$FAILED) {
511
557
  if (input.charCodeAt(peg$currPos) === 93) {
512
- s3 = peg$c9;
558
+ s3 = peg$c7;
513
559
  peg$currPos++;
514
560
  } else {
515
561
  s3 = peg$FAILED;
516
562
  if (peg$silentFails === 0) {
517
- peg$fail(peg$c10);
563
+ peg$fail(peg$c8);
518
564
  }
519
565
  }
520
566
  if (s3 !== peg$FAILED) {
@@ -554,12 +600,12 @@ function peg$parse(input, options) {
554
600
  s2 = peg$parseinteger();
555
601
  if (s2 !== peg$FAILED) {
556
602
  if (input.charCodeAt(peg$currPos) === 93) {
557
- s3 = peg$c9;
603
+ s3 = peg$c7;
558
604
  peg$currPos++;
559
605
  } else {
560
606
  s3 = peg$FAILED;
561
607
  if (peg$silentFails === 0) {
562
- peg$fail(peg$c10);
608
+ peg$fail(peg$c8);
563
609
  }
564
610
  }
565
611
  if (s3 !== peg$FAILED) {
@@ -719,21 +765,17 @@ function peg$parse(input, options) {
719
765
  peg$fail(peg$c22);
720
766
  }
721
767
  }
722
- if (s2 !== peg$FAILED) {
723
- while (s2 !== peg$FAILED) {
724
- s1.push(s2);
725
- if (peg$c21.test(input.charAt(peg$currPos))) {
726
- s2 = input.charAt(peg$currPos);
727
- peg$currPos++;
728
- } else {
729
- s2 = peg$FAILED;
730
- if (peg$silentFails === 0) {
731
- peg$fail(peg$c22);
732
- }
768
+ while (s2 !== peg$FAILED) {
769
+ s1.push(s2);
770
+ if (peg$c21.test(input.charAt(peg$currPos))) {
771
+ s2 = input.charAt(peg$currPos);
772
+ peg$currPos++;
773
+ } else {
774
+ s2 = peg$FAILED;
775
+ if (peg$silentFails === 0) {
776
+ peg$fail(peg$c22);
733
777
  }
734
778
  }
735
- } else {
736
- s1 = peg$FAILED;
737
779
  }
738
780
  if (s1 !== peg$FAILED) {
739
781
  peg$savedPos = s0;
@@ -758,21 +800,17 @@ function peg$parse(input, options) {
758
800
  peg$fail(peg$c24);
759
801
  }
760
802
  }
761
- if (s2 !== peg$FAILED) {
762
- while (s2 !== peg$FAILED) {
763
- s1.push(s2);
764
- if (peg$c23.test(input.charAt(peg$currPos))) {
765
- s2 = input.charAt(peg$currPos);
766
- peg$currPos++;
767
- } else {
768
- s2 = peg$FAILED;
769
- if (peg$silentFails === 0) {
770
- peg$fail(peg$c24);
771
- }
803
+ while (s2 !== peg$FAILED) {
804
+ s1.push(s2);
805
+ if (peg$c23.test(input.charAt(peg$currPos))) {
806
+ s2 = input.charAt(peg$currPos);
807
+ peg$currPos++;
808
+ } else {
809
+ s2 = peg$FAILED;
810
+ if (peg$silentFails === 0) {
811
+ peg$fail(peg$c24);
772
812
  }
773
813
  }
774
- } else {
775
- s1 = peg$FAILED;
776
814
  }
777
815
  if (s1 !== peg$FAILED) {
778
816
  peg$savedPos = s0;
@@ -31,6 +31,7 @@
31
31
  * 4. Keys containing square brackets and quotes
32
32
  * `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
33
33
  * Column `{ "Double.\"Quote\".[]" : "I was referred", "Sinlge.'Quote'.[]" : "Mee too!" }`
34
+ * 5. Empty keys `objectColumn:[""]` or `objectColumn:['']` Column `{ "" : "I was referred" }`
34
35
  * 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
35
36
  * not currently supported
36
37
  *
@@ -60,8 +61,8 @@ start =
60
61
  bracketStringRef =
61
62
  '['
62
63
  key:(
63
- '"' stringWithoutDoubleQuotes '"' /
64
- "'" stringWithoutSingleQuotes "'" /
64
+ '"' stringWithoutDoubleQuotes '"' &']' /
65
+ "'" stringWithoutSingleQuotes "'" &']' /
65
66
  stringWithoutSquareBrackets
66
67
  )
67
68
  ']'
@@ -86,10 +87,10 @@ stringWithoutColon =
86
87
  chars:([^:])+ { return chars.join(""); }
87
88
 
88
89
  stringWithoutDoubleQuotes =
89
- chars:([^"])+ { return chars.join(""); }
90
+ chars:([^"])* { return chars.join(""); }
90
91
 
91
92
  stringWithoutSingleQuotes =
92
- chars:([^'])+ { return chars.join(""); }
93
+ chars:([^'])* { return chars.join(""); }
93
94
 
94
95
  stringWithoutSquareBracketsOrDots =
95
96
  chars:([^.\x5D\x5B])+ { return chars.join(""); }
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const { WhereGroupOperation } = require('./operations/WhereGroupOperation');
4
+ const { isObject, isFunction } = require('../utils/objectUtils');
5
+ const { isKnexRaw } = require('../utils/knexUtils');
6
+
7
+ // Where operations of a query can come from different sources: the user, a
8
+ // relation's owner condition, the relation's `modify`/`filter` and so on. Each
9
+ // source forms its own "where group". All operations the user adds belong to
10
+ // the default group, and `addWhereGroup` assigns the operations added inside
11
+ // its callback to a new, separate group.
12
+ //
13
+ // Knex simply joins all where clauses with their `and` or `or` boolean. Since
14
+ // `and` binds tighter than `or`, a user's `orWhere` would escape the relation's
15
+ // owner condition: `where "ownerId" in (1) and a or b`. This would find, update
16
+ // and delete rows of other owners too. See issues #2191 and #1909.
17
+ //
18
+ // To prevent this, `wrapWhereGroups` is called after all `onBuild` hooks have
19
+ // run. If a query contains more than one where group, every group that may
20
+ // contain an `or` at its top level is wrapped in parentheses, so that the
21
+ // groups are always ANDed together: `where "ownerId" in (1) and (a or b)`.
22
+ // Groups without an `or` are left alone so that their SQL doesn't change.
23
+ function addWhereGroup(builder, callback) {
24
+ const parentWhereGroup = builder._whereGroup;
25
+ builder._whereGroup = {};
26
+
27
+ try {
28
+ return callback(builder);
29
+ } finally {
30
+ builder._whereGroup = parentWhereGroup;
31
+ }
32
+ }
33
+
34
+ function wrapWhereGroups(builder) {
35
+ const WhereSelector = builder.constructor.WhereSelector;
36
+ const whereOps = new Set();
37
+ const groups = new Map();
38
+
39
+ builder.forEachOperation(WhereSelector, (op) => {
40
+ // Only consider the top-most where operations. The descendants of a
41
+ // where operation are built as part of it.
42
+ if (op.isAncestorInSet(whereOps)) {
43
+ return;
44
+ }
45
+
46
+ whereOps.add(op);
47
+
48
+ const group = getWhereGroup(op);
49
+ let ops = groups.get(group);
50
+
51
+ if (!ops) {
52
+ ops = [];
53
+ groups.set(group, ops);
54
+ }
55
+
56
+ ops.push(op);
57
+ });
58
+
59
+ if (groups.size < 2) {
60
+ return builder;
61
+ }
62
+
63
+ for (const [group, ops] of groups) {
64
+ if (ops.some(mayContainTopLevelOr)) {
65
+ wrapOperations(builder, group, ops);
66
+ }
67
+ }
68
+
69
+ return builder;
70
+ }
71
+
72
+ function getWhereGroup(op) {
73
+ for (let it = op; it; it = it.parentOperation) {
74
+ if (it.whereGroup) {
75
+ return it.whereGroup;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function mayContainTopLevelOr(op) {
83
+ if (op instanceof WhereGroupOperation) {
84
+ return false;
85
+ }
86
+
87
+ // orWhere, orWhereIn, orWhereRaw, orWhereExists, orWhereJsonSupersetOf etc.
88
+ if (/^or[A-Z]/.test(op.name)) {
89
+ return true;
90
+ }
91
+
92
+ // Raw SQL isn't parenthesized by knex and can contain `or`.
93
+ if (/Raw$/.test(op.name)) {
94
+ return true;
95
+ }
96
+
97
+ return Array.isArray(op.args) && op.args.length === 1 && isRaw(op.args[0]);
98
+ }
99
+
100
+ function isRaw(arg) {
101
+ return isKnexRaw(arg) || (isObject(arg) && isFunction(arg.toKnexRaw));
102
+ }
103
+
104
+ function wrapOperations(builder, group, ops) {
105
+ const groupOp = new WhereGroupOperation('where');
106
+ groupOp.whereGroup = group;
107
+
108
+ builder.replaceOperation(ops[0], groupOp);
109
+
110
+ for (let i = 1; i < ops.length; ++i) {
111
+ builder.removeOperation(ops[i]);
112
+ }
113
+
114
+ for (const op of ops) {
115
+ op.parentOperation = null;
116
+ op.adderHookName = null;
117
+ }
118
+
119
+ groupOp.operations = ops;
120
+ }
121
+
122
+ module.exports = {
123
+ addWhereGroup,
124
+ wrapWhereGroups,
125
+ };
@@ -10,6 +10,7 @@ const { resolveModel } = require('../utils/resolveModel');
10
10
  const { get, isFunction } = require('../utils/objectUtils');
11
11
  const { mapAfterAllReturn } = require('../utils/promiseUtils');
12
12
  const { createModifier } = require('../utils/createModifier');
13
+ const { addWhereGroup } = require('../queryBuilder/whereGroups');
13
14
 
14
15
  class Relation {
15
16
  constructor(relationName, OwnerClass) {
@@ -115,14 +116,16 @@ class Relation {
115
116
 
116
117
  findQuery(builder, owner) {
117
118
  const relatedRefs = this.relatedProp.refs(builder);
118
- owner.buildFindQuery(builder, this, relatedRefs);
119
+ addWhereGroup(builder, () => owner.buildFindQuery(builder, this, relatedRefs));
119
120
 
120
121
  return this.applyModify(builder);
121
122
  }
122
123
 
123
124
  applyModify(builder) {
124
125
  try {
125
- return builder.modify(this.modify);
126
+ // Keep the relation's `modify` in a separate where group so that an
127
+ // `orWhere` in it can't escape the owner condition. See `whereGroups.js`.
128
+ return addWhereGroup(builder, () => builder.modify(this.modify));
126
129
  } catch (err) {
127
130
  if (err instanceof this.relatedModelClass.ModifierNotFoundError) {
128
131
  throw this.createError(err.message);
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { FindOperation } = require('../queryBuilder/operations/FindOperation');
4
+ const { deprecate } = require('../utils/deprecate');
4
5
 
5
6
  class RelationFindOperation extends FindOperation {
6
7
  constructor(name, opt) {
@@ -19,8 +20,12 @@ class RelationFindOperation extends FindOperation {
19
20
  this.maybeApplyAlias(builder);
20
21
  this.relation.findQuery(builder, this.owner);
21
22
 
22
- if (this.assignResultToOwner && this.owner.isModels) {
23
- this.selectMissingJoinColumns(builder);
23
+ if (this.owner.isModels) {
24
+ this.warnOnMissingOwnerProps();
25
+
26
+ if (this.assignResultToOwner) {
27
+ this.selectMissingJoinColumns(builder);
28
+ }
24
29
  }
25
30
  }
26
31
 
@@ -44,6 +49,8 @@ class RelationFindOperation extends FindOperation {
44
49
  arr.push(rel);
45
50
  }
46
51
 
52
+ this.warnOnMissingRelatedProps(related);
53
+
47
54
  for (let i = 0, l = owners.length; i < l; ++i) {
48
55
  const own = owners[i];
49
56
  const key = this.relation.ownerProp.propKey(own);
@@ -75,6 +82,43 @@ class RelationFindOperation extends FindOperation {
75
82
  return super.onAfter3(builder, related);
76
83
  }
77
84
 
85
+ // Owner models without the join columns, e.g. loaded with a partial
86
+ // `select()`, silently get no related models (#1832). Only `undefined`
87
+ // is reported, as `null` is a legit value for a missing relation.
88
+ // TODO: Consider throwing an error instead in objection 4.0.
89
+ warnOnMissingOwnerProps() {
90
+ const { ownerProp } = this.relation;
91
+
92
+ for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
93
+ if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
94
+ deprecate(
95
+ `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
96
+ `some owner models are missing the join property "${ownerProp.propDescription(c)}", ` +
97
+ 'so no related models are found for them. Select the column in the owner query.',
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ // Related models without the join properties can't be assigned to their
104
+ // owners and are silently dropped (#2258). This happens for example when the
105
+ // relation mapping uses column names but the rows come back with different
106
+ // property names, e.g. snake_case mappings with `knexSnakeCaseMappers`.
107
+ warnOnMissingRelatedProps(related) {
108
+ const { relatedProp } = this.relation;
109
+
110
+ for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
111
+ if (related.some((rel) => relatedProp.getProp(rel, c) === undefined)) {
112
+ deprecate(
113
+ `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
114
+ `some related models are missing the join property "${relatedProp.propDescription(c)}", ` +
115
+ 'so they are not assigned to any owner. Check that the relation mapping uses the ' +
116
+ 'property names of the models, e.g. camelCase names when using knexSnakeCaseMappers.',
117
+ );
118
+ }
119
+ }
120
+ }
121
+
78
122
  selectMissingJoinColumns(builder) {
79
123
  const relatedProp = this.relation.relatedProp;
80
124
  const addedSelects = [];
@@ -31,10 +31,11 @@ class RelationProperty {
31
31
  //
32
32
  // modelClassResolver must be a function that takes a table name
33
33
  // and returns a model class.
34
- constructor(references, modelClassResolver) {
34
+ constructor(references, modelClassResolver, { allowEmpty = false } = {}) {
35
35
  const refs = createRefs(asArray(references));
36
36
  const paths = createPaths(refs, modelClassResolver);
37
- const modelClass = resolveModelClass(paths);
37
+ const modelClass =
38
+ allowEmpty && refs.length === 0 ? modelClassResolver(null) : resolveModelClass(paths);
38
39
 
39
40
  this._refs = refs.map((ref) => ref.model(modelClass));
40
41
  this._modelClass = modelClass;