objection 3.1.5 → 3.2.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 +3 -3
- package/lib/model/AjvValidator.js +40 -15
- package/lib/model/Model.js +3 -1
- package/lib/model/graph/ModelGraphNode.js +12 -0
- package/lib/model/modelToJson.js +2 -1
- package/lib/queryBuilder/QueryBuilder.js +156 -45
- package/lib/queryBuilder/RelationExpression.js +8 -8
- package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
- package/lib/queryBuilder/graph/GraphOptions.js +21 -2
- package/lib/queryBuilder/graph/GraphUpsert.js +30 -0
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -0
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +2 -0
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
- package/lib/queryBuilder/join/RelationJoiner.js +17 -0
- package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
- package/lib/queryBuilder/operations/NoneOperation.js +37 -0
- package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -5
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +8 -2
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +15 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +8 -2
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
- package/lib/relations/RelationProperty.js +7 -3
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
- package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
- package/lib/relations/hasMany/HasManyRelateOperation.js +1 -0
- package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
- package/lib/utils/identifierMapping.js +8 -2
- package/lib/utils/objectUtils.js +9 -5
- package/package.json +14 -10
- package/typings/objection/index.d.ts +59 -40
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[](https://stand-with-ukraine.pp.ua)
|
|
2
2
|
|
|
3
|
-
[](https://github.com/ditojs/objection/actions)
|
|
4
4
|
[](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
5
5
|
|
|
6
6
|
# [Objection.js](https://vincit.github.io/objection.js)
|
|
@@ -9,7 +9,7 @@ Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping
|
|
|
9
9
|
|
|
10
10
|
Even though ORM is the best commonly known acronym to describe objection, a more accurate description is to call it **a relational query builder**. You get all the benefits of an SQL query builder but also a powerful set of tools for working with relations.
|
|
11
11
|
|
|
12
|
-
Objection.js is built on an SQL query builder called [knex](http://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/
|
|
12
|
+
Objection.js is built on an SQL query builder called [knex](http://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/ditojs/objection/actions).
|
|
13
13
|
|
|
14
14
|
What objection.js gives you:
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ What objection.js gives you:
|
|
|
17
17
|
- **Simple and fun way to [fetch, insert, update and delete](https://vincit.github.io/objection.js/guide/query-examples.html) objects using the full power of SQL**
|
|
18
18
|
- **Powerful mechanisms for [eager loading](https://vincit.github.io/objection.js/guide/query-examples.html#eager-loading), [inserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-inserts) and [upserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-upserts) object graphs**
|
|
19
19
|
- **Easy to use [transactions](https://vincit.github.io/objection.js/guide/transactions.html)**
|
|
20
|
-
- **Official [TypeScript](https://github.com/
|
|
20
|
+
- **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
|
|
21
21
|
- **Optional [JSON schema](https://vincit.github.io/objection.js/guide/validation.html) validation**
|
|
22
22
|
- **A way to [store complex documents](https://vincit.github.io/objection.js/guide/documents.html) as single rows**
|
|
23
23
|
|
|
@@ -35,6 +35,9 @@ class AjvValidator extends Validator {
|
|
|
35
35
|
// A cache for the compiled validator functions.
|
|
36
36
|
self.cache = new Map();
|
|
37
37
|
|
|
38
|
+
// The cache keys of the unmodified model class schemas.
|
|
39
|
+
self.modelCacheKeys = new WeakMap();
|
|
40
|
+
|
|
38
41
|
const setupAjv = (ajv) => {
|
|
39
42
|
if (conf.onCreateAjv) {
|
|
40
43
|
conf.onCreateAjv(ajv);
|
|
@@ -92,12 +95,23 @@ class AjvValidator extends Validator {
|
|
|
92
95
|
// Use the Ajv custom serializer if provided.
|
|
93
96
|
const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
jsonSchema
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
let cacheKey;
|
|
99
|
+
|
|
100
|
+
if (jsonSchema === modelClass.getJsonSchema()) {
|
|
101
|
+
// Optimization for the common case where jsonSchema is never modified.
|
|
102
|
+
// The key is only computed once per model class. It includes the schema
|
|
103
|
+
// contents so that model classes with the same `uniqueTag()` but
|
|
104
|
+
// different schemas don't share validators, while bound model classes
|
|
105
|
+
// with equal schemas still do.
|
|
106
|
+
cacheKey = this.modelCacheKeys.get(modelClass);
|
|
107
|
+
|
|
108
|
+
if (cacheKey === undefined) {
|
|
109
|
+
cacheKey = `${modelClass.uniqueTag()}:${createCacheKey(jsonSchema)}`;
|
|
110
|
+
this.modelCacheKeys.set(modelClass, cacheKey);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
cacheKey = createCacheKey(jsonSchema);
|
|
114
|
+
}
|
|
101
115
|
|
|
102
116
|
let validators = this.cache.get(cacheKey);
|
|
103
117
|
let validator = null;
|
|
@@ -263,24 +277,35 @@ function objectHasDefaults(obj) {
|
|
|
263
277
|
|
|
264
278
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
265
279
|
const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
|
|
280
|
+
// `definitions` was renamed to `$defs` in draft 2019-09. Support both.
|
|
281
|
+
const definitionProps = ['definitions', '$defs'];
|
|
266
282
|
return Object.assign(
|
|
267
283
|
omit(jsonSchema, ['required', ...subSchemaProps]),
|
|
268
284
|
...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
|
|
269
|
-
|
|
270
|
-
? {
|
|
271
|
-
definitions: Object.assign(
|
|
272
|
-
...Object.keys(jsonSchema.definitions).map((prop) => ({
|
|
273
|
-
[prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
|
|
274
|
-
})),
|
|
275
|
-
),
|
|
276
|
-
}
|
|
277
|
-
: {},
|
|
285
|
+
...definitionProps.map((prop) => definitionsWithoutRequired(jsonSchema, prop)),
|
|
278
286
|
jsonSchema.discriminator && jsonSchema.discriminator.propertyName
|
|
279
287
|
? { required: [jsonSchema.discriminator.propertyName] }
|
|
280
288
|
: {},
|
|
281
289
|
);
|
|
282
290
|
}
|
|
283
291
|
|
|
292
|
+
function definitionsWithoutRequired(jsonSchema, prop) {
|
|
293
|
+
const definitions = jsonSchema && jsonSchema[prop];
|
|
294
|
+
|
|
295
|
+
if (definitions && Object.keys(definitions).length > 0) {
|
|
296
|
+
return {
|
|
297
|
+
[prop]: Object.fromEntries(
|
|
298
|
+
Object.entries(definitions).map(([key, schema]) => [
|
|
299
|
+
key,
|
|
300
|
+
jsonSchemaWithoutRequired(schema),
|
|
301
|
+
]),
|
|
302
|
+
),
|
|
303
|
+
};
|
|
304
|
+
} else {
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
284
309
|
function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
285
310
|
if (jsonSchema[prop]) {
|
|
286
311
|
if (Array.isArray(jsonSchema[prop])) {
|
package/lib/model/Model.js
CHANGED
|
@@ -120,7 +120,7 @@ class Model {
|
|
|
120
120
|
return json;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
$formatJson(json) {
|
|
123
|
+
$formatJson(json, options) {
|
|
124
124
|
return json;
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -721,6 +721,8 @@ Model.uidProp = '#id';
|
|
|
721
721
|
Model.uidRefProp = '#ref';
|
|
722
722
|
Model.dbRefProp = '#dbRef';
|
|
723
723
|
Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
|
|
724
|
+
Model.graphUnrelateProp = '#unrelate';
|
|
725
|
+
Model.graphDeleteProp = '#delete';
|
|
724
726
|
Model.jsonAttributes = null;
|
|
725
727
|
Model.cloneObjectAttributes = true;
|
|
726
728
|
Model.virtualAttributes = null;
|
|
@@ -46,6 +46,18 @@ class ModelGraphNode {
|
|
|
46
46
|
return this.obj[this.modelClass.dbRefProp];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
get toBeUnrelated() {
|
|
50
|
+
return this.obj[this.modelClass.graphUnrelateProp] === true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get toBeDeleted() {
|
|
54
|
+
return this.obj[this.modelClass.graphDeleteProp] === true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get toBeUnrelatedOrDeleted() {
|
|
58
|
+
return this.toBeUnrelated || this.toBeDeleted;
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
get parentNode() {
|
|
50
62
|
if (this.parentEdge) {
|
|
51
63
|
return this.parentEdge.ownerNode;
|
package/lib/model/modelToJson.js
CHANGED
|
@@ -15,10 +15,11 @@ function toJson(model, optIn) {
|
|
|
15
15
|
pick: null,
|
|
16
16
|
omitFromJson: model.$omitFromJson() || null,
|
|
17
17
|
cloneObjects: modelClass.cloneObjectAttributes,
|
|
18
|
+
format: optIn && optIn.format,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
let json = toExternalJsonImpl(model, opt);
|
|
21
|
-
json = model.$formatJson(json);
|
|
22
|
+
json = model.$formatJson(json, opt.format);
|
|
22
23
|
|
|
23
24
|
return json;
|
|
24
25
|
}
|
|
@@ -43,6 +43,7 @@ const { SelectOperation } = require('./operations/select/SelectOperation');
|
|
|
43
43
|
const { EagerOperation } = require('./operations/eager/EagerOperation');
|
|
44
44
|
const { RangeOperation } = require('./operations/RangeOperation');
|
|
45
45
|
const { FirstOperation } = require('./operations/FirstOperation');
|
|
46
|
+
const { NoneOperation } = require('./operations/NoneOperation');
|
|
46
47
|
const { FromOperation } = require('./operations/FromOperation');
|
|
47
48
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
48
49
|
|
|
@@ -56,6 +57,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
56
57
|
self._modifiers = {};
|
|
57
58
|
|
|
58
59
|
self._allowedGraphExpression = null;
|
|
60
|
+
self._graphModifiersAtPath = [];
|
|
59
61
|
self._findOperationOptions = modelClass.defaultFindOptions;
|
|
60
62
|
self._relatedQueryFor = null;
|
|
61
63
|
|
|
@@ -203,6 +205,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
203
205
|
eagerOp.expression = eagerOp.expression.merge(parsedExp);
|
|
204
206
|
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...options };
|
|
205
207
|
|
|
208
|
+
checkMixedEager(this);
|
|
206
209
|
checkEager(this);
|
|
207
210
|
return this;
|
|
208
211
|
}
|
|
@@ -223,28 +226,30 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
graphExpressionObject() {
|
|
226
|
-
const
|
|
229
|
+
const expression = getGraphExpression(this);
|
|
227
230
|
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
231
|
+
if (expression && !expression.isEmpty) {
|
|
232
|
+
return expression.toPojo();
|
|
230
233
|
} else {
|
|
231
234
|
return null;
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
235
238
|
graphModifiersAtPath() {
|
|
236
|
-
const
|
|
239
|
+
const expression = getGraphExpression(this);
|
|
237
240
|
|
|
238
|
-
if (
|
|
239
|
-
return
|
|
241
|
+
if (expression && !expression.isEmpty) {
|
|
242
|
+
return this._graphModifiersAtPath.map((it) => Object.assign({}, it));
|
|
240
243
|
} else {
|
|
241
244
|
return [];
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
modifyGraph(path, modifier) {
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
// The modifiers are shared by all eager operations. They only take effect
|
|
250
|
+
// for the paths that exist in each operation's expression.
|
|
251
|
+
ensureEagerOperation(this);
|
|
252
|
+
this._graphModifiersAtPath.push({ path, modifier });
|
|
248
253
|
return this;
|
|
249
254
|
}
|
|
250
255
|
|
|
@@ -301,8 +306,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
hasWithGraph() {
|
|
304
|
-
|
|
305
|
-
return !!eagerOp && !eagerOp.expression.isEmpty;
|
|
309
|
+
return findEagerOperations(this).some((eagerOp) => !eagerOp.expression.isEmpty);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
isSelectAll() {
|
|
@@ -350,6 +354,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
350
354
|
builder._modifiers = { ...this._modifiers };
|
|
351
355
|
|
|
352
356
|
builder._allowedGraphExpression = this._allowedGraphExpression;
|
|
357
|
+
builder._graphModifiersAtPath = this._graphModifiersAtPath.slice();
|
|
353
358
|
builder._findOperationOptions = this._findOperationOptions;
|
|
354
359
|
builder._relatedQueryFor = this._relatedQueryFor;
|
|
355
360
|
|
|
@@ -421,7 +426,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
421
426
|
const builder = this.clone().clear(/orderBy|offset|limit/);
|
|
422
427
|
|
|
423
428
|
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
424
|
-
|
|
429
|
+
// Also clear the order on the built knex query, since `orderBy` calls
|
|
430
|
+
// can be added at build time, e.g. by a relation's `modify`.
|
|
431
|
+
builder.toKnexQuery(knexBuilder).clearOrder().as('temp');
|
|
425
432
|
});
|
|
426
433
|
|
|
427
434
|
if (this.internalOptions().debug) {
|
|
@@ -808,6 +815,10 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
808
815
|
return this.addOperation(new FirstOperation('first'), args);
|
|
809
816
|
}
|
|
810
817
|
|
|
818
|
+
none() {
|
|
819
|
+
return this.addOperation(new NoneOperation('none'), []);
|
|
820
|
+
}
|
|
821
|
+
|
|
811
822
|
joinRelated(expression, options) {
|
|
812
823
|
ensureJoinRelatedOperation(this, 'innerJoin').addCall({
|
|
813
824
|
expression,
|
|
@@ -922,6 +933,10 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
922
933
|
return this.addOperation(new FromOperation('from'), args);
|
|
923
934
|
}
|
|
924
935
|
|
|
936
|
+
updateFrom(...args) {
|
|
937
|
+
return this.addOperation(new KnexOperation('updateFrom'), args);
|
|
938
|
+
}
|
|
939
|
+
|
|
925
940
|
table(...args) {
|
|
926
941
|
return this.addOperation(new FromOperation('table'), args);
|
|
927
942
|
}
|
|
@@ -930,6 +945,15 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
930
945
|
if (arguments.length === 0) {
|
|
931
946
|
return this._relatedQueryFor;
|
|
932
947
|
} else {
|
|
948
|
+
// The write operation factories installed by the static `relatedQuery()`
|
|
949
|
+
// read `for()` when the write method is called. Setting it afterwards
|
|
950
|
+
// would make the query silently target the wrong rows.
|
|
951
|
+
if (this._findOperationFactory !== findOperationFactory && !this.isFind()) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
'for() must be called before insert, update, patch, delete, relate or unrelate on queries created using the static relatedQuery method',
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
933
957
|
this._relatedQueryFor = relatedQueryFor;
|
|
934
958
|
return this;
|
|
935
959
|
}
|
|
@@ -952,48 +976,83 @@ function getTableName(modelClassOrTableName) {
|
|
|
952
976
|
}
|
|
953
977
|
}
|
|
954
978
|
|
|
979
|
+
// A query builder can hold at most one eager operation of each kind: one
|
|
980
|
+
// `JoinEagerOperation` for `withGraphJoined` and one `WhereInEagerOperation`
|
|
981
|
+
// (or its subclass `NaiveEagerOperation`) for `withGraphFetched`.
|
|
955
982
|
function ensureEagerOperation(builder, algorithm = null) {
|
|
956
983
|
const modelClass = builder.modelClass();
|
|
957
984
|
const defaultGraphOptions = modelClass.getDefaultGraphOptions();
|
|
958
|
-
const eagerOp = builder.findOperation(EagerOperation);
|
|
959
985
|
|
|
960
|
-
if (algorithm) {
|
|
961
|
-
const
|
|
986
|
+
if (!algorithm) {
|
|
987
|
+
const eagerOp = builder.findOperation(EagerOperation);
|
|
962
988
|
|
|
963
|
-
if (eagerOp
|
|
989
|
+
if (eagerOp) {
|
|
964
990
|
return eagerOp;
|
|
965
|
-
}
|
|
966
|
-
const newEagerOp = new EagerOperationClass('eager', {
|
|
967
|
-
defaultGraphOptions,
|
|
968
|
-
});
|
|
991
|
+
}
|
|
969
992
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
993
|
+
algorithm = getWhereInEagerAlgorithm(builder);
|
|
994
|
+
}
|
|
973
995
|
|
|
974
|
-
|
|
975
|
-
|
|
996
|
+
const EagerOperationClass = getOperationClassForEagerAlgorithm(builder, algorithm);
|
|
997
|
+
const EagerOperationKind = getEagerOperationKind(EagerOperationClass);
|
|
998
|
+
const eagerOp = builder.findOperation(EagerOperationKind);
|
|
976
999
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
if (eagerOp) {
|
|
981
|
-
return eagerOp;
|
|
982
|
-
} else {
|
|
983
|
-
const EagerOperationClass = getOperationClassForEagerAlgorithm(
|
|
984
|
-
builder,
|
|
985
|
-
getWhereInEagerAlgorithm(builder),
|
|
986
|
-
);
|
|
1000
|
+
if (eagerOp && eagerOp.constructor === EagerOperationClass) {
|
|
1001
|
+
return eagerOp;
|
|
1002
|
+
}
|
|
987
1003
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1004
|
+
const newEagerOp = new EagerOperationClass('eager', {
|
|
1005
|
+
defaultGraphOptions,
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
if (eagerOp) {
|
|
1009
|
+
// Switching between algorithms of the same kind, e.g. from
|
|
1010
|
+
// `WhereInEagerOperation` to `NaiveEagerOperation`.
|
|
1011
|
+
newEagerOp.cloneFrom(eagerOp);
|
|
1012
|
+
builder.clear(EagerOperationKind);
|
|
1013
|
+
} else if (!builder.has(EagerOperation)) {
|
|
1014
|
+
// The `modifyGraph` modifiers only live as long as an eager operation.
|
|
1015
|
+
builder._graphModifiersAtPath = [];
|
|
1016
|
+
}
|
|
991
1017
|
|
|
992
|
-
|
|
1018
|
+
builder.addOperation(newEagerOp);
|
|
993
1019
|
|
|
994
|
-
|
|
995
|
-
|
|
1020
|
+
// Keep the joined operation before the fetched one (see `findEagerOperations`),
|
|
1021
|
+
// even if it is added after `addImplicitOperations`, e.g. in `runBefore`.
|
|
1022
|
+
const fetchOp =
|
|
1023
|
+
EagerOperationKind === JoinEagerOperation && builder.findOperation(WhereInEagerOperation);
|
|
1024
|
+
|
|
1025
|
+
if (fetchOp) {
|
|
1026
|
+
builder.removeOperation(fetchOp).addOperation(fetchOp);
|
|
996
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
return newEagerOp;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function getEagerOperationKind(EagerOperationClass) {
|
|
1033
|
+
return EagerOperationClass === JoinEagerOperation ? JoinEagerOperation : WhereInEagerOperation;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function findEagerOperations(builder) {
|
|
1037
|
+
// Joined operations first, so that their `onBuild` runs before the fetched
|
|
1038
|
+
// one's. See `WhereInEagerOperation.onBuild`.
|
|
1039
|
+
return [
|
|
1040
|
+
builder.findOperation(JoinEagerOperation),
|
|
1041
|
+
builder.findOperation(WhereInEagerOperation),
|
|
1042
|
+
].filter(Boolean);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function getGraphExpression(builder) {
|
|
1046
|
+
const eagerOps = findEagerOperations(builder);
|
|
1047
|
+
|
|
1048
|
+
if (eagerOps.length === 0) {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return eagerOps.reduce(
|
|
1053
|
+
(expression, eagerOp) => expression.merge(eagerOp.expression),
|
|
1054
|
+
RelationExpression.create(),
|
|
1055
|
+
);
|
|
997
1056
|
}
|
|
998
1057
|
|
|
999
1058
|
function getWhereInEagerAlgorithm(builder) {
|
|
@@ -1036,14 +1095,44 @@ function parseRelationExpression(modelClass, exp) {
|
|
|
1036
1095
|
}
|
|
1037
1096
|
}
|
|
1038
1097
|
|
|
1098
|
+
function checkMixedEager(builder) {
|
|
1099
|
+
const joinOp = builder.findOperation(JoinEagerOperation);
|
|
1100
|
+
const fetchOp = builder.findOperation(WhereInEagerOperation);
|
|
1101
|
+
|
|
1102
|
+
if (!joinOp || !fetchOp) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const joinNode = joinOp.expression.node;
|
|
1107
|
+
const fetchNode = fetchOp.expression.node;
|
|
1108
|
+
const isUsed = (node) => node.$allRecursive || node.$childNames.length > 0;
|
|
1109
|
+
|
|
1110
|
+
if (!isUsed(joinNode) || !isUsed(fetchNode)) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (joinNode.$allRecursive || fetchNode.$allRecursive) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
'relation expression `*` cannot be used when mixing withGraphJoined and withGraphFetched',
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
for (const childName of joinNode.$childNames) {
|
|
1121
|
+
if (fetchNode.$childNames.includes(childName)) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
`relation \`${childName}\` cannot be loaded with both withGraphJoined and withGraphFetched`,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1039
1129
|
function checkEager(builder) {
|
|
1040
|
-
const
|
|
1130
|
+
const expr = getGraphExpression(builder);
|
|
1041
1131
|
|
|
1042
|
-
if (!
|
|
1132
|
+
if (!expr) {
|
|
1043
1133
|
return;
|
|
1044
1134
|
}
|
|
1045
1135
|
|
|
1046
|
-
const expr = eagerOp.expression;
|
|
1047
1136
|
const allowedExpr = builder.allowedGraphExpression();
|
|
1048
1137
|
|
|
1049
1138
|
if (expr.numChildren > 0 && allowedExpr && !allowedExpr.isSubExpression(expr)) {
|
|
@@ -1065,6 +1154,7 @@ function findQueryExecutorOperation(builder) {
|
|
|
1065
1154
|
function beforeExecute(builder) {
|
|
1066
1155
|
let promise = Promise.resolve();
|
|
1067
1156
|
|
|
1157
|
+
checkRelatedQueryFor(builder);
|
|
1068
1158
|
builder = addImplicitOperations(builder);
|
|
1069
1159
|
|
|
1070
1160
|
// Resolve all before hooks before building and executing the query
|
|
@@ -1181,6 +1271,7 @@ function ensureJoinRelatedOperation(builder, joinOperation) {
|
|
|
1181
1271
|
}
|
|
1182
1272
|
|
|
1183
1273
|
function prebuildQuery(builder) {
|
|
1274
|
+
checkRelatedQueryFor(builder);
|
|
1184
1275
|
builder = addImplicitOperations(builder);
|
|
1185
1276
|
builder = callOnBuildHooks(builder);
|
|
1186
1277
|
|
|
@@ -1193,6 +1284,17 @@ function prebuildQuery(builder) {
|
|
|
1193
1284
|
}
|
|
1194
1285
|
}
|
|
1195
1286
|
|
|
1287
|
+
function checkRelatedQueryFor(builder) {
|
|
1288
|
+
// `for()` is only consumed by the operation factories installed by the static
|
|
1289
|
+
// `relatedQuery()`. The default ones ignore it, which would silently make the
|
|
1290
|
+
// query target all rows of the table.
|
|
1291
|
+
if (builder.for() !== null && builder._findOperationFactory === findOperationFactory) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
'for() can only be used with queries created using the static relatedQuery method',
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1196
1298
|
function addImplicitOperations(builder) {
|
|
1197
1299
|
if (builder.isFind()) {
|
|
1198
1300
|
// If no write operations have been called at this point this query is a
|
|
@@ -1215,10 +1317,13 @@ function addFindOperation(builder) {
|
|
|
1215
1317
|
}
|
|
1216
1318
|
|
|
1217
1319
|
function moveEagerOperationToEnd(builder) {
|
|
1218
|
-
const
|
|
1320
|
+
const eagerOps = findEagerOperations(builder);
|
|
1219
1321
|
|
|
1220
1322
|
builder.clear(EagerOperation);
|
|
1221
|
-
|
|
1323
|
+
|
|
1324
|
+
for (const eagerOp of eagerOps) {
|
|
1325
|
+
builder.addOperation(eagerOp);
|
|
1326
|
+
}
|
|
1222
1327
|
}
|
|
1223
1328
|
|
|
1224
1329
|
function callOnBuildHooks(builder) {
|
|
@@ -1240,6 +1345,12 @@ function callOnBuildFuncs(builder, func) {
|
|
|
1240
1345
|
function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
|
|
1241
1346
|
knexBuilder = builder.executeOnBuildKnex(knexBuilder);
|
|
1242
1347
|
|
|
1348
|
+
const noneOperation = builder.findOperation(NoneOperation);
|
|
1349
|
+
|
|
1350
|
+
if (noneOperation) {
|
|
1351
|
+
knexBuilder = noneOperation.onAfterBuildKnex(knexBuilder);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1243
1354
|
const fromOperation = builder.findLastOperation(QueryBuilderBase.FromSelector);
|
|
1244
1355
|
|
|
1245
1356
|
if (!builder.isPartial()) {
|
|
@@ -229,6 +229,14 @@ function toString(node) {
|
|
|
229
229
|
let childExpr = childNames.map((childName) => node[childName]).map(toString);
|
|
230
230
|
let str = node.$relation;
|
|
231
231
|
|
|
232
|
+
if (node.$modify.length) {
|
|
233
|
+
str += `(${node.$modify.join(', ')})`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (node.$name !== node.$relation) {
|
|
237
|
+
str += ` as ${node.$name}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
if (node.$recursive) {
|
|
233
241
|
if (isNumber(node.$recursive)) {
|
|
234
242
|
str += '.^' + node.$recursive;
|
|
@@ -245,14 +253,6 @@ function toString(node) {
|
|
|
245
253
|
childExpr = childExpr[0];
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
if (node.$modify.length) {
|
|
249
|
-
str += `(${node.$modify.join(', ')})`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (node.$name !== node.$relation) {
|
|
253
|
-
str += ` as ${node.$name}`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
256
|
if (childExpr) {
|
|
257
257
|
if (str) {
|
|
258
258
|
return `${str}.${childExpr}`;
|
|
@@ -141,7 +141,9 @@ function shouldSelectColumn(column, selects, node) {
|
|
|
141
141
|
!selects.has(column) &&
|
|
142
142
|
column !== modelClass.propertyNameToColumnName(modelClass.dbRefProp) &&
|
|
143
143
|
column !== modelClass.propertyNameToColumnName(modelClass.uidRefProp) &&
|
|
144
|
-
column !== modelClass.propertyNameToColumnName(modelClass.uidProp)
|
|
144
|
+
column !== modelClass.propertyNameToColumnName(modelClass.uidProp) &&
|
|
145
|
+
column !== modelClass.propertyNameToColumnName(modelClass.graphUnrelateProp) &&
|
|
146
|
+
column !== modelClass.propertyNameToColumnName(modelClass.graphDeleteProp)
|
|
145
147
|
);
|
|
146
148
|
}
|
|
147
149
|
|
|
@@ -51,6 +51,10 @@ class GraphOptions {
|
|
|
51
51
|
|
|
52
52
|
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
|
|
53
53
|
shouldRelateIgnoreDisable(node, graphData) {
|
|
54
|
+
if (node.toBeUnrelated || node.toBeDeleted) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
if (node.isReference || node.isDbReference) {
|
|
55
59
|
return true;
|
|
56
60
|
}
|
|
@@ -71,6 +75,10 @@ class GraphOptions {
|
|
|
71
75
|
|
|
72
76
|
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
|
|
73
77
|
shouldInsertIgnoreDisable(node, graphData) {
|
|
78
|
+
if (node.toBeUnrelated || node.toBeDeleted) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
return (
|
|
75
83
|
!getCurrentNode(node, graphData) &&
|
|
76
84
|
!this.shouldRelateIgnoreDisable(node, graphData) &&
|
|
@@ -89,6 +97,10 @@ class GraphOptions {
|
|
|
89
97
|
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
|
|
90
98
|
// update or patch operations.
|
|
91
99
|
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
|
|
100
|
+
if (node.toBeUnrelated || node.toBeDeleted) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
if (this.shouldRelate(node, graphData)) {
|
|
93
105
|
// We should update all nodes that are going to be related. Note that
|
|
94
106
|
// we don't actually update anything unless there is something to update
|
|
@@ -121,16 +133,23 @@ class GraphOptions {
|
|
|
121
133
|
}
|
|
122
134
|
|
|
123
135
|
shouldUnrelate(currentNode, graphData) {
|
|
136
|
+
const node = getNode(currentNode, graphData.graph);
|
|
137
|
+
if (node && node.toBeUnrelated) return true;
|
|
138
|
+
|
|
124
139
|
return (
|
|
125
|
-
!
|
|
140
|
+
!node &&
|
|
126
141
|
!this._hasOption(currentNode, NO_UNRELATE) &&
|
|
127
142
|
this.shouldUnrelateIgnoreDisable(currentNode)
|
|
128
143
|
);
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
shouldDelete(currentNode, graphData) {
|
|
147
|
+
const node = getNode(currentNode, graphData.graph);
|
|
148
|
+
// `#unrelate` takes precedence over `#delete` if both are set.
|
|
149
|
+
if (node && node.toBeDeleted && !node.toBeUnrelated) return true;
|
|
150
|
+
|
|
132
151
|
return (
|
|
133
|
-
!
|
|
152
|
+
!node &&
|
|
134
153
|
!this._hasOption(currentNode, NO_DELETE) &&
|
|
135
154
|
!this.shouldUnrelateIgnoreDisable(currentNode)
|
|
136
155
|
);
|
|
@@ -91,6 +91,7 @@ function pruneGraphs() {
|
|
|
91
91
|
|
|
92
92
|
if (!graphData.graphOptions.isInsertOnly()) {
|
|
93
93
|
pruneDeletedBranches(graphData);
|
|
94
|
+
pruneUnrelatedOrDeletedBranches(graphData);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
return graphData;
|
|
@@ -136,6 +137,19 @@ function pruneDeletedBranches(graphData) {
|
|
|
136
137
|
removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
// Models marked with `#unrelate` or `#delete` are unrelated or deleted
|
|
141
|
+
// as a whole. Their relations are ignored, both in the upserted graph
|
|
142
|
+
// and in the current graph.
|
|
143
|
+
function pruneUnrelatedOrDeletedBranches(graphData) {
|
|
144
|
+
const { graph, currentGraph } = graphData;
|
|
145
|
+
|
|
146
|
+
const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
|
|
147
|
+
const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
|
|
148
|
+
|
|
149
|
+
removeBranchesFromGraph(roots, graph);
|
|
150
|
+
removeBranchesFromGraph(currentRoots, currentGraph);
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
function findRoots(nodes) {
|
|
140
154
|
const nodeSet = new Set(nodes);
|
|
141
155
|
|
|
@@ -192,6 +206,7 @@ function checkForErrors(builder) {
|
|
|
192
206
|
checkForNotFoundErrors(graphData, builder);
|
|
193
207
|
checkForUnallowedRelationErrors(graphData, builder);
|
|
194
208
|
checkForUnallowedReferenceErrors(graphData, builder);
|
|
209
|
+
checkForRootUnrelateOrDeleteErrors(graphData, builder);
|
|
195
210
|
|
|
196
211
|
if (graphData.graphOptions.isInsertOnly()) {
|
|
197
212
|
checkForHasManyRelateErrors(graphData);
|
|
@@ -207,6 +222,8 @@ function checkForNotFoundErrors(graphData, builder) {
|
|
|
207
222
|
for (const node of graph.nodes) {
|
|
208
223
|
if (
|
|
209
224
|
node.obj.$hasId() &&
|
|
225
|
+
!node.toBeUnrelated &&
|
|
226
|
+
!node.toBeDeleted &&
|
|
210
227
|
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
|
|
211
228
|
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
|
|
212
229
|
!currentGraph.nodeForNode(node)
|
|
@@ -259,6 +276,19 @@ function checkForUnallowedReferenceErrors(graphData, builder) {
|
|
|
259
276
|
}
|
|
260
277
|
}
|
|
261
278
|
|
|
279
|
+
function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
|
|
280
|
+
for (const node of graphData.graph.nodes) {
|
|
281
|
+
if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
|
|
282
|
+
const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
|
|
283
|
+
|
|
284
|
+
throw builder.modelClass().createValidationError({
|
|
285
|
+
type: ValidationErrorType.InvalidGraph,
|
|
286
|
+
message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
262
292
|
function checkForHasManyRelateErrors(graphData) {
|
|
263
293
|
const { graph, graphOptions } = graphData;
|
|
264
294
|
|