objection 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/lib/model/AjvValidator.js +133 -3
- package/lib/queryBuilder/QueryBuilder.js +186 -5
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
- package/lib/queryBuilder/ReferenceBuilder.js +5 -3
- package/lib/queryBuilder/RelationExpression.js +13 -1
- package/lib/queryBuilder/StaticHookArguments.js +12 -1
- package/lib/queryBuilder/graph/GraphUpsert.js +22 -0
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
- package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
- package/lib/queryBuilder/join/JoinResultParser.js +22 -0
- package/lib/queryBuilder/join/RelationJoiner.js +71 -22
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
- package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
- package/lib/queryBuilder/operations/InsertOperation.js +121 -12
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
- package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
- package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
- package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
- package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
- package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
- package/lib/queryBuilder/operations/select/Selection.js +95 -4
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
- package/lib/queryBuilder/whereGroups.js +125 -0
- package/lib/relations/Relation.js +5 -2
- package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
- package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
- package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
- package/lib/utils/deprecate.js +6 -0
- package/lib/utils/identifierMapping.js +15 -1
- package/lib/utils/parseFieldExpression.js +28 -0
- package/package.json +10 -6
- package/typings/objection/index.d.ts +280 -28
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { GraphAction } = require('../GraphAction');
|
|
4
|
+
const { groupBy } = require('../../../utils/objectUtils');
|
|
5
|
+
const promiseUtils = require('../../../utils/promiseUtils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Patches in the foreign keys of `BelongsToOneRelation` edges that `GraphInsert`
|
|
9
|
+
* had to defer to break a dependency cycle. The owner nodes of these edges were
|
|
10
|
+
* inserted without the foreign keys. `GraphInsertAction` already copied them to
|
|
11
|
+
* the owner objects once the related nodes were inserted, so all that's left to
|
|
12
|
+
* do is one patch query per owner.
|
|
13
|
+
*/
|
|
14
|
+
class DeferredRelationGraphInsertAction extends GraphAction {
|
|
15
|
+
constructor(graphData, { edges }) {
|
|
16
|
+
super(graphData);
|
|
17
|
+
|
|
18
|
+
// `BelongsToOneRelation` edges whose owner node needs to be patched.
|
|
19
|
+
this.edges = edges;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
run(builder) {
|
|
23
|
+
const edgesByOwner = groupBy(this.edges, (edge) => edge.ownerNode);
|
|
24
|
+
const owners = [...edgesByOwner.keys()];
|
|
25
|
+
const concurrency = this._getConcurrency(builder, owners);
|
|
26
|
+
|
|
27
|
+
return promiseUtils.map(
|
|
28
|
+
owners,
|
|
29
|
+
(owner) => this._patchOwner(builder, owner, edgesByOwner.get(owner)),
|
|
30
|
+
{ concurrency },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_patchOwner(builder, ownerNode, edges) {
|
|
35
|
+
const { modelClass } = ownerNode;
|
|
36
|
+
const ownerIdProp = modelClass.getIdRelationProperty();
|
|
37
|
+
const patch = {};
|
|
38
|
+
|
|
39
|
+
for (const { relation } of edges) {
|
|
40
|
+
const { ownerProp } = relation;
|
|
41
|
+
|
|
42
|
+
ownerProp.forEach((i) => {
|
|
43
|
+
ownerProp.patch(patch, i, ownerProp.getProp(ownerNode.obj, i));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return modelClass
|
|
48
|
+
.query()
|
|
49
|
+
.childQueryOf(builder)
|
|
50
|
+
.patch(patch)
|
|
51
|
+
.whereInComposite(ownerIdProp.refs(builder), [ownerIdProp.getProps(ownerNode.obj)]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
DeferredRelationGraphInsertAction,
|
|
57
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { JoinRowGraphInsertAction } = require('./JoinRowGraphInsertAction');
|
|
4
|
+
const { DeferredRelationGraphInsertAction } = require('./DeferredRelationGraphInsertAction');
|
|
4
5
|
const { GraphInsertAction } = require('./GraphInsertAction');
|
|
5
6
|
const { GraphOperation } = require('../GraphOperation');
|
|
6
7
|
const { ModelGraphEdge } = require('../../../model/graph/ModelGraphEdge');
|
|
@@ -9,10 +10,15 @@ class GraphInsert extends GraphOperation {
|
|
|
9
10
|
constructor(...args) {
|
|
10
11
|
super(...args);
|
|
11
12
|
this.dependencies = this._createDependencyMap();
|
|
13
|
+
this.deferredEdges = new Set();
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
createActions() {
|
|
15
|
-
return [
|
|
17
|
+
return [
|
|
18
|
+
...this._createNormalActions(),
|
|
19
|
+
...this._createDeferredRelationActions(),
|
|
20
|
+
...this._createJoinRowActions(),
|
|
21
|
+
];
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
_createDependencyMap() {
|
|
@@ -76,6 +82,10 @@ class GraphInsert extends GraphOperation {
|
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
if (nodesToInsert.length === 0) {
|
|
85
|
+
if (handledNodes.size !== this.graph.nodes.length && this._breakCycle(handledNodes)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
break;
|
|
80
90
|
}
|
|
81
91
|
|
|
@@ -83,6 +93,7 @@ class GraphInsert extends GraphOperation {
|
|
|
83
93
|
new GraphInsertAction(this.graphData, {
|
|
84
94
|
nodes: nodesToInsert,
|
|
85
95
|
dependencies: this.dependencies,
|
|
96
|
+
deferredEdges: this.deferredEdges,
|
|
86
97
|
}),
|
|
87
98
|
);
|
|
88
99
|
|
|
@@ -98,19 +109,71 @@ class GraphInsert extends GraphOperation {
|
|
|
98
109
|
return actions;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
// Tries to break a dependency cycle by deferring one `BelongsToOneRelation`
|
|
113
|
+
// edge that points to a `#ref` node. The owner node is then inserted without
|
|
114
|
+
// the foreign key, which is patched in by `DeferredRelationGraphInsertAction`
|
|
115
|
+
// once the referenced node has been inserted. Cycles can only exist through
|
|
116
|
+
// `#ref` nodes, since the graph is a tree otherwise.
|
|
117
|
+
_breakCycle(handledNodes) {
|
|
118
|
+
for (const node of this.graph.nodes) {
|
|
119
|
+
if (this._isHandled(node, handledNodes)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const edge of this._pendingDependencyEdges(node, handledNodes)) {
|
|
124
|
+
if (
|
|
125
|
+
this._canDeferEdge(edge, node) &&
|
|
126
|
+
this._isBlockedBy(edge.relatedNode, node, handledNodes, new Set())
|
|
127
|
+
) {
|
|
128
|
+
this.deferredEdges.add(edge);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
103
135
|
}
|
|
104
136
|
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
_canDeferEdge(edge, node) {
|
|
138
|
+
return (
|
|
139
|
+
edge.type === ModelGraphEdge.Type.Relation &&
|
|
140
|
+
edge.relation.isObjectionBelongsToOneRelation &&
|
|
141
|
+
edge.isOwnerNode(node) &&
|
|
142
|
+
edge.relatedNode.isReference &&
|
|
143
|
+
// Both ends need to be inserted for the foreign key to be patched in later.
|
|
144
|
+
this.graphOptions.shouldInsert(node, this.graphData) &&
|
|
145
|
+
this.graphOptions.shouldInsert(edge.relatedNode.referencedNode, this.graphData) &&
|
|
146
|
+
!this._isReadByPropertyReference(node, edge.relation.ownerProp)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Returns true if a `#ref{}` property reference reads one of the `props` of
|
|
151
|
+
// `node`. Such references may be resolved before a deferred prop is set,
|
|
152
|
+
// which would resolve them to `undefined`.
|
|
153
|
+
_isReadByPropertyReference(node, props) {
|
|
154
|
+
return node.refEdges.some((edge) => {
|
|
155
|
+
return (
|
|
156
|
+
edge.refType === ModelGraphEdge.ReferenceType.Property &&
|
|
157
|
+
edge.isRelatedNode(node) &&
|
|
158
|
+
props.props.includes(edge.refRelatedDataPath[0])
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Returns true if `node` (transitively) waits for `blockingNode` to be handled.
|
|
164
|
+
_isBlockedBy(node, blockingNode, handledNodes, visitedNodes) {
|
|
165
|
+
if (node === blockingNode) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (visitedNodes.has(node)) {
|
|
107
170
|
return false;
|
|
108
171
|
}
|
|
109
172
|
|
|
110
|
-
|
|
111
|
-
const dependencyNode = edge.getOtherNode(node);
|
|
173
|
+
visitedNodes.add(node);
|
|
112
174
|
|
|
113
|
-
|
|
175
|
+
for (const edge of this._pendingDependencyEdges(node, handledNodes)) {
|
|
176
|
+
if (this._isBlockedBy(edge.getOtherNode(node), blockingNode, handledNodes, visitedNodes)) {
|
|
114
177
|
return true;
|
|
115
178
|
}
|
|
116
179
|
}
|
|
@@ -118,6 +181,42 @@ class GraphInsert extends GraphOperation {
|
|
|
118
181
|
return false;
|
|
119
182
|
}
|
|
120
183
|
|
|
184
|
+
_createDeferredRelationActions() {
|
|
185
|
+
if (this.deferredEdges.size === 0) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [
|
|
190
|
+
new DeferredRelationGraphInsertAction(this.graphData, {
|
|
191
|
+
edges: [...this.deferredEdges],
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_isHandled(node, handledNodes) {
|
|
197
|
+
return handledNodes.has(node);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_hasDependencies(node, handledNodes) {
|
|
201
|
+
return this._pendingDependencyEdges(node, handledNodes).length > 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Returns the dependency edges of `node` whose other node still needs to be
|
|
205
|
+
// inserted first. Deferred edges are ignored.
|
|
206
|
+
_pendingDependencyEdges(node, handledNodes) {
|
|
207
|
+
const edges = this.dependencies.get(node) || [];
|
|
208
|
+
|
|
209
|
+
return edges.filter((edge) => {
|
|
210
|
+
const dependencyNode = edge.getOtherNode(node);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
!this.deferredEdges.has(edge) &&
|
|
214
|
+
!handledNodes.has(dependencyNode) &&
|
|
215
|
+
!this.currentGraph.nodeForNode(dependencyNode)
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
121
220
|
_markHandled(node, handledNodes) {
|
|
122
221
|
handledNodes.add(node);
|
|
123
222
|
|
|
@@ -14,12 +14,15 @@ const promiseUtils = require('../../../utils/promiseUtils');
|
|
|
14
14
|
* when needed.
|
|
15
15
|
*/
|
|
16
16
|
class GraphInsertAction extends GraphAction {
|
|
17
|
-
constructor(graphData, { nodes, dependencies }) {
|
|
17
|
+
constructor(graphData, { nodes, dependencies, deferredEdges }) {
|
|
18
18
|
super(graphData);
|
|
19
19
|
|
|
20
20
|
// Nodes to insert.
|
|
21
21
|
this.nodes = nodes;
|
|
22
22
|
this.dependencies = dependencies;
|
|
23
|
+
// Dependency edges that are resolved once the dependency node has been
|
|
24
|
+
// inserted, and are patched in by `DeferredRelationGraphInsertAction`.
|
|
25
|
+
this.deferredEdges = deferredEdges;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
run(builder) {
|
|
@@ -63,6 +66,10 @@ class GraphInsertAction extends GraphAction {
|
|
|
63
66
|
|
|
64
67
|
if (edges) {
|
|
65
68
|
for (const edge of edges) {
|
|
69
|
+
if (this.deferredEdges.has(edge)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
// `node` needs `dependencyNode` to have been inserted (and it has been).
|
|
67
74
|
const dependencyNode = edge.getOtherNode(node);
|
|
68
75
|
|
|
@@ -22,6 +22,8 @@ class JoinResultParser {
|
|
|
22
22
|
return flatRows;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
this._checkIdColumns(flatRows[0]);
|
|
26
|
+
|
|
25
27
|
this.columnsByTableNode = this._createColumns(flatRows[0]);
|
|
26
28
|
this.parentMap = new Map();
|
|
27
29
|
this.rootModels = [];
|
|
@@ -93,6 +95,26 @@ class JoinResultParser {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
_checkIdColumns(row) {
|
|
99
|
+
for (const tableNode of this.tableTree.nodes) {
|
|
100
|
+
const idColumns = tableNode.modelClass.getIdColumnArray();
|
|
101
|
+
const hasIdColumns =
|
|
102
|
+
idColumns.length > 0 &&
|
|
103
|
+
idColumns.every((column) =>
|
|
104
|
+
Object.prototype.hasOwnProperty.call(row, tableNode.getColumnAliasForColumn(column)),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!hasIdColumns) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`withGraphJoined: the id column(s) [${idColumns.join(', ')}] of model ${tableNode.modelClass.name} ` +
|
|
110
|
+
`are missing from the result, so the joined rows can't be grouped into models. ` +
|
|
111
|
+
`withGraphJoined requires all models in the relation expression to have a primary key ` +
|
|
112
|
+
`(idColumn) that exists in the table. Use withGraphFetched for models without one.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
_createColumns(row) {
|
|
97
119
|
const columns = Object.keys(row).map((columnAlias) =>
|
|
98
120
|
JoinResultColumn.create({ tableTree: this.tableTree, columnAlias }),
|
|
@@ -24,6 +24,8 @@ class RelationJoiner {
|
|
|
24
24
|
this.modifiers = null;
|
|
25
25
|
|
|
26
26
|
this.options = defaultOptions();
|
|
27
|
+
// The `joinOperation` options of the individual `withGraphJoined` calls.
|
|
28
|
+
this.joinOperations = [];
|
|
27
29
|
this.tableTree = null;
|
|
28
30
|
this.internalSelections = null;
|
|
29
31
|
}
|
|
@@ -49,6 +51,11 @@ class RelationJoiner {
|
|
|
49
51
|
return this;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
setJoinOperations(joinOperations) {
|
|
55
|
+
this.joinOperations = joinOperations;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/**
|
|
53
60
|
* Fetches the column information needed for building the select clauses.
|
|
54
61
|
*
|
|
@@ -81,23 +88,6 @@ class RelationJoiner {
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
releaseInternalRootSelection(column) {
|
|
85
|
-
const rootTableNode = this.tableTree && this.tableTree.rootNode;
|
|
86
|
-
|
|
87
|
-
if (!rootTableNode || !this.internalSelections) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const alias = rootTableNode.getColumnAliasForColumn(column);
|
|
92
|
-
const count = this.internalSelections.length;
|
|
93
|
-
|
|
94
|
-
this.internalSelections = this.internalSelections.filter(
|
|
95
|
-
(it) => it.table !== rootTableNode.alias || it.alias !== alias,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
return this.internalSelections.length !== count;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
91
|
parseResult(builder, flatRows) {
|
|
102
92
|
const parser = JoinResultParser.create({
|
|
103
93
|
tableTree: this._getTableTree(builder),
|
|
@@ -134,12 +124,18 @@ class RelationJoiner {
|
|
|
134
124
|
// also contain selects.
|
|
135
125
|
userSelectQuery.modify(tableNode.relation.modify);
|
|
136
126
|
|
|
127
|
+
if (selectsOnlyAliasesKeepingSelectAll(userSelectQuery.findAllSelections())) {
|
|
128
|
+
// All columns are selected from the joined subquery in the main query,
|
|
129
|
+
// so the subquery needs to select them too.
|
|
130
|
+
subqueryToJoin.select(`${subqueryToJoin.tableRef()}.*`);
|
|
131
|
+
}
|
|
132
|
+
|
|
137
133
|
// Save the query that contains the user specified selects
|
|
138
134
|
// for later use.
|
|
139
135
|
userSelectQueries.set(tableNode, userSelectQuery);
|
|
140
136
|
|
|
141
137
|
tableNode.relation.join(builder, {
|
|
142
|
-
joinOperation: this.
|
|
138
|
+
joinOperation: this._getJoinOperation(tableNode),
|
|
143
139
|
|
|
144
140
|
ownerTable: tableNode.parentNode.alias,
|
|
145
141
|
relatedTableAlias: tableNode.alias,
|
|
@@ -156,6 +152,26 @@ class RelationJoiner {
|
|
|
156
152
|
}
|
|
157
153
|
}
|
|
158
154
|
|
|
155
|
+
// Returns the `joinOperation` of the last `withGraphJoined` call that
|
|
156
|
+
// contains the relation of `tableNode`, falling back to the default.
|
|
157
|
+
_getJoinOperation(tableNode) {
|
|
158
|
+
const path = [];
|
|
159
|
+
|
|
160
|
+
for (let node = tableNode; node.parentNode; node = node.parentNode) {
|
|
161
|
+
path.unshift(node.relationProperty);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = this.joinOperations.length - 1; i >= 0; --i) {
|
|
165
|
+
const { expression, joinOperation } = this.joinOperations[i];
|
|
166
|
+
|
|
167
|
+
if (containsPath(expression, path)) {
|
|
168
|
+
return joinOperation;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return this.options.joinOperation;
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
_buildSelects({ builder, tableNode, userSelectQueries }) {
|
|
160
176
|
const { selections, internalSelections } = getSelectionsForNode({
|
|
161
177
|
builder,
|
|
@@ -168,9 +184,21 @@ class RelationJoiner {
|
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
// Save the selections that were added internally (not by the user)
|
|
171
|
-
// so that we can later remove the corresponding properties
|
|
172
|
-
//
|
|
173
|
-
|
|
187
|
+
// so that we can later remove the corresponding properties. The root
|
|
188
|
+
// ones are registered with the builder, which removes them from the
|
|
189
|
+
// result models after all eager operations have run, as they may still
|
|
190
|
+
// need them. The nested ones are removed when parsing the result.
|
|
191
|
+
this.internalSelections = [];
|
|
192
|
+
|
|
193
|
+
for (const selection of internalSelections) {
|
|
194
|
+
if (selection.table === tableNode.alias) {
|
|
195
|
+
builder.addInternalSelection(
|
|
196
|
+
tableNode.modelClass.columnNameToPropertyName(selection.column),
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
this.internalSelections.push(selection);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
174
202
|
|
|
175
203
|
builder.select(selectionsToStrings(selections));
|
|
176
204
|
}
|
|
@@ -185,9 +213,21 @@ function defaultOptions() {
|
|
|
185
213
|
};
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
function containsPath(expression, path) {
|
|
217
|
+
for (const name of path) {
|
|
218
|
+
expression = expression.childExpression(name);
|
|
219
|
+
|
|
220
|
+
if (!expression) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
188
228
|
function createSubqueryToJoin({ builder, tableNode, modifiers }) {
|
|
189
229
|
const { relation, expression, modelClass } = tableNode;
|
|
190
|
-
const modifierQuery = modelClass.query().childQueryOf(builder);
|
|
230
|
+
const modifierQuery = modelClass.query().childQueryOf(builder, { isJoinChildQuery: true });
|
|
191
231
|
|
|
192
232
|
for (const modifierName of expression.node.$modify) {
|
|
193
233
|
const modifier = createModifier({
|
|
@@ -341,9 +381,18 @@ function isNotSelectAll(selection) {
|
|
|
341
381
|
}
|
|
342
382
|
|
|
343
383
|
function isSelectAllSelectionSet(selections) {
|
|
384
|
+
selections = selections.filter(isNotKeepingSelectAll);
|
|
344
385
|
return selections.length === 0 || selections.some(isSelectAll);
|
|
345
386
|
}
|
|
346
387
|
|
|
388
|
+
function isNotKeepingSelectAll(selection) {
|
|
389
|
+
return !selection.keepsSelectAll;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function selectsOnlyAliasesKeepingSelectAll(selections) {
|
|
393
|
+
return selections.length > 0 && selections.every((it) => it.keepsSelectAll);
|
|
394
|
+
}
|
|
395
|
+
|
|
347
396
|
function checkAliasLength(modelClass, alias) {
|
|
348
397
|
if (alias.length > ID_LENGTH_LIMIT) {
|
|
349
398
|
throw modelClass.createValidationError({
|
|
@@ -22,10 +22,16 @@ class InsertAndFetchOperation extends DelegateOperation {
|
|
|
22
22
|
const modelClass = builder.modelClass();
|
|
23
23
|
const insertedModels = await super.onAfter2(builder, inserted);
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Models without identifiers can't be fetched. This happens when a row
|
|
26
|
+
// with a database generated identifier is ignored by `onConflict()`.
|
|
27
|
+
const insertedModelArray = asArray(insertedModels).filter((model) => model && model.$hasId());
|
|
26
28
|
const idProps = modelClass.getIdPropertyArray();
|
|
27
29
|
const ids = insertedModelArray.map((model) => model.$id());
|
|
28
30
|
|
|
31
|
+
if (ids.length === 0) {
|
|
32
|
+
return insertedModels;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
const fetchedModels = await modelClass
|
|
30
36
|
.query()
|
|
31
37
|
.childQueryOf(builder)
|
|
@@ -4,7 +4,7 @@ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
|
4
4
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
5
5
|
const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
|
|
6
6
|
const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
|
|
7
|
-
const { isObject } = require('../../utils/objectUtils');
|
|
7
|
+
const { isObject, isString } = require('../../utils/objectUtils');
|
|
8
8
|
|
|
9
9
|
// Base class for all insert operations.
|
|
10
10
|
class InsertOperation extends QueryBuilderOperation {
|
|
@@ -36,26 +36,46 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
onBuildKnex(knexBuilder, builder) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const knex = builder.knex();
|
|
40
|
+
|
|
41
|
+
if (!builder.has(/returning/)) {
|
|
42
|
+
if (!isSqlite(knex) && !isMySql(knex)) {
|
|
43
|
+
// If the user hasn't specified a `returning` clause, we make sure
|
|
44
|
+
// that at least the identifier is returned. With `onConflict()` the
|
|
45
|
+
// conflict columns are returned too, so that the returned rows can
|
|
46
|
+
// be matched to the models when some of the rows are ignored.
|
|
47
|
+
knexBuilder = knexBuilder.returning(getDefaultReturning(builder));
|
|
48
|
+
} else if (isSqlite(knex) && builder.has('onConflict')) {
|
|
49
|
+
// Without a `returning` clause, SQLite returns the id of the last
|
|
50
|
+
// inserted row, which belongs to a previous insert if the row was
|
|
51
|
+
// ignored. Use `returning` to find out what was actually inserted.
|
|
52
|
+
knexBuilder = knexBuilder.returning(getDefaultReturning(builder));
|
|
53
|
+
}
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
return knexBuilder.insert(this.models.map((model) => model.$toDatabaseJson(builder)));
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
onAfter1(
|
|
59
|
+
onAfter1(builder, ret) {
|
|
49
60
|
if (!Array.isArray(ret) || !ret.length || ret === this.models) {
|
|
50
61
|
// Early exit if there is nothing to do.
|
|
51
62
|
return this.models;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
const isOnConflict = builder.has('onConflict');
|
|
66
|
+
|
|
54
67
|
if (isObject(ret[0])) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if (isOnConflict && ret.length !== this.models.length) {
|
|
69
|
+
// With `onConflict()`, ignored rows (and rows not updated because of a
|
|
70
|
+
// `where` clause after `merge()`) are not returned, so the returned rows
|
|
71
|
+
// can't be merged to the models based on their position.
|
|
72
|
+
mergeMatchingRows(builder, this.models, ret);
|
|
73
|
+
} else {
|
|
74
|
+
// If the user specified a `returning` clause the result may be an array of objects.
|
|
75
|
+
// Merge all values of the objects to our models.
|
|
76
|
+
for (let i = 0, l = this.models.length; i < l; ++i) {
|
|
77
|
+
this.models[i].$setDatabaseJson(ret[i]);
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
} else {
|
|
61
81
|
// If the return value is not an array of objects, we assume it is an array of identifiers.
|
|
@@ -63,8 +83,9 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
63
83
|
const model = this.models[i];
|
|
64
84
|
|
|
65
85
|
// Don't set the id if the model already has one. MySQL and Sqlite don't return the correct
|
|
66
|
-
// primary key value if the id is not generated in db, but given explicitly.
|
|
67
|
-
|
|
86
|
+
// primary key value if the id is not generated in db, but given explicitly. With
|
|
87
|
+
// `onConflict()`, MySQL returns `0` for ignored rows, which is not a valid id.
|
|
88
|
+
if (!model.$id() && !(isOnConflict && !ret[i])) {
|
|
68
89
|
model.$id(ret[i]);
|
|
69
90
|
}
|
|
70
91
|
}
|
|
@@ -92,6 +113,94 @@ class InsertOperation extends QueryBuilderOperation {
|
|
|
92
113
|
}
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
function getDefaultReturning(builder) {
|
|
117
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
118
|
+
const columns = Array.from(new Set(idColumns.concat(getConflictColumns(builder))));
|
|
119
|
+
|
|
120
|
+
return columns.length === 1 ? columns[0] : columns;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getConflictColumns(builder) {
|
|
124
|
+
if (!builder.has('onConflict')) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const op = builder.findOperation('onConflict');
|
|
129
|
+
const arg = op.args ? op.args[0] : undefined;
|
|
130
|
+
const columns = Array.isArray(arg) ? arg : [arg];
|
|
131
|
+
|
|
132
|
+
// Raw conflict targets (or no target at all) can't be used for matching.
|
|
133
|
+
return columns.every(isString) ? columns : [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Merges the rows returned by an insert with `onConflict()` into the models with
|
|
137
|
+
// matching key values. Models whose rows were not returned are left untouched.
|
|
138
|
+
function mergeMatchingRows(builder, models, rows) {
|
|
139
|
+
const jsons = models.map((model) => model.$toDatabaseJson(builder));
|
|
140
|
+
const idColumns = builder.modelClass().getIdColumnArray();
|
|
141
|
+
|
|
142
|
+
// Use the conflict columns, or else the identifiers, to match the rows to
|
|
143
|
+
// the models. The columns must be present in the input and the result.
|
|
144
|
+
const keyColumns = [getConflictColumns(builder), idColumns].find((columns) => {
|
|
145
|
+
return (
|
|
146
|
+
columns.length > 0 &&
|
|
147
|
+
columns.every((col) => {
|
|
148
|
+
return (
|
|
149
|
+
rows.every((row) => row[col] !== undefined) &&
|
|
150
|
+
jsons.every((json) => json[col] !== undefined)
|
|
151
|
+
);
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!keyColumns) {
|
|
157
|
+
throw createMatchError();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const createKey = (obj) => JSON.stringify(keyColumns.map((col) => normalizeKey(obj[col])));
|
|
161
|
+
const indicesByKey = new Map();
|
|
162
|
+
|
|
163
|
+
jsons.forEach((json, index) => {
|
|
164
|
+
const key = createKey(json);
|
|
165
|
+
const indices = indicesByKey.get(key);
|
|
166
|
+
|
|
167
|
+
if (indices) {
|
|
168
|
+
indices.push(index);
|
|
169
|
+
} else {
|
|
170
|
+
indicesByKey.set(key, [index]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const indices = indicesByKey.get(createKey(row));
|
|
176
|
+
const index = indices ? indices.shift() : undefined;
|
|
177
|
+
|
|
178
|
+
if (index === undefined) {
|
|
179
|
+
throw createMatchError();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
models[index].$setDatabaseJson(row);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeKey(value) {
|
|
187
|
+
if (value === null || value === undefined) {
|
|
188
|
+
return null;
|
|
189
|
+
} else if (isObject(value)) {
|
|
190
|
+
return JSON.stringify(value);
|
|
191
|
+
} else {
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createMatchError() {
|
|
197
|
+
return new Error(
|
|
198
|
+
'Could not match the rows returned by an insert with `onConflict()` to the inserted models. ' +
|
|
199
|
+
'Pass the conflict columns to `onConflict()` and make sure they are returned, or provide ' +
|
|
200
|
+
'the identifiers of the inserted models.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
95
204
|
function callBeforeInsert(builder, models) {
|
|
96
205
|
const maybePromise = callInstanceBeforeInsert(builder, models);
|
|
97
206
|
return after(maybePromise, () => callStaticBeforeInsert(builder));
|
|
@@ -41,6 +41,9 @@ class QueryBuilderOperation {
|
|
|
41
41
|
|
|
42
42
|
// Operations this operation added in any of its hooks.
|
|
43
43
|
this.childOperations = [];
|
|
44
|
+
|
|
45
|
+
// The where group this operation belongs to. See `whereGroups.js`.
|
|
46
|
+
this.whereGroup = null;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
is(OperationClass) {
|
|
@@ -217,6 +220,7 @@ class QueryBuilderOperation {
|
|
|
217
220
|
|
|
218
221
|
clone.adderHookName = this.adderHookName;
|
|
219
222
|
clone.parentOperation = this.parentOperation;
|
|
223
|
+
clone.whereGroup = this.whereGroup;
|
|
220
224
|
|
|
221
225
|
clone.childOperations = this.childOperations.map((childOp) => {
|
|
222
226
|
const childOpClone = childOp.clone();
|