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