objection 4.0.0-rc.4 → 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.
@@ -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,18 +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.nodesByRow = new Map();
21
- // The cells in their order, and those of each row by their props:
22
+ this.rowsByKey = new Map();
23
+ // The cells in their order:
22
24
  this.cells = [];
23
- this.cellsByRow = new Map();
24
25
  // The props that each node only reads, and that other nodes write:
25
26
  this.readProps = new Map();
26
27
  // The cells that each place sets or reads:
27
28
  this.placeCells = new Map();
28
- // The rows that the graph inserts, and the ones that it inserted already:
29
- this.insertRows = new Set();
30
- this.insertedRows = new Set();
31
29
  // The nodes whose `#ref{}` values are resolved:
32
30
  this.resolvedNodes = new Set();
33
31
 
@@ -36,7 +34,7 @@ export class GraphKeys {
36
34
  const row = this._getRow(node);
37
35
 
38
36
  if (plan.actions.get(node) === 'insert') {
39
- this.insertRows.add(row);
37
+ row.isInsert = true;
40
38
  }
41
39
  }
42
40
 
@@ -85,18 +83,20 @@ export class GraphKeys {
85
83
 
86
84
  // 4. The clears, with the nodes that clear the cells. The places of a node
87
85
  // read them as `null` if another node clears them:
88
- this.clearingNodes = new Map();
89
86
  const addClearingNode = (row, prop, node) =>
90
87
  prop.props.forEach((_, i) => {
91
- const key = this._getKey(row, prop, i);
92
- this.clearingNodes.set(key, (this.clearingNodes.get(key) || new Set()).add(node));
88
+ const description = prop.propDescription(i);
89
+ row.clearingNodes.set(
90
+ description,
91
+ (row.clearingNodes.get(description) || new Set()).add(node),
92
+ );
93
93
  });
94
94
 
95
95
  for (const currentNode of plan.activeCurrentNodes) {
96
96
  const { relation, parent } = currentNode;
97
97
 
98
98
  if (relation?.keySide === 'related' && plan.actions.get(currentNode) === 'unrelate') {
99
- addClearingNode(currentNode.rowKey, relation.relatedProp, parent.node);
99
+ addClearingNode(this._getRowByKey(currentNode.rowKey), relation.relatedProp, parent.node);
100
100
  }
101
101
  }
102
102
 
@@ -113,7 +113,7 @@ export class GraphKeys {
113
113
  // Bind the cells of the rows of the inserted `nodes` to their values.
114
114
  setInserted(nodes) {
115
115
  for (const node of nodes) {
116
- this.insertedRows.add(this.rows.get(node));
116
+ this.rows.get(node).isInserted = true;
117
117
  }
118
118
  }
119
119
 
@@ -172,9 +172,8 @@ export class GraphKeys {
172
172
  // that moves the row.
173
173
  isSetByOtherPlaces(node, relation) {
174
174
  const row = this._getRow(node);
175
- const cells = this.cellsByRow.get(row);
176
175
  return relation.ownerProp.props.some((_, i) =>
177
- [...(cells?.get(relation.ownerProp.propDescription(i))?.placeNodes || [])].some(
176
+ [...(row.cells.get(relation.ownerProp.propDescription(i))?.placeNodes || [])].some(
178
177
  (other) => !(other.relation === relation && this._getRow(other.parent) === row),
179
178
  ),
180
179
  );
@@ -194,9 +193,7 @@ export class GraphKeys {
194
193
  getReadProps(node, prop) {
195
194
  return this._getCells(node, prop).map((cell) => {
196
195
  const values = this._getAnchorCells(cell.slot).map((anchor) =>
197
- this.insertRows.has(anchor.row)
198
- ? null
199
- : (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
196
+ anchor.row.isInsert ? null : (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
200
197
  );
201
198
  const value = this._getSlotValue(cell);
202
199
  return values.length > 0
@@ -213,7 +210,7 @@ export class GraphKeys {
213
210
 
214
211
  // Return true if a place of `node` sets the key column `prop` of its row:
215
212
  isSet(node, prop, index) {
216
- const cell = this.cellsByRow.get(this._getRow(node))?.get(prop.propDescription(index));
213
+ const cell = this._getRow(node).cells.get(prop.propDescription(index));
217
214
  return !!cell?.setNodes.has(node);
218
215
  }
219
216
 
@@ -234,13 +231,11 @@ export class GraphKeys {
234
231
  for (const cell of this.placeCells.get(node) || []) {
235
232
  for (const anchorCell of this._getAnchorCells(cell.slot)) {
236
233
  const { row, prop, index } = anchorCell;
237
- anchorCell.waitedNodes ??= this.nodesByRow
238
- .get(row)
239
- .filter(
240
- (other) =>
241
- (this.insertRows.has(row) && this.plan.actions.get(other) === 'insert') ||
242
- this._hasReference(other, prop, index),
243
- );
234
+ anchorCell.waitedNodes ??= row.nodes.filter(
235
+ (other) =>
236
+ (row.isInsert && this.plan.actions.get(other) === 'insert') ||
237
+ this._hasReference(other, prop, index),
238
+ );
244
239
  addWaits(cell, anchorCell.waitedNodes, false);
245
240
 
246
241
  // Only the places that set a cell write the changes of its slot:
@@ -263,6 +258,29 @@ export class GraphKeys {
263
258
  return waits;
264
259
  }
265
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
+
266
284
  // Return the patch or relate that changes the value of an anchor cell, or
267
285
  // may change it, when its row isn't loaded. A foreign key may reference the
268
286
  // cell, so the rows that read the new value wait for it (#428). Ids don't
@@ -272,7 +290,7 @@ export class GraphKeys {
272
290
  const { writtenValue, writingNode } = this._getGivenValues(cell);
273
291
  return writingNode &&
274
292
  !cell.isId &&
275
- (this.insertRows.has(cell.row) || (writingNode.referencedNode || writingNode).obj.$hasId()) &&
293
+ (cell.row.isInsert || (writingNode.referencedNode || writingNode).obj.$hasId()) &&
276
294
  !nonStrictEquals(writtenValue, this._getStoredValue(cell))
277
295
  ? writingNode
278
296
  : null;
@@ -311,7 +329,7 @@ export class GraphKeys {
311
329
  // parent and the relation.
312
330
  _getPlaceKey(node, prop) {
313
331
  const { parent, relation } = node;
314
- 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}`;
315
333
  }
316
334
 
317
335
  _addCells(node, cells) {
@@ -337,48 +355,49 @@ export class GraphKeys {
337
355
  _getRow(node) {
338
356
  let row = this.rows.get(node);
339
357
 
340
- if (row === undefined) {
358
+ if (!row) {
341
359
  // A `#ref` model has the row of the model it refers to, and a new model
342
360
  // a row of its own:
343
361
  const { referencedNode } = node;
344
362
  row = referencedNode
345
363
  ? this._getRow(referencedNode)
346
- : node.obj.$hasId()
347
- ? node.rowKey
348
- : `new:${this.rows.size}`;
364
+ : this._getRowByKey(node.obj.$hasId() ? node.rowKey : `new:${this.rows.size}`);
365
+ row.nodes.push(node);
349
366
  this.rows.set(node, row);
350
-
351
- const nodes = this.nodesByRow.get(row);
352
-
353
- if (nodes) {
354
- nodes.push(node);
355
- } else {
356
- this.nodesByRow.set(row, [node]);
357
- }
358
367
  }
359
368
 
360
369
  return row;
361
370
  }
362
371
 
363
- _getKey(row, prop, index) {
364
- return `${row}|${prop.propDescription(index)}`;
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;
365
391
  }
366
392
 
367
393
  _getCell(node, prop, index) {
368
394
  const row = this._getRow(node);
369
395
  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);
396
+ let cell = row.cells.get(description);
378
397
 
379
398
  if (!cell) {
380
399
  cell = {
381
- key: this._getKey(row, prop, index),
400
+ description,
382
401
  row,
383
402
  node,
384
403
  prop,
@@ -391,7 +410,7 @@ export class GraphKeys {
391
410
  };
392
411
  cell.slot = { cells: [cell] };
393
412
  this.cells.push(cell);
394
- cells.set(description, cell);
413
+ row.cells.set(description, cell);
395
414
  }
396
415
 
397
416
  return cell;
@@ -415,7 +434,7 @@ export class GraphKeys {
415
434
  const { actions, graphOptions } = this.plan;
416
435
  const givenValues = {};
417
436
 
418
- for (const node of this.nodesByRow.get(row)) {
437
+ for (const node of row.nodes) {
419
438
  const action = actions.get(node);
420
439
 
421
440
  if (
@@ -476,12 +495,12 @@ export class GraphKeys {
476
495
  // current value.
477
496
  _getAnchorValue(cell) {
478
497
  const { row, prop, index } = cell;
479
- const nodes = this.nodesByRow.get(row);
498
+ const { nodes } = row;
480
499
  const [node] = nodes;
481
500
  const getValue = (obj) => (obj ? prop.getProp(obj, index) : undefined);
482
501
  const { writtenValue } = this._getGivenValues(cell);
483
502
 
484
- if (this.insertRows.has(row)) {
503
+ if (row.isInsert) {
485
504
  const insertNode = nodes.find((node) => this.plan.actions.get(node) === 'insert');
486
505
  return writtenValue ?? getValue(insertNode.obj) ?? null;
487
506
  } else if (writtenValue !== undefined) {
@@ -505,7 +524,7 @@ export class GraphKeys {
505
524
  // Return true if an anchor cell waits for an insert or a `#ref{}` value:
506
525
  _isWaiting(cell) {
507
526
  return (
508
- (this.insertRows.has(cell.row) && !this.insertedRows.has(cell.row)) ||
527
+ (cell.row.isInsert && !cell.row.isInserted) ||
509
528
  this._getGivenValues(cell).writtenValue === UNRESOLVED
510
529
  );
511
530
  }
@@ -516,14 +535,14 @@ export class GraphKeys {
516
535
  _getKnownKey(cell) {
517
536
  const { row, prop, index } = cell;
518
537
 
519
- if (this.insertRows.has(row)) {
538
+ if (row.isInsert) {
520
539
  const { value } = this._getGivenValues(cell);
521
540
  return value === UNRESOLVED
522
541
  ? undefined
523
542
  : value !== undefined
524
543
  ? `=${value}`
525
544
  : cell.isId
526
- ? row
545
+ ? row.key
527
546
  : undefined;
528
547
  } else {
529
548
  const value = this._isWaiting(cell) ? undefined : this._getAnchorValue(cell);
@@ -566,7 +585,7 @@ export class GraphKeys {
566
585
  _getStoredValue(cell) {
567
586
  const { row, prop, index } = cell;
568
587
 
569
- for (const node of this.nodesByRow.get(row)) {
588
+ for (const node of row.nodes) {
570
589
  const value =
571
590
  !this.activeNodes.has(node) && prop.getProp(node.obj, index) !== undefined
572
591
  ? prop.getProp(node.obj, index)
@@ -582,7 +601,7 @@ export class GraphKeys {
582
601
 
583
602
  // Return true if another node than the ones that read `cell` clears it:
584
603
  _isCleared(cell) {
585
- const clearingNodes = this.clearingNodes.get(cell.key);
604
+ const clearingNodes = cell.row.clearingNodes.get(cell.description);
586
605
  return (cell.isCleared ??=
587
606
  !!clearingNodes && [...clearingNodes].some((node) => !cell.readerNodes.has(node)));
588
607
  }
@@ -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,17 +66,26 @@ export class UpdateOperation extends QueryBuilderOperation {
65
66
  }
66
67
 
67
68
  onBuildKnex(knexBuilder, builder) {
68
- const json = this.model.$toDatabaseJson(builder);
69
+ if (!this.modelOptions.patch) {
70
+ checkFieldExpressions(builder.modelClass(), this.model, 'update');
71
+ }
69
72
 
70
- if (this.modelOptions.patch) {
71
- return knexBuilder.update(convertFieldExpressionsToRaw(builder, json));
72
- } else {
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
- );
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
+ }
78
86
  }
87
+
88
+ return knexBuilder.update(convertFieldExpressionsToRaw(builder, json));
79
89
  }
80
90
 
81
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';
@@ -7,14 +7,13 @@ 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'`, 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 = []) {
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) {
14
13
  const keys = getFieldExpressionKeys(modelClass, [
15
14
  ...Object.keys(model),
16
15
  ...getQueryPropKeys(model),
17
- ]).filter((key) => !allowedKeys.includes(key));
16
+ ]);
18
17
 
19
18
  if (keys.length > 0) {
20
19
  throw modelClass.createValidationError({
@@ -29,6 +28,15 @@ export function checkFieldExpressions(modelClass, model, operation, allowedKeys
29
28
  }
30
29
  }
31
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
+
32
40
  // Field expressions only describe the writes into json columns, they aren't
33
41
  // properties of the models that the writes return:
34
42
  export function removeFieldExpressions(modelClass, model) {
@@ -126,6 +126,11 @@ 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
+
129
134
  // Returns true if the index:th column is a key at the top level of a json
130
135
  // column.
131
136
  isJsonKey(index) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "objection",
3
- "version": "4.0.0-rc.4",
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",
@@ -29,7 +29,7 @@
29
29
  "docs:preview": "vitepress preview doc"
30
30
  },
31
31
  "publishConfig": {
32
- "tag": "next"
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",