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
|
@@ -2,6 +2,7 @@ import { getModel } from '../../model/getModel.js';
|
|
|
2
2
|
import { restoreOmitFromDatabaseJson } from '../../model/modelUtils.js';
|
|
3
3
|
import { valuesKey } from '../../model/modelValues.js';
|
|
4
4
|
import { UpdateOperation } from '../operations/UpdateOperation.js';
|
|
5
|
+
import { getJsonKeyPatch } from '../operations/fieldExpressions.js';
|
|
5
6
|
import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
|
|
6
7
|
import { isInternalProp } from '../../utils/internalPropUtils.js';
|
|
7
8
|
import {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
difference,
|
|
10
11
|
get,
|
|
11
12
|
groupBy,
|
|
13
|
+
isEmpty,
|
|
12
14
|
isObject,
|
|
13
15
|
jsonEquals,
|
|
14
16
|
nonStrictEquals,
|
|
@@ -25,6 +27,10 @@ export class GraphWriter {
|
|
|
25
27
|
constructor(schedule) {
|
|
26
28
|
this.schedule = schedule;
|
|
27
29
|
this.plan = schedule.plan;
|
|
30
|
+
// The rows that are written, as the nodes of their models, and whether the
|
|
31
|
+
// key columns of relations may be null:
|
|
32
|
+
this.writtenRows = new Set();
|
|
33
|
+
this.nullableProps = new Map();
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
async write(builder) {
|
|
@@ -125,14 +131,15 @@ export class GraphWriter {
|
|
|
125
131
|
await promiseMap(batches, (batch) => this._insertBatch(builder, batch), {
|
|
126
132
|
concurrency: getConcurrency(builder, nodes),
|
|
127
133
|
});
|
|
128
|
-
await this._patch(
|
|
129
|
-
builder,
|
|
130
|
-
nodes.filter((node) => this.schedule.slotMovedNodes.has(node)),
|
|
131
|
-
);
|
|
132
134
|
|
|
133
135
|
for (const node of references) {
|
|
134
136
|
resolveDependencies(this.schedule, node);
|
|
135
137
|
}
|
|
138
|
+
|
|
139
|
+
await this._patch(
|
|
140
|
+
builder,
|
|
141
|
+
[...nodes, ...references].filter((node) => this.schedule.earlyNodes.has(node)),
|
|
142
|
+
);
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
async _insertBatch(builder, nodes) {
|
|
@@ -152,6 +159,7 @@ export class GraphWriter {
|
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
const insertNodes = nodes.filter((node) => actions.get(node) === 'insert');
|
|
162
|
+
await this._clearSlots(builder, insertNodes);
|
|
155
163
|
|
|
156
164
|
if (insertNodes.length > 0) {
|
|
157
165
|
const [{ modelClass }] = insertNodes;
|
|
@@ -177,6 +185,10 @@ export class GraphWriter {
|
|
|
177
185
|
await fetchGeneratedKeys(builder, modelClass, insertNodes);
|
|
178
186
|
this.schedule.keys.setInserted(insertNodes);
|
|
179
187
|
}
|
|
188
|
+
|
|
189
|
+
for (const node of insertNodes) {
|
|
190
|
+
this.writtenRows.add(node);
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
_writeDeferredKeys(builder, dependencies) {
|
|
@@ -251,7 +263,60 @@ export class GraphWriter {
|
|
|
251
263
|
});
|
|
252
264
|
}
|
|
253
265
|
|
|
266
|
+
// Rows that swap slots in a cycle can't take them while the other rows still
|
|
267
|
+
// hold their keys, if they're unique. Under `unrelate`, the rows that leave
|
|
268
|
+
// the slots clear their keys first, as 3.x does, if they may be null, and
|
|
269
|
+
// take their new keys with their own writes, once the other rows took their
|
|
270
|
+
// slots (#489). Keys that can't be null are swapped directly, and so are keys
|
|
271
|
+
// in json columns, which hold other values:
|
|
272
|
+
async _clearSlots(builder, nodes) {
|
|
273
|
+
for (const { dependent, node, isSlotMove } of this.schedule.deferredDependencies) {
|
|
274
|
+
// The row that moves out of the slot, with its key:
|
|
275
|
+
const { modelClass } = node;
|
|
276
|
+
const relatedProp = isSlotMove && node.relation.relatedProp;
|
|
277
|
+
if (
|
|
278
|
+
relatedProp &&
|
|
279
|
+
nodes.includes(dependent) &&
|
|
280
|
+
!this.writtenRows.has(node.referencedNode || node) &&
|
|
281
|
+
relatedProp.props.every((_, i) => relatedProp.isColumn(i)) &&
|
|
282
|
+
this.plan.graphOptions.has(node, 'unrelate') &&
|
|
283
|
+
(await this._isNullable(builder, relatedProp))
|
|
284
|
+
) {
|
|
285
|
+
// Without validation or hooks, as the row's own write follows:
|
|
286
|
+
const { schema } = builder.internalOptions();
|
|
287
|
+
const query = builder.unsafeKnex()(modelClass.getTableName());
|
|
288
|
+
const idColumns = modelClass.getIdColumnArray();
|
|
289
|
+
const ids = modelClass.getIdRelationProperty().getProps(node.obj);
|
|
290
|
+
await (schema ? query.withSchema(schema) : query)
|
|
291
|
+
.where(Object.fromEntries(idColumns.map((column, i) => [column, ids[i]])))
|
|
292
|
+
.update(Object.fromEntries(relatedProp.cols.map((column) => [column, null])));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Return true if the database allows null in the columns of `prop`:
|
|
298
|
+
_isNullable(builder, prop) {
|
|
299
|
+
let isNullable = this.nullableProps.get(prop);
|
|
300
|
+
|
|
301
|
+
if (!isNullable) {
|
|
302
|
+
isNullable = prop.modelClass
|
|
303
|
+
.query()
|
|
304
|
+
.childQueryOf(builder)
|
|
305
|
+
.columnInfo()
|
|
306
|
+
.then((columnInfo) => prop.cols.every((column) => columnInfo[column]?.nullable));
|
|
307
|
+
this.nullableProps.set(prop, isNullable);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return isNullable;
|
|
311
|
+
}
|
|
312
|
+
|
|
254
313
|
async _patch(builder, nodes) {
|
|
314
|
+
await this._clearSlots(builder, nodes);
|
|
315
|
+
|
|
316
|
+
for (const node of nodes) {
|
|
317
|
+
this.writtenRows.add(node.referencedNode || node);
|
|
318
|
+
}
|
|
319
|
+
|
|
255
320
|
// The patches leave out the props that don't change, but the models that
|
|
256
321
|
// the graph returns get them back once the step is written (#371):
|
|
257
322
|
const omittedProps = new Map(nodes.map((node) => [node, node.obj.$omitFromDatabaseJson()]));
|
|
@@ -284,6 +349,12 @@ export class GraphWriter {
|
|
|
284
349
|
_preparePatch(builder, node) {
|
|
285
350
|
const { referencedNode } = node;
|
|
286
351
|
|
|
352
|
+
// A row that the graph relates by its key alone isn't looked up, its key is
|
|
353
|
+
// only written to its owner:
|
|
354
|
+
if (this.plan.isRelatedByKey(node)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
287
358
|
// The props that the graph fills into the model of a `#ref` model are only
|
|
288
359
|
// final now:
|
|
289
360
|
if (referencedNode) {
|
|
@@ -304,15 +375,24 @@ export class GraphWriter {
|
|
|
304
375
|
? node.relation.relatedProp.props
|
|
305
376
|
: []),
|
|
306
377
|
];
|
|
378
|
+
// The path updates of the keys in json columns that the write sets or
|
|
379
|
+
// clears, by column:
|
|
380
|
+
let jsonKeyPatches = new Map();
|
|
307
381
|
// Changing the id never makes sense with `upsertGraph()`. A many-to-many
|
|
308
382
|
// `#ref` model whose model writes its row only writes its join row:
|
|
309
383
|
const idProps = node.modelClass.getIdPropertyArray();
|
|
310
384
|
const isJoinRowOnly =
|
|
311
385
|
!!referencedNode && node.relation.keySide === 'join' && this.plan.isCoveredByModel(node);
|
|
312
|
-
const isWritable = (prop) =>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
386
|
+
const isWritable = (prop) => {
|
|
387
|
+
// A json column that its key paths write is unchanged if they are:
|
|
388
|
+
const jsonKeyPatch = getJsonKeyPatch(node.obj, prop, jsonKeyPatches);
|
|
389
|
+
return (
|
|
390
|
+
!idProps.includes(prop) &&
|
|
391
|
+
!(jsonKeyPatch && isEmpty(jsonKeyPatch)) &&
|
|
392
|
+
!this.schedule.keys.isOnlyRead(node, prop) &&
|
|
393
|
+
(!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop))
|
|
394
|
+
);
|
|
395
|
+
};
|
|
316
396
|
|
|
317
397
|
const getPropsToUpdate = (changedProps) =>
|
|
318
398
|
(shouldPatch || shouldUpdate ? changedProps : keyProps).filter(isWritable);
|
|
@@ -335,21 +415,38 @@ export class GraphWriter {
|
|
|
335
415
|
// what to update. Otherwise, the hook may still change the model, and we
|
|
336
416
|
// check again after it runs, but only if the model has props to update, as
|
|
337
417
|
// relates are no updates (#2233, #2605).
|
|
338
|
-
const changes = this._findChanges(node);
|
|
339
|
-
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
340
418
|
const hasBeforeUpdate = hasHook(node.obj, 'beforeUpdate');
|
|
341
419
|
|
|
342
|
-
|
|
420
|
+
const changes = this._findChanges(node);
|
|
421
|
+
|
|
422
|
+
if (getPropsToUpdate(changes.changedProps).length === 0) {
|
|
343
423
|
return null;
|
|
344
|
-
} else if (!hasBeforeUpdate) {
|
|
345
|
-
handleUpdate(changes);
|
|
346
424
|
}
|
|
347
425
|
|
|
348
426
|
node.obj.$validate(null, {
|
|
349
427
|
dataPath: node.dataPathKey,
|
|
350
|
-
patch:
|
|
428
|
+
patch: !shouldUpdate,
|
|
351
429
|
});
|
|
352
430
|
|
|
431
|
+
// Write the keys in json columns as path updates where their column holds
|
|
432
|
+
// nothing else, which keeps its other values (#415):
|
|
433
|
+
jsonKeyPatches = getJsonKeyPatches(node, [
|
|
434
|
+
...this.schedule.keys.getSetCells(node),
|
|
435
|
+
...this.plan
|
|
436
|
+
.getRemovedOwnerRelations(node)
|
|
437
|
+
.flatMap(({ ownerProp }) =>
|
|
438
|
+
ownerProp.props.map((_, index) => ({ prop: ownerProp, index })),
|
|
439
|
+
),
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
443
|
+
|
|
444
|
+
if (propsToUpdate.length === 0) {
|
|
445
|
+
return null;
|
|
446
|
+
} else if (!hasBeforeUpdate) {
|
|
447
|
+
handleUpdate(changes);
|
|
448
|
+
}
|
|
449
|
+
|
|
353
450
|
// Rows that their owners relate by their keys can't be found through the
|
|
354
451
|
// relation until the owners are patched, so they're patched directly.
|
|
355
452
|
const { relation } = node;
|
|
@@ -360,17 +457,26 @@ export class GraphWriter {
|
|
|
360
457
|
|
|
361
458
|
return {
|
|
362
459
|
node,
|
|
363
|
-
|
|
460
|
+
shouldUpdate,
|
|
364
461
|
hasBeforeUpdate,
|
|
365
462
|
handleUpdate,
|
|
366
463
|
propsToUpdate,
|
|
464
|
+
jsonKeyPatches,
|
|
367
465
|
isPatchedThroughRelation,
|
|
368
466
|
};
|
|
369
467
|
}
|
|
370
468
|
|
|
371
469
|
_writePatch(
|
|
372
470
|
builder,
|
|
373
|
-
{
|
|
471
|
+
{
|
|
472
|
+
node,
|
|
473
|
+
shouldUpdate,
|
|
474
|
+
hasBeforeUpdate,
|
|
475
|
+
handleUpdate,
|
|
476
|
+
propsToUpdate,
|
|
477
|
+
jsonKeyPatches,
|
|
478
|
+
isPatchedThroughRelation,
|
|
479
|
+
},
|
|
374
480
|
) {
|
|
375
481
|
const query = (
|
|
376
482
|
isPatchedThroughRelation
|
|
@@ -392,6 +498,7 @@ export class GraphWriter {
|
|
|
392
498
|
// Join rows are patched with the extras among the props to update. The
|
|
393
499
|
// extras of new join rows are written with them, see `_findChanges()`:
|
|
394
500
|
joinTableExtras: node.relation?.keySide === 'join' && !node.current ? [] : propsToUpdate,
|
|
501
|
+
jsonKeyPatches,
|
|
395
502
|
})
|
|
396
503
|
.copyFrom(builder, isReturningAll);
|
|
397
504
|
|
|
@@ -405,7 +512,8 @@ export class GraphWriter {
|
|
|
405
512
|
});
|
|
406
513
|
}
|
|
407
514
|
|
|
408
|
-
|
|
515
|
+
// Writes of only the keys under `noUpdate` are patches too:
|
|
516
|
+
return (shouldUpdate ? query.update(node.obj) : query.patch(node.obj))
|
|
409
517
|
.runAfter(async (result, query) => {
|
|
410
518
|
if (isObject(result) && result.$isObjectionModel) {
|
|
411
519
|
// Handle `returning('*')`.
|
|
@@ -554,7 +662,7 @@ function resolveDependencies(schedule, node) {
|
|
|
554
662
|
// Copy the keys and `#ref{}` values that `node` waits for into its model,
|
|
555
663
|
// after those that the graph fills into the models that it reads them from,
|
|
556
664
|
// e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
|
|
557
|
-
// move first only wait for rows that exist, see `GraphSchedule#
|
|
665
|
+
// move first only wait for rows that exist, see `GraphSchedule#_waitsForWrites()`.
|
|
558
666
|
function resolveFilledDependencies(schedule, node, visitedNodes = new Set()) {
|
|
559
667
|
visitedNodes.add(node);
|
|
560
668
|
|
|
@@ -746,6 +854,39 @@ async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
|
746
854
|
}
|
|
747
855
|
}
|
|
748
856
|
|
|
857
|
+
// Return the path updates of the changed keys of `cells` at the top level of
|
|
858
|
+
// json columns, by the columns that hold nothing but these keys. PostgreSQL and
|
|
859
|
+
// MySQL can't write nested paths whose parents are missing, so their columns
|
|
860
|
+
// are written whole.
|
|
861
|
+
function getJsonKeyPatches(node, cells) {
|
|
862
|
+
const patchesByColumn = new Map();
|
|
863
|
+
const pathCells = cells.filter(({ prop, index }) => prop.isJsonKey(index));
|
|
864
|
+
|
|
865
|
+
for (const [column, columnCells] of groupBy(pathCells, ({ prop, index }) => prop.props[index])) {
|
|
866
|
+
const keys = {};
|
|
867
|
+
|
|
868
|
+
for (const { prop, index } of columnCells) {
|
|
869
|
+
prop.setProp(keys, index, prop.getProp(node.obj, index));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (jsonEquals(node.obj[column], keys[column])) {
|
|
873
|
+
const patch = {};
|
|
874
|
+
|
|
875
|
+
for (const { prop, index } of columnCells) {
|
|
876
|
+
const value = prop.getProp(node.obj, index);
|
|
877
|
+
|
|
878
|
+
if (!node.current || !nonStrictEquals(prop.getProp(node.current.obj, index), value)) {
|
|
879
|
+
prop.patch(patch, index, value);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
patchesByColumn.set(column, { keys: keys[column], patch });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return patchesByColumn;
|
|
888
|
+
}
|
|
889
|
+
|
|
749
890
|
function hasOwnerKeyArray(relation) {
|
|
750
891
|
return relation.keySide === 'owner' && !relation.isOneToOne();
|
|
751
892
|
}
|
|
@@ -223,7 +223,7 @@ export function checkPlacements(builder, nodes) {
|
|
|
223
223
|
} else if (!nonStrictEquals(other.value, value)) {
|
|
224
224
|
throw builder.modelClass().createValidationError({
|
|
225
225
|
type: ValidationErrorType.InvalidGraph,
|
|
226
|
-
message: `the graph gives ${prop} of one ${name} two values, at ${formatPath(other.path)} and at ${formatPath(path)}`,
|
|
226
|
+
message: `the graph gives ${prop} of one ${name} two values, at ${formatPath(other.path())} and at ${formatPath(path())}`,
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
};
|
|
@@ -243,7 +243,8 @@ export function checkPlacements(builder, nodes) {
|
|
|
243
243
|
.map(([cols, rowId]) => `${cols}=${rowId}`),
|
|
244
244
|
].join(':');
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
// The data paths are only built for the errors:
|
|
247
|
+
const getPath = (node, key) => () => [node.dataPathKey, key].filter(Boolean).join('.');
|
|
247
248
|
const isSlot = (relation) => relation.keySide === 'related' && relation.isOneToOne();
|
|
248
249
|
|
|
249
250
|
// The props that the models give, except for the ids and the keys that the
|
|
@@ -405,7 +406,7 @@ export function checkPlacements(builder, nodes) {
|
|
|
405
406
|
const other = rows.get(otherRowId).get(relatedProp.props[0]);
|
|
406
407
|
throw builder.modelClass().createValidationError({
|
|
407
408
|
type: ValidationErrorType.InvalidGraph,
|
|
408
|
-
message: `the graph puts two ${relatedModelClass.name} rows in ${relation.name} of one ${owner.modelClass.name}, at ${formatPath(path)} and at ${formatPath(other.path)}`,
|
|
409
|
+
message: `the graph puts two ${relatedModelClass.name} rows in ${relation.name} of one ${owner.modelClass.name}, at ${formatPath(path())} and at ${formatPath(other.path())}`,
|
|
409
410
|
});
|
|
410
411
|
}
|
|
411
412
|
}
|
|
@@ -444,6 +445,42 @@ export function checkReferences(builder, plan) {
|
|
|
444
445
|
}
|
|
445
446
|
}
|
|
446
447
|
|
|
448
|
+
// A model that the graph relates by the key of its BelongsToOneRelation alone
|
|
449
|
+
// isn't looked up: only its key is written to its owner. So it can't give other
|
|
450
|
+
// props, unless they aren't written, relations or values that others refer to.
|
|
451
|
+
export function checkKeyRelates(builder, plan) {
|
|
452
|
+
const { graphOptions } = plan;
|
|
453
|
+
|
|
454
|
+
for (const node of plan.activeNodes.filter((node) => plan.isRelatedByKey(node))) {
|
|
455
|
+
const { modelClass, relation, obj } = node;
|
|
456
|
+
const relationNames = modelClass.getRelationNames();
|
|
457
|
+
const idProps = modelClass.getIdPropertyArray();
|
|
458
|
+
const isWritten = graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node);
|
|
459
|
+
const name = Object.keys(obj).find((key) => {
|
|
460
|
+
const value = obj[key];
|
|
461
|
+
return (
|
|
462
|
+
value !== undefined &&
|
|
463
|
+
!isInternalProp(key) &&
|
|
464
|
+
!relation.relatedProp.props.includes(key) &&
|
|
465
|
+
!(value === null && idProps.includes(key)) &&
|
|
466
|
+
(relationNames.includes(key) || isWritten)
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
const isReferenced =
|
|
470
|
+
node.propertyReferencingNodes.length > 0 ||
|
|
471
|
+
plan.nodes.some((other) => other.referencedNode === node);
|
|
472
|
+
|
|
473
|
+
if (name || isReferenced) {
|
|
474
|
+
throw builder.modelClass().createValidationError({
|
|
475
|
+
type: ValidationErrorType.InvalidGraph,
|
|
476
|
+
message: `the ${modelClass.name} at ${formatPath(node.dataPathKey)} is related by its key alone, so ${
|
|
477
|
+
name ? `its ${name} can't be written` : "it can't be referenced"
|
|
478
|
+
}`,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
447
484
|
// A `#ref{}` waits for the model it reads if the value only exists once that
|
|
448
485
|
// model is written. These waits follow from the graph alone, so their cycles
|
|
449
486
|
// are found before the first write.
|