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.
- package/README.md +13 -13
- package/lib/model/AjvValidator.js +220 -13
- package/lib/model/Model.js +3 -1
- package/lib/model/modelId.js +10 -2
- package/lib/model/modelQueryProps.js +7 -0
- package/lib/queryBuilder/QueryBuilder.js +191 -8
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
- package/lib/queryBuilder/ReferenceBuilder.js +5 -3
- package/lib/queryBuilder/RelationExpression.js +13 -1
- package/lib/queryBuilder/StaticHookArguments.js +12 -1
- package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
- package/lib/queryBuilder/graph/GraphOptions.js +30 -1
- package/lib/queryBuilder/graph/GraphUpsert.js +38 -1
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
- package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
- package/lib/queryBuilder/join/JoinResultParser.js +22 -0
- package/lib/queryBuilder/join/RelationJoiner.js +80 -27
- package/lib/queryBuilder/join/utils.js +20 -2
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
- package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
- package/lib/queryBuilder/operations/InsertOperation.js +129 -13
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
- package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
- package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
- package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
- package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
- package/lib/queryBuilder/operations/select/Selection.js +95 -4
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
- package/lib/queryBuilder/whereGroups.js +125 -0
- package/lib/relations/Relation.js +5 -2
- package/lib/relations/RelationFindOperation.js +46 -2
- package/lib/relations/RelationProperty.js +3 -2
- package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
- package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
- package/lib/relations/manyToMany/ManyToManyRelation.js +104 -51
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +12 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
- package/lib/utils/assert.js +9 -0
- package/lib/utils/deprecate.js +6 -0
- package/lib/utils/identifierMapping.js +15 -1
- package/lib/utils/parseFieldExpression.js +28 -0
- package/lib/utils/wrapDbError.js +36 -0
- package/package.json +8 -7
- 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
|
|
66
|
+
return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
|
|
54
67
|
} else {
|
|
55
68
|
return (
|
|
56
|
-
selectionToTest.table === null ||
|
|
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 ||
|
|
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 (
|
|
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(
|
|
171
|
-
peg$c9 = '
|
|
172
|
-
peg$c10 = peg$literalExpectation('
|
|
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
|
-
|
|
455
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
511
|
+
peg$fail(peg$c10);
|
|
489
512
|
}
|
|
490
513
|
}
|
|
491
514
|
if (s5 !== peg$FAILED) {
|
|
492
|
-
|
|
493
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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:([^"])
|
|
90
|
+
chars:([^"])* { return chars.join(""); }
|
|
90
91
|
|
|
91
92
|
stringWithoutSingleQuotes =
|
|
92
|
-
chars:([^'])
|
|
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
|
-
|
|
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.
|
|
23
|
-
this.
|
|
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 =
|
|
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;
|