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.
- package/lib/model/AjvValidator.js +22 -6
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -16
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +78 -6
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +9 -2
- package/lib/model/modelUtils.js +1 -1
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +17 -7
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +7 -1
- package/lib/queryBuilder/graph/GraphOptions.js +66 -12
- package/lib/queryBuilder/graph/GraphUpsert.js +92 -10
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +7 -6
- package/lib/queryBuilder/graph/insert/GraphInsert.js +66 -22
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -13
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +16 -3
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +3 -2
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/operations/DeleteOperation.js +20 -0
- package/lib/queryBuilder/operations/InsertOperation.js +4 -2
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
- package/lib/queryBuilder/operations/select/Selection.js +25 -3
- package/lib/relations/RelationFindOperation.js +7 -4
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/hasMany/HasManyInsertOperation.js +4 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +80 -21
- package/lib/utils/knexUtils.js +7 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +17 -5
- package/lib/utils/promiseUtils/map.js +24 -7
- package/lib/utils/wrapDbError.js +14 -0
- package/package.json +1 -1
- 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
|
-
: [
|
|
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
|
-
//
|
|
132
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}/${
|
|
138
|
+
return `${parentKey}/${escapedId}`;
|
|
134
139
|
} else {
|
|
135
|
-
return
|
|
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 === '*' &&
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
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 {
|
|
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 && !
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
66
|
+
return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
|
|
67
67
|
} else {
|
|
68
68
|
return (
|
|
69
69
|
selectionToTest.table === null ||
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
89
|
+
// Creates a key from the property values of the given object.
|
|
90
90
|
propKey(obj) {
|
|
91
|
-
|
|
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
|
-
|
|
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;
|