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
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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
|
-
# [Objection.js](https://
|
|
6
|
+
# [Objection.js](https://ditojs.github.io/objection/)
|
|
7
7
|
|
|
8
8
|
Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [Node.js](https://nodejs.org/) that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.
|
|
9
9
|
|
|
@@ -13,13 +13,13 @@ Objection.js is built on an SQL query builder called [knex](http://knexjs.org).
|
|
|
13
13
|
|
|
14
14
|
What objection.js gives you:
|
|
15
15
|
|
|
16
|
-
- **An easy declarative way of [defining models](https://
|
|
17
|
-
- **Simple and fun way to [fetch, insert, update and delete](https://
|
|
18
|
-
- **Powerful mechanisms for [eager loading](https://
|
|
19
|
-
- **Easy to use [transactions](https://
|
|
16
|
+
- **An easy declarative way of [defining models](https://ditojs.github.io/objection/guide/models.html) and relationships between them**
|
|
17
|
+
- **Simple and fun way to [fetch, insert, update and delete](https://ditojs.github.io/objection/guide/query-examples.html) objects using the full power of SQL**
|
|
18
|
+
- **Powerful mechanisms for [eager loading](https://ditojs.github.io/objection/guide/query-examples.html#eager-loading), [inserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-inserts) and [upserting](https://ditojs.github.io/objection/guide/query-examples.html#graph-upserts) object graphs**
|
|
19
|
+
- **Easy to use [transactions](https://ditojs.github.io/objection/guide/transactions.html)**
|
|
20
20
|
- **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
|
|
21
|
-
- **Optional [JSON schema](https://
|
|
22
|
-
- **A way to [store complex documents](https://
|
|
21
|
+
- **Optional [JSON schema](https://ditojs.github.io/objection/guide/validation.html) validation**
|
|
22
|
+
- **A way to [store complex documents](https://ditojs.github.io/objection/guide/documents.html) as single rows**
|
|
23
23
|
|
|
24
24
|
What objection.js **doesn't** give you:
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ What objection.js **doesn't** give you:
|
|
|
31
31
|
- **A custom query DSL. SQL is used as a query language.**
|
|
32
32
|
This doesn't mean you have to write SQL strings though. A query builder based on [knex](http://knexjs.org) is
|
|
33
33
|
used to build the SQL. However, if the query builder fails you for some reason, raw SQL strings can be easily
|
|
34
|
-
written using the [raw](https://
|
|
34
|
+
written using the [raw](https://ditojs.github.io/objection/api/objection/#raw) helper function.
|
|
35
35
|
- **Automatic database schema creation and migration from model definitions.**
|
|
36
36
|
For simple things it is useful that the database schema is automatically generated from the model definitions,
|
|
37
37
|
but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
|
|
@@ -45,9 +45,9 @@ Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to se
|
|
|
45
45
|
Shortcuts:
|
|
46
46
|
|
|
47
47
|
- [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
|
|
48
|
-
- [API reference](https://
|
|
48
|
+
- [API reference](https://ditojs.github.io/objection/api/query-builder/)
|
|
49
49
|
- [Example projects](https://github.com/Vincit/objection.js/tree/main/examples)
|
|
50
|
-
- [Changelog](https://
|
|
51
|
-
- [v1 -> v2 -> v3 migration guide](https://
|
|
52
|
-
- [Contribution guide](https://
|
|
53
|
-
- [Plugins](https://
|
|
50
|
+
- [Changelog](https://ditojs.github.io/objection/release-notes/changelog.html)
|
|
51
|
+
- [v1 -> v2 -> v3 migration guide](https://ditojs.github.io/objection/release-notes/migration.html)
|
|
52
|
+
- [Contribution guide](https://ditojs.github.io/objection/guide/contributing.html)
|
|
53
|
+
- [Plugins](https://ditojs.github.io/objection/guide/plugins.html)
|
|
@@ -5,6 +5,8 @@ const addFormats = require('ajv-formats');
|
|
|
5
5
|
const { Validator } = require('./Validator');
|
|
6
6
|
const { ValidationErrorType } = require('../model/ValidationError');
|
|
7
7
|
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
|
|
8
|
+
const { parseFieldExpression } = require('../utils/parseFieldExpression');
|
|
9
|
+
const { getQueryPropKeys } = require('./modelQueryProps');
|
|
8
10
|
|
|
9
11
|
class AjvValidator extends Validator {
|
|
10
12
|
static init(self, conf) {
|
|
@@ -50,6 +52,10 @@ class AjvValidator extends Validator {
|
|
|
50
52
|
|
|
51
53
|
setupAjv(self.ajv);
|
|
52
54
|
setupAjv(self.ajvNoDefaults);
|
|
55
|
+
|
|
56
|
+
// Created when needed by `compileQueryPropValidator()`.
|
|
57
|
+
self.ajvQueryProps = null;
|
|
58
|
+
self.setupAjv = setupAjv;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
beforeValidate({ json, model, options, ctx }) {
|
|
@@ -74,24 +80,40 @@ class AjvValidator extends Validator {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
const modelClass = model.constructor;
|
|
77
|
-
const validator = this.getValidator(
|
|
83
|
+
const validator = this.getValidator(
|
|
84
|
+
modelClass,
|
|
85
|
+
ctx.jsonSchema,
|
|
86
|
+
!!options.patch,
|
|
87
|
+
options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
|
|
88
|
+
);
|
|
78
89
|
|
|
79
90
|
// We need to clone the input json if we are about to set default values.
|
|
80
91
|
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
|
|
81
92
|
json = cloneDeep(json);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
|
|
95
|
+
// Patch objects can contain field expression keys like `'meta:b'` that update
|
|
96
|
+
// a single property inside a json column. Validate them against the nested
|
|
97
|
+
// schema by converting them into nested objects like `{ meta: { b: value } }`.
|
|
98
|
+
const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
|
|
99
|
+
const validationJson = fieldExpressions ? fieldExpressions.json : json;
|
|
100
|
+
|
|
101
|
+
validator.call(model, validationJson);
|
|
85
102
|
const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
|
|
86
103
|
|
|
87
104
|
if (error) {
|
|
88
105
|
throw error;
|
|
89
106
|
}
|
|
90
107
|
|
|
108
|
+
if (fieldExpressions) {
|
|
109
|
+
// Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
|
|
110
|
+
unnestFieldExpressions(json, fieldExpressions);
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
return json;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
getValidator(modelClass, jsonSchema, isPatchObject) {
|
|
116
|
+
getValidator(modelClass, jsonSchema, isPatchObject, queryPropKeys = null) {
|
|
95
117
|
// Use the Ajv custom serializer if provided.
|
|
96
118
|
const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
|
|
97
119
|
|
|
@@ -124,6 +146,10 @@ class AjvValidator extends Validator {
|
|
|
124
146
|
|
|
125
147
|
// Validator created for the unmodified schema.
|
|
126
148
|
normalValidator: null,
|
|
149
|
+
|
|
150
|
+
// Validators created for the schema object without the `required`
|
|
151
|
+
// properties that are given as query properties, keyed by their names.
|
|
152
|
+
queryPropValidators: null,
|
|
127
153
|
};
|
|
128
154
|
|
|
129
155
|
this.cache.set(cacheKey, validators);
|
|
@@ -136,6 +162,17 @@ class AjvValidator extends Validator {
|
|
|
136
162
|
validator = this.compilePatchValidator(jsonSchema);
|
|
137
163
|
validators.patchValidator = validator;
|
|
138
164
|
}
|
|
165
|
+
} else if (queryPropKeys) {
|
|
166
|
+
const key = JSON.stringify(queryPropKeys);
|
|
167
|
+
validators.queryPropValidators = validators.queryPropValidators || new Map();
|
|
168
|
+
validator = validators.queryPropValidators.get(key);
|
|
169
|
+
|
|
170
|
+
if (!validator) {
|
|
171
|
+
validator = this.compileQueryPropValidator(
|
|
172
|
+
jsonSchemaWithoutRequiredKeys(jsonSchema, queryPropKeys),
|
|
173
|
+
);
|
|
174
|
+
validators.queryPropValidators.set(key, validator);
|
|
175
|
+
}
|
|
139
176
|
} else {
|
|
140
177
|
validator = validators.normalValidator;
|
|
141
178
|
|
|
@@ -157,6 +194,29 @@ class AjvValidator extends Validator {
|
|
|
157
194
|
compileNormalValidator(jsonSchema) {
|
|
158
195
|
return this.ajv.compile(jsonSchema);
|
|
159
196
|
}
|
|
197
|
+
|
|
198
|
+
compileQueryPropValidator(jsonSchema) {
|
|
199
|
+
// The schema is a modified copy that keeps the `$id`s of the original, so
|
|
200
|
+
// it's compiled on an instance that doesn't add compiled schemas by their
|
|
201
|
+
// `$id`. Otherwise ajv would throw because the ids already exist.
|
|
202
|
+
if (!this.ajvQueryProps) {
|
|
203
|
+
this.ajvQueryProps = new Ajv(
|
|
204
|
+
Object.assign(
|
|
205
|
+
{
|
|
206
|
+
useDefaults: true,
|
|
207
|
+
},
|
|
208
|
+
this.ajvOptions,
|
|
209
|
+
{
|
|
210
|
+
addUsedSchema: false,
|
|
211
|
+
},
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
this.setupAjv(this.ajvQueryProps);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this.ajvQueryProps.compile(jsonSchema);
|
|
219
|
+
}
|
|
160
220
|
}
|
|
161
221
|
|
|
162
222
|
function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
@@ -223,6 +283,116 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
|
|
|
223
283
|
});
|
|
224
284
|
}
|
|
225
285
|
|
|
286
|
+
function nestFieldExpressions(json) {
|
|
287
|
+
const keys = Object.keys(json);
|
|
288
|
+
|
|
289
|
+
if (!keys.some(isFieldExpression)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const nested = Object.create(null);
|
|
294
|
+
const paths = new Map();
|
|
295
|
+
// The objects created here for the nested paths. Other objects are values
|
|
296
|
+
// of the input json and must not be modified.
|
|
297
|
+
const createdObjects = new Set();
|
|
298
|
+
|
|
299
|
+
for (const key of keys) {
|
|
300
|
+
if (!isFieldExpression(key)) {
|
|
301
|
+
nested[key] = json[key];
|
|
302
|
+
paths.set(key, [key]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const key of keys) {
|
|
307
|
+
if (isFieldExpression(key)) {
|
|
308
|
+
const path = fieldExpressionPath(key);
|
|
309
|
+
|
|
310
|
+
// Field expressions with array access, and those that conflict with other
|
|
311
|
+
// keys of the patch, can't be validated without the current value of the
|
|
312
|
+
// json column. Leave them out of the validation.
|
|
313
|
+
if (path && setPath(nested, path, json[key], createdObjects)) {
|
|
314
|
+
paths.set(key, path);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { json: nested, paths };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function unnestFieldExpressions(json, { json: nested, paths }) {
|
|
323
|
+
for (const [key, path] of paths) {
|
|
324
|
+
let value = nested;
|
|
325
|
+
let found = true;
|
|
326
|
+
|
|
327
|
+
for (const prop of path) {
|
|
328
|
+
found = isObject(value) && hasOwn(value, prop);
|
|
329
|
+
|
|
330
|
+
if (!found) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
value = value[prop];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!found) {
|
|
338
|
+
// Removed by `removeAdditional`.
|
|
339
|
+
delete json[key];
|
|
340
|
+
} else if (json[key] !== value) {
|
|
341
|
+
json[key] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isFieldExpression(key) {
|
|
347
|
+
return key.indexOf(':') !== -1;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function fieldExpressionPath(key) {
|
|
351
|
+
let parsed;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
parsed = parseFieldExpression(key);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
// Not a valid field expression, validate it as a top-level property.
|
|
357
|
+
return [key];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (parsed.access.some((access) => access.type !== 'object')) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function setPath(obj, path, value, createdObjects) {
|
|
368
|
+
const last = path.length - 1;
|
|
369
|
+
|
|
370
|
+
for (let i = 0; i < last; ++i) {
|
|
371
|
+
const prop = path[i];
|
|
372
|
+
|
|
373
|
+
if (!hasOwn(obj, prop)) {
|
|
374
|
+
const child = Object.create(null);
|
|
375
|
+
createdObjects.add(child);
|
|
376
|
+
obj[prop] = child;
|
|
377
|
+
} else if (!createdObjects.has(obj[prop])) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
obj = obj[prop];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (hasOwn(obj, path[last])) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
obj[path[last]] = value;
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function hasOwn(obj, prop) {
|
|
393
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
394
|
+
}
|
|
395
|
+
|
|
226
396
|
function cloneDeep(obj) {
|
|
227
397
|
if (isObject(obj) && obj.$isObjectionModel) {
|
|
228
398
|
return obj.$clone();
|
|
@@ -275,8 +445,37 @@ function objectHasDefaults(obj) {
|
|
|
275
445
|
return false;
|
|
276
446
|
}
|
|
277
447
|
|
|
448
|
+
// Returns the keys of the query properties of `model` (`raw()`, `val()`,
|
|
449
|
+
// `ref()`, subqueries...) that are required by the schema but missing from
|
|
450
|
+
// `json`, sorted, or `null` if there are none. Query properties are split from
|
|
451
|
+
// the json before validation, but they do provide a value for the property.
|
|
452
|
+
function getMissingQueryPropKeys(model, json, jsonSchema) {
|
|
453
|
+
const required = jsonSchema.required;
|
|
454
|
+
|
|
455
|
+
if (!Array.isArray(required) || required.length === 0) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const keys = getQueryPropKeys(model).filter(
|
|
460
|
+
(key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return keys.length > 0 ? keys.sort() : null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
|
|
467
|
+
return Object.assign({}, jsonSchema, {
|
|
468
|
+
required: jsonSchema.required.filter((key) => !keys.includes(key)),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
278
472
|
function jsonSchemaWithoutRequired(jsonSchema) {
|
|
279
|
-
|
|
473
|
+
// `not` is intentionally left untouched: Stripping `required` inside it
|
|
474
|
+
// would turn it into a schema that rejects everything (e.g.
|
|
475
|
+
// `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
|
|
476
|
+
// patches: Any property present in the patch is also present in the
|
|
477
|
+
// resulting object.
|
|
478
|
+
const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'then', 'else', 'properties'];
|
|
280
479
|
// `definitions` was renamed to `$defs` in draft 2019-09. Support both.
|
|
281
480
|
const definitionProps = ['definitions', '$defs'];
|
|
282
481
|
return Object.assign(
|
|
@@ -309,16 +508,28 @@ function definitionsWithoutRequired(jsonSchema, prop) {
|
|
|
309
508
|
function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
310
509
|
if (jsonSchema[prop]) {
|
|
311
510
|
if (Array.isArray(jsonSchema[prop])) {
|
|
312
|
-
const schemaArray =
|
|
313
|
-
|
|
314
|
-
|
|
511
|
+
const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
|
|
512
|
+
const nonEmptySchemaArray = schemaArray.filter(isNotEmptyObject);
|
|
513
|
+
|
|
514
|
+
// An `anyOf` or `oneOf` option that only consisted of `required` is
|
|
515
|
+
// empty once `required` is stripped, and an empty schema matches any
|
|
516
|
+
// patch, just like the stripped `required` keywords elsewhere. Drop the
|
|
517
|
+
// whole keyword then: requiring the patch to match one of the remaining
|
|
518
|
+
// options would reject valid patches, and with `oneOf` keeping the empty
|
|
519
|
+
// option would fail every patch that also matches another option.
|
|
520
|
+
if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
|
|
521
|
+
return {};
|
|
522
|
+
} else if (nonEmptySchemaArray.length !== 0) {
|
|
315
523
|
return {
|
|
316
|
-
[prop]:
|
|
524
|
+
[prop]: nonEmptySchemaArray,
|
|
317
525
|
};
|
|
318
526
|
} else {
|
|
319
527
|
return {};
|
|
320
528
|
}
|
|
321
|
-
} else if (
|
|
529
|
+
} else if (prop === 'properties') {
|
|
530
|
+
// `properties` is a map of property names to schemas, regardless of the
|
|
531
|
+
// schema's `type` (which may also be e.g. `['object', 'null']`, or be
|
|
532
|
+
// missing entirely).
|
|
322
533
|
return {
|
|
323
534
|
[prop]: Object.fromEntries(
|
|
324
535
|
Object.entries(jsonSchema[prop]).map(([key, schema]) => [
|
|
@@ -337,10 +548,6 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
|
|
|
337
548
|
}
|
|
338
549
|
}
|
|
339
550
|
|
|
340
|
-
function jsonSchemaArrayWithoutRequired(jsonSchemaArray) {
|
|
341
|
-
return jsonSchemaArray.map(jsonSchemaWithoutRequired).filter(isNotEmptyObject);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
551
|
function isNotEmptyObject(obj) {
|
|
345
552
|
return Object.keys(obj).length !== 0;
|
|
346
553
|
}
|
package/lib/model/Model.js
CHANGED
|
@@ -839,11 +839,13 @@ function getColumnNameMappers(modelClass) {
|
|
|
839
839
|
}
|
|
840
840
|
|
|
841
841
|
function getIdRelationProperty(modelClass) {
|
|
842
|
-
|
|
842
|
+
// `idColumn = null` means that the model has no primary key.
|
|
843
|
+
const idColumn = modelClass.getIdColumn() === null ? [] : asArray(modelClass.getIdColumn());
|
|
843
844
|
|
|
844
845
|
return new RelationProperty(
|
|
845
846
|
idColumn.map((idCol) => `${modelClass.getTableName()}.${idCol}`),
|
|
846
847
|
() => modelClass,
|
|
848
|
+
{ allowEmpty: true },
|
|
847
849
|
);
|
|
848
850
|
}
|
|
849
851
|
|
package/lib/model/modelId.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { assertHasIdColumn } = require('../utils/assert');
|
|
4
|
+
|
|
3
5
|
function getSetId(model, maybeId) {
|
|
4
6
|
if (maybeId !== undefined) {
|
|
5
7
|
return setId(model, maybeId);
|
|
@@ -9,10 +11,13 @@ function getSetId(model, maybeId) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function hasId(model) {
|
|
12
|
-
|
|
14
|
+
const idProps = model.constructor.getIdPropertyArray();
|
|
15
|
+
// Models without an `idColumn` never have an id.
|
|
16
|
+
return idProps.length > 0 && model.$hasProps(idProps);
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
function setId(model, id) {
|
|
20
|
+
assertHasIdColumn(model.constructor);
|
|
16
21
|
const idProp = model.constructor.getIdProperty();
|
|
17
22
|
const isCompositeId = Array.isArray(idProp);
|
|
18
23
|
|
|
@@ -49,7 +54,10 @@ function getId(model) {
|
|
|
49
54
|
const idProp = model.constructor.getIdProperty();
|
|
50
55
|
const isCompositeId = Array.isArray(idProp);
|
|
51
56
|
|
|
52
|
-
if (isCompositeId) {
|
|
57
|
+
if (isCompositeId && idProp.length === 0) {
|
|
58
|
+
// Models without an `idColumn` have no id.
|
|
59
|
+
return undefined;
|
|
60
|
+
} else if (isCompositeId) {
|
|
53
61
|
return model.$values(idProp);
|
|
54
62
|
} else {
|
|
55
63
|
return model[idProp];
|
|
@@ -108,6 +108,12 @@ function convertAndMergeHiddenQueryProps(model, json, omitProps, builder) {
|
|
|
108
108
|
return json;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Returns the keys of the query properties that were split from `model`.
|
|
112
|
+
function getQueryPropKeys(model) {
|
|
113
|
+
const queryProps = model[QUERY_PROPS_PROPERTY];
|
|
114
|
+
return queryProps ? Object.keys(queryProps) : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
// Converts a query property into a knex `raw` instance.
|
|
112
118
|
function queryPropToKnexRaw(queryProp, builder) {
|
|
113
119
|
if (!queryProp) {
|
|
@@ -144,4 +150,5 @@ function isKnexRawConvertable(queryProp) {
|
|
|
144
150
|
module.exports = {
|
|
145
151
|
splitQueryProps,
|
|
146
152
|
mergeQueryProps,
|
|
153
|
+
getQueryPropKeys,
|
|
147
154
|
};
|