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.
Files changed (64) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +220 -13
  3. package/lib/model/Model.js +3 -1
  4. package/lib/model/modelId.js +10 -2
  5. package/lib/model/modelQueryProps.js +7 -0
  6. package/lib/queryBuilder/QueryBuilder.js +191 -8
  7. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  8. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  9. package/lib/queryBuilder/RelationExpression.js +13 -1
  10. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  11. package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
  12. package/lib/queryBuilder/graph/GraphOptions.js +30 -1
  13. package/lib/queryBuilder/graph/GraphUpsert.js +38 -1
  14. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  15. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  16. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  17. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
  18. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  19. package/lib/queryBuilder/join/RelationJoiner.js +80 -27
  20. package/lib/queryBuilder/join/utils.js +20 -2
  21. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  22. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  23. package/lib/queryBuilder/operations/InsertOperation.js +129 -13
  24. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  25. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  26. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  27. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  28. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  29. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  30. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  31. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  32. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  33. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  34. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  35. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  36. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  37. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  38. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  39. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  40. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  41. package/lib/queryBuilder/whereGroups.js +125 -0
  42. package/lib/relations/Relation.js +5 -2
  43. package/lib/relations/RelationFindOperation.js +46 -2
  44. package/lib/relations/RelationProperty.js +3 -2
  45. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  46. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  47. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  48. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  49. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  50. package/lib/relations/manyToMany/ManyToManyRelation.js +104 -51
  51. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +4 -2
  52. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  53. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  54. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  55. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  56. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +12 -2
  57. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  58. package/lib/utils/assert.js +9 -0
  59. package/lib/utils/deprecate.js +6 -0
  60. package/lib/utils/identifierMapping.js +15 -1
  61. package/lib/utils/parseFieldExpression.js +28 -0
  62. package/lib/utils/wrapDbError.js +36 -0
  63. package/package.json +8 -7
  64. package/typings/objection/index.d.ts +281 -29
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
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
 
@@ -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://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)**
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://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**
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,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(modelClass, ctx.jsonSchema, !!options.patch);
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
- validator.call(model, json);
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
- const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
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 = jsonSchemaArrayWithoutRequired(jsonSchema[prop]);
313
-
314
- if (schemaArray.length !== 0) {
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]: schemaArray,
524
+ [prop]: nonEmptySchemaArray,
317
525
  };
318
526
  } else {
319
527
  return {};
320
528
  }
321
- } else if (jsonSchema.type === 'object' && prop === 'properties') {
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
  }
@@ -839,11 +839,13 @@ function getColumnNameMappers(modelClass) {
839
839
  }
840
840
 
841
841
  function getIdRelationProperty(modelClass) {
842
- const idColumn = asArray(modelClass.getIdColumn());
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
 
@@ -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
- return model.$hasProps(model.constructor.getIdPropertyArray());
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
  };