objection 3.1.5 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +16 -16
  2. package/lib/model/AjvValidator.js +173 -18
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/graph/ModelGraphNode.js +12 -0
  5. package/lib/model/modelToJson.js +2 -1
  6. package/lib/queryBuilder/QueryBuilder.js +339 -47
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  8. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  9. package/lib/queryBuilder/RelationExpression.js +21 -9
  10. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  11. package/lib/queryBuilder/graph/GraphFetcher.js +3 -1
  12. package/lib/queryBuilder/graph/GraphOptions.js +21 -2
  13. package/lib/queryBuilder/graph/GraphUpsert.js +52 -0
  14. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +20 -14
  15. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  16. package/lib/queryBuilder/graph/insert/GraphInsert.js +113 -8
  17. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +10 -1
  18. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +15 -3
  19. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  20. package/lib/queryBuilder/join/RelationJoiner.js +71 -5
  21. package/lib/queryBuilder/operations/DelegateOperation.js +4 -0
  22. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  23. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  24. package/lib/queryBuilder/operations/InsertOperation.js +121 -12
  25. package/lib/queryBuilder/operations/NoneOperation.js +37 -0
  26. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +5 -0
  27. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  28. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  29. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +20 -0
  30. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  31. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  32. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  33. package/lib/queryBuilder/operations/eager/EagerOperation.js +7 -5
  34. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +4 -2
  35. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +9 -20
  36. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  37. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  38. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +45 -32
  39. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  40. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  41. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  42. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  43. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  44. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  45. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +9 -0
  46. package/lib/queryBuilder/whereGroups.js +125 -0
  47. package/lib/relations/Relation.js +5 -2
  48. package/lib/relations/RelationProperty.js +7 -3
  49. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +1 -0
  50. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +5 -0
  51. package/lib/relations/hasMany/HasManyRelateOperation.js +6 -2
  52. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  53. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  54. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  55. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  56. package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
  57. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +6 -0
  58. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  59. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  60. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  61. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  62. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
  63. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  64. package/lib/utils/deprecate.js +6 -0
  65. package/lib/utils/identifierMapping.js +23 -3
  66. package/lib/utils/objectUtils.js +9 -5
  67. package/lib/utils/parseFieldExpression.js +28 -0
  68. package/package.json +22 -14
  69. package/typings/objection/index.d.ts +339 -68
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
1
  [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
2
2
 
3
- [![Tests](https://github.com/Vincit/objection.js/actions/workflows/test.yml/badge.svg)](https://github.com/Vincit/objection.js)
3
+ [![Tests](https://github.com/ditojs/objection/actions/workflows/test.yml/badge.svg)](https://github.com/ditojs/objection/actions)
4
4
  [![Join the chat at https://gitter.im/Vincit/objection.js](https://badges.gitter.im/Vincit/objection.js.svg)](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://vincit.github.io/objection.js)
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
 
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/Vincit/objection.js/actions).
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
 
16
- - **An easy declarative way of [defining models](https://vincit.github.io/objection.js/guide/models.html) and relationships between them**
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
- - **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
- - **Easy to use [transactions](https://vincit.github.io/objection.js/guide/transactions.html)**
20
- - **Official [TypeScript](https://github.com/Vincit/objection.js/blob/main/typings/objection/index.d.ts) support**
21
- - **Optional [JSON schema](https://vincit.github.io/objection.js/guide/validation.html) validation**
22
- - **A way to [store complex documents](https://vincit.github.io/objection.js/guide/documents.html) as single rows**
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
+ - **Official [TypeScript](https://github.com/ditojs/objection/blob/main/typings/objection/index.d.ts) support**
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://vincit.github.io/objection.js/api/objection/#raw) helper function.
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://vincit.github.io/objection.js/api/query-builder/)
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://vincit.github.io/objection.js/release-notes/changelog.html)
51
- - [v1 -> v2 -> v3 migration guide](https://vincit.github.io/objection.js/release-notes/migration.html)
52
- - [Contribution guide](https://vincit.github.io/objection.js/guide/contributing.html)
53
- - [Plugins](https://vincit.github.io/objection.js/guide/plugins.html)
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,7 @@ 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');
8
9
 
9
10
  class AjvValidator extends Validator {
10
11
  static init(self, conf) {
@@ -35,6 +36,9 @@ class AjvValidator extends Validator {
35
36
  // A cache for the compiled validator functions.
36
37
  self.cache = new Map();
37
38
 
39
+ // The cache keys of the unmodified model class schemas.
40
+ self.modelCacheKeys = new WeakMap();
41
+
38
42
  const setupAjv = (ajv) => {
39
43
  if (conf.onCreateAjv) {
40
44
  conf.onCreateAjv(ajv);
@@ -78,13 +82,24 @@ class AjvValidator extends Validator {
78
82
  json = cloneDeep(json);
79
83
  }
80
84
 
81
- validator.call(model, json);
85
+ // Patch objects can contain field expression keys like `'meta:b'` that update
86
+ // a single property inside a json column. Validate them against the nested
87
+ // schema by converting them into nested objects like `{ meta: { b: value } }`.
88
+ const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
89
+ const validationJson = fieldExpressions ? fieldExpressions.json : json;
90
+
91
+ validator.call(model, validationJson);
82
92
  const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
83
93
 
84
94
  if (error) {
85
95
  throw error;
86
96
  }
87
97
 
98
+ if (fieldExpressions) {
99
+ // Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
100
+ unnestFieldExpressions(json, fieldExpressions);
101
+ }
102
+
88
103
  return json;
89
104
  }
90
105
 
@@ -92,12 +107,23 @@ class AjvValidator extends Validator {
92
107
  // Use the Ajv custom serializer if provided.
93
108
  const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
94
109
 
95
- // Optimization for the common case where jsonSchema is never modified.
96
- // In that case we don't need to call the costly createCacheKey function.
97
- const cacheKey =
98
- jsonSchema === modelClass.getJsonSchema()
99
- ? modelClass.uniqueTag()
100
- : createCacheKey(jsonSchema);
110
+ let cacheKey;
111
+
112
+ if (jsonSchema === modelClass.getJsonSchema()) {
113
+ // Optimization for the common case where jsonSchema is never modified.
114
+ // The key is only computed once per model class. It includes the schema
115
+ // contents so that model classes with the same `uniqueTag()` but
116
+ // different schemas don't share validators, while bound model classes
117
+ // with equal schemas still do.
118
+ cacheKey = this.modelCacheKeys.get(modelClass);
119
+
120
+ if (cacheKey === undefined) {
121
+ cacheKey = `${modelClass.uniqueTag()}:${createCacheKey(jsonSchema)}`;
122
+ this.modelCacheKeys.set(modelClass, cacheKey);
123
+ }
124
+ } else {
125
+ cacheKey = createCacheKey(jsonSchema);
126
+ }
101
127
 
102
128
  let validators = this.cache.get(cacheKey);
103
129
  let validator = null;
@@ -209,6 +235,116 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
209
235
  });
210
236
  }
211
237
 
238
+ function nestFieldExpressions(json) {
239
+ const keys = Object.keys(json);
240
+
241
+ if (!keys.some(isFieldExpression)) {
242
+ return null;
243
+ }
244
+
245
+ const nested = Object.create(null);
246
+ const paths = new Map();
247
+ // The objects created here for the nested paths. Other objects are values
248
+ // of the input json and must not be modified.
249
+ const createdObjects = new Set();
250
+
251
+ for (const key of keys) {
252
+ if (!isFieldExpression(key)) {
253
+ nested[key] = json[key];
254
+ paths.set(key, [key]);
255
+ }
256
+ }
257
+
258
+ for (const key of keys) {
259
+ if (isFieldExpression(key)) {
260
+ const path = fieldExpressionPath(key);
261
+
262
+ // Field expressions with array access, and those that conflict with other
263
+ // keys of the patch, can't be validated without the current value of the
264
+ // json column. Leave them out of the validation.
265
+ if (path && setPath(nested, path, json[key], createdObjects)) {
266
+ paths.set(key, path);
267
+ }
268
+ }
269
+ }
270
+
271
+ return { json: nested, paths };
272
+ }
273
+
274
+ function unnestFieldExpressions(json, { json: nested, paths }) {
275
+ for (const [key, path] of paths) {
276
+ let value = nested;
277
+ let found = true;
278
+
279
+ for (const prop of path) {
280
+ found = isObject(value) && hasOwn(value, prop);
281
+
282
+ if (!found) {
283
+ break;
284
+ }
285
+
286
+ value = value[prop];
287
+ }
288
+
289
+ if (!found) {
290
+ // Removed by `removeAdditional`.
291
+ delete json[key];
292
+ } else if (json[key] !== value) {
293
+ json[key] = value;
294
+ }
295
+ }
296
+ }
297
+
298
+ function isFieldExpression(key) {
299
+ return key.indexOf(':') !== -1;
300
+ }
301
+
302
+ function fieldExpressionPath(key) {
303
+ let parsed;
304
+
305
+ try {
306
+ parsed = parseFieldExpression(key);
307
+ } catch (err) {
308
+ // Not a valid field expression, validate it as a top-level property.
309
+ return [key];
310
+ }
311
+
312
+ if (parsed.access.some((access) => access.type !== 'object')) {
313
+ return null;
314
+ }
315
+
316
+ return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
317
+ }
318
+
319
+ function setPath(obj, path, value, createdObjects) {
320
+ const last = path.length - 1;
321
+
322
+ for (let i = 0; i < last; ++i) {
323
+ const prop = path[i];
324
+
325
+ if (!hasOwn(obj, prop)) {
326
+ const child = Object.create(null);
327
+ createdObjects.add(child);
328
+ obj[prop] = child;
329
+ } else if (!createdObjects.has(obj[prop])) {
330
+ return false;
331
+ }
332
+
333
+ obj = obj[prop];
334
+ }
335
+
336
+ if (hasOwn(obj, path[last])) {
337
+ return false;
338
+ }
339
+
340
+ obj[path[last]] = value;
341
+ return true;
342
+ }
343
+
344
+ function hasOwn(obj, prop) {
345
+ return Object.prototype.hasOwnProperty.call(obj, prop);
346
+ }
347
+
212
348
  function cloneDeep(obj) {
213
349
  if (isObject(obj) && obj.$isObjectionModel) {
214
350
  return obj.$clone();
@@ -262,25 +398,41 @@ function objectHasDefaults(obj) {
262
398
  }
263
399
 
264
400
  function jsonSchemaWithoutRequired(jsonSchema) {
265
- const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
401
+ // `not` is intentionally left untouched: Stripping `required` inside it
402
+ // would turn it into a schema that rejects everything (e.g.
403
+ // `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
404
+ // patches: Any property present in the patch is also present in the
405
+ // resulting object.
406
+ const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'then', 'else', 'properties'];
407
+ // `definitions` was renamed to `$defs` in draft 2019-09. Support both.
408
+ const definitionProps = ['definitions', '$defs'];
266
409
  return Object.assign(
267
410
  omit(jsonSchema, ['required', ...subSchemaProps]),
268
411
  ...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
269
- jsonSchema && jsonSchema.definitions && Object.keys(jsonSchema.definitions).length > 0
270
- ? {
271
- definitions: Object.assign(
272
- ...Object.keys(jsonSchema.definitions).map((prop) => ({
273
- [prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
274
- })),
275
- ),
276
- }
277
- : {},
412
+ ...definitionProps.map((prop) => definitionsWithoutRequired(jsonSchema, prop)),
278
413
  jsonSchema.discriminator && jsonSchema.discriminator.propertyName
279
414
  ? { required: [jsonSchema.discriminator.propertyName] }
280
415
  : {},
281
416
  );
282
417
  }
283
418
 
419
+ function definitionsWithoutRequired(jsonSchema, prop) {
420
+ const definitions = jsonSchema && jsonSchema[prop];
421
+
422
+ if (definitions && Object.keys(definitions).length > 0) {
423
+ return {
424
+ [prop]: Object.fromEntries(
425
+ Object.entries(definitions).map(([key, schema]) => [
426
+ key,
427
+ jsonSchemaWithoutRequired(schema),
428
+ ]),
429
+ ),
430
+ };
431
+ } else {
432
+ return {};
433
+ }
434
+ }
435
+
284
436
  function subSchemaWithoutRequired(jsonSchema, prop) {
285
437
  if (jsonSchema[prop]) {
286
438
  if (Array.isArray(jsonSchema[prop])) {
@@ -293,7 +445,10 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
293
445
  } else {
294
446
  return {};
295
447
  }
296
- } else if (jsonSchema.type === 'object' && prop === 'properties') {
448
+ } else if (prop === 'properties') {
449
+ // `properties` is a map of property names to schemas, regardless of the
450
+ // schema's `type` (which may also be e.g. `['object', 'null']`, or be
451
+ // missing entirely).
297
452
  return {
298
453
  [prop]: Object.fromEntries(
299
454
  Object.entries(jsonSchema[prop]).map(([key, schema]) => [
@@ -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;
@@ -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
  }