objection 3.4.0 → 3.5.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.
@@ -616,9 +616,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
616
616
  // whereJson(Not)SupersetOf / whereJson(Not)SubsetOf are now supported by knex >= 1.0, but for now
617
617
  // objection handles them differently and only for postgres.
618
618
  // Changing them to utilize knex methods directly may require a major version bump and upgrade guide.
619
+ // On MySQL, the simple `(column, object | array)` form is passed on to knex (ditojs#113).
619
620
  whereJsonSupersetOf(...args) {
620
621
  return this.addOperation(
621
- new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
622
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', {
623
+ operator: '@>',
624
+ bool: 'and',
625
+ knexMethod: 'whereJsonSupersetOf',
626
+ }),
622
627
  args,
623
628
  );
624
629
  }
@@ -629,7 +634,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
629
634
 
630
635
  orWhereJsonSupersetOf(...args) {
631
636
  return this.addOperation(
632
- new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
637
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
638
+ operator: '@>',
639
+ bool: 'or',
640
+ knexMethod: 'whereJsonSupersetOf',
641
+ }),
633
642
  args,
634
643
  );
635
644
  }
@@ -640,13 +649,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
640
649
  operator: '@>',
641
650
  bool: 'and',
642
651
  prefix: 'not',
652
+ knexMethod: 'whereJsonNotSupersetOf',
643
653
  }),
644
654
  args,
645
655
  );
646
656
  }
647
657
 
648
658
  andWhereJsonNotSupersetOf(...args) {
649
- return this.andWhereJsonNotSupersetOf(...args);
659
+ return this.whereJsonNotSupersetOf(...args);
650
660
  }
651
661
 
652
662
  orWhereJsonNotSupersetOf(...args) {
@@ -655,6 +665,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
655
665
  operator: '@>',
656
666
  bool: 'or',
657
667
  prefix: 'not',
668
+ knexMethod: 'whereJsonNotSupersetOf',
658
669
  }),
659
670
  args,
660
671
  );
@@ -662,7 +673,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
662
673
 
663
674
  whereJsonSubsetOf(...args) {
664
675
  return this.addOperation(
665
- new WhereJsonPostgresOperation('whereJsonSubsetOf', { operator: '<@', bool: 'and' }),
676
+ new WhereJsonPostgresOperation('whereJsonSubsetOf', {
677
+ operator: '<@',
678
+ bool: 'and',
679
+ knexMethod: 'whereJsonSubsetOf',
680
+ }),
666
681
  args,
667
682
  );
668
683
  }
@@ -673,7 +688,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
673
688
 
674
689
  orWhereJsonSubsetOf(...args) {
675
690
  return this.addOperation(
676
- new WhereJsonPostgresOperation('orWhereJsonSubsetOf', { operator: '<@', bool: 'or' }),
691
+ new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
692
+ operator: '<@',
693
+ bool: 'or',
694
+ knexMethod: 'whereJsonSubsetOf',
695
+ }),
677
696
  args,
678
697
  );
679
698
  }
@@ -684,6 +703,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
684
703
  operator: '<@',
685
704
  bool: 'and',
686
705
  prefix: 'not',
706
+ knexMethod: 'whereJsonNotSubsetOf',
687
707
  }),
688
708
  args,
689
709
  );
@@ -699,6 +719,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
699
719
  operator: '<@',
700
720
  bool: 'or',
701
721
  prefix: 'not',
722
+ knexMethod: 'whereJsonNotSubsetOf',
702
723
  }),
703
724
  args,
704
725
  );
@@ -772,20 +793,34 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
772
793
  );
773
794
  }
774
795
 
796
+ // Not passed on to knex like `whereJsonSupersetOf()`, as MySQL's `json_contains()`
797
+ // also matches `{}` in arrays of objects.
775
798
  whereJsonIsArray(fieldExpression) {
776
- return this.whereJsonSupersetOf(fieldExpression, []);
799
+ return this.addOperation(
800
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
801
+ [fieldExpression, []],
802
+ );
777
803
  }
778
804
 
779
805
  orWhereJsonIsArray(fieldExpression) {
780
- return this.orWhereJsonSupersetOf(fieldExpression, []);
806
+ return this.addOperation(
807
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
808
+ [fieldExpression, []],
809
+ );
781
810
  }
782
811
 
783
812
  whereJsonIsObject(fieldExpression) {
784
- return this.whereJsonSupersetOf(fieldExpression, {});
813
+ return this.addOperation(
814
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
815
+ [fieldExpression, {}],
816
+ );
785
817
  }
786
818
 
787
819
  orWhereJsonIsObject(fieldExpression) {
788
- return this.orWhereJsonSupersetOf(fieldExpression, {});
820
+ return this.addOperation(
821
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
822
+ [fieldExpression, {}],
823
+ );
789
824
  }
790
825
 
