objection 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/model/AjvValidator.js +22 -6
  2. package/lib/model/Model.js +2 -0
  3. package/lib/model/graph/ModelGraphNode.js +2 -16
  4. package/lib/model/modelJsonAttributes.js +20 -11
  5. package/lib/model/modelQueryProps.js +78 -6
  6. package/lib/model/modelSet.js +6 -8
  7. package/lib/model/modelToJson.js +9 -2
  8. package/lib/model/modelUtils.js +1 -1
  9. package/lib/model/modelValidate.js +2 -1
  10. package/lib/model/modelValues.js +43 -50
  11. package/lib/queryBuilder/QueryBuilder.js +17 -7
  12. package/lib/queryBuilder/graph/GraphData.js +39 -0
  13. package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
  14. package/lib/queryBuilder/graph/GraphOptions.js +66 -12
  15. package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
  16. package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
  17. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
  18. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
  19. package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
  20. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
  21. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
  22. package/lib/queryBuilder/join/JoinResultParser.js +13 -8
  23. package/lib/queryBuilder/join/RelationJoiner.js +3 -2
  24. package/lib/queryBuilder/join/TableNode.js +6 -68
  25. package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
  26. package/lib/queryBuilder/operations/InsertOperation.js +4 -2
  27. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
  28. package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
  29. package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
  30. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
  31. package/lib/queryBuilder/operations/select/Selection.js +25 -3
  32. package/lib/relations/RelationFindOperation.js +7 -4
  33. package/lib/relations/RelationOwner.js +2 -5
  34. package/lib/relations/RelationProperty.js +3 -14
  35. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
  36. package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
  37. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
  38. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
  39. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
  40. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
  41. package/lib/utils/knexUtils.js +7 -0
  42. package/lib/utils/objectUtils.js +5 -0
  43. package/lib/utils/parseFieldExpression.js +17 -5
  44. package/lib/utils/promiseUtils/map.js +24 -7
  45. package/lib/utils/wrapDbError.js +14 -0
  46. package/package.json +1 -1
  47. package/typings/objection/index.d.ts +21 -11
