objection 3.8.0 → 3.9.1
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/lib/model/AjvValidator.js +22 -6
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -16
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +78 -6
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +9 -2
- package/lib/model/modelUtils.js +1 -1
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +17 -7
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
- package/lib/queryBuilder/graph/GraphOptions.js +66 -12
- package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
- package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +3 -2
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
- package/lib/queryBuilder/operations/InsertOperation.js +4 -2
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
- package/lib/queryBuilder/operations/select/Selection.js +25 -3
- package/lib/relations/RelationFindOperation.js +7 -4
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
- package/lib/utils/knexUtils.js +7 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +17 -5
- package/lib/utils/promiseUtils/map.js +24 -7
- package/lib/utils/wrapDbError.js +14 -0
- package/package.json +1 -1
- package/typings/objection/index.d.ts +21 -11
|
@@ -4,7 +4,13 @@ const Ajv = require('ajv');
|
|
|
4
4
|
const addFormats = require('ajv-formats');
|
|
5
5
|
const { Validator } = require('./Validator');
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
isObject,
|
|
9
|
+
hasOwn,
|
|
10
|
+
once,
|
|
11
|
+
cloneDeep: lodashCloneDeep,
|
|
12
|
+
omit,
|
|
13
|
+
} = require('../utils/objectUtils');
|
|
8
14
|
const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
|
|
9
15
|
const { getQueryPropKeys } = require('./modelQueryProps');
|
|
10
16
|
|
|
@@ -28,7 +34,7 @@ class AjvValidator extends Validator {
|
|
|
28
34
|
|
|
29
35
|
// Create an instance that doesn't set default values. We need this one
|
|
30
36
|
// to validate `patch` objects (objects that have a subset of properties).
|
|
31
|
-
self.ajvNoDefaults = new
|
|
37
|
+
self.ajvNoDefaults = new PatchAjv(
|
|
32
38
|
Object.assign({}, self.ajvOptions, {
|
|
33
39
|
useDefaults: false,
|
|
34
40
|
}),
|
|
@@ -268,6 +274,20 @@ class AjvValidator extends Validator {
|
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
|
|
277
|
+
// Patches don't need to contain the `required` properties, also not those of
|
|
278
|
+
// the schemas that model schemas reference by `$id`, like the ones added in
|
|
279
|
+
// `onCreateAjv()`. Strip them from all schemas added to the patch instance:
|
|
280
|
+
class PatchAjv extends Ajv {
|
|
281
|
+
addSchema(schema, key, meta, ...args) {
|
|
282
|
+
return super.addSchema(
|
|
283
|
+
meta || Array.isArray(schema) ? schema : jsonSchemaWithoutRequired(schema),
|
|
284
|
+
key,
|
|
285
|
+
meta,
|
|
286
|
+
...args,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
271
291
|
function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
272
292
|
if (!errors) {
|
|
273
293
|
return null;
|
|
@@ -465,10 +485,6 @@ function setPath(obj, path, value, createdObjects) {
|
|
|
465
485
|
return true;
|
|
466
486
|
}
|
|
467
487
|
|
|
468
|
-
function hasOwn(obj, prop) {
|
|
469
|
-
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
488
|
function cloneDeep(obj) {
|
|
473
489
|
if (isObject(obj) && obj.$isObjectionModel) {
|
|
474
490
|
return obj.$clone();
|
package/lib/model/Model.js
CHANGED
|
@@ -11,6 +11,7 @@ const { toJson, toDatabaseJson } = require('./modelToJson');
|
|
|
11
11
|
const { values, propKey, hasProps } = require('./modelValues');
|
|
12
12
|
const { defineNonEnumerableProperty } = require('./modelUtils');
|
|
13
13
|
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
|
|
14
|
+
const { removeOverriddenQueryProps } = require('./modelQueryProps');
|
|
14
15
|
const { fetchTableMetadata, tableMetadata } = require('./modelTableMetadata');
|
|
15
16
|
const { asArray, isFunction, isString, asSingle } = require('../utils/objectUtils');
|
|
16
17
|
const { setJson, setFast, setRelated, appendRelated, setDatabaseJson } = require('./modelSet');
|
|
@@ -576,6 +577,7 @@ class Model {
|
|
|
576
577
|
}
|
|
577
578
|
|
|
578
579
|
if (model instanceof modelClass) {
|
|
580
|
+
removeOverriddenQueryProps(model);
|
|
579
581
|
return parseRelationsIntoModelInstances(model, model, options);
|
|
580
582
|
} else {
|
|
581
583
|
return modelClass.fromJson(model, options);
|
|
@@ -143,21 +143,6 @@ class ModelGraphNode {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* Returns all nodes that are references to this node.
|
|
148
|
-
*/
|
|
149
|
-
get referencingNodes() {
|
|
150
|
-
const nodes = [];
|
|
151
|
-
|
|
152
|
-
for (const edge of this.refEdges) {
|
|
153
|
-
if (edge.refType === ModelGraphEdge.ReferenceType.Object && edge.isRelatedNode(this)) {
|
|
154
|
-
nodes.push(edge.ownerNode);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return nodes;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
146
|
get descendantRelationNodes() {
|
|
162
147
|
return this._collectDescendantRelationNodes([]);
|
|
163
148
|
}
|
|
@@ -240,7 +225,8 @@ class ModelGraphNode {
|
|
|
240
225
|
const idPath = this.idPath;
|
|
241
226
|
|
|
242
227
|
if (idPath) {
|
|
243
|
-
|
|
228
|
+
// JSON keeps the ids and relation names apart, whatever they contain.
|
|
229
|
+
return JSON.stringify(idPath);
|
|
244
230
|
} else {
|
|
245
231
|
return null;
|
|
246
232
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { asArray, isObject, flatten, isString } = require('../utils/objectUtils');
|
|
4
|
+
const { isQueryProp } = require('./modelQueryProps');
|
|
4
5
|
|
|
5
6
|
function parseJsonAttributes(json, modelClass) {
|
|
6
7
|
const jsonAttr = modelClass.getJsonAttributes();
|
|
@@ -31,11 +32,12 @@ function formatJsonAttributes(json, modelClass) {
|
|
|
31
32
|
|
|
32
33
|
if (jsonAttr.length) {
|
|
33
34
|
// All database clients want JSON columns as strings. Do the conversion here.
|
|
35
|
+
// Leave query properties set in hooks like `$beforeUpdate()` as they are.
|
|
34
36
|
for (let i = 0, l = jsonAttr.length; i < l; ++i) {
|
|
35
37
|
const attr = jsonAttr[i];
|
|
36
38
|
const value = json[attr];
|
|
37
39
|
|
|
38
|
-
if (value != null) {
|
|
40
|
+
if (value != null && !isQueryProp(value)) {
|
|
39
41
|
json[attr] = JSON.stringify(value);
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -57,16 +59,7 @@ function getJsonAttributes(modelClass) {
|
|
|
57
59
|
const props = modelClass.getJsonSchema().properties || {};
|
|
58
60
|
|
|
59
61
|
for (const propName of Object.keys(props)) {
|
|
60
|
-
const
|
|
61
|
-
let types = asArray(prop.type).filter((it) => !!it);
|
|
62
|
-
|
|
63
|
-
if (types.length === 0 && Array.isArray(prop.anyOf)) {
|
|
64
|
-
types = flatten(prop.anyOf.map((it) => it.type));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (types.length === 0 && Array.isArray(prop.oneOf)) {
|
|
68
|
-
types = flatten(prop.oneOf.map((it) => it.type));
|
|
69
|
-
}
|
|
62
|
+
const types = getPropertyTypes(props[propName]);
|
|
70
63
|
|
|
71
64
|
if (types.indexOf('object') !== -1 || types.indexOf('array') !== -1) {
|
|
72
65
|
jsonAttributes.push(propName);
|
|
@@ -77,6 +70,21 @@ function getJsonAttributes(modelClass) {
|
|
|
77
70
|
return jsonAttributes;
|
|
78
71
|
}
|
|
79
72
|
|
|
73
|
+
// The types of a property schema, from `type` or else from `anyOf` / `oneOf`.
|
|
74
|
+
function getPropertyTypes(prop) {
|
|
75
|
+
let types = asArray(prop.type).filter((it) => !!it);
|
|
76
|
+
|
|
77
|
+
if (types.length === 0 && Array.isArray(prop.anyOf)) {
|
|
78
|
+
types = flatten(prop.anyOf.map((it) => it.type));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (types.length === 0 && Array.isArray(prop.oneOf)) {
|
|
82
|
+
types = flatten(prop.oneOf.map((it) => it.type));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return types;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
function tryParseJson(maybeJsonStr) {
|
|
81
89
|
try {
|
|
82
90
|
return JSON.parse(maybeJsonStr);
|
|
@@ -89,4 +97,5 @@ module.exports = {
|
|
|
89
97
|
parseJsonAttributes,
|
|
90
98
|
formatJsonAttributes,
|
|
91
99
|
getJsonAttributes,
|
|
100
|
+
getPropertyTypes,
|
|
92
101
|
};
|
|
@@ -1,39 +1,107 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isObject, isFunction } = require('../utils/objectUtils');
|
|
3
|
+
const { isObject, isFunction, omit } = require('../utils/objectUtils');
|
|
4
4
|
const { defineNonEnumerableProperty } = require('./modelUtils');
|
|
5
5
|
const { isKnexRaw, isKnexQueryBuilder } = require('../utils/knexUtils');
|
|
6
|
+
const { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
|
|
6
7
|
|
|
7
8
|
const QUERY_PROPS_PROPERTY = '$$queryProps';
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
9
|
+
// The keys of the field expression query properties like `'data:a'` that were
|
|
10
|
+
// given while their json column `data` had a value:
|
|
11
|
+
const COMBINED_QUERY_PROPS_PROPERTY = '$$combinedQueryProps';
|
|
12
|
+
|
|
13
|
+
// Moves the query properties of `json` into a hidden property of `model` so
|
|
14
|
+
// that they can be later merged back to its database json. A key holds either
|
|
15
|
+
// a value or a query property, so the keys given in `json` replace the ones
|
|
16
|
+
// that the model holds as the other kind.
|
|
11
17
|
function splitQueryProps(model, json) {
|
|
12
18
|
const keys = Object.keys(json);
|
|
19
|
+
const oldQueryProps = model[QUERY_PROPS_PROPERTY];
|
|
13
20
|
|
|
14
|
-
if (hasQueryProps(json, keys)) {
|
|
15
|
-
const queryProps = {};
|
|
21
|
+
if (!json.$isObjectionModel && hasQueryProps(json, keys)) {
|
|
22
|
+
const queryProps = Object.assign({}, oldQueryProps);
|
|
23
|
+
const combinedKeys = new Set(model[COMBINED_QUERY_PROPS_PROPERTY]);
|
|
16
24
|
const modelProps = {};
|
|
25
|
+
const jsonSchema = model.constructor.getJsonSchema();
|
|
26
|
+
const properties = jsonSchema && jsonSchema.properties;
|
|
17
27
|
|
|
18
28
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
19
29
|
const key = keys[i];
|
|
20
30
|
const value = json[key];
|
|
21
31
|
|
|
22
32
|
if (isQueryProp(value)) {
|
|
33
|
+
const column = getFieldExpressionColumn(key, properties);
|
|
34
|
+
|
|
35
|
+
if (column !== null && (json[column] !== undefined || model[column] !== undefined)) {
|
|
36
|
+
combinedKeys.add(key);
|
|
37
|
+
} else {
|
|
38
|
+
combinedKeys.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
queryProps[key] = value;
|
|
42
|
+
delete model[key];
|
|
24
43
|
} else {
|
|
25
44
|
modelProps[key] = value;
|
|
45
|
+
delete queryProps[key];
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, queryProps);
|
|
50
|
+
defineNonEnumerableProperty(model, COMBINED_QUERY_PROPS_PROPERTY, [...combinedKeys]);
|
|
30
51
|
|
|
31
52
|
return modelProps;
|
|
32
53
|
} else {
|
|
54
|
+
if (oldQueryProps) {
|
|
55
|
+
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(oldQueryProps, keys));
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
return json;
|
|
34
59
|
}
|
|
35
60
|
}
|
|
36
61
|
|
|
62
|
+
// Omits the values that parsing and validation add to `json` for the keys that
|
|
63
|
+
// `model` holds query properties for, like Ajv defaults.
|
|
64
|
+
function omitQueryProps(model, json) {
|
|
65
|
+
const keys = getQueryPropKeys(model);
|
|
66
|
+
return keys.length > 0 ? omit(json, keys) : json;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Removes the query properties of the keys that were given a value since, by an
|
|
70
|
+
// assignment, `$set()` or a write that fetched the value from the database. We
|
|
71
|
+
// do this when the model is passed to a write, and not when its database json
|
|
72
|
+
// is built, so that the values set by its hooks don't override them.
|
|
73
|
+
function removeOverriddenQueryProps(model) {
|
|
74
|
+
const queryProps = model[QUERY_PROPS_PROPERTY];
|
|
75
|
+
|
|
76
|
+
if (queryProps) {
|
|
77
|
+
const jsonSchema = model.constructor.getJsonSchema();
|
|
78
|
+
const properties = jsonSchema && jsonSchema.properties;
|
|
79
|
+
const combinedKeys = model[COMBINED_QUERY_PROPS_PROPERTY] || [];
|
|
80
|
+
const keys = Object.keys(queryProps).filter((key) => {
|
|
81
|
+
const column = getFieldExpressionColumn(key, properties);
|
|
82
|
+
return (
|
|
83
|
+
model[key] !== undefined ||
|
|
84
|
+
// The json column of a field expression key like `data:a` overrides it
|
|
85
|
+
// too, unless it was given while the column had a value (#206):
|
|
86
|
+
(column !== null && !combinedKeys.includes(key) && model[column] !== undefined)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (keys.length > 0) {
|
|
91
|
+
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, omit(queryProps, keys));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Removes the values that the hooks of a write set for the keys that `model`
|
|
97
|
+
// holds query properties for. The query properties are written instead, so the
|
|
98
|
+
// values must not count as given in later writes.
|
|
99
|
+
function removeOverriddenValues(model) {
|
|
100
|
+
for (const key of getQueryPropKeys(model)) {
|
|
101
|
+
delete model[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
37
105
|
function hasQueryProps(json, keys) {
|
|
38
106
|
for (let i = 0, l = keys.length; i < l; ++i) {
|
|
39
107
|
if (isQueryProp(json[keys[i]])) {
|
|
@@ -149,6 +217,10 @@ function isKnexRawConvertable(queryProp) {
|
|
|
149
217
|
|
|
150
218
|
module.exports = {
|
|
151
219
|
splitQueryProps,
|
|
220
|
+
omitQueryProps,
|
|
221
|
+
removeOverriddenQueryProps,
|
|
222
|
+
removeOverriddenValues,
|
|
152
223
|
mergeQueryProps,
|
|
153
224
|
getQueryPropKeys,
|
|
225
|
+
isQueryProp,
|
|
154
226
|
};
|
package/lib/model/modelSet.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isInternalProp } = require('../utils/internalPropUtils');
|
|
4
|
-
const { splitQueryProps } = require('./modelQueryProps');
|
|
4
|
+
const { splitQueryProps, omitQueryProps } = require('./modelQueryProps');
|
|
5
5
|
const { isFunction, isString, isPlainObject } = require('../utils/objectUtils');
|
|
6
6
|
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
|
|
7
7
|
|
|
@@ -17,15 +17,13 @@ function setJson(model, json, options) {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
json = splitQueryProps(model, json);
|
|
25
|
-
}
|
|
26
|
-
|
|
20
|
+
// json can contain "query properties" like `raw` instances, query builders etc.
|
|
21
|
+
// We take them out of `json` and store them to a hidden property $$queryProps
|
|
22
|
+
// in the model instance for later use.
|
|
23
|
+
json = splitQueryProps(model, json);
|
|
27
24
|
json = model.$parseJson(json, options);
|
|
28
25
|
json = model.$validate(json, options);
|
|
26
|
+
json = omitQueryProps(model, json);
|
|
29
27
|
|
|
30
28
|
model.$set(json);
|
|
31
29
|
|
package/lib/model/modelToJson.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { isInternalProp } = require('../utils/internalPropUtils');
|
|
4
4
|
const { mergeQueryProps } = require('./modelQueryProps');
|
|
5
|
-
const { isObject, cloneDeep, isFunction } = require('../utils/objectUtils');
|
|
5
|
+
const { isObject, hasOwn, cloneDeep, isFunction } = require('../utils/objectUtils');
|
|
6
|
+
const { getFieldExpressionColumn } = require('../utils/parseFieldExpression');
|
|
6
7
|
const EMPTY_ARRAY = [];
|
|
7
8
|
|
|
8
9
|
function toJson(model, optIn) {
|
|
@@ -134,7 +135,13 @@ function shouldOmit(opt, key) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
function shouldPick(opt, key) {
|
|
137
|
-
return opt.pick === null || key
|
|
138
|
+
return opt.pick === null || hasOwn(opt.pick, key) || isPickedFieldExpression(opt.pick, key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Pick field expressions like `'meta:a'` by their json column.
|
|
142
|
+
function isPickedFieldExpression(pick, key) {
|
|
143
|
+
const column = getFieldExpressionColumn(key, pick);
|
|
144
|
+
return column !== null && hasOwn(pick, column);
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
function assignVirtualAttributes(json, model, vAttr, opt) {
|
package/lib/model/modelUtils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { clone } = require('./modelClone');
|
|
4
|
+
const { omitQueryProps } = require('./modelQueryProps');
|
|
4
5
|
|
|
5
6
|
function validate(model, json, options = {}) {
|
|
6
7
|
json = json || model;
|
|
@@ -37,7 +38,7 @@ function validate(model, json, options = {}) {
|
|
|
37
38
|
|
|
38
39
|
if (validatingModelInstance) {
|
|
39
40
|
// If we cloned `json`, we need to copy the possible default values.
|
|
40
|
-
return inputJson.$set(json);
|
|
41
|
+
return inputJson.$set(omitQueryProps(inputJson, json));
|
|
41
42
|
} else {
|
|
42
43
|
return json;
|
|
43
44
|
}
|
package/lib/model/modelValues.js
CHANGED
|
@@ -19,16 +19,33 @@ function values(model, args) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Turn the props of a model into a key to look up models by.
|
|
22
23
|
function propKey(model, props) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
if (props.length === 1) {
|
|
25
|
+
return PROP_KEY_PREFIX + propToStr(model[props[0]]);
|
|
26
|
+
} else {
|
|
27
|
+
let key = PROP_KEY_PREFIX + compositePropToStr(model[props[0]]);
|
|
28
|
+
|
|
29
|
+
for (let i = 1, l = props.length; i < l; ++i) {
|
|
30
|
+
key += ',' + compositePropToStr(model[props[i]]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Same as `propKey()`, for values that were already collected.
|
|
38
|
+
function valuesKey(values) {
|
|
39
|
+
if (values.length === 1) {
|
|
40
|
+
return PROP_KEY_PREFIX + propToStr(values[0]);
|
|
41
|
+
} else {
|
|
42
|
+
let key = PROP_KEY_PREFIX + compositePropToStr(values[0]);
|
|
43
|
+
|
|
44
|
+
for (let i = 1, l = values.length; i < l; ++i) {
|
|
45
|
+
key += ',' + compositePropToStr(values[i]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return key;
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -66,50 +83,27 @@ function valuesN(model, args) {
|
|
|
66
83
|
return ret;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
function propKey1(model, props) {
|
|
70
|
-
return PROP_KEY_PREFIX + propToStr(model[props[0]]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function propKey2(model, props) {
|
|
74
|
-
return PROP_KEY_PREFIX + propToStr(model[props[0]]) + ',' + propToStr(model[props[1]]);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function propKey3(model, props) {
|
|
78
|
-
return (
|
|
79
|
-
PROP_KEY_PREFIX +
|
|
80
|
-
propToStr(model[props[0]]) +
|
|
81
|
-
',' +
|
|
82
|
-
propToStr(model[props[1]]) +
|
|
83
|
-
',' +
|
|
84
|
-
propToStr(model[props[2]])
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function propKeyN(model, props) {
|
|
89
|
-
let key = PROP_KEY_PREFIX;
|
|
90
|
-
|
|
91
|
-
for (let i = 0, l = props.length; i < l; ++i) {
|
|
92
|
-
key += propToStr(model[props[i]]);
|
|
93
|
-
|
|
94
|
-
if (i < l - 1) {
|
|
95
|
-
key += ',';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return key;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
86
|
function propToStr(value) {
|
|
103
|
-
if (value
|
|
104
|
-
return
|
|
105
|
-
} else if (value
|
|
106
|
-
return
|
|
87
|
+
if (!isObject(value)) {
|
|
88
|
+
return `${value}`;
|
|
89
|
+
} else if (value instanceof Date) {
|
|
90
|
+
// Key dates by their time, as some drivers return them as numbers.
|
|
91
|
+
return `${value.getTime()}`;
|
|
107
92
|
} else if (Buffer.isBuffer(value)) {
|
|
108
93
|
return value.toString('hex');
|
|
109
|
-
} else if (isObject(value)) {
|
|
110
|
-
return JSON.stringify(value);
|
|
111
94
|
} else {
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Escape the separators in composite keys, since strings can contain them.
|
|
100
|
+
// Numbers can't, so we don't need to check them.
|
|
101
|
+
function compositePropToStr(value) {
|
|
102
|
+
if (typeof value === 'number') {
|
|
112
103
|
return `${value}`;
|
|
104
|
+
} else {
|
|
105
|
+
const str = propToStr(value);
|
|
106
|
+
return str.includes(',') || str.includes('\\') ? str.replace(/[,\\]/g, '\\$&') : str;
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
|
|
@@ -118,9 +112,8 @@ function isNullOrUndefined(val) {
|
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
module.exports = {
|
|
121
|
-
PROP_KEY_PREFIX,
|
|
122
|
-
propToStr,
|
|
123
115
|
values,
|
|
124
116
|
hasProps,
|
|
125
117
|
propKey,
|
|
118
|
+
valuesKey,
|
|
126
119
|
};
|
|
@@ -50,6 +50,7 @@ const { LimitPerParentOperation } = require('./operations/LimitPerParentOperatio
|
|
|
50
50
|
const { FromOperation } = require('./operations/FromOperation');
|
|
51
51
|
const { KnexOperation } = require('./operations/KnexOperation');
|
|
52
52
|
const { clearUnsupportedOrder } = require('./withoutOrder');
|
|
53
|
+
const { addWhereGroup } = require('./whereGroups');
|
|
53
54
|
|
|
54
55
|
class QueryBuilder extends QueryBuilderBase {
|
|
55
56
|
static init(self, modelClass) {
|
|
@@ -512,7 +513,10 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
512
513
|
|
|
513
514
|
async resultSize() {
|
|
514
515
|
const knex = this.knex();
|
|
515
|
-
|
|
516
|
+
// `distinctOn` picks the first row of each group in the order of the query,
|
|
517
|
+
// which decides which root models are counted with to-many joins.
|
|
518
|
+
const shouldKeepOrder = this.has('distinctOn');
|
|
519
|
+
const builder = this.clone().clear(shouldKeepOrder ? /offset|limit/ : /orderBy|offset|limit/);
|
|
516
520
|
const joinEagerOp = builder.findOperation(JoinEagerOperation);
|
|
517
521
|
|
|
518
522
|
if (joinEagerOp) {
|
|
@@ -530,16 +534,20 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
530
534
|
|
|
531
535
|
// Also clear the order on the built knex query, since `orderBy` calls
|
|
532
536
|
// can be added at build time, e.g. by a relation's `modify`.
|
|
533
|
-
const buildQuery = (knexBuilder) =>
|
|
537
|
+
const buildQuery = (knexBuilder) => {
|
|
538
|
+
const query = builder.toKnexQuery(knexBuilder);
|
|
539
|
+
return shouldKeepOrder ? query : query.clearOrder();
|
|
540
|
+
};
|
|
534
541
|
|
|
535
542
|
const countQuery = knex.count('* as count').from((knexBuilder) => {
|
|
536
543
|
if (!joinEagerOp || idColumns.length === 0) {
|
|
537
544
|
// Without ids, the joins are all to-one and return one row per root
|
|
538
545
|
// model.
|
|
539
546
|
buildQuery(knexBuilder);
|
|
540
|
-
} else if (refersToSelections(builder)) {
|
|
547
|
+
} else if (refersToSelections(builder) || builder.has('distinctOn')) {
|
|
541
548
|
// `having`, and `groupBy` with an alias, may refer to aliased
|
|
542
|
-
// selections,
|
|
549
|
+
// selections, and clearing the selections would also clear
|
|
550
|
+
// `distinctOn`, so keep them and select the distinct ids from the
|
|
543
551
|
// result instead.
|
|
544
552
|
knexBuilder.distinct(idColumns).from((knexBuilder) => {
|
|
545
553
|
buildQuery(knexBuilder).as('inner');
|
|
@@ -1015,7 +1023,9 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1015
1023
|
}
|
|
1016
1024
|
|
|
1017
1025
|
updateById(id, modelOrObject) {
|
|
1018
|
-
|
|
1026
|
+
// Keep the id condition in its own where group, so that a user's `orWhere()`
|
|
1027
|
+
// can't get around it. See `whereGroups.js`.
|
|
1028
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1019
1029
|
.update(modelOrObject)
|
|
1020
1030
|
.runBefore((_, builder) => {
|
|
1021
1031
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -1025,7 +1035,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1025
1035
|
}
|
|
1026
1036
|
|
|
1027
1037
|
patchById(id, modelOrObject) {
|
|
1028
|
-
return this.findById(id)
|
|
1038
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1029
1039
|
.patch(modelOrObject)
|
|
1030
1040
|
.runBefore((_, builder) => {
|
|
1031
1041
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -1035,7 +1045,7 @@ class QueryBuilder extends QueryBuilderBase {
|
|
|
1035
1045
|
}
|
|
1036
1046
|
|
|
1037
1047
|
deleteById(id) {
|
|
1038
|
-
return this.findById(id)
|
|
1048
|
+
return addWhereGroup(this, () => this.findById(id))
|
|
1039
1049
|
.delete()
|
|
1040
1050
|
.runBefore((_, builder) => {
|
|
1041
1051
|
if (!builder.internalOptions().skipUndefined) {
|
|
@@ -6,7 +6,46 @@ class GraphData {
|
|
|
6
6
|
this.currentGraph = currentGraph;
|
|
7
7
|
this.graphOptions = graphOptions;
|
|
8
8
|
this.nodeDbExistence = nodeDbExistence;
|
|
9
|
+
// Pruning replaces `graph.nodes`, but `keptNodeForRow()` also needs the
|
|
10
|
+
// branches below related nodes.
|
|
11
|
+
this._unprunedNodes = graph.nodes;
|
|
12
|
+
this._keptNodesByRowKey = null;
|
|
9
13
|
}
|
|
14
|
+
|
|
15
|
+
// Find the node that keeps the row of `currentNode` in another place of the
|
|
16
|
+
// graph, by relating or patching it there, or by upserting it below a related
|
|
17
|
+
// node. Only call this after pruning.
|
|
18
|
+
keptNodeForRow(currentNode) {
|
|
19
|
+
if (!this._keptNodesByRowKey) {
|
|
20
|
+
this._keptNodesByRowKey = new Map();
|
|
21
|
+
const nodes = new Set(this.graph.nodes);
|
|
22
|
+
|
|
23
|
+
// Nested upserts write the pruned branches below related nodes, except
|
|
24
|
+
// for models marked with `#unrelate` or `#delete` and their descendants:
|
|
25
|
+
const isUpsertedBelowRelatedNode = (node) =>
|
|
26
|
+
!node.toBeUnrelatedOrDeleted &&
|
|
27
|
+
(nodes.has(node.parentNode)
|
|
28
|
+
? this.graphOptions.shouldRelate(node.parentNode, this)
|
|
29
|
+
: isUpsertedBelowRelatedNode(node.parentNode));
|
|
30
|
+
|
|
31
|
+
for (const node of this._unprunedNodes) {
|
|
32
|
+
if (
|
|
33
|
+
node.obj.$hasId() &&
|
|
34
|
+
(nodes.has(node)
|
|
35
|
+
? this.graphOptions.shouldPatchOrUpdateIgnoreDisable(node, this)
|
|
36
|
+
: isUpsertedBelowRelatedNode(node))
|
|
37
|
+
) {
|
|
38
|
+
this._keptNodesByRowKey.set(getRowKey(node), node);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this._keptNodesByRowKey.get(getRowKey(currentNode)) || null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getRowKey(node) {
|
|
48
|
+
return `${node.modelClass.getTableName()}:${node.obj.$idKey()}`;
|
|
10
49
|
}
|
|
11
50
|
|
|
12
51
|
module.exports = {
|
|
@@ -4,6 +4,8 @@ const { asArray, groupBy } = require('../../utils/objectUtils');
|
|
|
4
4
|
const { ModelGraph } = require('../../model/graph/ModelGraph');
|
|
5
5
|
const { FetchStrategy } = require('./GraphOptions');
|
|
6
6
|
const { RelationExpression } = require('../RelationExpression');
|
|
7
|
+
const { getDeclaredColumns } = require('../operations/UpdateOperation');
|
|
8
|
+
const { getFieldExpressionColumn } = require('../../utils/parseFieldExpression');
|
|
7
9
|
|
|
8
10
|
class GraphFetcher {
|
|
9
11
|
/**
|
|
@@ -106,8 +108,12 @@ function createInputColumnSelector(nodes) {
|
|
|
106
108
|
|
|
107
109
|
for (const node of nodes) {
|
|
108
110
|
const databaseJson = node.obj.$toDatabaseJson(builder);
|
|
111
|
+
const declaredColumns = getDeclaredColumns(node.modelClass);
|
|
112
|
+
|
|
113
|
+
for (const key of Object.keys(databaseJson)) {
|
|
114
|
+
// Select the json column of field expressions like `'meta:a'`:
|
|
115
|
+
const column = getFieldExpressionColumn(key, declaredColumns) ?? key;
|
|
109
116
|
|
|
110
|
-
for (const column of Object.keys(databaseJson)) {
|
|
111
117
|
if (!shouldSelectColumn(column, selects, node)) {
|
|
112
118
|
continue;
|
|
113
119
|
}
|