791
826
  whereColumn(...args) {
@@ -5,6 +5,7 @@ const {
5
5
  toPostgresJsonPathLiteral,
6
6
  } = require('../utils/parseFieldExpression');
7
7
  const { isObject } = require('../utils/objectUtils');
8
+ const { warnIfNotPostgres } = require('./operations/jsonApi/postgresJsonApi');
8
9
 
9
10
  class ReferenceBuilder {
10
11
  constructor(expr) {
@@ -163,7 +164,13 @@ class ReferenceBuilder {
163
164
  }
164
165
 
165
166
  toKnexRaw(builder) {
166
- return builder.knex().raw(...this._createRawArgs(builder));
167
+ const knex = builder.knex();
168
+
169
+ if (this._parsedExpr.access.length > 0) {
170
+ warnIfNotPostgres(knex);
171
+ }
172
+
173
+ return knex.raw(...this._createRawArgs(builder));
167
174
  }
168
175
 
169
176
  _parseExpression(expr) {
@@ -208,7 +208,11 @@ class GraphInsertAction extends GraphAction {
208
208
  return Promise.all(
209
209
  nodes.map((node) => {
210
210
  if (node.parentEdge) {
211
- return node.parentEdge.relation.beforeInsert(node.obj, parentBuilder.context());
211
+ return node.parentEdge.relation.beforeInsert(
212
+ node.obj,
213
+ parentBuilder.context(),
214
+ node.parentNode.obj,
215
+ );
212
216
  } else {
213
217
  return null;
214
218
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { uniqBy } = require('../../utils/objectUtils');
4
4
  const { Selection } = require('../operations/select/Selection');
5
+ const { SelectOperation } = require('../operations/select/SelectOperation');
5
6
  const { createModifier } = require('../../utils/createModifier');
6
7
  const { map: mapPromise } = require('../../utils/promiseUtils');
7
8
  const { ValidationErrorType } = require('../../model/ValidationError');
@@ -124,7 +125,7 @@ class RelationJoiner {
124
125
  // also contain selects.
125
126
  userSelectQuery.modify(tableNode.relation.modify);
126
127
 
127
- if (selectsOnlyAliasesKeepingSelectAll(userSelectQuery.findAllSelections())) {
128
+ if (selectsOnlyAliasesKeepingSelectAll(findJoinSelections(userSelectQuery))) {
128
129
  // All columns are selected from the joined subquery in the main query,
129
130
  // so the subquery needs to select them too.
130
131
  subqueryToJoin.select(`${subqueryToJoin.tableRef()}.*`);
@@ -268,7 +269,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
268
269
  ];
269
270
 
270
271
  const selectStrings = uniqBy(cols)
271
- .filter((col) => !builder.hasSelectionAs(col, col))
272
+ .filter((col) => !hasSelectionAs(builder, col))
272
273
  .map((col) => `${tableRef}.${col}`);
273
274
 
274
275
  return builder.select(selectStrings);
@@ -276,7 +277,7 @@ function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
276
277
 
277
278
  function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
278
279
  const userSelectQuery = userSelectQueries.get(tableNode);
279
- const userSelections = userSelectQuery.findAllSelections();
280
+ const userSelections = findUserSelections(userSelectQuery, tableNode);
280
281
  const userSelectedAllColumns = isSelectAllSelectionSet(userSelections);
281
282
 
282
283
  let selections = [];
@@ -298,7 +299,7 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
298
299
  const idSelections = getIdSelectionsForNode({ tableNode });
299
300
 
300
301
  for (const idSelection of idSelections) {
301
- if (!userSelectQuery.hasSelectionAs(idSelection.column, idSelection.column)) {
302
+ if (!hasUserSelectionAs(userSelectQuery, tableNode, idSelection.column)) {
302
303
  selections.push(idSelection);
303
304
  internalSelections.push(idSelection);
304
305
  }
@@ -318,6 +319,54 @@ function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
318
319
  };
319
320
  }
320
321
 
322
+ // Like `builder.findAllSelections()`, but aggregates select their aliases
323
+ // instead of the columns they're passed. Aggregates without an alias are left
324
+ // out, as their column name differs between databases.
325
+ function findJoinSelections(builder) {
326
+ let selections = [];
327
+
328
+ builder.forEachOperation(true, (op) => {
329
+ if (op.constructor === SelectOperation) {
330
+ selections = selections.concat(op.aggregateSelections || op.selections);
331
+ }
332
+ });
333
+
334
+ return selections;
335
+ }
336
+
337
+ // Like `builder.hasSelectionAs(column, column)`, but ignoring the columns
338
+ // passed to aggregates, as they don't select them.
339
+ function hasSelectionAs(builder, column) {
340
+ if (builder.has(isAggregateOperation)) {
341
+ // Explicit, as a query with only aggregates doesn't select all columns.
342
+ return builder.clone().clear(isAggregateOperation).hasSelectionAs(column, column, true);
343
+ } else {
344
+ return builder.hasSelectionAs(column, column);
345
+ }
346
+ }
347
+
348
+ // Only the joined subqueries select aggregates by their aliases. The root
349
+ // query keeps treating the columns passed to them as selected.
350
+ function findUserSelections(builder, tableNode) {
351
+ if (tableNode.parentNode) {
352
+ return findJoinSelections(builder);
353
+ } else {
354
+ return builder.findAllSelections();
355
+ }
356
+ }
357
+
358
+ function hasUserSelectionAs(builder, tableNode, column) {
359
+ if (tableNode.parentNode) {
360
+ return hasSelectionAs(builder, column);
361
+ } else {
362
+ return builder.hasSelectionAs(column, column);
363
+ }
364
+ }
365
+
366
+ function isAggregateOperation(op) {
367
+ return op.constructor === SelectOperation && op.aggregateSelections !== null;
368
+ }
369
+
321
370
  function mapUserSelectionsFromSubqueryToMainQuery({ userSelections, tableNode }) {
322
371
  return userSelections.filter(isNotSelectAll).map((selection) => {
323
372
  return new Selection(
@@ -63,7 +63,12 @@ class InsertGraphOperation extends DelegateOperation {
63
63
  if (this.relation) {
64
64
  // We still need to call the relation before insert hook if the the
65
65
  // delegate operation is a RelationInsertOperation.
66
- return this.relation.executeBeforeInsert(this.models, builder.context(), result);
66
+ return this.relation.executeBeforeInsert(
67
+ this.models,
68
+ builder.context(),
69
+ result,
70
+ this.delegate.ownerModel,
71
+ );
67
72
  } else {
68
73
  return result;
69
74
  }
@@ -5,6 +5,7 @@ const { isEmpty } = require('../../utils/objectUtils');
5
5
  const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
6
6
  const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
7
7
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
8
+ const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
8
9
  const { StaticHookArguments } = require('../StaticHookArguments');
9
10
 
10
11
  class UpdateOperation extends QueryBuilderOperation {
@@ -103,6 +104,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
103
104
  if (key.indexOf(':') > -1) {
104
105
  // 'col:attr' : ref('other:lol') is transformed to
105
106
  // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
107
+ warnIfNotPostgres(knex);
106
108
 
107
109
  let parsed = ref(key);
108
110
  let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
@@ -5,6 +5,7 @@ const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConver
5
5
 
6
6
  class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
7
7
  onBuildKnex(knexBuilder, builder) {
8
+ jsonApi.warnIfNotPostgres(builder.knex());
8
9
  const args = this.getKnexArgs(builder);
9
10
 
10
11
  const rawArgs = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
@@ -5,6 +5,7 @@ const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConver
5
5
 
6
6
  class WhereJsonNotObjectPostgresOperation extends ObjectionToKnexConvertingOperation {
7
7
  onBuildKnex(knexBuilder, builder) {
8
+ jsonApi.warnIfNotPostgres(builder.knex());
8
9
  return this.whereJsonNotObject(knexBuilder, builder.knex(), this.getKnexArgs(builder)[0]);
9
10
  }
10
11
 
@@ -2,9 +2,28 @@
2
2
 
3
3
  const jsonApi = require('./postgresJsonApi');
4
4
  const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
5
+ const { parseFieldExpression } = require('../../../utils/parseFieldExpression');
6
+ const { isPlainObject, isString, isFunction } = require('../../../utils/objectUtils');
7
+ const { isMySql } = require('../../../utils/knexUtils');
5
8
 
6
9
  class WhereJsonPostgresOperation extends ObjectionToKnexConvertingOperation {
7
10
  onBuildKnex(knexBuilder, builder) {
11
+ const knex = builder.knex();
12
+
13
+ if (isMySql(knex) && this.canDelegateToKnex(knexBuilder)) {
14
+ // On MySQL, knex's version of the method works (ditojs#113). The `or`
15
+ // variants are wrapped in `orWhere()`, as knex < 3 ignores their `or`.
16
+ const [column, value] = this.getKnexArgs(builder);
17
+ const { knexMethod } = this.opt;
18
+
19
+ if (this.opt.bool === 'or') {
20
+ return knexBuilder.orWhere((qb) => qb[knexMethod](column, value));
21
+ } else {
22
+ return knexBuilder[knexMethod](column, value);
23
+ }
24
+ }
25
+
26
+ jsonApi.warnIfNotPostgres(knex);
8
27
  const args = this.getKnexArgs(builder);
9
28
 
10
29
  const rawArgs = jsonApi.whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
@@ -22,6 +41,20 @@ class WhereJsonPostgresOperation extends ObjectionToKnexConvertingOperation {
22
41
 
23
42
  return knexBuilder;
24
43
  }
44
+
45
+ // Only the simple form with a plain column and a plain object or array value
46
+ // can be passed on to knex, as knex doesn't understand field expressions.
47
+ canDelegateToKnex(knexBuilder) {
48
+ const [column, value] = this.args;
49
+
50
+ return (
51
+ !!this.opt.knexMethod &&
52
+ isFunction(knexBuilder[this.opt.knexMethod]) &&
53
+ isString(column) &&
54
+ parseFieldExpression(column).access.length === 0 &&
55
+ (isPlainObject(value) || Array.isArray(value))
56
+ );
57
+ }
25
58
  }
26
59
 
27
60
  module.exports = {
@@ -2,7 +2,8 @@
2
2
 
3
3
  const parser = require('../../../utils/parseFieldExpression');
4
4
  const { asArray, isObject, isString } = require('../../../utils/objectUtils');
5
- const { isKnexRaw, isKnexQueryBuilder } = require('../../../utils/knexUtils');
5
+ const { isKnexRaw, isKnexQueryBuilder, isPostgres } = require('../../../utils/knexUtils');
6
+ const { deprecate } = require('../../../utils/deprecate');
6
7
 
7
8
  /**
8
9
  * @typedef {String} FieldExpression
@@ -44,6 +45,19 @@ const { isKnexRaw, isKnexQueryBuilder } = require('../../../utils/knexUtils');
44
45
  * not currently supported
45
46
  */
46
47
 
48
+ // JSON field expressions and the `whereJson*()` methods generate Postgres-only
49
+ // SQL (ditojs#113). As some of it happens to run on other databases, only warn.
50
+ // TODO: Consider throwing an error instead in objection 4.0.
51
+ function warnIfNotPostgres(knex) {
52
+ if (!isPostgres(knex)) {
53
+ deprecate(
54
+ 'JSON field expressions and the whereJson*() methods of objection are only supported ' +
55
+ 'on PostgreSQL. Use the JSON methods of knex like whereJsonPath() or jsonExtract() ' +
56
+ 'with other databases.',
57
+ );
58
+ }
59
+ }
60
+
47
61
  // Returns the SQL and bindings of a field expression. The column is passed as an
48
62
  // identifier binding (`??`), so that knex's `wrapIdentifier` (e.g. used by
49
63
  // `knexSnakeCaseMappers()`) is applied to it.
@@ -168,6 +182,7 @@ function normalizeOperator(knex, operator) {
168
182
  }
169
183
 
170
184
  module.exports = {
185
+ warnIfNotPostgres,
171
186
  parseFieldExpression: parseFieldExpression,
172
187
  whereJsonbRefOnLeftJsonbValOrRefOnRight: whereJsonbRefOnLeftJsonbValOrRefOnRight,
173
188
  whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams:
@@ -1,15 +1,30 @@
1
1
  'use strict';
2
2
 
3
- const { flatten } = require('../../../utils/objectUtils');
3
+ const { flatten, isString, isPlainObject } = require('../../../utils/objectUtils');
4
4
  const { Selection } = require('./Selection');
5
5
  const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
6
6
 
7
7
  const COUNT_REGEX = /count/i;
8
+ const ALIAS_REGEX = /\s+as\s+/i;
9
+
10
+ const AGGREGATE_METHODS = new Set([
11
+ 'count',
12
+ 'countDistinct',
13
+ 'min',
14
+ 'max',
15
+ 'sum',
16
+ 'sumDistinct',
17
+ 'avg',
18
+ 'avgDistinct',
19
+ ]);
8
20
 
9
21
  class SelectOperation extends ObjectionToKnexConvertingOperation {
10
22
  constructor(name, opt) {
11
23
  super(name, opt);
12
24
  this.selections = [];
25
+ // The selections that aggregates select, by their aliases. `null` for
26
+ // non-aggregate operations.
27
+ this.aggregateSelections = AGGREGATE_METHODS.has(name) ? [] : null;
13
28
  }
14
29
 
15
30
  onAdd(builder, args) {
@@ -31,6 +46,12 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
31
46
  }
32
47
  }
33
48
 
49
+ if (this.aggregateSelections) {
50
+ this.aggregateSelections = getAggregateAliases(selections).map(
51
+ (alias) => new Selection(null, null, alias),
52
+ );
53
+ }
54
+
34
55
  return ret;
35
56
  }
36
57
 
@@ -57,10 +78,27 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
57
78
  clone() {
58
79
  const clone = super.clone();
59
80
  clone.selections = this.selections.slice();
81
+ clone.aggregateSelections = this.aggregateSelections && this.aggregateSelections.slice();
60
82
  return clone;
61
83
  }
62
84
  }
63
85
 
86
+ // Returns the aliases of the columns an aggregate selects, supporting the same
87
+ // argument forms as knex: `count('col as alias')`, `count('col', { as: 'alias' })`
88
+ // and `count({ alias: 'col' })`. Aggregates without an alias are skipped, as
89
+ // their column name differs between databases.
90
+ function getAggregateAliases([column, options]) {
91
+ if (isString(column) && ALIAS_REGEX.test(column)) {
92
+ return [column.split(ALIAS_REGEX)[1].trim()];
93
+ } else if (isPlainObject(options) && isString(options.as)) {
94
+ return [options.as];
95
+ } else if (isPlainObject(column)) {
96
+ return Object.keys(column);
97
+ } else {
98
+ return [];
99
+ }
100
+ }
101
+
64
102
  module.exports = {
65
103
  SelectOperation,
66
104
  };
@@ -219,8 +219,12 @@ class Relation {
219
219
  });
220
220
  }
221
221
 
222
- executeBeforeInsert(models, queryContext, result) {
223
- return mapAfterAllReturn(models, (model) => this.beforeInsert(model, queryContext), result);
222
+ executeBeforeInsert(models, queryContext, result, owner) {
223
+ return mapAfterAllReturn(
224
+ models,
225
+ (model) => this.beforeInsert(model, queryContext, owner),
226
+ result,
227
+ );
224
228
  }
225
229
 
226
230
  createError(message) {
@@ -24,6 +24,7 @@ class RelationFindOperation extends FindOperation {
24
24
  this.warnOnMissingOwnerProps();
25
25
 
26
26
  if (this.assignResultToOwner) {
27
+ this.warnOnArrayOwnerProps();
27
28
  this.selectMissingJoinColumns(builder);
28
29
  }
29
30
  }
@@ -100,6 +101,25 @@ class RelationFindOperation extends FindOperation {
100
101
  }
101
102
  }
102
103
 
104
+ // Owner join properties holding an array of keys, e.g. a JSON array column,
105
+ // aren't supported: The query finds the related models, but they can't be
106
+ // assigned to their owners, so all owners silently get no related models
107
+ // (#2667). `$relatedQuery()` without assigning the results isn't affected.
108
+ warnOnArrayOwnerProps() {
109
+ const { ownerProp } = this.relation;
110
+
111
+ for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
112
+ if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
113
+ deprecate(
114
+ `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
115
+ `the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
116
+ 'an array, which is not supported, so no related models are found for them. ' +
117
+ 'Use a ManyToManyRelation through a join table instead.',
118
+ );
119
+ }
120
+ }
121
+ }
122
+
103
123
  // Related models without the join properties can't be assigned to their
104
124
  // owners and are silently dropped (#2258). This happens for example when the
105
125
  // relation mapping uses column names but the rows come back with different
@@ -11,9 +11,21 @@ class RelationInsertOperation extends InsertOperation {
11
11
  this.assignResultToOwner = true;
12
12
  }
13
13
 
14
+ get ownerModel() {
15
+ // The owner model passed to the relation's `beforeInsert` hook. Only known
16
+ // if the query was started from a single model instance.
17
+ const { owner } = this;
18
+ return owner && owner.isModels && owner.modelArray.length === 1 ? owner.model : undefined;
19
+ }
20
+
14
21
  async onBefore2(builder, result) {
15
22
  const queryContext = builder.context();
16
- result = await this.relation.executeBeforeInsert(this.models, queryContext, result);
23
+ result = await this.relation.executeBeforeInsert(
24
+ this.models,
25
+ queryContext,
26
+ result,
27
+ this.ownerModel,
28
+ );
17
29
  return super.onBefore2(builder, result);
18
30
  }
19
31
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { RelationFindOperation } = require('../../RelationFindOperation');
4
4
  const { getTempColumn } = require('../../../utils/tmpColumnUtils');
5
+ const { deprecate } = require('../../../utils/deprecate');
5
6
 
6
7
  class ManyToManyFindOperation extends RelationFindOperation {
7
8
  constructor(name, opt) {
@@ -34,8 +35,12 @@ class ManyToManyFindOperation extends RelationFindOperation {
34
35
  }
35
36
  }
36
37
 
37
- if (this.assignResultToOwner && this.owner.isModels) {
38
- this.selectMissingJoinColumns(builder);
38
+ if (this.owner.isModels) {
39
+ this.warnOnMissingOwnerProps();
40
+
41
+ if (this.assignResultToOwner) {
42
+ this.selectMissingJoinColumns(builder);
43
+ }
39
44
  }
40
45
  }
41
46
 
@@ -60,6 +65,8 @@ class ManyToManyFindOperation extends RelationFindOperation {
60
65
  arr.push(rel);
61
66
  }
62
67
 
68
+ this.warnOnMissingRelatedProps(related);
69
+
63
70
  for (let i = 0, l = owners.length; i < l; ++i) {
64
71
  const own = owners[i];
65
72
  const key = ownerProp.propKey(own);
@@ -76,6 +83,27 @@ class ManyToManyFindOperation extends RelationFindOperation {
76
83
  return related;
77
84
  }
78
85
 
86
+ // The related models are assigned to their owners by the owner join columns
87
+ // selected from the join table. If they get lost, e.g. in a custom
88
+ // `$parseDatabaseJson()`, the related models are silently dropped (#2258).
89
+ warnOnMissingRelatedProps(related) {
90
+ const { joinTableOwnerProp } = this.relation;
91
+
92
+ for (let c = 0, lc = this.ownerJoinColumnAlias.length; c < lc; ++c) {
93
+ const alias = this.ownerJoinColumnAlias[c];
94
+
95
+ if (related.some((rel) => rel[alias] === undefined)) {
96
+ deprecate(
97
+ `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
98
+ `some related models are missing the join table column ` +
99
+ `"${joinTableOwnerProp.propDescription(c)}" selected as "${alias}", so they are not ` +
100
+ 'assigned to any owner. Check that $parseDatabaseJson() and the column name mappers ' +
101
+ 'of the related model keep it.',
102
+ );
103
+ }
104
+ }
105
+ }
106
+
79
107
  clone() {
80
108
  const clone = super.clone();
81
109
  clone.ownerJoinColumnAlias = this.ownerJoinColumnAlias.slice();
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { isObject } = require('./objectUtils');
3
+ const { isObject, isPlainObject } = require('./objectUtils');
4
4
 
5
5
  // Super fast memoize for single argument functions.
6
6
  function memoize(func) {
@@ -149,8 +149,9 @@ function mapLastPart(mapper, separator) {
149
149
  }
150
150
 
151
151
  // Returns a function that takes an object as an input and maps the object's keys
152
- // using `mapper`. If the input is not an object, the input is returned unchanged.
153
- function keyMapper(mapper) {
152
+ // using `mapper`, and its values using the optional `valueMapper`. If the input
153
+ // is not an object, the input is returned unchanged.
154
+ function keyMapper(mapper, valueMapper) {
154
155
  return (obj) => {
155
156
  if (!isObject(obj) || Array.isArray(obj)) {
156
157
  return obj;
@@ -161,7 +162,7 @@ function keyMapper(mapper) {
161
162
 
162
163
  for (let i = 0, l = keys.length; i < l; ++i) {
163
164
  const key = keys[i];
164
- out[mapper(key)] = obj[key];
165
+ out[mapper(key)] = valueMapper ? valueMapper(obj[key]) : obj[key];
165
166
  }
166
167
 
167
168
  return out;
@@ -189,10 +190,16 @@ function snakeCaseMappers(opt = {}) {
189
190
  };
190
191
  }
191
192
 
192
- function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
193
+ function knexIdentifierMappers({ parse, format, idSeparator = ':', mapNestedKeys = false } = {}) {
193
194
  const formatId = memoize(mapLastPart(format, idSeparator));
194
195
  const parseId = memoize(mapLastPart(parse, idSeparator));
195
196
  const parseKeys = keyMapper(parseId);
197
+ // With `mapNestedKeys`, also map the keys of plain objects one level down, as
198
+ // in the rows knex returns with its `nestTables: true` option on MySQL:
199
+ // `{ tableName: { columnName: value } }`.
200
+ const parseRow = mapNestedKeys
201
+ ? keyMapper(parseId, (value) => (isPlainObject(value) ? parseKeys(value) : value))
202
+ : parseKeys;
196
203
 
197
204
  return {
198
205
  wrapIdentifier(identifier, origWrap) {
@@ -204,12 +211,12 @@ function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
204
211
  const output = new Array(result.length);
205
212
 
206
213
  for (let i = 0, l = result.length; i < l; ++i) {
207
- output[i] = parseKeys(result[i]);
214
+ output[i] = parseRow(result[i]);
208
215
  }
209
216
 
210
217
  return output;
211
218
  } else {
212
- return parseKeys(result);
219
+ return parseRow(result);
213
220
  }
214
221
  },
215
222
  };
@@ -219,10 +226,11 @@ function knexSnakeCaseMappers(opt = {}) {
219
226
  return knexIdentifierMappers({
220
227
  parse: (str) => camelCase(str, opt),
221
228
  format: (str) => snakeCase(str, opt),
229
+ mapNestedKeys: opt.mapNestedKeys,
222
230
  });
223
231
  }
224
232
 
225
- function knexIdentifierMapping(colToProp) {
233
+ function knexIdentifierMapping(colToProp, { mapNestedKeys } = {}) {
226
234
  const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
227
235
  propToCol[colToProp[column]] = column;
228
236
  return propToCol;
@@ -231,6 +239,7 @@ function knexIdentifierMapping(colToProp) {
231
239
  return knexIdentifierMappers({
232
240
  parse: (column) => colToProp[column] || column,
233
241
  format: (prop) => propToCol[prop] || prop,
242
+ mapNestedKeys,
234
243
  });
235
244
  }
236
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objection",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "An SQL-friendly ORM for Node.js",
5
5
  "main": "lib/objection.js",
6
6
  "license": "MIT",
@@ -19,6 +19,11 @@ import { Knex } from 'knex';
19
19
  // Export the entire Objection namespace.
20
20
  export = Objection;
21
21
 
22
+ // Phantom brand of the relation types, used by `TypedRelationMappings`.
23
+ declare const relationKind: unique symbol;
24
+ // Phantom brand of `Generated` properties.
25
+ declare const generated: unique symbol;
26
+
22
27
  declare namespace Objection {
23
28
  const raw: RawFunction;
24
29
  const val: ValueFunction;
@@ -462,6 +467,65 @@ declare namespace Objection {
462
467
  : Expression<F>
463
468
  : Expression<F>;
464
469
 
470
+ /**
471
+ * Marks a model property whose value the database generates, like an
472
+ * auto-incremented id, a timestamp or a column with a default value. It
473
+ * reads as T and accepts T, but `Insertable` and `InsertableGraph` treat
474
+ * the property as optional:
475
+ *
476
+ * id!: Generated<number>;
477
+ */
478
+ export type Generated<T> = T extends null | undefined ? T : T & GeneratedBrand;
479
+
480
+ interface GeneratedBrand {
481
+ readonly [generated]?: true;
482
+ }
483
+
484
+ /**
485
+ * Insert keys are optional for optional, nullable and generated properties.
486
+ */
487
+ type OptionalInsertPropertyNames<M> = {
488
+ [K in keyof M]-?: {} extends Pick<M, K>
489
+ ? K
490
+ : null extends M[K]
491
+ ? K
492
+ : typeof generated extends keyof NonNullable<M[K]>
493
+ ? K
494
+ : never;
495
+ }[keyof M];
496
+
497
+ type InsertablePropertyNames<M> = Exclude<DataPropertyNames<M>, RelationPropertyNames<M>>;
498
+
499
+ /**
500
+ * The data to insert a model M with `insert()`. Unlike `PartialModelObject`,
501
+ * all properties are required except optional, nullable and `Generated`
502
+ * ones. Relation properties are left out.
503
+ */
504
+ export type Insertable<M extends Model> = {
505
+ [K in Exclude<InsertablePropertyNames<M>, OptionalInsertPropertyNames<M>>]: Expression<M[K]>;
506
+ } & {
507
+ [K in Extract<InsertablePropertyNames<M>, OptionalInsertPropertyNames<M>>]?: Expression<M[K]>;
508
+ };
509
+
510
+ /**
511
+ * Like `Insertable`, but for `insertGraph()`: relation properties are
512
+ * optional and hold the insertable graphs of the related models, or
513
+ * references to existing ones through `#dbRef` or `#ref`.
514
+ */
515
+ export type InsertableGraph<M extends Model> = Insertable<M> &
516
+ Omit<GraphParameters, '#dbRef' | '#ref'> & {
517
+ [K in RelationPropertyNames<M>]?: InsertableGraphField<M[K]>;
518
+ };
519
+
520
+ type InsertableGraphReference = { '#dbRef': MaybeCompositeId } | { '#ref': string };
521
+
522
+ type InsertableGraphField<F> =
523
+ NonNullable<F> extends (infer I extends Model)[]
524
+ ? (InsertableGraph<I> | InsertableGraphReference)[]
525
+ : NonNullable<F> extends infer I extends Model
526
+ ? InsertableGraph<I> | InsertableGraphReference | Extract<F, null>
527
+ : never;
528
+
465
529
  /**
466
530
  * Extracts the property names (excluding relations) of a model class.
467
531
  */
@@ -1488,6 +1552,11 @@ declare namespace Objection {
1488
1552
  model: M,
1489
1553
  context: QueryContext,
1490
1554
  ) => Promise<void> | void;
1555
+ type RelationMappingBeforeInsertHook<M extends Model> = (
1556
+ model: M,
1557
+ context: QueryContext,
1558
+ owner?: Model,
1559
+ ) => Promise<void> | void;
1491
1560
  type StringOrReferenceBuilder = string | ReferenceBuilder;
1492
1561
  type RelationMappingColumnRef = StringOrReferenceBuilder | StringOrReferenceBuilder[];
1493
1562
 
@@ -1497,7 +1566,7 @@ declare namespace Objection {
1497
1566
  join: RelationJoin;
1498
1567
  modify?: Modifier<QueryBuilderType<M>>;
1499
1568
  filter?: Modifier<QueryBuilderType<M>>;
1500
- beforeInsert?: RelationMappingHook<M>;
1569
+ beforeInsert?: RelationMappingBeforeInsertHook<M>;
1501
1570
  }
1502
1571
 
1503
1572
  export interface RelationJoin {
@@ -1518,6 +1587,67 @@ declare namespace Objection {
1518
1587
 
1519
1588
  export interface RelationType extends Constructor<Relation> {}
1520
1589
 
1590
+ // The relation types are branded so that `TypedRelationMappings` can tell
1591
+ // them apart. The brand is optional, so any `RelationType` is assignable.
1592
+ export interface BelongsToOneRelationType extends RelationType {
1593
+ readonly [relationKind]?: 'BelongsToOne';
1594
+ }
1595
+ export interface HasOneRelationType extends RelationType {
1596
+ readonly [relationKind]?: 'HasOne';
1597
+ }
1598
+ export interface HasManyRelationType extends RelationType {
1599
+ readonly [relationKind]?: 'HasMany';
1600
+ }
1601
+ export interface ManyToManyRelationType extends RelationType {
1602
+ readonly [relationKind]?: 'ManyToMany';
1603
+ }
1604
+ export interface HasOneThroughRelationType extends RelationType {
1605
+ readonly [relationKind]?: 'HasOneThrough';
1606
+ }
1607
+
1608
+ /**
1609
+ * Names of the properties of M that hold related models.
1610
+ */
1611
+ type RelationPropertyNames<M> = {
1612
+ [K in keyof M]-?: NonNullable<M[K]> extends Model | Model[] ? K : never;
1613
+ }[keyof M];
1614
+
1615
+ type TypedModelClassSpecifier<M extends Model> =
1616
+ string | ModelConstructor<M> | (() => ModelConstructor<M>);
1617
+
1618
+ /**
1619
+ * A `RelationMapping` for related models of type M with relation type R.
1620
+ */
1621
+ export interface TypedRelationMapping<
1622
+ M extends Model,
1623
+ R extends RelationType = RelationType,
1624
+ > extends RelationMapping<M> {
1625
+ relation: R;
1626
+ modelClass: TypedModelClassSpecifier<M>;
1627
+ }
1628
+
1629
+ type TypedRelationMappingFor<T> =
1630
+ NonNullable<T> extends (infer I extends Model)[]
1631
+ ? TypedRelationMapping<I, HasManyRelationType | ManyToManyRelationType>
1632
+ : NonNullable<T> extends infer I extends Model
1633
+ ? TypedRelationMapping<
1634
+ I,
1635
+ BelongsToOneRelationType | HasOneRelationType | HasOneThroughRelationType
1636
+ >
1637
+ : never;
1638
+
1639
+ /**
1640
+ * Relation mappings checked against the relation properties of model M.
1641
+ * Use it with `satisfies`, so the inferred type of the mappings is kept:
1642
+ *
1643
+ * static relationMappings = {
1644
+ * pets: { ... },
1645
+ * } satisfies TypedRelationMappings<Person>;
1646
+ */
1647
+ export type TypedRelationMappings<M extends Model> = [RelationPropertyNames<M>] extends [never]
1648
+ ? Record<string, never>
1649
+ : { [K in RelationPropertyNames<M>]?: TypedRelationMappingFor<M[K]> };
1650
+
1521
1651
  export interface Relation {
1522
1652
  name: string;
1523
1653
  ownerModelClass: typeof Model;
@@ -1528,6 +1658,7 @@ declare namespace Objection {
1528
1658
  joinTable: string;
1529
1659
  joinTableOwnerProp: RelationProperty;
1530
1660
  joinTableRelatedProp: RelationProperty;
1661
+ isOneToOne(): boolean;
1531
1662
  }
1532
1663
 
1533
1664
  export interface RelationProperty {
@@ -1598,6 +1729,22 @@ declare namespace Objection {
1598
1729
  * Only used by `snakeCaseMappers`.
1599
1730
  */
1600
1731
  preserveJsonKeys?: boolean;
1732
+ /**
1733
+ * Also convert the keys of plain objects one level down in the results,
1734
+ * e.g. the rows of knex's `nestTables: true` option on MySQL. Also applies
1735
+ * to JSON columns parsed by the driver.
1736
+ * Only used by `knexSnakeCaseMappers`.
1737
+ */
1738
+ mapNestedKeys?: boolean;
1739
+ }
1740
+
1741
+ export interface KnexIdentifierMappingOptions {
1742
+ /**
1743
+ * Also convert the keys of plain objects one level down in the results,
1744
+ * e.g. the rows of knex's `nestTables: true` option on MySQL. Also applies
1745
+ * to JSON columns parsed by the driver.
1746
+ */
1747
+ mapNestedKeys?: boolean;
1601
1748
  }
1602
1749
 
1603
1750
  export interface ColumnNameMappers {
@@ -1619,7 +1766,7 @@ declare namespace Objection {
1619
1766
  }
1620
1767
 
1621
1768
  export interface KnexIdentifierMappingFactory {
1622
- (colToProp: Record<string, string>): KnexMappers;
1769
+ (colToProp: Record<string, string>, options?: KnexIdentifierMappingOptions): KnexMappers;
1623
1770
  }
1624
1771
 
1625
1772
  export type ValidationErrorType =
@@ -1732,11 +1879,11 @@ declare namespace Objection {
1732
1879
  ref: ReferenceFunction;
1733
1880
  fn: FunctionFunction;
1734
1881
 
1735
- BelongsToOneRelation: RelationType;
1736
- HasOneRelation: RelationType;
1737
- HasManyRelation: RelationType;
1738
- ManyToManyRelation: RelationType;
1739
- HasOneThroughRelation: RelationType;
1882
+ BelongsToOneRelation: BelongsToOneRelationType;
1883
+ HasOneRelation: HasOneRelationType;
1884
+ HasManyRelation: HasManyRelationType;
1885
+ ManyToManyRelation: ManyToManyRelationType;
1886
+ HasOneThroughRelation: HasOneThroughRelationType;
1740
1887
 
1741
1888
  defaultGraphOptions?: GraphOptions;
1742
1889
 
@@ -1843,11 +1990,11 @@ declare namespace Objection {
1843
1990
  static ref: ReferenceFunction;
1844
1991
  static fn: FunctionFunction;
1845
1992
 
1846
- static BelongsToOneRelation: RelationType;
1847
- static HasOneRelation: RelationType;
1848
- static HasManyRelation: RelationType;
1849
- static ManyToManyRelation: RelationType;
1850
- static HasOneThroughRelation: RelationType;
1993
+ static BelongsToOneRelation: BelongsToOneRelationType;
1994
+ static HasOneRelation: HasOneRelationType;
1995
+ static HasManyRelation: HasManyRelationType;
1996
+ static ManyToManyRelation: ManyToManyRelationType;
1997
+ static HasOneThroughRelation: HasOneThroughRelationType;
1851
1998
 
1852
1999
  static defaultGraphOptions?: GraphOptions;
1853
2000