objection 4.0.0-rc.3 → 4.0.0-rc.4

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.
@@ -6,6 +6,9 @@ export class InternalOptions {
6
6
  this.skipRelationFilter = false;
7
7
  // The join table extras that related updates write, all of them if null:
8
8
  this.joinTableExtras = null;
9
+ // The field expressions that `upsertGraph()` writes keys in json columns of
10
+ // updated rows with:
11
+ this.fieldExpressionKeys = [];
9
12
  this.debug = false;
10
13
  this.schema = undefined;
11
14
  }
@@ -18,6 +21,7 @@ export class InternalOptions {
18
21
  copy.isInternalQuery = this.isInternalQuery;
19
22
  copy.skipRelationFilter = this.skipRelationFilter;
20
23
  copy.joinTableExtras = this.joinTableExtras;
24
+ copy.fieldExpressionKeys = this.fieldExpressionKeys;
21
25
  copy.debug = this.debug;
22
26
  copy.schema = this.schema;
23
27
 
@@ -191,6 +191,14 @@ export class GraphNode {
191
191
 
192
192
  for (const node of nodes) {
193
193
  if (node.isReference) {
194
+ // A `#ref` places its model in a relation, which a root doesn't have:
195
+ if (!node.parent) {
196
+ throw node.modelClass.createValidationError({
197
+ type: ValidationErrorType.InvalidGraph,
198
+ message: `the reference at ${formatPath(node.dataPathKey)} has no relation to place its model in`,
199
+ });
200
+ }
201
+
194
202
  node.referencedNode = getNode(node.reference);
195
203
  }
196
204
 
@@ -91,6 +91,19 @@ export class GraphPlan {
91
91
  );
92
92
  }
93
93
 
94
+ // Return true if the graph relates the row of `node` by the key of its
95
+ // BelongsToOneRelation alone, without an id or a reference.
96
+ isRelatedByKey(node) {
97
+ return (
98
+ this.actions.get(node) === 'relate' &&
99
+ node.relation.keySide === 'owner' &&
100
+ !node.hasId &&
101
+ !node.isReference &&
102
+ !node.isDbReference &&
103
+ node.propertyReferences.length === 0
104
+ );
105
+ }
106
+
94
107
  // Return true if the model of the `#ref` model `node` writes what `node`
95
108
  // would write to the row: The model is inserted, or it's written and its path
96
109
  // allows any write that the path of `node` allows.
@@ -1,17 +1,22 @@
1
1
  import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
2
2
  import { groupBy } from '../../utils/objectUtils.js';
3
+ import { ValidationErrorType } from '../../model/ValidationError.js';
4
+ import { formatPath } from './GraphNode.js';
3
5
 
4
6
  // Order the writes of a planned graph in steps that keep keys and constraints
5
7
  // intact (#214), which `GraphWriter` writes:
6
8
  // 1. Move the rows that the graph relates to existing owners of HasMany and
7
9
  // HasOne relations, as they may still sit below rows that the next step
8
- // deletes. Rows that wait for inserts are related in step 3 or 4.
10
+ // deletes. Rows that wait for inserts or changed keys are related in step 3
11
+ // or 4.
9
12
  // 2. Remove rows that reference their owners, before the inserts that may need
10
13
  // their unique keys.
11
14
  // 3. Insert the new rows in the order of their keys and references, then the
12
15
  // keys deferred to break cycles and the join rows. Rows that move out of
13
16
  // the slots of HasOneRelations that new rows go into are related in the
14
- // same order, as the foreign key may be unique.
17
+ // same order, as the foreign key may be unique, and so are the patches and
18
+ // relates that change the keys that these rows read, as a foreign key may
19
+ // reference them (#428).
15
20
  // 4. Patch the changed rows, the other relates, and the keys of owners whose
16
21
  // BelongsToOne rows were inserted, related or removed.
17
22
  // 5. Unrelate the rows that their own to-many relation on their own key leaves
@@ -25,7 +30,8 @@ export class GraphSchedule {
25
30
  this.plan = plan;
26
31
  this.keys = keys;
27
32
  // The nodes that each node waits for, through its keys, its references and
28
- // the rows that move out of the slots that it goes into:
33
+ // the rows that move out of the slots that it goes into. `isKeyChange`
34
+ // marks the patches and relates that change the keys it reads:
29
35
  this.dependencies = createDependencies(plan, this.keys);
30
36
  // Dependencies that aren't waited for, to break cycles. Deferred keys are
31
37
  // written after the inserts:
@@ -36,9 +42,8 @@ export class GraphSchedule {
36
42
  ? []
37
43
  : plan.activeNodes.filter((node) => this._isMovedFirst(node)),
38
44
  );
39
- // Relates that move the rows out of the slots of HasOneRelations that new
40
- // rows go into, and that aren't moved first:
41
- this.slotMovedNodes = this._getSlotMovedNodes();
45
+ // Relates and patches that aren't moved first but written in the inserts:
46
+ this.earlyNodes = this._getEarlyNodes();
42
47
  this.steps = this._planSteps();
43
48
  }
44
49
 
@@ -91,10 +96,9 @@ export class GraphSchedule {
91
96
  activeNodes.filter((node) => {
92
97
  const action = actions.get(node);
93
98
  return (
94
- (action === 'patch' ||
95
- (action === 'relate' &&
96
- !this.movedNodes.has(node) &&
97
- !this.slotMovedNodes.has(node))) &&
99
+ ['patch', 'relate'].includes(action) &&
100
+ !this.movedNodes.has(node) &&
101
+ !this.earlyNodes.has(node) &&
98
102
  // The row of a `#ref` model is written once, by its model:
99
103
  !this._isWrittenByModel(node)
100
104
  );
@@ -212,7 +216,25 @@ export class GraphSchedule {
212
216
  }
213
217
  }
214
218
  } else if (!breakCycle()) {
215
- throw new Error('the object graph contains cyclic references');
219
+ // A patch that changes a key that the rows it waits for read can be
220
+ // written neither before nor after them:
221
+ const { node: changingNode } =
222
+ nodes
223
+ .filter((node) => !handledNodes.has(node))
224
+ .flatMap(getPendingDependencies)
225
+ .find(
226
+ ({ node, dependent, isKeyChange }) => isKeyChange && isBlockedBy(node, dependent),
227
+ ) || {};
228
+
229
+ if (changingNode) {
230
+ const { modelClass, dataPathKey } = changingNode;
231
+ throw modelClass.createValidationError({
232
+ type: ValidationErrorType.InvalidGraph,
233
+ message: `the graph changes a key of the ${modelClass.name} at ${formatPath(dataPathKey)} that rows read which it also waits for; write the key in a separate query first`,
234
+ });
235
+ } else {
236
+ throw new Error('the object graph contains cyclic references');
237
+ }
216
238
  }
217
239
  }
218
240
 
@@ -245,9 +267,10 @@ export class GraphSchedule {
245
267
 
246
268
  // Return true if the HasMany or HasOne relate of `node` moves its row to an
247
269
  // existing owner before the removals, see the steps above. `#ref` models may
248
- // get their ids from the inserts, rows that wait for the inserts are written
249
- // once they're known, and a relate into the slot of a HasOne row waits for
250
- // the removal of that row, or its move, as its key may be unique.
270
+ // get their ids from the inserts, rows that wait for the inserts or for
271
+ // changed keys are written after them, and a relate into the slot of a HasOne
272
+ // row waits for the removal of that row, or its move, as its key may be
273
+ // unique.
251
274
  _isMovedFirst(node, visitedNodes = new Set()) {
252
275
  const { actions } = this.plan;
253
276
  visitedNodes.add(node);
@@ -257,7 +280,7 @@ export class GraphSchedule {
257
280
  !node.isReference &&
258
281
  node.relation.keySide === 'related' &&
259
282
  node.parent.existing &&
260
- !this._waitsForInserts(node)
283
+ !this._waitsForWrites(node)
261
284
  ) {
262
285
  const slotNode = getSlotNode(node);
263
286
  const moverNode = getSlotMover(this.plan, node);
@@ -271,24 +294,32 @@ export class GraphSchedule {
271
294
  }
272
295
  }
273
296
 
274
- // Find the relates that move the rows out of the slots of HasOneRelations
275
- // that new rows go into, and that aren't moved first. They're related in the
276
- // inserts, along with the relates that move the rows out of their own slots,
277
- // as the foreign key may be unique.
278
- _getSlotMovedNodes() {
297
+ // Find the relates and patches that are written in the inserts, as the rows
298
+ // written there wait for them: The relates that move the rows out of the
299
+ // slots of HasOneRelations that new rows go into, along with the relates that
300
+ // move the rows out of their own slots, as the foreign key may be unique, and
301
+ // the patches and relates that change the keys that these rows or join rows
302
+ // read, as a foreign key may reference them (#428).
303
+ _getEarlyNodes() {
304
+ const { actions } = this.plan;
279
305
  const nodes = new Set();
280
- const addMoverNodes = (node) => {
281
- for (const { node: moverNode, isSlotMove } of this.dependencies.get(node) || []) {
282
- if (isSlotMove && !this.movedNodes.has(moverNode) && !nodes.has(moverNode)) {
283
- nodes.add(moverNode);
284
- addMoverNodes(moverNode);
306
+ const addEarlyNodes = (node) => {
307
+ for (const dependency of this.dependencies.get(node) || []) {
308
+ const { node: earlyNode, isSlotMove, isKeyChange } = dependency;
309
+ if (
310
+ (isSlotMove || isKeyChange) &&
311
+ !this.movedNodes.has(earlyNode) &&
312
+ !nodes.has(earlyNode)
313
+ ) {
314
+ nodes.add(earlyNode);
315
+ addEarlyNodes(earlyNode);
285
316
  }
286
317
  }
287
318
  };
288
319
 
289
320
  for (const node of this.plan.activeNodes) {
290
- if (this.plan.actions.get(node) === 'insert') {
291
- addMoverNodes(node);
321
+ if (actions.get(node) === 'insert' || (node.relation?.keySide === 'join' && !node.current)) {
322
+ addEarlyNodes(node);
292
323
  }
293
324
  }
294
325
 
@@ -317,19 +348,21 @@ export class GraphSchedule {
317
348
  }
318
349
 
319
350
  // Return true if `node` waits for the keys or references of inserted models,
320
- // also through the models that it references (#356), and through the models
321
- // that it waits for to get its keys.
322
- _waitsForInserts(node, visitedNodes = new Set()) {
351
+ // or for keys that patches change (#428), also through the models that it
352
+ // references (#356), and through the models that it waits for to get its
353
+ // keys.
354
+ _waitsForWrites(node, visitedNodes = new Set()) {
323
355
  const { actions } = this.plan;
324
356
  visitedNodes.add(node);
325
357
  return (this.dependencies.get(node) || []).some((dependency) => {
326
- const { node: dependencyNode, relation } = dependency;
358
+ const { node: dependencyNode, relation, isKeyWait, isKeyChange } = dependency;
327
359
  const referencedNode = dependencyNode.referencedNode || dependencyNode;
328
360
  return (
329
361
  actions.get(referencedNode) === 'insert' ||
330
- ((!relation || dependency.isKeyWait) &&
362
+ isKeyChange ||
363
+ ((!relation || isKeyWait) &&
331
364
  !visitedNodes.has(referencedNode) &&
332
- this._waitsForInserts(referencedNode, visitedNodes))
365
+ this._waitsForWrites(referencedNode, visitedNodes))
333
366
  );
334
367
  });
335
368
  }
@@ -458,18 +491,23 @@ function createDependencies(plan, keys) {
458
491
 
459
492
  // A place waits for the inserts and `#ref{}` values that bind the slots of
460
493
  // its keys. A dependency on the same row through the same key becomes such
461
- // a wait, and stays deferrable to break cycles. The other waits aren't:
462
- for (const { node: waitedNode, prop } of keys.waitsFor(node)) {
494
+ // a wait, and stays deferrable to break cycles. The other waits aren't. A
495
+ // key change is waited for at the node that writes it, not at a `#ref`
496
+ // model of its row, which writes nothing:
497
+ for (const { node: waitedNode, prop, isKeyChange } of keys.waitsFor(node)) {
463
498
  const dependency = (dependencies.get(node) || []).find(
464
499
  (dependency) =>
465
- getRow(dependency.node) === getRow(waitedNode) &&
500
+ (isKeyChange
501
+ ? dependency.node === waitedNode
502
+ : getRow(dependency.node) === getRow(waitedNode)) &&
466
503
  (!dependency.relation || getKeyProps(dependency, node).includes(prop)),
467
504
  );
468
505
 
469
506
  if (dependency) {
470
507
  dependency.isKeyWait = true;
508
+ dependency.isKeyChange ||= isKeyChange;
471
509
  } else {
472
- addDependency(node, { node: waitedNode, isKeyWait: true });
510
+ addDependency(node, { node: waitedNode, isKeyWait: true, isKeyChange });
473
511
  }
474
512
  }
475
513
 
@@ -9,6 +9,7 @@ import {
9
9
  checkChangedIds,
10
10
  checkGraph,
11
11
  checkJoinTables,
12
+ checkKeyRelates,
12
13
  checkMissingModels,
13
14
  checkPlacements,
14
15
  checkReadKeys,
@@ -100,6 +101,7 @@ export class GraphUpsert {
100
101
  }
101
102
 
102
103
  checkChangedIds(builder, plan);
104
+ checkKeyRelates(builder, plan);
103
105
  checkReferences(builder, plan);
104
106
  checkJoinTables(builder, plan);
105
107
  await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
@@ -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 { removeFieldExpressions } from '../operations/fieldExpressions.js';
5
6
  import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
6
7
  import { isInternalProp } from '../../utils/internalPropUtils.js';
7
8
  import {
@@ -125,14 +126,15 @@ export class GraphWriter {
125
126
  await promiseMap(batches, (batch) => this._insertBatch(builder, batch), {
126
127
  concurrency: getConcurrency(builder, nodes),
127
128
  });
128
- await this._patch(
129
- builder,
130
- nodes.filter((node) => this.schedule.slotMovedNodes.has(node)),
131
- );
132
129
 
133
130
  for (const node of references) {
134
131
  resolveDependencies(this.schedule, node);
135
132
  }
133
+
134
+ await this._patch(
135
+ builder,
136
+ [...nodes, ...references].filter((node) => this.schedule.earlyNodes.has(node)),
137
+ );
136
138
  }
137
139
 
138
140
  async _insertBatch(builder, nodes) {
@@ -275,6 +277,8 @@ export class GraphWriter {
275
277
  } finally {
276
278
  for (const [node, props] of omittedProps) {
277
279
  restoreOmitFromDatabaseJson(node.obj, props);
280
+ // The keys of `setKeyPaths()` stay where a hook resolves the patch:
281
+ removeFieldExpressions(node.modelClass, node.obj);
278
282
  }
279
283
  }
280
284
  }
@@ -284,6 +288,12 @@ export class GraphWriter {
284
288
  _preparePatch(builder, node) {
285
289
  const { referencedNode } = node;
286
290
 
291
+ // A row that the graph relates by its key alone isn't looked up, its key is
292
+ // only written to its owner:
293
+ if (this.plan.isRelatedByKey(node)) {
294
+ return null;
295
+ }
296
+
287
297
  // The props that the graph fills into the model of a `#ref` model are only
288
298
  // final now:
289
299
  if (referencedNode) {
@@ -304,6 +314,8 @@ export class GraphWriter {
304
314
  ? node.relation.relatedProp.props
305
315
  : []),
306
316
  ];
317
+ // The keys in json paths that the write sets or clears:
318
+ let pathKeys = new Map();
307
319
  // Changing the id never makes sense with `upsertGraph()`. A many-to-many
308
320
  // `#ref` model whose model writes its row only writes its join row:
309
321
  const idProps = node.modelClass.getIdPropertyArray();
@@ -311,11 +323,15 @@ export class GraphWriter {
311
323
  !!referencedNode && node.relation.keySide === 'join' && this.plan.isCoveredByModel(node);
312
324
  const isWritable = (prop) =>
313
325
  !idProps.includes(prop) &&
326
+ !pathKeys.has(prop) &&
314
327
  !this.schedule.keys.isOnlyRead(node, prop) &&
315
328
  (!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop));
316
329
 
317
330
  const getPropsToUpdate = (changedProps) =>
318
- (shouldPatch || shouldUpdate ? changedProps : keyProps).filter(isWritable);
331
+ (shouldPatch || shouldUpdate
332
+ ? changedProps
333
+ : keyProps.flatMap((prop) => pathKeys.get(prop) || prop)
334
+ ).filter(isWritable);
319
335
 
320
336
  // Leave out the props that don't change, and return true if any do.
321
337
  const handleUpdate = ({ changedProps, unchangedProps } = this._findChanges(node)) => {
@@ -335,21 +351,43 @@ export class GraphWriter {
335
351
  // what to update. Otherwise, the hook may still change the model, and we
336
352
  // check again after it runs, but only if the model has props to update, as
337
353
  // relates are no updates (#2233, #2605).
338
- const changes = this._findChanges(node);
339
- const propsToUpdate = getPropsToUpdate(changes.changedProps);
340
354
  const hasBeforeUpdate = hasHook(node.obj, 'beforeUpdate');
341
355
 
342
- if (propsToUpdate.length === 0) {
356
+ let changes = this._findChanges(node);
357
+
358
+ if (getPropsToUpdate(changes.changedProps).length === 0) {
343
359
  return null;
344
- } else if (!hasBeforeUpdate) {
345
- handleUpdate(changes);
346
360
  }
347
361
 
348
362
  node.obj.$validate(null, {
349
363
  dataPath: node.dataPathKey,
350
- patch: shouldPatch || !shouldUpdate,
364
+ patch: !shouldUpdate,
351
365
  });
352
366
 
367
+ // Write the keys in json columns as path updates where their column holds
368
+ // nothing else, which keeps its other values (#415). After the validation,
369
+ // as `update()` validates them as properties of their own:
370
+ pathKeys = setKeyPaths(node, [
371
+ ...this.schedule.keys.getSetCells(node),
372
+ ...this.plan
373
+ .getRemovedOwnerRelations(node)
374
+ .flatMap(({ ownerProp }) =>
375
+ ownerProp.props.map((_, index) => ({ prop: ownerProp, index })),
376
+ ),
377
+ ]);
378
+
379
+ if (pathKeys.size > 0) {
380
+ changes = this._findChanges(node);
381
+ }
382
+
383
+ const propsToUpdate = getPropsToUpdate(changes.changedProps);
384
+
385
+ if (propsToUpdate.length === 0) {
386
+ return null;
387
+ } else if (!hasBeforeUpdate) {
388
+ handleUpdate(changes);
389
+ }
390
+
353
391
  // Rows that their owners relate by their keys can't be found through the
354
392
  // relation until the owners are patched, so they're patched directly.
355
393
  const { relation } = node;
@@ -360,17 +398,26 @@ export class GraphWriter {
360
398
 
361
399
  return {
362
400
  node,
363
- shouldPatch,
401
+ shouldUpdate,
364
402
  hasBeforeUpdate,
365
403
  handleUpdate,
366
404
  propsToUpdate,
405
+ fieldExpressionKeys: [...pathKeys.values()].flat(),
367
406
  isPatchedThroughRelation,
368
407
  };
369
408
  }
370
409
 
371
410
  _writePatch(
372
411
  builder,
373
- { node, shouldPatch, hasBeforeUpdate, handleUpdate, propsToUpdate, isPatchedThroughRelation },
412
+ {
413
+ node,
414
+ shouldUpdate,
415
+ hasBeforeUpdate,
416
+ handleUpdate,
417
+ propsToUpdate,
418
+ fieldExpressionKeys,
419
+ isPatchedThroughRelation,
420
+ },
374
421
  ) {
375
422
  const query = (
376
423
  isPatchedThroughRelation
@@ -392,6 +439,8 @@ export class GraphWriter {
392
439
  // Join rows are patched with the extras among the props to update. The
393
440
  // extras of new join rows are written with them, see `_findChanges()`:
394
441
  joinTableExtras: node.relation?.keySide === 'join' && !node.current ? [] : propsToUpdate,
442
+ // The keys in json columns, also in updates:
443
+ fieldExpressionKeys,
395
444
  })
396
445
  .copyFrom(builder, isReturningAll);
397
446
 
@@ -405,7 +454,8 @@ export class GraphWriter {
405
454
  });
406
455
  }
407
456
 
408
- return (shouldPatch ? query.patch(node.obj) : query.update(node.obj))
457
+ // Writes of only the keys under `noUpdate` are patches too:
458
+ return (shouldUpdate ? query.update(node.obj) : query.patch(node.obj))
409
459
  .runAfter(async (result, query) => {
410
460
  if (isObject(result) && result.$isObjectionModel) {
411
461
  // Handle `returning('*')`.
@@ -554,7 +604,7 @@ function resolveDependencies(schedule, node) {
554
604
  // Copy the keys and `#ref{}` values that `node` waits for into its model,
555
605
  // after those that the graph fills into the models that it reads them from,
556
606
  // 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#_waitsForInserts()`.
607
+ // move first only wait for rows that exist, see `GraphSchedule#_waitsForWrites()`.
558
608
  function resolveFilledDependencies(schedule, node, visitedNodes = new Set()) {
559
609
  visitedNodes.add(node);
560
610
 
@@ -746,6 +796,40 @@ async function fetchGeneratedKeys(builder, modelClass, nodes) {
746
796
  }
747
797
  }
748
798
 
799
+ // Add the changed keys of `cells` at the top level of json columns to `node` as
800
+ // field expressions where their column holds nothing but these keys, and return
801
+ // them by column. PostgreSQL and MySQL can't write nested paths whose parents
802
+ // are missing, so their columns are written whole.
803
+ function setKeyPaths(node, cells) {
804
+ const keysByColumn = new Map();
805
+ const pathCells = cells.filter(({ prop, index }) => prop.isJsonKey(index));
806
+
807
+ for (const [column, columnCells] of groupBy(pathCells, ({ prop, index }) => prop.props[index])) {
808
+ const keys = {};
809
+
810
+ for (const { prop, index } of columnCells) {
811
+ prop.setProp(keys, index, prop.getProp(node.obj, index));
812
+ }
813
+
814
+ if (jsonEquals(node.obj[column], keys[column])) {
815
+ const patch = {};
816
+
817
+ for (const { prop, index } of columnCells) {
818
+ const value = prop.getProp(node.obj, index);
819
+
820
+ if (!node.current || !nonStrictEquals(prop.getProp(node.current.obj, index), value)) {
821
+ prop.patch(patch, index, value);
822
+ }
823
+ }
824
+
825
+ Object.assign(node.obj, patch);
826
+ keysByColumn.set(column, Object.keys(patch));
827
+ }
828
+ }
829
+
830
+ return keysByColumn;
831
+ }
832
+
749
833
  function hasOwnerKeyArray(relation) {
750
834
  return relation.keySide === 'owner' && !relation.isOneToOne();
751
835
  }
@@ -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
- const getPath = (node, ...keys) => [node.dataPathKey, ...keys].filter(Boolean).join('.');
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.
@@ -18,7 +18,9 @@ export class GraphKeys {
18
18
  this.activeNodes = new Set(plan.activeNodes);
19
19
  this.rows = new Map();
20
20
  this.nodesByRow = new Map();
21
- this.cells = new Map();
21
+ // The cells in their order, and those of each row by their props:
22
+ this.cells = [];
23
+ this.cellsByRow = new Map();
22
24
  // The props that each node only reads, and that other nodes write:
23
25
  this.readProps = new Map();
24
26
  // The cells that each place sets or reads:
@@ -153,13 +155,26 @@ 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);
175
+ const cells = this.cellsByRow.get(row);
161
176
  return relation.ownerProp.props.some((_, i) =>
162
- [...(this.cells.get(this._getKey(row, relation.ownerProp, i))?.placeNodes || [])].some(
177
+ [...(cells?.get(relation.ownerProp.propDescription(i))?.placeNodes || [])].some(
163
178
  (other) => !(other.relation === relation && this._getRow(other.parent) === row),
164
179
  ),
165
180
  );
@@ -198,13 +213,23 @@ export class GraphKeys {
198
213
 
199
214
  // Return true if a place of `node` sets the key column `prop` of its row:
200
215
  isSet(node, prop, index) {
201
- return !!this.cells.get(this._getKey(this._getRow(node), prop, index))?.setNodes.has(node);
216
+ const cell = this.cellsByRow.get(this._getRow(node))?.get(prop.propDescription(index));
217
+ return !!cell?.setNodes.has(node);
202
218
  }
203
219
 
204
- // Return the nodes whose inserts or `#ref{}` values bind the slots of the
205
- // cells that the place of `node` sets or reads.
220
+ // Return the nodes whose inserts, `#ref{}` values or changes bind the slots
221
+ // of the cells that the place of `node` sets or reads, and whether they
222
+ // change them.
206
223
  waitsFor(node) {
224
+ const { relation, parent } = node;
207
225
  const waits = [];
226
+ const addWaits = (cell, nodes, isKeyChange) => {
227
+ for (const other of nodes) {
228
+ if (this.rows.get(other) !== this.rows.get(node)) {
229
+ waits.push({ node: other, prop: cell.prop.props[cell.index], isKeyChange });
230
+ }
231
+ }
232
+ };
208
233
 
209
234
  for (const cell of this.placeCells.get(node) || []) {
210
235
  for (const anchorCell of this._getAnchorCells(cell.slot)) {
@@ -216,11 +241,21 @@ export class GraphKeys {
216
241
  (this.insertRows.has(row) && this.plan.actions.get(other) === 'insert') ||
217
242
  this._hasReference(other, prop, index),
218
243
  );
244
+ addWaits(cell, anchorCell.waitedNodes, false);
219
245
 
220
- for (const other of anchorCell.waitedNodes) {
221
- if (this.rows.get(other) !== this.rows.get(node)) {
222
- waits.push({ node: other, prop: cell.prop.props[cell.index] });
223
- }
246
+ // Only the places that set a cell write the changes of its slot:
247
+ if (cell.setNodes.has(node)) {
248
+ addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
249
+ }
250
+ }
251
+ }
252
+
253
+ // Join rows are inserted after all rows, but read the keys of their owners,
254
+ // which patches may change:
255
+ if (relation?.keySide === 'join') {
256
+ for (const cell of this._getCells(parent, relation.ownerProp)) {
257
+ for (const anchorCell of this._getAnchorCells(cell.slot)) {
258
+ addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
224
259
  }
225
260
  }
226
261
  }
@@ -228,6 +263,21 @@ export class GraphKeys {
228
263
  return waits;
229
264
  }
230
265
 
266
+ // Return the patch or relate that changes the value of an anchor cell, or
267
+ // may change it, when its row isn't loaded. A foreign key may reference the
268
+ // cell, so the rows that read the new value wait for it (#428). Ids don't
269
+ // change, see `checkChangedIds()`, and rows without ids are found by the
270
+ // values that the graph gives them.
271
+ _getChangingNode(cell) {
272
+ const { writtenValue, writingNode } = this._getGivenValues(cell);
273
+ return writingNode &&
274
+ !cell.isId &&
275
+ (this.insertRows.has(cell.row) || (writingNode.referencedNode || writingNode).obj.$hasId()) &&
276
+ !nonStrictEquals(writtenValue, this._getStoredValue(cell))
277
+ ? writingNode
278
+ : null;
279
+ }
280
+
231
281
  // The place of `node` gives `target` the key of `source`, for each column of
232
282
  // the relation:
233
283
  _addPlace(node, target, targetProp, source, sourceProp) {
@@ -244,8 +294,11 @@ export class GraphKeys {
244
294
  sourceCells[i].readerNodes.add(source);
245
295
 
246
296
  // A whole `#ref{}` value wins over the key of its place, also where the
247
- // place lists the row again:
248
- if (!this.wholeReferences.has(this._getPlaceKey(target, cell.prop.props[cell.index]))) {
297
+ // place lists the row again. Most graphs have none, so skip the keys:
298
+ if (
299
+ this.wholeReferences.size === 0 ||
300
+ !this.wholeReferences.has(this._getPlaceKey(target, cell.prop.props[cell.index]))
301
+ ) {
249
302
  cell.setNodes.add(target);
250
303
  cell.placeNodes.add(node);
251
304
  joinCells(cell, sourceCells[i]);
@@ -313,12 +366,19 @@ export class GraphKeys {
313
366
 
314
367
  _getCell(node, prop, index) {
315
368
  const row = this._getRow(node);
316
- const key = this._getKey(row, prop, index);
317
- let cell = this.cells.get(key);
369
+ const description = prop.propDescription(index);
370
+ let cells = this.cellsByRow.get(row);
371
+
372
+ if (!cells) {
373
+ cells = new Map();
374
+ this.cellsByRow.set(row, cells);
375
+ }
376
+
377
+ let cell = cells.get(description);
318
378
 
319
379
  if (!cell) {
320
380
  cell = {
321
- key,
381
+ key: this._getKey(row, prop, index),
322
382
  row,
323
383
  node,
324
384
  prop,
@@ -330,7 +390,8 @@ export class GraphKeys {
330
390
  slot: null,
331
391
  };
332
392
  cell.slot = { cells: [cell] };
333
- this.cells.set(key, cell);
393
+ this.cells.push(cell);
394
+ cells.set(description, cell);
334
395
  }
335
396
 
336
397
  return cell;
@@ -376,8 +437,13 @@ export class GraphKeys {
376
437
  givenValues.value = value;
377
438
  }
378
439
 
379
- if (givenValues.writtenValue === undefined && action !== 'insert') {
440
+ if (
441
+ givenValues.writtenValue === undefined &&
442
+ action !== 'insert' &&
443
+ value !== undefined
444
+ ) {
380
445
  givenValues.writtenValue = value;
446
+ givenValues.writingNode = node;
381
447
  }
382
448
 
383
449
  if (givenValues.writtenValue !== undefined) {
@@ -524,7 +590,7 @@ export class GraphKeys {
524
590
  // Throw for slots whose anchors, or whose rows without any, give different
525
591
  // values that are known before the writes.
526
592
  _checkSlots() {
527
- const slots = new Set([...this.cells.values()].map((cell) => cell.slot));
593
+ const slots = new Set(this.cells.map((cell) => cell.slot));
528
594
 
529
595
  for (const slot of [...slots].filter(({ cells }) => cells.length > 1)) {
530
596
  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) {
@@ -70,8 +70,11 @@ export class UpdateOperation extends QueryBuilderOperation {
70
70
  if (this.modelOptions.patch) {
71
71
  return knexBuilder.update(convertFieldExpressionsToRaw(builder, json));
72
72
  } else {
73
- checkFieldExpressions(builder.modelClass(), this.model, 'update');
74
- return knexBuilder.update(json);
73
+ const { fieldExpressionKeys } = builder.internalOptions();
74
+ checkFieldExpressions(builder.modelClass(), this.model, 'update', fieldExpressionKeys);
75
+ return knexBuilder.update(
76
+ fieldExpressionKeys.length > 0 ? convertFieldExpressionsToRaw(builder, json) : json,
77
+ );
75
78
  }
76
79
  }
77
80
 
@@ -7,13 +7,14 @@ import { ValidationErrorType } from '../../model/ValidationError.js';
7
7
  import { getQueryPropKeys } from '../../model/modelQueryProps.js';
8
8
 
9
9
  // Inserts and updates write whole values, so they can't write into json
10
- // columns with field expressions like `'meta:color'`. Keys with a ':' that don't
11
- // parse as field expressions are columns, as in the query properties.
12
- export function checkFieldExpressions(modelClass, model, operation) {
10
+ // columns with field expressions like `'meta:color'`, except for the
11
+ // `allowedKeys` of internal writes. Keys with a ':' that don't parse as field
12
+ // expressions are columns, as in the query properties.
13
+ export function checkFieldExpressions(modelClass, model, operation, allowedKeys = []) {
13
14
  const keys = getFieldExpressionKeys(modelClass, [
14
15
  ...Object.keys(model),
15
16
  ...getQueryPropKeys(model),
16
- ]);
17
+ ]).filter((key) => !allowedKeys.includes(key));
17
18
 
18
19
  if (keys.length > 0) {
19
20
  throw modelClass.createValidationError({
@@ -48,7 +49,7 @@ export function convertFieldExpressionsToRaw(builder, json) {
48
49
 
49
50
  if (getFieldExpressionColumn(key, declaredColumns) !== null) {
50
51
  // 'col:attr' : ref('other:lol') is transformed to
51
- // "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
52
+ // "col" : raw(`jsonb_set(COALESCE("table"."col", '{}'), '{attr}', to_jsonb("other"#>'{lol}'), true)`)
52
53
  // on Postgres, with the column qualified, as it is ambiguous with
53
54
  // `excluded` in merge().
54
55
  dialect.checkSupport();
@@ -82,9 +83,11 @@ export function convertFieldExpressionsToRaw(builder, json) {
82
83
  val = JSON.stringify(val);
83
84
  }
84
85
 
86
+ const { access } = parsed.parsedExpr;
87
+ const previous = convertedJson[parsed.column];
85
88
  convertedJson[parsed.column] = knex.raw(
86
- dialect.set('??', parsed.parsedExpr.access, valuePlaceholder),
87
- [convertedJson[parsed.column] || `${builder.tableRef()}.${parsed.column}`, val],
89
+ dialect.set(previous ? '??' : dialect.setBase('??', access), access, valuePlaceholder),
90
+ [previous || `${builder.tableRef()}.${parsed.column}`, val],
88
91
  );
89
92
  } else {
90
93
  convertedJson[key] = val;
@@ -126,6 +126,12 @@ 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 key at the top level of a json
130
+ // column.
131
+ isJsonKey(index) {
132
+ return this._refs[index].parsedExpr.access.length === 1;
133
+ }
134
+
129
135
  // Returns an instance of ReferenceBuilder that points to the index:th
130
136
  // value of a row.
131
137
  ref(builder, index) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objection",
3
- "version": "4.0.0-rc.3",
3
+ "version": "4.0.0-rc.4",
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",
@@ -91,6 +91,7 @@
91
91
  "sqlite3": "^6.0.1",
92
92
  "tedious": "^20.3.7",
93
93
  "typescript": "^7.0.0",
94
+ "typescript-5": "npm:typescript@5.8.3",
94
95
  "vitepress": "1.6.4",
95
96
  "vitest": "^4.1.11"
96
97
  }
@@ -475,7 +475,7 @@ export type ItemType<T> = T extends Array<unknown> ? T[number] : T;
475
475
  * Type for keys of non-function properties of T.
476
476
  */
477
477
  export type NonFunctionPropertyNames<T> = {
478
- [K in keyof T]: T[K] extends Function ? never : K;
478
+ [K in keyof T]-?: T[K] extends Function ? never : K;
479
479
  }[keyof T];
480
480
 
481
481
  // The type-only members of models, which aren't data, also the keys of the
@@ -540,7 +540,7 @@ export type PartialModelObject<T extends Model> = {
540
540
  ? I[] | PartialModelObject<I>[]
541
541
  : Expression<T[K]>
542
542
  : NonNullable<T[K]> extends Model
543
- ? Expression<T[K]> | PartialModelObject<NonNullable<T[K]>> // nullable to-one relations
543
+ ? Expression<T[K]> | PartialModelObject<NonNullable<T[K]>> // Nullable to-one relations
544
544
  : Expression<T[K]>;
545
545
  };
546
546
 
@@ -591,7 +591,7 @@ export type GraphParameters = {
591
591
  export type PartialModelGraph<M, T = M & GraphParameters> = T extends any
592
592
  ? {
593
593
  [K in DataPropertyNames<T>]?: null extends T[K]
594
- ? PartialModelGraphField<NonNullable<T[K]>> | null // handle nullable BelongsToOneRelations
594
+ ? PartialModelGraphField<NonNullable<T[K]>> | null // Strip null to recognize to-one relations
595
595
  : PartialModelGraphField<T[K]>;
596
596
  }
597
597
  : never;
@@ -635,9 +635,10 @@ export type OptionalInsertPropertyNames<M> = {
635
635
  export type InsertablePropertyNames<M> = Exclude<DataPropertyNames<M>, RelationPropertyNames<M>>;
636
636
 
637
637
  /**
638
- * The data to insert a model M with `insert()`. Unlike `PartialModelObject`,
639
- * all properties are required except optional, nullable and `Generated`
640
- * ones. Relation properties are left out.
638
+ * The complete data of a new row of model M, to check what is passed to
639
+ * `insert()`, which itself takes any subset of the properties. Unlike
640
+ * `PartialModelObject`, all properties are required except optional, nullable
641
+ * and `Generated` ones. Relation properties are left out.
641
642
  */
642
643
  export type Insertable<M extends Model> = {
643
644
  [K in Exclude<InsertablePropertyNames<M>, OptionalInsertPropertyNames<M>>]: Expression<M[K]>;
@@ -646,9 +647,9 @@ export type Insertable<M extends Model> = {
646
647
  };
647
648
 
648
649
  /**
649
- * Like `Insertable`, but for `insertGraph()`: relation properties are
650
- * optional and hold the insertable graphs of the related models, or
651
- * references to existing ones through `#dbRef` or `#ref`.
650
+ * Like `Insertable`, but for graphs passed to `insertGraph()`: relation
651
+ * properties are optional and hold the insertable graphs of the related
652
+ * models, or references to existing ones through `#dbRef` or `#ref`.
652
653
  */
653
654
  export type InsertableGraph<M extends Model> = Insertable<M> &
654
655
  Omit<GraphParameters, '#dbRef' | '#ref'> & {
@@ -667,38 +668,15 @@ export type InsertableGraphField<F> =
667
668
  /**
668
669
  * Extracts the property names (excluding relations) of a model class.
669
670
  */
670
- export type ModelProps<T extends Model> = Exclude<
671
- {
672
- [K in keyof T]?: Defined<T[K]> extends Model
673
- ? never
674
- : Defined<T[K]> extends Array<infer I>
675
- ? I extends Model
676
- ? never
677
- : K
678
- : T[K] extends Function
679
- ? never
680
- : K;
681
- }[keyof T],
682
- undefined | ModelTypeMembers
683
- >;
671
+ export type ModelProps<T extends Model> = InsertablePropertyNames<T>;
684
672
 
685
673
  /**
686
- * Extracts the relation names of the a model class.
674
+ * Extracts the relation names of a model class.
687
675
  */
688
- export type ModelRelations<T extends Model> = Defined<
689
- {
690
- [K in keyof T]?: Defined<T[K]> extends Model
691
- ? K
692
- : Defined<T[K]> extends Array<infer I>
693
- ? I extends Model
694
- ? K
695
- : never
696
- : never;
697
- }[keyof T]
698
- >;
676
+ export type ModelRelations<T extends Model> = RelationPropertyNames<T>;
699
677
 
700
678
  /**
701
- * Given a model property type, returns a query builer type of
679
+ * Given a model property type, returns a query builder type of the
702
680
  * correct kind if the property is a model or a model array. Queries for
703
681
  * to-one relations resolve to `undefined` when there's no related row.
704
682
  */
@@ -760,6 +738,18 @@ type Rebind<
760
738
  */
761
739
  export type ExtraType<T extends { ExtraType: any }> = T['ExtraType'];
762
740
 
741
+ /**
742
+ * The extra type of queries in strict mode: the model class C that started the
743
+ * query, e.g. with `Person.query()`, and the modifiers P registered with
744
+ * `modifiers()`, to check the names of modifiers, and the id I of the class,
745
+ * for `findById()` and the other `*ById()` methods.
746
+ */
747
+ export interface QueryExtra<C = unknown, P = unknown, I = MaybeCompositeId> {
748
+ modelClass: C;
749
+ registered: P;
750
+ id: I;
751
+ }
752
+
763
753
  type WithExtra<QB extends AnyQueryBuilder, X> = Rebind<QB, ModelType<QB>, ResultType<QB>, X>;
764
754
 
765
755
  // The narrowed query builders of `withGraph*()` start without the extra type
@@ -792,15 +782,16 @@ export type NumberQueryBuilder<QB extends AnyQueryBuilder> = QB['~number'];
792
782
  */
793
783
  export type PageQueryBuilder<QB extends AnyQueryBuilder> = QB['~page'];
794
784
 
795
- // Strict mode checks the column names and values of queries against the
796
- // models. Augment `TypeConfig` to opt in:
797
- //
798
- // declare module 'objection' {
799
- // interface TypeConfig {
800
- // strict: true;
801
- // }
802
- // }
803
-
785
+ /**
786
+ * Strict mode checks the column names and values of queries against the
787
+ * models. Augment `TypeConfig` to opt in:
788
+ *
789
+ * declare module 'objection' {
790
+ * interface TypeConfig {
791
+ * strict: true;
792
+ * }
793
+ * }
794
+ */
804
795
  export interface TypeConfig {}
805
796
 
806
797
  type IsStrict = TypeConfig extends { strict: true } ? true : false;
@@ -919,10 +910,11 @@ type CheckedColumn<M, C extends string> =
919
910
  IsConcreteModel<M> extends true ? CheckColumn<M, C> : string;
920
911
 
921
912
  /**
922
- * A column argument: checked against model M in strict mode, any column in
923
- * loose mode, where the model's properties come first, for autocompletion.
913
+ * A column argument: checked against model M in strict mode, the model's
914
+ * properties in loose mode, for autocompletion. In loose mode, other columns
915
+ * go through the RefArg overloads.
924
916
  */
925
- type ColumnArg<M, C extends string> = IfStrict<ModelProps<M & Model>, CheckedColumn<M, C>>;
917
+ type ColumnArg<M, C extends string> = IfStrict<InsertablePropertyNames<M>, CheckedColumn<M, C>>;
926
918
 
927
919
  /**
928
920
  * The value of a column of type T in conditions: `null` is always accepted,
@@ -1071,8 +1063,8 @@ type StrictSelectionObject<M, O> = {
1071
1063
  /**
1072
1064
  * The query builder QB with its result rows narrowed to the properties P in
1073
1065
  * strict mode, next to the model's methods and relations, as after `select()`
1074
- * and the aggregates, marked with SelectedBrand. Repeated selections add up. Queries
1075
- * that aren't checked, e.g. in generic code, aren't narrowed.
1066
+ * and the aggregates, marked with SelectedBrand. Repeated selections add up.
1067
+ * Queries that aren't checked, e.g. in generic code, aren't narrowed.
1076
1068
  */
1077
1069
  type NarrowRows<QB extends AnyQueryBuilder, P, All extends boolean = false> = RowsQueryBuilder<
1078
1070
  QB,
@@ -1188,18 +1180,6 @@ export type SelectedBrand<A> = { readonly [selected]: A };
1188
1180
  */
1189
1181
  export type ExtractedBrand<A> = { readonly [extracted]: A };
1190
1182
 
1191
- /**
1192
- * The extra type of queries in strict mode: the model class C that started the
1193
- * query, e.g. with `Person.query()`, and the modifiers P registered with
1194
- * `modifiers()`, to check the names of modifiers, and the id I of the class,
1195
- * for `findById()` and the other `*ById()` methods.
1196
- */
1197
- export interface QueryExtra<C = unknown, P = unknown, I = MaybeCompositeId> {
1198
- modelClass: C;
1199
- registered: P;
1200
- id: I;
1201
- }
1202
-
1203
1183
  // The results whose rows have `jsonExtract()` values, which `withGraph*()`
1204
1184
  // keeps instead of converting the result kind:
1205
1185
  type ExtractedRows =
@@ -1265,9 +1245,9 @@ type SelectedType<M, C extends string> = C extends `${infer Q}.${infer Col}`
1265
1245
  : ColumnType<M, C>;
1266
1246
 
1267
1247
  /**
1268
- * The property of the column or alias C in the result rows: `snakeCaseMappers()`
1269
- * converts them to camel case, with `upperCase: true` after lowering the case
1270
- * of names in upper case.
1248
+ * The property of the column or alias C in the result rows:
1249
+ * `snakeCaseMappers()` converts them to camel case, with `upperCase: true`
1250
+ * after lowering the case of names in upper case.
1271
1251
  */
1272
1252
  type ResultKey<M, C extends string> = M extends { ColumnNameMapping: 'snakeCase' }
1273
1253
  ? CamelCase<C>
@@ -1547,15 +1527,18 @@ export type JsonObjectOrFieldExpression = object | object[] | FieldExpression;
1547
1527
  export type Selection<QB extends AnyQueryBuilder> = ColumnRef | AnyQueryBuilder | CallbackVoid<QB>;
1548
1528
 
1549
1529
  export interface SelectMethod<out QB extends AnyQueryBuilder> {
1550
- // These must come first so that we get autocomplete.
1551
- <QBP extends QB>(...columns: IfStrict<ModelProps<ModelType<QBP>>, never>[]): QB;
1530
+ // The column overload comes first, for autocompletion, and last in strict
1531
+ // mode, so that errors are reported for it:
1532
+ <QBP extends QB, const C extends readonly [unknown, ...unknown[]]>(
1533
+ ...columns: IfStrict<ModelProps<ModelType<QBP>>[], StrictSelections<ModelType<QBP>, C>>
1534
+ ): IfStrict<QB, SelectQueryBuilder<QBP, C>>;
1552
1535
  <QBP extends QB>(columns: IfStrict<ModelProps<ModelType<QBP>>, never>[]): QB;
1553
1536
 
1554
1537
  <QBP extends QB>(...columns: IfStrict<Selection<QBP>, never>[]): QB;
1555
1538
  <QBP extends QB>(columns: IfStrict<Selection<QBP>, never>[]): QB;
1556
1539
 
1557
- // Allows things like `select(1)`, not sure if we should be more specific here?
1558
- <QBP extends QB>(...args: IfStrict<any, never>[]): QB;
1540
+ // Allow anything else in loose mode, like `select(1)`:
1541
+ <QBP extends QB>(...args: IfStrict<{} | null | undefined, never>[]): QB;
1559
1542
 
1560
1543
  <QBP extends QB, const C extends readonly unknown[] = []>(
1561
1544
  ...columns: StrictSelections<ModelType<QBP>, C>
@@ -1584,7 +1567,7 @@ export interface JsonExtraction {
1584
1567
 
1585
1568
  // Extractions without an alias, or with several, add any properties.
1586
1569
  export interface JsonExtract<out QB extends AnyQueryBuilder> {
1587
- // These must come first so that we get autocomplete.
1570
+ // The column overloads come first, for autocompletion:
1588
1571
  <QBP extends QB, const C extends string = never, const A extends string = string>(
1589
1572
  column: ColumnArg<ModelType<QBP>, C>,
1590
1573
  path: string,
@@ -1605,7 +1588,7 @@ export interface JsonExtract<out QB extends AnyQueryBuilder> {
1605
1588
  }
1606
1589
 
1607
1590
  export interface JsonSet<out QB extends AnyQueryBuilder> {
1608
- // These must come first so that we get autocomplete.
1591
+ // The column overloads come first, for autocompletion:
1609
1592
  <QBP extends QB, const C extends string = never>(
1610
1593
  column: ColumnArg<ModelType<QBP>, C>,
1611
1594
  path: string,
@@ -1617,7 +1600,7 @@ export interface JsonSet<out QB extends AnyQueryBuilder> {
1617
1600
  }
1618
1601
 
1619
1602
  export interface JsonInsert<out QB extends AnyQueryBuilder> {
1620
- // These must come first so that we get autocomplete.
1603
+ // The column overloads come first, for autocompletion:
1621
1604
  <QBP extends QB, const C extends string = never>(
1622
1605
  column: ColumnArg<ModelType<QBP>, C>,
1623
1606
  path: string,
@@ -1629,7 +1612,7 @@ export interface JsonInsert<out QB extends AnyQueryBuilder> {
1629
1612
  }
1630
1613
 
1631
1614
  export interface JsonRemove<out QB extends AnyQueryBuilder> {
1632
- // These must come first so that we get autocomplete.
1615
+ // The column overloads come first, for autocompletion:
1633
1616
  <QBP extends QB, const C extends string = never>(
1634
1617
  column: ColumnArg<ModelType<QBP>, C>,
1635
1618
  path: string,
@@ -1640,6 +1623,17 @@ export interface JsonRemove<out QB extends AnyQueryBuilder> {
1640
1623
  }
1641
1624
 
1642
1625
  export interface WhereMethod<out QB extends AnyQueryBuilder> {
1626
+ // The column overloads come first, for autocompletion:
1627
+ <QBP extends QB, const C extends string = never, const O extends Operator = Operator>(
1628
+ col: ColumnArg<ModelType<QBP>, C>,
1629
+ op: O,
1630
+ expr: OperatorValueArg<ModelType<QBP>, C, O>,
1631
+ ): QB;
1632
+ <QBP extends QB, const C extends string = never>(
1633
+ col: ColumnArg<ModelType<QBP>, C>,
1634
+ expr: ValueArg<ModelType<QBP>, C>,
1635
+ ): QB;
1636
+
1643
1637
  (condition: boolean): QB;
1644
1638
  (cb: CallbackVoid<QB>): QB;
1645
1639
  (raw: Raw): QB;
@@ -1648,7 +1642,7 @@ export interface WhereMethod<out QB extends AnyQueryBuilder> {
1648
1642
  (col: RefArg<QB>, op: Operator, expr: Expression<PrimitiveValue>): QB;
1649
1643
  (col: RefArg<QB>, expr: Expression<PrimitiveValue>): QB;
1650
1644
 
1651
- // The column overloads come last, so that errors are reported for them.
1645
+ // The column overloads come last, so that errors are reported for them:
1652
1646
  <QBP extends QB, const C extends string = never, const O extends Operator = Operator>(
1653
1647
  col: ColumnArg<ModelType<QBP>, C>,
1654
1648
  op: O,
@@ -1660,14 +1654,19 @@ export interface WhereMethod<out QB extends AnyQueryBuilder> {
1660
1654
  ): QB;
1661
1655
 
1662
1656
  <QBP extends QB>(obj: IfStrict<PartialModelProps<ModelType<QBP>>, never>): QB;
1663
- // We must allow any keys in the object. The previous type
1664
- // is kind of useless, but maybe one day vscode and other
1665
- // tools can autocomplete using it.
1657
+ // Allow any keys in loose mode. The model's properties above only serve
1658
+ // autocompletion.
1666
1659
  (obj: IfStrict<object, never>): QB;
1667
1660
  <QBP extends QB, const O extends object = {}>(obj: O & StrictWhereObject<ModelType<QBP>, O>): QB;
1668
1661
  }
1669
1662
 
1670
1663
  export interface WhereLikeMethod<out QB extends AnyQueryBuilder> {
1664
+ // The column overload comes first, for autocompletion, and last, so that
1665
+ // errors are reported for it:
1666
+ <QBP extends QB, const C extends string = never>(
1667
+ col: ColumnArg<ModelType<QBP>, C>,
1668
+ expr: Expression<string>,
1669
+ ): QB;
1671
1670
  (col: RefArg<QB>, expr: Expression<string>): QB;
1672
1671
  <QBP extends QB, const C extends string = never>(
1673
1672
  col: ColumnArg<ModelType<QBP>, C>,
@@ -1688,13 +1687,12 @@ export interface WhereExistsMethod<out QB extends AnyQueryBuilder> {
1688
1687
  }
1689
1688
 
1690
1689
  export interface WhereInMethod<out QB extends AnyQueryBuilder> {
1691
- // These must come first so that we get autocomplete.
1692
- <QBP extends QB>(
1693
- col: IfStrict<ModelProps<ModelType<QBP>>, never>,
1694
- expr: Expression<PrimitiveValue>,
1690
+ // The column overload comes first, for autocompletion, and last in strict
1691
+ // mode, so that errors are reported for it:
1692
+ <QBP extends QB, const C extends string = never>(
1693
+ col: ColumnArg<ModelType<QBP>, C>,
1694
+ expr: IfStrict<Expression<PrimitiveValue> | CallbackVoid<QB>, WhereInValues<QB, QBP, C>>,
1695
1695
  ): QB;
1696
- <QBP extends QB>(col: IfStrict<ModelProps<ModelType<QBP>>, never>, cb: CallbackVoid<QB>): QB;
1697
- <QBP extends QB>(col: IfStrict<ModelProps<ModelType<QBP>>, never>, qb: AnyQueryBuilder): QB;
1698
1696
 
1699
1697
  (col: RefArg<QB> | RefArg<QB>[], expr: readonly Expression<PrimitiveValue>[]): QB;
1700
1698
  (col: RefArg<QB> | RefArg<QB>[], cb: CallbackVoid<QB>): QB;
@@ -1702,7 +1700,7 @@ export interface WhereInMethod<out QB extends AnyQueryBuilder> {
1702
1700
 
1703
1701
  <QBP extends QB, const C extends string = never>(
1704
1702
  col: StrictColumn<ModelType<QBP>, C>,
1705
- expr: readonly ValueArg<ModelType<QBP>, C>[] | Raw | AnyQueryBuilder | CallbackVoid<QB>,
1703
+ expr: WhereInValues<QB, QBP, C>,
1706
1704
  ): QB;
1707
1705
  <QBP extends QB, const C extends readonly unknown[] = []>(
1708
1706
  col: StrictColumns<ModelType<QBP>, C>,
@@ -1710,6 +1708,9 @@ export interface WhereInMethod<out QB extends AnyQueryBuilder> {
1710
1708
  ): QB;
1711
1709
  }
1712
1710
 
1711
+ type WhereInValues<QB extends AnyQueryBuilder, QBP extends QB, C extends string> =
1712
+ readonly ValueArg<ModelType<QBP>, C>[] | Raw | AnyQueryBuilder | CallbackVoid<QB>;
1713
+
1713
1714
  export interface WhereBetweenMethod<out QB extends AnyQueryBuilder> {
1714
1715
  (column: RefArg<QB>, range: [Expression<PrimitiveValue>, Expression<PrimitiveValue>]): QB;
1715
1716
  <QBP extends QB, const C extends string = never>(
@@ -1724,7 +1725,7 @@ export interface WhereNullMethod<out QB extends AnyQueryBuilder> {
1724
1725
  }
1725
1726
 
1726
1727
  export interface WhereColumnMethod<out QB extends AnyQueryBuilder> {
1727
- // These must come first so that we get autocomplete.
1728
+ // The column overloads come first, for autocompletion:
1728
1729
  <QBP extends QB, const C extends string = never, const D extends string = never>(
1729
1730
  col1: ColumnArg<ModelType<QBP>, C>,
1730
1731
  op: Operator,
@@ -1747,7 +1748,7 @@ export interface WhereColumnMethod<out QB extends AnyQueryBuilder> {
1747
1748
  }
1748
1749
 
1749
1750
  export interface WhereJsonObject<out QB extends AnyQueryBuilder> {
1750
- // These must come first so that we get autocomplete.
1751
+ // The column overloads come first, for autocompletion:
1751
1752
  <QBP extends QB, const C extends string = never>(
1752
1753
  col: ColumnArg<ModelType<QBP>, C>,
1753
1754
  value: any,
@@ -1757,7 +1758,7 @@ export interface WhereJsonObject<out QB extends AnyQueryBuilder> {
1757
1758
  }
1758
1759
 
1759
1760
  export interface WhereJsonPath<out QB extends AnyQueryBuilder> {
1760
- // These must come first so that we get autocomplete.
1761
+ // The column overloads come first, for autocompletion:
1761
1762
  <QBP extends QB, const C extends string = never>(
1762
1763
  col: ColumnArg<ModelType<QBP>, C>,
1763
1764
  jsonPath: string,
@@ -2114,7 +2115,7 @@ type WriteQueryBuilder<QB extends AnyQueryBuilder, M extends Model, R> = [R] ext
2114
2115
  * The writes after `returning()` on single and maybe-single query builders,
2115
2116
  * which resolve to one row.
2116
2117
  */
2117
- interface ReturningWrites<QB extends AnyQueryBuilder, M extends Model, W> {
2118
+ export interface ReturningWrites<QB extends AnyQueryBuilder, M extends Model, W> {
2118
2119
  update(update: PartialModelRow<M>): Rebind<QB, M, W>;
2119
2120
  update(): Rebind<QB, M, W>;
2120
2121
  patch(update: PartialModelPatch<M>): Rebind<QB, M, W>;
@@ -2239,7 +2240,8 @@ export interface GraphOptions {
2239
2240
  maxBatchSize?: number;
2240
2241
  /**
2241
2242
  * Apply `limit()`, `offset()` and `first()` in graph modifiers to the related
2242
- * items of each parent. Defaults to `true`. Only applies to `withGraphFetched()`.
2243
+ * items of each parent. Defaults to `true`. Only applies to
2244
+ * `withGraphFetched()`.
2243
2245
  */
2244
2246
  limitPerParent?: boolean;
2245
2247
  }
@@ -2706,8 +2708,6 @@ export class QueryBuilder<
2706
2708
  ): Promise<R | FR>;
2707
2709
  }
2708
2710
 
2709
- export type X<T> = Promise<T>;
2710
-
2711
2711
  export interface FetchGraphOptions {
2712
2712
  transaction?: TransactionOrKnex;
2713
2713
  skipFetched?: boolean;
@@ -3055,7 +3055,8 @@ export interface ConstructorFunctionType<T = any> extends PrototypeType<T> {
3055
3055
  new (...args: any[]): T;
3056
3056
  }
3057
3057
 
3058
- // for internal use on generic static this deduction, copied from https://github.com/microsoft/TypeScript/issues/5863#issuecomment-1483978415
3058
+ // Used internally to infer the generic static `this`, copied from:
3059
+ // https://github.com/microsoft/TypeScript/issues/5863#issuecomment-1483978415
3059
3060
  export type ConstructorType<T = unknown, Static extends Record<string, any> = PrototypeType<T>> = (
3060
3061
  ConstructorFunctionType<T> | PrototypeType<T>
3061
3062
  ) & {
@@ -3132,7 +3133,7 @@ export interface ModelClass<M extends Model> extends ModelConstructor<M> {
3132
3133
 
3133
3134
  transaction<T>(callback: (trx: Transaction) => Promise<T>): Promise<T>;
3134
3135
  transaction<T>(
3135
- trxOrKnex: TransactionOrKnex,
3136
+ trxOrKnex: TransactionOrKnex | undefined,
3136
3137
  callback: (trx: Transaction) => Promise<T>,
3137
3138
  ): Promise<T>;
3138
3139