@@ -31,6 +31,14 @@ class GraphPatchAction extends GraphAction {
31
31
  // properties. That's why we handle them here.
32
32
  const changePropsBecauseOfBelongsToOneDelete = this._handleBelongsToOneDeletes(node);
33
33
 
34
+ // HasManyRelation and HasOneRelation relates write the related object's
35
+ // foreign keys, also with `noUpdate`.
36
+ const changedPropsBecauseOfRelate =
37
+ this.graphOptions.shouldRelate(node, this.graphData) &&
38
+ node.parentEdge.relation.isObjectionHasManyRelation
39
+ ? node.parentEdge.relation.relatedProp.props
40
+ : [];
41
+
34
42
  const handleUpdate = () => {
35
43
  const { changedProps, unchangedProps } = this._findChanges(node);
36
44
  const allProps = [...changedProps, ...unchangedProps];
@@ -38,7 +46,11 @@ class GraphPatchAction extends GraphAction {
38
46
  const propsToUpdate = difference(
39
47
  shouldPatch || shouldUpdate
40
48
  ? changedProps
41
- : [...changedPropsBecauseOfBelongsToOneInsert, ...changePropsBecauseOfBelongsToOneDelete],
49
+ : [
50
+ ...changedPropsBecauseOfBelongsToOneInsert,
51
+ ...changePropsBecauseOfBelongsToOneDelete,
52
+ ...changedPropsBecauseOfRelate,
53
+ ],
42
54
 
43
55
  // Remove id properties from the props to update. With upsertGraph
44
56
  // it never makes sense to change the id.
@@ -128,8 +140,9 @@ class GraphPatchAction extends GraphAction {
128
140
  edge.relation &&
129
141
  edge.relation.isObjectionBelongsToOneRelation &&
130
142
  edge.relation.relatedProp.hasProps(edge.relatedNode.obj) &&
131
- // Models marked with `#unrelate` or `#delete` are handled below.
132
- !edge.relatedNode.toBeUnrelatedOrDeleted
143
+ // Ignored models don't replace the current one, and models marked with
144
+ // `#unrelate` or `#delete` are handled below.
145
+ !this.graphOptions.shouldIgnore(edge.relatedNode, this.graphData)
133
146
  ) {
134
147
  const { relation } = edge;
135
148
 
@@ -9,7 +9,7 @@ class JoinResultParser {
9
9
  this.omitColumnAliases = new Set(omitColumnAliases);
10
10
 
11
11
  this.columnsByTableNode = null;
12
- this.parentMap = null;
12
+ this.modelsByTableNode = null;
13
13
  this.rootModels = null;
14
14
  }
15
15
 
@@ -25,7 +25,8 @@ class JoinResultParser {
25
25
  this._checkIdColumns(flatRows[0]);
26
26
 
27
27
  this.columnsByTableNode = this._createColumns(flatRows[0]);
28
- this.parentMap = new Map();
28
+ // Key the models of each table node by the ids on their path from the root:
29
+ this.modelsByTableNode = new Map(this.tableTree.nodes.map((node) => [node, new Map()]));
29
30
  this.rootModels = [];
30
31
 
31
32
  for (const flatRow of flatRows) {
@@ -42,14 +43,15 @@ class JoinResultParser {
42
43
  return;
43
44
  }
44
45
 
45
- const key = getKey(parentKey, id, tableNode);
46
- let model = this.parentMap.get(key);
46
+ const key = getKey(parentKey, id);
47
+ const models = this.modelsByTableNode.get(tableNode);
48
+ let model = models.get(key);
47
49
 
48
50
  if (!model) {
49
51
  model = this._createModel(tableNode, flatRow);
50
52
 
51
53
  this._addToParent(tableNode, model, parentModel);
52
- this.parentMap.set(key, model);
54
+ models.set(key, model);
53
55
  }
54
56
 
55
57
  for (const childNode of tableNode.childNodes) {
@@ -128,11 +130,14 @@ function getTableNode(column) {
128
130
  return column.tableNode;
129
131
  }
130
132
 
131
- function getKey(parentKey, id, tableNode) {
133
+ // Escape the separator in the ids, since string ids can contain it.
134
+ function getKey(parentKey, id) {
135
+ const escapedId = id.includes('/') || id.includes('\\') ? id.replace(/[/\\]/g, '\\$&') : id;
136
+
132
137
  if (parentKey !== null) {
133
- return `${parentKey}/${tableNode.relationProperty}/${id}`;
138
+ return `${parentKey}/${escapedId}`;
134
139
  } else {
135
- return `/${id}`;
140
+ return escapedId;
136
141
  }
137
142
  }
138
143
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { uniqBy } = require('../../utils/objectUtils');
4
- const { Selection, mapIdentifier } = require('../operations/select/Selection');
4
+ const { Selection, isSameTable, mapIdentifier } = require('../operations/select/Selection');
5
5
  const { SelectOperation } = require('../operations/select/SelectOperation');
6
6
  const { createModifier } = require('../../utils/createModifier');
7
7
  const { map: mapPromise } = require('../../utils/promiseUtils');
@@ -487,7 +487,8 @@ function getIdSelectionsForNode({ tableNode }) {
487
487
  function removeRootSelectAll(builder) {
488
488
  const tableRef = builder.tableRef();
489
489
  const isRootSelectAll = (selection) =>
490
- selection.column === '*' && (!selection.table || selection.table === tableRef);
490
+ selection.column === '*' &&
491
+ (!selection.table || isSameTable(builder, selection.table, tableRef));
491
492
  const isSelect = (op) =>
492
493
  op.constructor === SelectOperation && !isAggregateOperation(op) && !op.isDistinctOn;
493
494
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { propKey, hasProps } = require('../../model/modelValues');
4
+
3
5
  class TableNode {
4
6
  constructor({ tableTree, modelClass, expression, parentNode = null, relation = null }) {
5
7
  this.tableTree = tableTree;
@@ -86,78 +88,14 @@ class TableNode {
86
88
  }
87
89
 
88
90
  _createIdGetter() {
89
- const idColumns = this.modelClass.getIdColumnArray();
90
- const columnAliases = idColumns.map((column) => this.getColumnAliasForColumn(column));
91
-
92
- if (idColumns.length === 1) {
93
- return createIdGetter(columnAliases);
94
- } else {
95
- return createCompositeIdGetter(columnAliases);
96
- }
97
- }
98
- }
99
-
100
- function createIdGetter(columnAliases) {
101
- const columnAlias = columnAliases[0];
91
+ const columnAliases = this.modelClass
92
+ .getIdColumnArray()
93
+ .map((column) => this.getColumnAliasForColumn(column));
102
94
 
103
- return (flatRow) => {
104
- const id = flatRow[columnAlias];
105
-
106
- if (id === null) {
107
- return null;
108
- }
109
-
110
- return `${id}`;
111
- };
112
- }
113
-
114
- function createCompositeIdGetter(columnAliases) {
115
- if (columnAliases.length === 2) {
116
- return createTwoIdGetter(columnAliases);
117
- } else {
118
- return createMultiIdGetter(columnAliases);
95
+ return (flatRow) => (hasProps(flatRow, columnAliases) ? propKey(flatRow, columnAliases) : null);
119
96
  }
120
97
  }
121
98
 
122
- function createTwoIdGetter(columnAliases) {
123
- const columnAlias1 = columnAliases[0];
124
- const columnAlias2 = columnAliases[1];
125
-
126
- return (flatRow) => {
127
- const id1 = flatRow[columnAlias1];
128
- const id2 = flatRow[columnAlias2];
129
-
130
- if (id1 === null || id2 === null) {
131
- return null;
132
- }
133
-
134
- return `${id1},${id2}`;
135
- };
136
- }
137
-
138
- function createMultiIdGetter(columnAliases) {
139
- return (flatRow) => {
140
- let idStr = '';
141
-
142
- for (let i = 0, l = columnAliases.length; i < l; ++i) {
143
- const columnAlias = columnAliases[i];
144
- const id = flatRow[columnAlias];
145
-
146
- if (id === null) {
147
- return null;
148
- }
149
-
150
- idStr += id;
151
-
152
- if (i !== l - 1) {
153
- idStr += ',';
154
- }
155
- }
156
-
157
- return idStr;
158
- };
159
- }
160
-
161
99
  module.exports = {
162
100
  TableNode,
163
101
  };
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
+ const { isMySql } = require('../../utils/knexUtils');
6
+ const { warnOnce } = require('../../utils/warnings');
5
7
 
6
8
  class DeleteOperation extends QueryBuilderOperation {
7
9
  async onBefore2(builder, result) {
@@ -9,6 +11,24 @@ class DeleteOperation extends QueryBuilderOperation {
9
11
  return result;
10
12
  }
11
13
 
14
+ // knex leaves the offset out of deletes. On MySQL, it leaves out the order as
15
+ // well, but keeps the limit, so the order doesn't pick the deleted rows. Warn
16
+ // when the query is executed.
17
+ onAfterBuild(builder) {
18
+ if (builder.has('offset')) {
19
+ warnOnce(
20
+ 'offset() has no effect in delete queries, so no rows are skipped. ' +
21
+ 'This will throw an error in objection 4.0.',
22
+ );
23
+ }
24
+ if (isMySql(builder.knex()) && builder.has('limit') && builder.has(/^orderBy/)) {
25
+ warnOnce(
26
+ 'orderBy() has no effect in delete queries with limit() on MySQL, so the limit ' +
27
+ 'deletes rows in any order. This will throw an error in objection 4.0.',
28
+ );
29
+ }
30
+ }
31
+
12
32
  onBuildKnex(knexBuilder) {
13
33
  return knexBuilder.delete();
14
34
  }
@@ -3,8 +3,9 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
  const { StaticHookArguments } = require('../StaticHookArguments');
5
5
  const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
6
- const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
6
+ const { isSqlite, isMySql, hasBatchInsert } = require('../../utils/knexUtils');
7
7
  const { isObject, isString } = require('../../utils/objectUtils');
8
+ const { removeOverriddenValues } = require('../../model/modelQueryProps');
8
9
 
9
10
  // Base class for all insert operations.
10
11
  class InsertOperation extends QueryBuilderOperation {
@@ -27,10 +28,11 @@ class InsertOperation extends QueryBuilderOperation {
27
28
  }
28
29
 
29
30
  async onBefore2(builder, result) {
30
- if (this.models.length > 1 && !isPostgres(builder.knex()) && !isMsSql(builder.knex())) {
31
+ if (this.models.length > 1 && !hasBatchInsert(builder.knex())) {
31
32
  throw new Error('batch insert only works with Postgresql and SQL Server');
32
33
  } else {
33
34
  await callBeforeInsert(builder, this.models);
35
+ this.models.forEach(removeOverriddenValues);
34
36
  return result;
35
37
  }
36
38
  }
@@ -5,6 +5,7 @@ const { InstanceFindOperation } = require('./InstanceFindOperation');
5
5
  const { assertHasId } = require('../../utils/assert');
6
6
  const { isObject } = require('../../utils/objectUtils');
7
7
  const { addWhereGroup } = require('../whereGroups');
8
+ const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
8
9
 
9
10
  class InstanceUpdateOperation extends UpdateOperation {
10
11
  constructor(name, opt) {
@@ -19,6 +20,7 @@ class InstanceUpdateOperation extends UpdateOperation {
19
20
 
20
21
  if (!this.model) {
21
22
  this.model = this.instance;
23
+ removeOverriddenQueryProps(this.model);
22
24
  }
23
25
 
24
26
  return retVal;
@@ -5,6 +5,7 @@ const { RangeOperation } = require('./RangeOperation');
5
5
  const { FirstOperation } = require('./FirstOperation');
6
6
  const { isString, isNumber } = require('../../utils/objectUtils');
7
7
  const { hasWindowFunctions, isKnexRaw } = require('../../utils/knexUtils');
8
+ const { Selection, isSameTable, withoutSchema } = require('./select/Selection');
8
9
 
9
10
  // Without underscores, so that column name mappers don't change it.
10
11
  const rowNumberColumn = 'objectiontmprownum';
@@ -55,23 +56,18 @@ class LimitPerParentOperation extends QueryBuilderOperation {
55
56
  // table, so that the order can refer to the selected columns and their
56
57
  // aliases as in the query itself, e.g. to `id` when a joined table has
57
58
  // an `id` too. The order moves into the window function, as SQL Server
58
- // doesn't allow it in subqueries.
59
+ // doesn't allow it in subqueries. An alias can't have a schema.
60
+ const table = withoutSchema(builder.tableRef());
61
+ // knex only accepts raw partitions wrapped in objects:
62
+ const partition = this.findOperation.ownerKeys(builder, table).map((column) => ({ column }));
59
63
  const numberedQuery = knex
60
64
  .queryBuilder()
61
65
  .select('*')
62
66
  .rowNumber(rowNumberColumn, (window) =>
63
- window
64
- .orderBy(getWindowOrder(builder, knex))
65
- .partitionBy(this.findOperation.ownerKeyColumns()),
67
+ window.orderBy(getWindowOrder(builder, knex, table)).partitionBy(partition),
66
68
  )
67
69
  .from(
68
- knexBuilder
69
- .clone()
70
- .clear('with')
71
- .clear('limit')
72
- .clear('offset')
73
- .clear('order')
74
- .as(builder.tableRef()),
70
+ knexBuilder.clone().clear('with').clear('limit').clear('offset').clear('order').as(table),
75
71
  );
76
72
 
77
73
  // Select the rows in range with the query itself, to keep its settings
@@ -103,9 +99,18 @@ class LimitPerParentOperation extends QueryBuilderOperation {
103
99
  }
104
100
 
105
101
  // Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
106
- // of knex's window functions.
107
- function getWindowOrder(builder, knex) {
102
+ // of knex's window functions, which can only refer to the related table by its
103
+ // alias `table`.
104
+ function getWindowOrder(builder, knex, table) {
108
105
  const order = [];
106
+ const tableRef = builder.tableRef();
107
+
108
+ const getAliasedColumn = (column) => {
109
+ const selection = isString(column) && Selection.create(column);
110
+ return selection?.table && isSameTable(builder, selection.table, tableRef)
111
+ ? `${table}.${selection.column}`
112
+ : column;
113
+ };
109
114
 
110
115
  builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
111
116
  const args = op.getKnexArgs(builder);
@@ -120,18 +125,20 @@ function getWindowOrder(builder, knex) {
120
125
  : [{ column, order: direction, nulls }];
121
126
 
122
127
  for (const item of items) {
128
+ const aliasedColumn = getAliasedColumn(item.column);
129
+
123
130
  if (item.nulls) {
124
131
  // Not all databases support `nulls first` and `nulls last`:
125
132
  order.push({
126
133
  column: knex.raw('(case when ? is null then 0 else 1 end)', [
127
- isString(item.column) ? knex.ref(item.column) : item.column,
134
+ isString(aliasedColumn) ? knex.ref(aliasedColumn) : aliasedColumn,
128
135
  ]),
129
136
  order: item.nulls === 'first' ? 'asc' : 'desc',
130
137
  });
131
138
  }
132
139
 
133
140
  // Unlike in `orderBy()`, knex adds the direction to the window as it is:
134
- order.push({ column: item.column, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
141
+ order.push({ column: aliasedColumn, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
135
142
  }
136
143
  }
137
144
  });
@@ -10,6 +10,9 @@ const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
10
10
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
11
11
  const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
12
12
  const { StaticHookArguments } = require('../StaticHookArguments');
13
+ const { ValidationErrorType } = require('../../model/ValidationError');
14
+ const { removeOverriddenValues } = require('../../model/modelQueryProps');
15
+ const { warnOnce } = require('../../utils/warnings');
13
16
 
14
17
  class UpdateOperation extends QueryBuilderOperation {
15
18
  constructor(name, opt) {
@@ -30,6 +33,7 @@ class UpdateOperation extends QueryBuilderOperation {
30
33
 
31
34
  async onBefore2(builder, result) {
32
35
  await callBeforeUpdate(builder, this.model, this.modelOptions);
36
+ removeOverriddenValues(this.model);
33
37
  return result;
34
38
  }
35
39
 
@@ -51,6 +55,17 @@ class UpdateOperation extends QueryBuilderOperation {
51
55
  return builder.isExplicitlyResolvedOrRejected() && !this.hasNothingToUpdate;
52
56
  }
53
57
 
58
+ // knex leaves the offset out of updates, and on SQL Server the limit as well.
59
+ // Warn when the query is executed.
60
+ onAfterBuild(builder) {
61
+ if (builder.has('offset')) {
62
+ warnOnce(
63
+ 'offset() has no effect in update queries, so no rows are skipped, and on SQL Server ' +
64
+ 'limit() is ignored too. This will throw an error in objection 4.0.',
65
+ );
66
+ }
67
+ }
68
+
54
69
  onBuildKnex(knexBuilder, builder) {
55
70
  const json = this.model.$toDatabaseJson(builder);
56
71
  const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
@@ -110,7 +125,8 @@ async function callStaticAfterUpdate(builder, result) {
110
125
 
111
126
  function convertFieldExpressionsToRaw(builder, model, json) {
112
127
  const knex = builder.knex();
113
- const declaredColumns = getDeclaredColumns(builder.modelClass());
128
+ const modelClass = builder.modelClass();
129
+ const declaredColumns = getDeclaredColumns(modelClass);
114
130
  const convertedJson = {};
115
131
 
116
132
  for (const key of Object.keys(json)) {
@@ -118,10 +134,30 @@ function convertFieldExpressionsToRaw(builder, model, json) {
118
134
 
119
135
  if (isFieldExpressionKey(key, declaredColumns)) {
120
136
  // 'col:attr' : ref('other:lol') is transformed to
121
- // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
137
+ // "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
138
+ // with the column qualified, as it is ambiguous with `excluded` in merge().
122
139
  warnIfNotPostgres(knex);
123
140
 
124
141
  let parsed = ref(key);
142
+
143
+ // The field expression would be lost or produce invalid SQL next to a
144
+ // value for the whole json column. Report it with the property names:
145
+ if (Object.prototype.hasOwnProperty.call(json, parsed.column)) {
146
+ const property = modelClass.columnNameToPropertyName(parsed.column);
147
+ throw modelClass.createValidationError({
148
+ type: ValidationErrorType.ModelValidation,
149
+ data: {
150
+ [modelClass.columnNameToPropertyName(key)]: [
151
+ {
152
+ message: `can't be combined with a value for the json column '${property}'`,
153
+ keyword: null,
154
+ params: { property },
155
+ },
156
+ ],
157
+ },
158
+ });
159
+ }
160
+
125
161
  let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
126
162
  let valuePlaceholder = '?';
127
163
 
@@ -133,7 +169,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
133
169
 
134
170
  convertedJson[parsed.column] = knex.raw(
135
171
  `jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
136
- [convertedJson[parsed.column] || parsed.column, val],
172
+ [convertedJson[parsed.column] || `${builder.tableRef()}.${parsed.column}`, val],
137
173
  );
138
174
 
139
175
  delete model[key];
@@ -170,4 +206,5 @@ function getDeclaredColumns(modelClass) {
170
206
  module.exports = {
171
207
  UpdateOperation,
172
208
  convertFieldExpressionsToRaw,
209
+ getDeclaredColumns,
173
210
  };
@@ -47,7 +47,7 @@ const { warnOnce } = require('../../../utils/warnings');
47
47
 
48
48
  // JSON field expressions and the `whereJson*()` methods generate Postgres-only
49
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.
50
+ // TODO: Consider throwing an error instead in objection 4.0 (#272).
51
51
  function warnIfNotPostgres(knex) {
52
52
  if (!isPostgres(knex)) {
53
53
  warnOnce(
@@ -63,11 +63,11 @@ class Selection {
63
63
  if (selectionInBuilder.column === '*') {
64
64
  if (selectionInBuilder.table) {
65
65
  if (selectionToTest.column === '*') {
66
- return isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table);
66
+ return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
67
67
  } else {
68
68
  return (
69
69
  selectionToTest.table === null ||
70
- isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table)
70
+ isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
71
71
  );
72
72
  }
73
73
  } else {
@@ -82,7 +82,7 @@ class Selection {
82
82
  return (
83
83
  isSameIdentifier(builder, selectionToTest.column, selectionInBuilder.column) &&
84
84
  (selectionToTest.table === null ||
85
- isSameIdentifier(builder, selectionToTest.table, selectionInBuilderTable))
85
+ isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
86
86
  );
87
87
  }
88
88
  }
@@ -110,6 +110,26 @@ function isSameIdentifier(builder, identifier1, identifier2) {
110
110
  }
111
111
  }
112
112
 
113
+ // As in SQL, a table name without a schema refers to the table with a schema:
114
+ // `persons` and `public.persons` are the same table.
115
+ function isSameTable(builder, table1, table2) {
116
+ if (isSameIdentifier(builder, table1, table2)) {
117
+ return true;
118
+ } else if (!table1 || !table2 || hasSchema(table1) === hasSchema(table2)) {
119
+ return false;
120
+ } else {
121
+ return isSameIdentifier(builder, withoutSchema(table1), withoutSchema(table2));
122
+ }
123
+ }
124
+
125
+ function hasSchema(table) {
126
+ return table.includes('.');
127
+ }
128
+
129
+ function withoutSchema(table) {
130
+ return table.slice(table.lastIndexOf('.') + 1);
131
+ }
132
+
113
133
  function mapIdentifier(builder, identifier) {
114
134
  const knex = builder.unsafeKnex();
115
135
  const wrapIdentifier = knex && knex.client && knex.client.config.wrapIdentifier;
@@ -211,5 +231,7 @@ function createSelectionFromString(selection) {
211
231
  module.exports = {
212
232
  Selection,
213
233
  isSameIdentifier,
234
+ isSameTable,
214
235
  mapIdentifier,
236
+ withoutSchema,
215
237
  };
@@ -76,9 +76,12 @@ class RelationFindOperation extends FindOperation {
76
76
  return super.onAfter3(builder, related);
77
77
  }
78
78
 
79
- // The columns of the result rows that hold the keys of their owners:
80
- ownerKeyColumns() {
81
- return this.relation.relatedProp.cols;
79
+ // The expressions of the result rows in `table` that hold the keys of their
80
+ // owners, including json references:
81
+ ownerKeys(builder, table) {
82
+ return this.relation.relatedProp
83
+ .refs(builder)
84
+ .map((ref) => ref.table(table).toKnexRaw(builder));
82
85
  }
83
86
 
84
87
  warnOnInvalidOwnerProps() {
@@ -93,7 +96,7 @@ class RelationFindOperation extends FindOperation {
93
96
  // `select()`, silently get no related models (#1832). Only `undefined`
94
97
  // is reported, as `null` is a legit value for a missing relation, and
95
98
  // owner props that may legitimately be unset are skipped (#132).
96
- // TODO: Consider throwing an error instead in objection 4.0.
99
+ // TODO: Consider throwing an error instead in objection 4.0 (#272).
97
100
  warnOnMissingOwnerProps() {
98
101
  const { ownerProp } = this.relation;
99
102
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { isObject, asArray, asSingle, uniqBy } = require('../utils/objectUtils');
4
4
  const { normalizeIds } = require('../utils/normalizeIds');
5
+ const { valuesKey } = require('../model/modelValues');
5
6
 
6
7
  const Type = {
7
8
  Models: 'Models',
@@ -123,7 +124,7 @@ class RelationOwner {
123
124
  return null;
124
125
  }
125
126
 
126
- return uniqBy(props, join);
127
+ return uniqBy(props, valuesKey);
127
128
  }
128
129
 
129
130
  _getPropsFromIdentifiers(relation, ownerProp) {
@@ -218,10 +219,6 @@ function containsNonNull(arr) {
218
219
  return false;
219
220
  }
220
221
 
221
- function join(id) {
222
- return id.map((x) => (Buffer.isBuffer(x) ? x.toString('hex') : x)).join(',');
223
- }
224
-
225
222
  function isIdProp(relationProp) {
226
223
  const idProp = relationProp.modelClass.getIdRelationProperty();
227
224
  return idProp.props.every((prop, i) => prop === relationProp.props[i]);
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { asArray, isObject, uniqBy, get, set } = require('../utils/objectUtils');
4
4
  const { ref: createRef } = require('../queryBuilder/ReferenceBuilder');
5
- const { propToStr, PROP_KEY_PREFIX } = require('../model/modelValues');
5
+ const { valuesKey } = require('../model/modelValues');
6
6
 
7
7
  class ModelNotFoundError extends Error {
8
8
  constructor(tableName) {
@@ -86,20 +86,9 @@ class RelationProperty {
86
86
  }
87
87
  }
88
88
 
89
- // Creates a concatenated string from the property values of the given object.
89
+ // Creates a key from the property values of the given object.
90
90
  propKey(obj) {
91
- const size = this.size;
92
- let key = PROP_KEY_PREFIX;
93
-
94
- for (let i = 0; i < size; ++i) {
95
- key += propToStr(this.getProp(obj, i));
96
-
97
- if (i !== size - 1) {
98
- key += ',';
99
- }
100
- }
101
-
102
- return key;
91
+ return valuesKey(this.getProps(obj));
103
92
  }
104
93
 
105
94
  // Returns the property values of the given object as an array.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { RelationInsertOperation } = require('../RelationInsertOperation');
4
4
  const { BelongsToOneRelateOperation } = require('./BelongsToOneRelateOperation');
5
+ const { InsertGraphOperation } = require('../../queryBuilder/operations/InsertGraphOperation');
5
6
 
6
7
  class BelongsToOneInsertOperation extends RelationInsertOperation {
7
8
  onAdd(builder, args) {
@@ -17,7 +18,10 @@ class BelongsToOneInsertOperation extends RelationInsertOperation {
17
18
  async onAfter1(builder, ret) {
18
19
  const inserted = await super.onAfter1(builder, ret);
19
20
 
20
- if (!builder.isExecutable()) {
21
+ // A query resolved without executing it, e.g. with `resolve()`, inserted
22
+ // nothing. `insertGraph()` resolves the query itself, but always inserts
23
+ // the graph.
24
+ if (!builder.isExecutable() && !builder.has(InsertGraphOperation)) {
21
25
  return inserted;
22
26
  }
23
27
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { RelationInsertOperation } = require('../RelationInsertOperation');
4
+ const { removeOverriddenQueryProps } = require('../../model/modelQueryProps');
4
5
 
5
6
  class HasManyInsertOperation extends RelationInsertOperation {
6
7
  onAdd(builder, args) {
@@ -14,6 +15,9 @@ class HasManyInsertOperation extends RelationInsertOperation {
14
15
  for (let j = 0, lp = relatedProp.size; j < lp; ++j) {
15
16
  relatedProp.setProp(model, j, ownerValues[0][j]);
16
17
  }
18
+
19
+ // The foreign keys set here beat query properties given for them:
20
+ removeOverriddenQueryProps(model);
17
21
  }
18
22
 
19
23
  return retVal;
@@ -84,7 +84,7 @@ class ManyToManyFindOperation extends RelationFindOperation {
84
84
  }
85
85
 
86
86
  // @override
87
- ownerKeyColumns() {
87
+ ownerKeys() {
88
88
  return this.ownerJoinColumnAlias;
89
89
  }
90
90