objection 4.0.0-rc.3 → 4.0.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/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/CheckViolationError/parser.js +2 -9
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +2 -9
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/NotNullViolationError/parser.js +4 -2
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/UniqueViolationError/parser.js +4 -2
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/constraintMessage.js +16 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/parser.js +2 -1
- package/lib/dbErrors/parsers/sqlite/DBError/parser.js +2 -1
- package/lib/model/modelJsonAttributes.js +15 -6
- package/lib/objection.js +1 -0
- package/lib/queryBuilder/InternalOptions.js +4 -0
- package/lib/queryBuilder/StaticHookArguments.js +4 -2
- package/lib/queryBuilder/graph/GraphNode.js +8 -0
- package/lib/queryBuilder/graph/GraphPlan.js +13 -0
- package/lib/queryBuilder/graph/GraphSchedule.js +92 -40
- package/lib/queryBuilder/graph/GraphUpsert.js +2 -0
- package/lib/queryBuilder/graph/GraphWriter.js +159 -18
- package/lib/queryBuilder/graph/graphChecks.js +40 -3
- package/lib/queryBuilder/graph/graphKeys.js +146 -61
- package/lib/queryBuilder/jsonDialects/JsonDialect.js +6 -0
- package/lib/queryBuilder/operations/AsArrayOperation.js +10 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +20 -7
- package/lib/queryBuilder/operations/fieldExpressions.js +15 -4
- package/lib/relations/RelationProperty.js +11 -0
- package/package.json +11 -4
- package/typings/objection/index.d.ts +191 -184
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nonStrictEquals } from '../../utils/objectUtils.js';
|
|
2
2
|
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
3
3
|
import { formatPath } from './GraphNode.js';
|
|
4
|
+
import { RelationProperty } from '../../relations/RelationProperty.js';
|
|
4
5
|
|
|
5
6
|
const UNRESOLVED = Symbol('unresolved');
|
|
6
7
|
|
|
@@ -16,16 +17,15 @@ export class GraphKeys {
|
|
|
16
17
|
constructor(plan) {
|
|
17
18
|
this.plan = plan;
|
|
18
19
|
this.activeNodes = new Set(plan.activeNodes);
|
|
20
|
+
// The row of each node, and the rows by their keys:
|
|
19
21
|
this.rows = new Map();
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
+
this.rowsByKey = new Map();
|
|
23
|
+
// The cells in their order:
|
|
24
|
+
this.cells = [];
|
|
22
25
|
// The props that each node only reads, and that other nodes write:
|
|
23
26
|
this.readProps = new Map();
|
|
24
27
|
// The cells that each place sets or reads:
|
|
25
28
|
this.placeCells = new Map();
|
|
26
|
-
// The rows that the graph inserts, and the ones that it inserted already:
|
|
27
|
-
this.insertRows = new Set();
|
|
28
|
-
this.insertedRows = new Set();
|
|
29
29
|
// The nodes whose `#ref{}` values are resolved:
|
|
30
30
|
this.resolvedNodes = new Set();
|
|
31
31
|
|
|
@@ -34,7 +34,7 @@ export class GraphKeys {
|
|
|
34
34
|
const row = this._getRow(node);
|
|
35
35
|
|
|
36
36
|
if (plan.actions.get(node) === 'insert') {
|
|
37
|
-
|
|
37
|
+
row.isInsert = true;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -83,18 +83,20 @@ export class GraphKeys {
|
|
|
83
83
|
|
|
84
84
|
// 4. The clears, with the nodes that clear the cells. The places of a node
|
|
85
85
|
// read them as `null` if another node clears them:
|
|
86
|
-
this.clearingNodes = new Map();
|
|
87
86
|
const addClearingNode = (row, prop, node) =>
|
|
88
87
|
prop.props.forEach((_, i) => {
|
|
89
|
-
const
|
|
90
|
-
|
|
88
|
+
const description = prop.propDescription(i);
|
|
89
|
+
row.clearingNodes.set(
|
|
90
|
+
description,
|
|
91
|
+
(row.clearingNodes.get(description) || new Set()).add(node),
|
|
92
|
+
);
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
for (const currentNode of plan.activeCurrentNodes) {
|
|
94
96
|
const { relation, parent } = currentNode;
|
|
95
97
|
|
|
96
98
|
if (relation?.keySide === 'related' && plan.actions.get(currentNode) === 'unrelate') {
|
|
97
|
-
addClearingNode(currentNode.rowKey, relation.relatedProp, parent.node);
|
|
99
|
+
addClearingNode(this._getRowByKey(currentNode.rowKey), relation.relatedProp, parent.node);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -111,7 +113,7 @@ export class GraphKeys {
|
|
|
111
113
|
// Bind the cells of the rows of the inserted `nodes` to their values.
|
|
112
114
|
setInserted(nodes) {
|
|
113
115
|
for (const node of nodes) {
|
|
114
|
-
this.
|
|
116
|
+
this.rows.get(node).isInserted = true;
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -153,13 +155,25 @@ export class GraphKeys {
|
|
|
153
155
|
.map(({ prop, index }) => prop.props[index]);
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
// Return the cells of the row of `node` that the graph sets, unless `node`
|
|
159
|
+
// only reads them:
|
|
160
|
+
getSetCells(node) {
|
|
161
|
+
const row = this._getRow(node);
|
|
162
|
+
return [...(this.placeCells.get(node) || [])].filter(
|
|
163
|
+
(cell) =>
|
|
164
|
+
cell.row === row &&
|
|
165
|
+
cell.setNodes.size > 0 &&
|
|
166
|
+
!this.isOnlyRead(node, cell.prop.props[cell.index]),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
// Return true if a place other than the places of the row of `node` through
|
|
157
171
|
// its BelongsToOne `relation` sets the key of the relation, e.g. the place
|
|
158
172
|
// that moves the row.
|
|
159
173
|
isSetByOtherPlaces(node, relation) {
|
|
160
174
|
const row = this._getRow(node);
|
|
161
175
|
return relation.ownerProp.props.some((_, i) =>
|
|
162
|
-
[...(
|
|
176
|
+
[...(row.cells.get(relation.ownerProp.propDescription(i))?.placeNodes || [])].some(
|
|
163
177
|
(other) => !(other.relation === relation && this._getRow(other.parent) === row),
|
|
164
178
|
),
|
|
165
179
|
);
|
|
@@ -179,9 +193,7 @@ export class GraphKeys {
|
|
|
179
193
|
getReadProps(node, prop) {
|
|
180
194
|
return this._getCells(node, prop).map((cell) => {
|
|
181
195
|
const values = this._getAnchorCells(cell.slot).map((anchor) =>
|
|
182
|
-
this.
|
|
183
|
-
? null
|
|
184
|
-
: (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
|
|
196
|
+
anchor.row.isInsert ? null : (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
|
|
185
197
|
);
|
|
186
198
|
const value = this._getSlotValue(cell);
|
|
187
199
|
return values.length > 0
|
|
@@ -198,29 +210,47 @@ export class GraphKeys {
|
|
|
198
210
|
|
|
199
211
|
// Return true if a place of `node` sets the key column `prop` of its row:
|
|
200
212
|
isSet(node, prop, index) {
|
|
201
|
-
|
|
213
|
+
const cell = this._getRow(node).cells.get(prop.propDescription(index));
|
|
214
|
+
return !!cell?.setNodes.has(node);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
// Return the nodes whose inserts
|
|
205
|
-
// cells that the place of `node` sets or reads
|
|
217
|
+
// Return the nodes whose inserts, `#ref{}` values or changes bind the slots
|
|
218
|
+
// of the cells that the place of `node` sets or reads, and whether they
|
|
219
|
+
// change them.
|
|
206
220
|
waitsFor(node) {
|
|
221
|
+
const { relation, parent } = node;
|
|
207
222
|
const waits = [];
|
|
223
|
+
const addWaits = (cell, nodes, isKeyChange) => {
|
|
224
|
+
for (const other of nodes) {
|
|
225
|
+
if (this.rows.get(other) !== this.rows.get(node)) {
|
|
226
|
+
waits.push({ node: other, prop: cell.prop.props[cell.index], isKeyChange });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
208
230
|
|
|
209
231
|
for (const cell of this.placeCells.get(node) || []) {
|
|
210
232
|
for (const anchorCell of this._getAnchorCells(cell.slot)) {
|
|
211
233
|
const { row, prop, index } = anchorCell;
|
|
212
|
-
anchorCell.waitedNodes ??=
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
(other)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
anchorCell.waitedNodes ??= row.nodes.filter(
|
|
235
|
+
(other) =>
|
|
236
|
+
(row.isInsert && this.plan.actions.get(other) === 'insert') ||
|
|
237
|
+
this._hasReference(other, prop, index),
|
|
238
|
+
);
|
|
239
|
+
addWaits(cell, anchorCell.waitedNodes, false);
|
|
240
|
+
|
|
241
|
+
// Only the places that set a cell write the changes of its slot:
|
|
242
|
+
if (cell.setNodes.has(node)) {
|
|
243
|
+
addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Join rows are inserted after all rows, but read the keys of their owners,
|
|
249
|
+
// which patches may change:
|
|
250
|
+
if (relation?.keySide === 'join') {
|
|
251
|
+
for (const cell of this._getCells(parent, relation.ownerProp)) {
|
|
252
|
+
for (const anchorCell of this._getAnchorCells(cell.slot)) {
|
|
253
|
+
addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
|
|
224
254
|
}
|
|
225
255
|
}
|
|
226
256
|
}
|
|
@@ -228,6 +258,44 @@ export class GraphKeys {
|
|
|
228
258
|
return waits;
|
|
229
259
|
}
|
|
230
260
|
|
|
261
|
+
// Return the patches and relates that change the column of another row that
|
|
262
|
+
// the `#ref{}` of `reference` copies into `node`, as for the keys:
|
|
263
|
+
getChangingNodes(node, { referencedNode, refPath }) {
|
|
264
|
+
const { modelClass } = referencedNode;
|
|
265
|
+
const [propName] = refPath;
|
|
266
|
+
// The cell of a key on the column, or one of its own:
|
|
267
|
+
const cell =
|
|
268
|
+
[...this._getRow(referencedNode).cells.values()].find(
|
|
269
|
+
({ prop, index }) => prop.props[index] === propName && prop.isColumn(index),
|
|
270
|
+
) ??
|
|
271
|
+
this._getCell(
|
|
272
|
+
referencedNode,
|
|
273
|
+
new RelationProperty(
|
|
274
|
+
`${modelClass.getTableName()}.${modelClass.propertyNameToColumnName(propName)}`,
|
|
275
|
+
() => modelClass,
|
|
276
|
+
),
|
|
277
|
+
0,
|
|
278
|
+
);
|
|
279
|
+
return this._getAnchorCells(cell.slot)
|
|
280
|
+
.map((anchorCell) => this._getChangingNode(anchorCell))
|
|
281
|
+
.filter((other) => other && this.rows.get(other) !== this.rows.get(node));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Return the patch or relate that changes the value of an anchor cell, or
|
|
285
|
+
// may change it, when its row isn't loaded. A foreign key may reference the
|
|
286
|
+
// cell, so the rows that read the new value wait for it (#428). Ids don't
|
|
287
|
+
// change, see `checkChangedIds()`, and rows without ids are found by the
|
|
288
|
+
// values that the graph gives them.
|
|
289
|
+
_getChangingNode(cell) {
|
|
290
|
+
const { writtenValue, writingNode } = this._getGivenValues(cell);
|
|
291
|
+
return writingNode &&
|
|
292
|
+
!cell.isId &&
|
|
293
|
+
(cell.row.isInsert || (writingNode.referencedNode || writingNode).obj.$hasId()) &&
|
|
294
|
+
!nonStrictEquals(writtenValue, this._getStoredValue(cell))
|
|
295
|
+
? writingNode
|
|
296
|
+
: null;
|
|
297
|
+
}
|
|
298
|
+
|
|
231
299
|
// The place of `node` gives `target` the key of `source`, for each column of
|
|
232
300
|
// the relation:
|
|
233
301
|
_addPlace(node, target, targetProp, source, sourceProp) {
|
|
@@ -244,8 +312,11 @@ export class GraphKeys {
|
|
|
244
312
|
sourceCells[i].readerNodes.add(source);
|
|
245
313
|
|
|
246
314
|
// A whole `#ref{}` value wins over the key of its place, also where the
|
|
247
|
-
// place lists the row again:
|
|
248
|
-
if (
|
|
315
|
+
// place lists the row again. Most graphs have none, so skip the keys:
|
|
316
|
+
if (
|
|
317
|
+
this.wholeReferences.size === 0 ||
|
|
318
|
+
!this.wholeReferences.has(this._getPlaceKey(target, cell.prop.props[cell.index]))
|
|
319
|
+
) {
|
|
249
320
|
cell.setNodes.add(target);
|
|
250
321
|
cell.placeNodes.add(node);
|
|
251
322
|
joinCells(cell, sourceCells[i]);
|
|
@@ -258,7 +329,7 @@ export class GraphKeys {
|
|
|
258
329
|
// parent and the relation.
|
|
259
330
|
_getPlaceKey(node, prop) {
|
|
260
331
|
const { parent, relation } = node;
|
|
261
|
-
return `${this._getRow(node)}|${parent ? this._getRow(parent) : ''}|${relation?.name}|${prop}`;
|
|
332
|
+
return `${this._getRow(node).key}|${parent ? this._getRow(parent).key : ''}|${relation?.name}|${prop}`;
|
|
262
333
|
}
|
|
263
334
|
|
|
264
335
|
_addCells(node, cells) {
|
|
@@ -284,41 +355,49 @@ export class GraphKeys {
|
|
|
284
355
|
_getRow(node) {
|
|
285
356
|
let row = this.rows.get(node);
|
|
286
357
|
|
|
287
|
-
if (row
|
|
358
|
+
if (!row) {
|
|
288
359
|
// A `#ref` model has the row of the model it refers to, and a new model
|
|
289
360
|
// a row of its own:
|
|
290
361
|
const { referencedNode } = node;
|
|
291
362
|
row = referencedNode
|
|
292
363
|
? this._getRow(referencedNode)
|
|
293
|
-
: node.obj.$hasId()
|
|
294
|
-
|
|
295
|
-
: `new:${this.rows.size}`;
|
|
364
|
+
: this._getRowByKey(node.obj.$hasId() ? node.rowKey : `new:${this.rows.size}`);
|
|
365
|
+
row.nodes.push(node);
|
|
296
366
|
this.rows.set(node, row);
|
|
297
|
-
|
|
298
|
-
const nodes = this.nodesByRow.get(row);
|
|
299
|
-
|
|
300
|
-
if (nodes) {
|
|
301
|
-
nodes.push(node);
|
|
302
|
-
} else {
|
|
303
|
-
this.nodesByRow.set(row, [node]);
|
|
304
|
-
}
|
|
305
367
|
}
|
|
306
368
|
|
|
307
369
|
return row;
|
|
308
370
|
}
|
|
309
371
|
|
|
310
|
-
|
|
311
|
-
|
|
372
|
+
// The row with the `key` of its table and id, or of a new row:
|
|
373
|
+
_getRowByKey(key) {
|
|
374
|
+
let row = this.rowsByKey.get(key);
|
|
375
|
+
|
|
376
|
+
if (!row) {
|
|
377
|
+
row = {
|
|
378
|
+
key,
|
|
379
|
+
nodes: [],
|
|
380
|
+
// The cells by their props, and the nodes that clear them:
|
|
381
|
+
cells: new Map(),
|
|
382
|
+
clearingNodes: new Map(),
|
|
383
|
+
// Whether the graph inserts the row, and whether it is inserted:
|
|
384
|
+
isInsert: false,
|
|
385
|
+
isInserted: false,
|
|
386
|
+
};
|
|
387
|
+
this.rowsByKey.set(key, row);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return row;
|
|
312
391
|
}
|
|
313
392
|
|
|
314
393
|
_getCell(node, prop, index) {
|
|
315
394
|
const row = this._getRow(node);
|
|
316
|
-
const
|
|
317
|
-
let cell =
|
|
395
|
+
const description = prop.propDescription(index);
|
|
396
|
+
let cell = row.cells.get(description);
|
|
318
397
|
|
|
319
398
|
if (!cell) {
|
|
320
399
|
cell = {
|
|
321
|
-
|
|
400
|
+
description,
|
|
322
401
|
row,
|
|
323
402
|
node,
|
|
324
403
|
prop,
|
|
@@ -330,7 +409,8 @@ export class GraphKeys {
|
|
|
330
409
|
slot: null,
|
|
331
410
|
};
|
|
332
411
|
cell.slot = { cells: [cell] };
|
|
333
|
-
this.cells.
|
|
412
|
+
this.cells.push(cell);
|
|
413
|
+
row.cells.set(description, cell);
|
|
334
414
|
}
|
|
335
415
|
|
|
336
416
|
return cell;
|
|
@@ -354,7 +434,7 @@ export class GraphKeys {
|
|
|
354
434
|
const { actions, graphOptions } = this.plan;
|
|
355
435
|
const givenValues = {};
|
|
356
436
|
|
|
357
|
-
for (const node of
|
|
437
|
+
for (const node of row.nodes) {
|
|
358
438
|
const action = actions.get(node);
|
|
359
439
|
|
|
360
440
|
if (
|
|
@@ -376,8 +456,13 @@ export class GraphKeys {
|
|
|
376
456
|
givenValues.value = value;
|
|
377
457
|
}
|
|
378
458
|
|
|
379
|
-
if (
|
|
459
|
+
if (
|
|
460
|
+
givenValues.writtenValue === undefined &&
|
|
461
|
+
action !== 'insert' &&
|
|
462
|
+
value !== undefined
|
|
463
|
+
) {
|
|
380
464
|
givenValues.writtenValue = value;
|
|
465
|
+
givenValues.writingNode = node;
|
|
381
466
|
}
|
|
382
467
|
|
|
383
468
|
if (givenValues.writtenValue !== undefined) {
|
|
@@ -410,12 +495,12 @@ export class GraphKeys {
|
|
|
410
495
|
// current value.
|
|
411
496
|
_getAnchorValue(cell) {
|
|
412
497
|
const { row, prop, index } = cell;
|
|
413
|
-
const nodes =
|
|
498
|
+
const { nodes } = row;
|
|
414
499
|
const [node] = nodes;
|
|
415
500
|
const getValue = (obj) => (obj ? prop.getProp(obj, index) : undefined);
|
|
416
501
|
const { writtenValue } = this._getGivenValues(cell);
|
|
417
502
|
|
|
418
|
-
if (
|
|
503
|
+
if (row.isInsert) {
|
|
419
504
|
const insertNode = nodes.find((node) => this.plan.actions.get(node) === 'insert');
|
|
420
505
|
return writtenValue ?? getValue(insertNode.obj) ?? null;
|
|
421
506
|
} else if (writtenValue !== undefined) {
|
|
@@ -439,7 +524,7 @@ export class GraphKeys {
|
|
|
439
524
|
// Return true if an anchor cell waits for an insert or a `#ref{}` value:
|
|
440
525
|
_isWaiting(cell) {
|
|
441
526
|
return (
|
|
442
|
-
(
|
|
527
|
+
(cell.row.isInsert && !cell.row.isInserted) ||
|
|
443
528
|
this._getGivenValues(cell).writtenValue === UNRESOLVED
|
|
444
529
|
);
|
|
445
530
|
}
|
|
@@ -450,14 +535,14 @@ export class GraphKeys {
|
|
|
450
535
|
_getKnownKey(cell) {
|
|
451
536
|
const { row, prop, index } = cell;
|
|
452
537
|
|
|
453
|
-
if (
|
|
538
|
+
if (row.isInsert) {
|
|
454
539
|
const { value } = this._getGivenValues(cell);
|
|
455
540
|
return value === UNRESOLVED
|
|
456
541
|
? undefined
|
|
457
542
|
: value !== undefined
|
|
458
543
|
? `=${value}`
|
|
459
544
|
: cell.isId
|
|
460
|
-
? row
|
|
545
|
+
? row.key
|
|
461
546
|
: undefined;
|
|
462
547
|
} else {
|
|
463
548
|
const value = this._isWaiting(cell) ? undefined : this._getAnchorValue(cell);
|
|
@@ -500,7 +585,7 @@ export class GraphKeys {
|
|
|
500
585
|
_getStoredValue(cell) {
|
|
501
586
|
const { row, prop, index } = cell;
|
|
502
587
|
|
|
503
|
-
for (const node of
|
|
588
|
+
for (const node of row.nodes) {
|
|
504
589
|
const value =
|
|
505
590
|
!this.activeNodes.has(node) && prop.getProp(node.obj, index) !== undefined
|
|
506
591
|
? prop.getProp(node.obj, index)
|
|
@@ -516,7 +601,7 @@ export class GraphKeys {
|
|
|
516
601
|
|
|
517
602
|
// Return true if another node than the ones that read `cell` clears it:
|
|
518
603
|
_isCleared(cell) {
|
|
519
|
-
const clearingNodes =
|
|
604
|
+
const clearingNodes = cell.row.clearingNodes.get(cell.description);
|
|
520
605
|
return (cell.isCleared ??=
|
|
521
606
|
!!clearingNodes && [...clearingNodes].some((node) => !cell.readerNodes.has(node)));
|
|
522
607
|
}
|
|
@@ -524,7 +609,7 @@ export class GraphKeys {
|
|
|
524
609
|
// Throw for slots whose anchors, or whose rows without any, give different
|
|
525
610
|
// values that are known before the writes.
|
|
526
611
|
_checkSlots() {
|
|
527
|
-
const slots = new Set(
|
|
612
|
+
const slots = new Set(this.cells.map((cell) => cell.slot));
|
|
528
613
|
|
|
529
614
|
for (const slot of [...slots].filter(({ cells }) => cells.length > 1)) {
|
|
530
615
|
const anchorCells = this._getAnchorCells(slot);
|
|
@@ -17,6 +17,12 @@ export class JsonDialect {
|
|
|
17
17
|
return sql;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// The value that the path updates of a column start from, an empty one where
|
|
21
|
+
// the column is NULL, as `set()` leaves NULL as it is.
|
|
22
|
+
setBase(sql, access) {
|
|
23
|
+
return `COALESCE(${sql}, '${access[0].type === 'array' ? '[]' : '{}'}')`;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
// Reference the bare column if there is no path, so that the database can use
|
|
21
27
|
// its indexes.
|
|
22
28
|
field(sql, access) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { QueryBuilderOperation } from './QueryBuilderOperation.js';
|
|
2
|
+
import { asArray } from '../../utils/objectUtils.js';
|
|
3
|
+
|
|
4
|
+
// Wrap the result in an array in the order of the operations, so a `first()`
|
|
5
|
+
// added after this unwraps it again.
|
|
6
|
+
export class AsArrayOperation extends QueryBuilderOperation {
|
|
7
|
+
onAfter3(_, result) {
|
|
8
|
+
return asArray(result);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { isEmpty } from '../../utils/objectUtils.js';
|
|
1
|
+
import { hasOwn, isEmpty } from '../../utils/objectUtils.js';
|
|
2
2
|
import { QueryBuilderOperation } from './QueryBuilderOperation.js';
|
|
3
3
|
import { StaticHookArguments } from '../StaticHookArguments.js';
|
|
4
4
|
import { removeOverriddenValues } from '../../model/modelQueryProps.js';
|
|
5
5
|
import {
|
|
6
6
|
checkFieldExpressions,
|
|
7
7
|
convertFieldExpressionsToRaw,
|
|
8
|
+
getJsonKeyPatch,
|
|
8
9
|
removeFieldExpressions,
|
|
9
10
|
} from './fieldExpressions.js';
|
|
10
11
|
|
|
@@ -65,14 +66,26 @@ export class UpdateOperation extends QueryBuilderOperation {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
onBuildKnex(knexBuilder, builder) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (this.modelOptions.patch) {
|
|
71
|
-
return knexBuilder.update(convertFieldExpressionsToRaw(builder, json));
|
|
72
|
-
} else {
|
|
69
|
+
if (!this.modelOptions.patch) {
|
|
73
70
|
checkFieldExpressions(builder.modelClass(), this.model, 'update');
|
|
74
|
-
return knexBuilder.update(json);
|
|
75
71
|
}
|
|
72
|
+
|
|
73
|
+
const json = this.model.$toDatabaseJson(builder);
|
|
74
|
+
const { jsonKeyPatches } = builder.internalOptions();
|
|
75
|
+
|
|
76
|
+
// The path updates are join expressions in column names, so they replace
|
|
77
|
+
// the mapped columns in the json:
|
|
78
|
+
for (const prop of jsonKeyPatches?.keys() || []) {
|
|
79
|
+
const patch = getJsonKeyPatch(this.model, prop, jsonKeyPatches);
|
|
80
|
+
const column = builder.modelClass().propertyNameToColumnName(prop);
|
|
81
|
+
|
|
82
|
+
if (patch && hasOwn(json, column)) {
|
|
83
|
+
delete json[column];
|
|
84
|
+
Object.assign(json, patch);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return knexBuilder.update(convertFieldExpressionsToRaw(builder, json));
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
async onAfter2(builder, numUpdated) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ref } from '../ReferenceBuilder.js';
|
|
2
|
-
import { isObject } from '../../utils/objectUtils.js';
|
|
2
|
+
import { isObject, jsonEquals } from '../../utils/objectUtils.js';
|
|
3
3
|
import { getFieldExpressionColumn } from '../../utils/parseFieldExpression.js';
|
|
4
4
|
import { isKnexRaw, isKnexQueryBuilder } from '../../utils/knexUtils.js';
|
|
5
5
|
import { getJsonDialect } from '../jsonDialects/index.js';
|
|
@@ -28,6 +28,15 @@ export function checkFieldExpressions(modelClass, model, operation) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Return the path updates that write the keys that `upsertGraph()` gives the
|
|
32
|
+
// json column `prop` of `model`, while the column holds nothing but these keys,
|
|
33
|
+
// which keeps its other values (#415). A column that hooks change is written
|
|
34
|
+
// whole (#488):
|
|
35
|
+
export function getJsonKeyPatch(model, prop, jsonKeyPatches) {
|
|
36
|
+
const entry = jsonKeyPatches?.get(prop);
|
|
37
|
+
return entry && jsonEquals(model[prop], entry.keys) ? entry.patch : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
// Field expressions only describe the writes into json columns, they aren't
|
|
32
41
|
// properties of the models that the writes return:
|
|
33
42
|
export function removeFieldExpressions(modelClass, model) {
|
|
@@ -48,7 +57,7 @@ export function convertFieldExpressionsToRaw(builder, json) {
|
|
|
48
57
|
|
|
49
58
|
if (getFieldExpressionColumn(key, declaredColumns) !== null) {
|
|
50
59
|
// 'col:attr' : ref('other:lol') is transformed to
|
|
51
|
-
// "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
60
|
+
// "col" : raw(`jsonb_set(COALESCE("table"."col", '{}'), '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
52
61
|
// on Postgres, with the column qualified, as it is ambiguous with
|
|
53
62
|
// `excluded` in merge().
|
|
54
63
|
dialect.checkSupport();
|
|
@@ -82,9 +91,11 @@ export function convertFieldExpressionsToRaw(builder, json) {
|
|
|
82
91
|
val = JSON.stringify(val);
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
const { access } = parsed.parsedExpr;
|
|
95
|
+
const previous = convertedJson[parsed.column];
|
|
85
96
|
convertedJson[parsed.column] = knex.raw(
|
|
86
|
-
dialect.set('??',
|
|
87
|
-
[
|
|
97
|
+
dialect.set(previous ? '??' : dialect.setBase('??', access), access, valuePlaceholder),
|
|
98
|
+
[previous || `${builder.tableRef()}.${parsed.column}`, val],
|
|
88
99
|
);
|
|
89
100
|
} else {
|
|
90
101
|
convertedJson[key] = val;
|
|
@@ -126,6 +126,17 @@ export class RelationProperty {
|
|
|
126
126
|
return this._propSetters[index](obj, value);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Returns true if the index:th column is a whole column.
|
|
130
|
+
isColumn(index) {
|
|
131
|
+
return this._refs[index].isPlainColumnRef;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Returns true if the index:th column is a key at the top level of a json
|
|
135
|
+
// column.
|
|
136
|
+
isJsonKey(index) {
|
|
137
|
+
return this._refs[index].parsedExpr.access.length === 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
// Returns an instance of ReferenceBuilder that points to the index:th
|
|
130
141
|
// value of a row.
|
|
131
142
|
ref(builder, index) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "objection",
|
|
3
|
-
"version": "4.0.0
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "An SQL-friendly ORM for Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/objection.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "pnpm eslint && vitest run && pnpm test:typings",
|
|
18
18
|
"test:fast": "vitest run --bail 1",
|
|
19
19
|
"test:watch": "vitest",
|
|
20
|
-
"test:typings": "tsc && tsc -p tests/ts/strict && tsc -p tests/bench/typings && prettier --check \"typings/**/*.ts\" \"tests/ts/**/*.ts\" \"tests/bench/typings/*.ts\" \"tests/bench/typings/app/*.ts\"",
|
|
20
|
+
"test:typings": "tsc && tsc -p tests/ts/strict && tsc -p tests/bench/typings && node tests/ts/completions.js && prettier --check \"typings/**/*.ts\" \"tests/ts/**/*.ts\" \"tests/bench/typings/*.ts\" \"tests/bench/typings/app/*.ts\"",
|
|
21
21
|
"bench:typings": "node tests/bench/typings/bench.js",
|
|
22
22
|
"db:up": "docker compose up -d --wait",
|
|
23
23
|
"db:down": "docker compose --profile mssql down -v",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"docs:preview": "vitepress preview doc"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
|
-
"tag": "
|
|
32
|
+
"tag": "latest"
|
|
33
33
|
},
|
|
34
34
|
"author": {
|
|
35
35
|
"name": "Sami Koskimäki",
|
|
@@ -76,7 +76,13 @@
|
|
|
76
76
|
"ajv-formats": "^3.0.1"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"knex": "^3.3.0"
|
|
79
|
+
"knex": "^3.3.0",
|
|
80
|
+
"typescript": ">=5.8"
|
|
81
|
+
},
|
|
82
|
+
"peerDependenciesMeta": {
|
|
83
|
+
"typescript": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
80
86
|
},
|
|
81
87
|
"devDependencies": {
|
|
82
88
|
"@types/node": "^22.15.3",
|
|
@@ -91,6 +97,7 @@
|
|
|
91
97
|
"sqlite3": "^6.0.1",
|
|
92
98
|
"tedious": "^20.3.7",
|
|
93
99
|
"typescript": "^7.0.0",
|
|
100
|
+
"typescript-5": "npm:typescript@5.8.3",
|
|
94
101
|
"vitepress": "1.6.4",
|
|
95
102
|
"vitest": "^4.1.11"
|
|
96
103
|
}
|