joist-core 2.3.0-next.31 → 2.3.0-next.32

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 (42) hide show
  1. package/build/EntityManager.d.ts +31 -0
  2. package/build/EntityManager.js +93 -22
  3. package/build/EntityManager.js.map +1 -1
  4. package/build/InstanceData.d.ts +17 -2
  5. package/build/InstanceData.js +19 -3
  6. package/build/InstanceData.js.map +1 -1
  7. package/build/PluginManager.d.ts +13 -4
  8. package/build/PluginManager.js +4 -0
  9. package/build/PluginManager.js.map +1 -1
  10. package/build/RowData.d.ts +49 -0
  11. package/build/RowData.js +37 -0
  12. package/build/RowData.js.map +1 -0
  13. package/build/batchloaders/loadBatchLoader.js +19 -13
  14. package/build/batchloaders/loadBatchLoader.js.map +1 -1
  15. package/build/batchloaders/oneToManyBatchLoader.js +2 -2
  16. package/build/batchloaders/oneToManyBatchLoader.js.map +1 -1
  17. package/build/batchloaders/oneToOneBatchLoader.js +2 -2
  18. package/build/batchloaders/oneToOneBatchLoader.js.map +1 -1
  19. package/build/batchloaders/recursiveChildrenBatchLoader.js +2 -2
  20. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
  21. package/build/batchloaders/recursiveParentsBatchLoader.js +2 -2
  22. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
  23. package/build/dataloaders/findDataLoader.js +17 -14
  24. package/build/dataloaders/findDataLoader.js.map +1 -1
  25. package/build/drivers/Driver.d.ts +12 -0
  26. package/build/fields.js +3 -2
  27. package/build/fields.js.map +1 -1
  28. package/build/index.d.ts +3 -2
  29. package/build/index.js +5 -2
  30. package/build/index.js.map +1 -1
  31. package/build/plugins/PreloadPlugin.d.ts +10 -5
  32. package/build/preloading/JsonAggregatePreloader.js +10 -6
  33. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  34. package/build/relations/LazyField.js +6 -4
  35. package/build/relations/LazyField.js.map +1 -1
  36. package/build/serde.d.ts +21 -16
  37. package/build/serde.js +27 -26
  38. package/build/serde.js.map +1 -1
  39. package/build/temporalMappers.d.ts +11 -3
  40. package/build/temporalMappers.js +22 -11
  41. package/build/temporalMappers.js.map +1 -1
  42. package/package.json +2 -2
@@ -29,6 +29,7 @@ import { PreloadPlugin } from "./plugins/PreloadPlugin";
29
29
  import { ReactionsManager } from "./ReactionsManager";
30
30
  import { Collection } from "./relations/Collection";
31
31
  import { lazyColumnLoadOperation } from "./relations/LazyField";
32
+ import { RowData } from "./RowData";
32
33
  import { OptsOf, OrderOf } from "./typeMap";
33
34
  import { MaybePromise } from "./utils";
34
35
  /**
@@ -167,8 +168,16 @@ export declare class EntityManager<C = unknown, Entity extends EntityW = EntityW
167
168
  }): Promise<Loaded<T, H>[]>;
168
169
  /** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
169
170
  private executeFind;
171
+ /** Like {@link executeFind}, but returns a {@link RowData}; see {@link executePreparedFindRowData}. */
172
+ private executeFindRowData;
170
173
  /** Executes a query that has already had find hooks and optimizations applied. */
171
174
  private executePreparedFind;
175
+ /**
176
+ * Like {@link executePreparedFind}, but returns a {@link RowData}: lazy wire rows when the
177
+ * driver has `lazyRows` enabled, else classic POJO rows wrapped in a `PojoRowData` — so
178
+ * loaders can hydrate from one shape without caring which the driver provides.
179
+ */
180
+ private executePreparedFindRowData;
172
181
  /**
173
182
  * Runs pre-SQL find hooks and optimizations against a parsed query.
174
183
  *
@@ -547,6 +556,28 @@ export declare class EntityManager<C = unknown, Entity extends EntityW = EntityW
547
556
  hydrate<T extends EntityW>(type: MaybeAbstractEntityConstructor<T>, rows: readonly any[], options?: {
548
557
  overwriteExisting?: boolean;
549
558
  }): T[];
559
+ /**
560
+ * Hydrates a query's `RowData`, running any sidecar reads, and always `finalize`s the result.
561
+ *
562
+ * This is the internal API for Joist's find/load loaders: `sidecars` is where reads of
563
+ * non-entity columns (preload aggregates, `_tags`, found-id checks) belong — they must run
564
+ * before `finalize`, because compaction drops unretained rows — and the `try/finally`
565
+ * guarantees a failed hydration or sidecar read still releases the result's unretained
566
+ * buffers instead of pinning them.
567
+ */
568
+ private hydrateAndFinalize;
569
+ /**
570
+ * Like {@link hydrate}, but reads rows from a {@link RowData}, i.e. a driver-produced
571
+ * lazy query result, instead of an array of POJO rows.
572
+ *
573
+ * This is an internal API used by Joist's own find loaders (usually via
574
+ * {@link hydrateAndFinalize}); rows whose entities are kept (newly created or
575
+ * overwrite-refreshed) are `retain`-ed on the `RowData` so the caller can later `finalize`
576
+ * (trim/compact) the result.
577
+ */
578
+ hydrateFromRowData<T extends EntityW>(type: MaybeAbstractEntityConstructor<T>, rowData: RowData, options?: {
579
+ overwriteExisting?: boolean;
580
+ }): T[];
550
581
  /**
551
582
  * Mark an entity as needing to be flushed regardless of its state.
552
583
  *
@@ -54,6 +54,7 @@ const AsyncProperty_1 = require("./relations/AsyncProperty");
54
54
  const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
55
55
  const LazyField_1 = require("./relations/LazyField");
56
56
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
57
+ const RowData_1 = require("./RowData");
57
58
  const scopes_1 = require("./scopes");
58
59
  const Todo_1 = require("./Todo");
59
60
  const trusted_1 = require("./trusted");
@@ -296,6 +297,11 @@ class EntityManager {
296
297
  const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
297
298
  return this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit);
298
299
  }
300
+ /** Like {@link executeFind}, but returns a {@link RowData}; see {@link executePreparedFindRowData}. */
301
+ async executeFindRowData(meta, operation, parsed, settings) {
302
+ const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
303
+ return this.executePreparedFindRowData(meta, operation, parsed, findSettings, checkLimit);
304
+ }
299
305
  /** Executes a query that has already had find hooks and optimizations applied. */
300
306
  async executePreparedFind(meta, operation, parsed, findSettings, checkLimit) {
301
307
  const { pluginManager } = getEmInternalApi(this);
@@ -305,9 +311,33 @@ class EntityManager {
305
311
  if (shouldCheck && rows.length >= this.entityLimit) {
306
312
  throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
307
313
  }
308
- pluginManager.afterFind(meta, operation, rows);
314
+ if (pluginManager.hasHook("afterFind")) {
315
+ pluginManager.afterFind(meta, operation, new RowData_1.PojoRowData(rows));
316
+ }
309
317
  return rows;
310
318
  }
319
+ /**
320
+ * Like {@link executePreparedFind}, but returns a {@link RowData}: lazy wire rows when the
321
+ * driver has `lazyRows` enabled, else classic POJO rows wrapped in a `PojoRowData` — so
322
+ * loaders can hydrate from one shape without caring which the driver provides.
323
+ */
324
+ async executePreparedFindRowData(meta, operation, parsed, findSettings, checkLimit) {
325
+ const { executeFindRowData } = this.driver;
326
+ if (executeFindRowData === undefined) {
327
+ return new RowData_1.PojoRowData(await this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit));
328
+ }
329
+ const { pluginManager } = getEmInternalApi(this);
330
+ const rowData = await executeFindRowData.call(this.driver, this, parsed, findSettings);
331
+ // Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
332
+ const shouldCheck = checkLimit ?? !("limit" in findSettings && findSettings.limit === undefined);
333
+ if (shouldCheck && rowData.rowCount >= this.entityLimit) {
334
+ throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
335
+ }
336
+ // The hook receives the lazy RowData view directly, so observe-only hooks (i.e. metrics
337
+ // reading `rowCount`) never materialize cells; hooks call `toRows()` if they want POJOs
338
+ pluginManager.afterFind(meta, operation, rowData);
339
+ return rowData;
340
+ }
311
341
  /**
312
342
  * Runs pre-SQL find hooks and optimizations against a parsed query.
313
343
  *
@@ -1415,22 +1445,55 @@ class EntityManager {
1415
1445
  * the WIP entity state.
1416
1446
  */
1417
1447
  hydrate(type, rows, options) {
1448
+ return this.hydrateFromRowData(type, new RowData_1.PojoRowData(rows), options);
1449
+ }
1450
+ /**
1451
+ * Hydrates a query's `RowData`, running any sidecar reads, and always `finalize`s the result.
1452
+ *
1453
+ * This is the internal API for Joist's find/load loaders: `sidecars` is where reads of
1454
+ * non-entity columns (preload aggregates, `_tags`, found-id checks) belong — they must run
1455
+ * before `finalize`, because compaction drops unretained rows — and the `try/finally`
1456
+ * guarantees a failed hydration or sidecar read still releases the result's unretained
1457
+ * buffers instead of pinning them.
1458
+ */
1459
+ hydrateAndFinalize(type, rowData, opts) {
1460
+ try {
1461
+ const entities = this.hydrateFromRowData(type, rowData, opts);
1462
+ opts?.sidecars?.(entities);
1463
+ return entities;
1464
+ }
1465
+ finally {
1466
+ rowData.finalize?.();
1467
+ }
1468
+ }
1469
+ /**
1470
+ * Like {@link hydrate}, but reads rows from a {@link RowData}, i.e. a driver-produced
1471
+ * lazy query result, instead of an array of POJO rows.
1472
+ *
1473
+ * This is an internal API used by Joist's own find loaders (usually via
1474
+ * {@link hydrateAndFinalize}); rows whose entities are kept (newly created or
1475
+ * overwrite-refreshed) are `retain`-ed on the `RowData` so the caller can later `finalize`
1476
+ * (trim/compact) the result.
1477
+ */
1478
+ hydrateFromRowData(type, rowData, options) {
1418
1479
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1419
1480
  const overwriteExisting = options?.overwriteExisting === true;
1420
- let i = 0;
1421
- const entities = new Array(rows.length);
1422
- for (const row of rows) {
1423
- const id = row["id"];
1481
+ const count = rowData.rowCount;
1482
+ const entities = new Array(count);
1483
+ for (let i = 0; i < count; i++) {
1484
+ const id = rowData.get(i, "id");
1424
1485
  const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : (0, index_1.keyToTaggedId)(maybeBaseMeta, id);
1425
1486
  // See if this is already in our UoW
1426
1487
  let entity = this.findExistingInstance(taggedId);
1427
1488
  if (!entity) {
1428
1489
  // Look for __class from the driver telling us which subtype to instantiate
1429
- const meta = findConcreteMeta(maybeBaseMeta, row);
1490
+ const meta = findConcreteMeta(maybeBaseMeta, rowData, i);
1430
1491
  // Pass id as a hint that we're in hydrate mode
1431
1492
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1432
1493
  const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1433
- instanceData.row = row;
1494
+ instanceData.rowData = rowData;
1495
+ instanceData.rowIndex = i;
1496
+ rowData.retain?.(i);
1434
1497
  // Seed the id to share the identity-map key string and skip the id serde on first read
1435
1498
  instanceData.data["id"] = taggedId;
1436
1499
  this.#doRegister(entity, taggedId, meta, true);
@@ -1440,7 +1503,9 @@ class EntityManager {
1440
1503
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1441
1504
  // First swap out the old row with the new row
1442
1505
  const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1443
- instanceData.row = row;
1506
+ instanceData.rowData = rowData;
1507
+ instanceData.rowIndex = i;
1508
+ rowData.retain?.(i);
1444
1509
  // And then only refresh the data keys that have already been serde-d from rows
1445
1510
  // (this keeps us from deserializing data out of rows that we don't need).
1446
1511
  const { data } = instanceData;
@@ -1457,7 +1522,7 @@ class EntityManager {
1457
1522
  if (fieldName === "id")
1458
1523
  continue;
1459
1524
  const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1460
- serde.setOnEntity(data, row);
1525
+ serde.setOnEntityFromRowData(data, rowData, i);
1461
1526
  }
1462
1527
  }
1463
1528
  else {
@@ -1465,7 +1530,7 @@ class EntityManager {
1465
1530
  if (fieldName === "id")
1466
1531
  continue;
1467
1532
  const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1468
- serde.setOnEntity(data, row);
1533
+ serde.setOnEntityFromRowData(data, rowData, i);
1469
1534
  // Make the field look not-dirty
1470
1535
  if (changedFields.includes(fieldName)) {
1471
1536
  instanceData.markFieldClean(fieldName);
@@ -1474,7 +1539,7 @@ class EntityManager {
1474
1539
  }
1475
1540
  }
1476
1541
  }
1477
- entities[i++] = entity;
1542
+ entities[i] = entity;
1478
1543
  }
1479
1544
  return entities;
1480
1545
  }
@@ -2357,22 +2422,28 @@ function getNow() {
2357
2422
  return now;
2358
2423
  }
2359
2424
  /** Given a `row` from the db, resolves the CTI/STI subtype, if applicable. */
2360
- function findConcreteMeta(maybeBaseMeta, row) {
2361
- // Common case of no CTI or STI inheritance
2362
- if (!row.__class && maybeBaseMeta.inheritanceType !== "sti") {
2425
+ function findConcreteMeta(maybeBaseMeta, rowData, rowIndex) {
2426
+ // Common case of no CTI or STI inheritance; only probe __class for CTI metas so that
2427
+ // lazy/columnar stores don't pay a missing-column lookup per row for regular entities
2428
+ if (maybeBaseMeta.inheritanceType === undefined) {
2429
+ return maybeBaseMeta;
2430
+ }
2431
+ const __class = rowData.get(rowIndex, "__class");
2432
+ if (!__class && maybeBaseMeta.inheritanceType !== "sti") {
2363
2433
  return maybeBaseMeta;
2364
2434
  }
2365
- if (row.__class) {
2366
- if (row.__class === "_" && maybeBaseMeta.ctiAbstract) {
2367
- throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2435
+ if (__class) {
2436
+ if (__class === "_" && maybeBaseMeta.ctiAbstract) {
2437
+ const id = rowData.get(rowIndex, "id");
2438
+ throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, id)} must be instantiated via a subtype`);
2368
2439
  }
2369
2440
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2370
- return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2441
+ return maybeBaseMeta.subTypesByType.get(__class) ?? maybeBaseMeta;
2371
2442
  }
2372
2443
  else if (maybeBaseMeta.inheritanceType === "sti") {
2373
2444
  // Look for the STI discriminator value
2374
2445
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2375
- const value = row[baseMeta.stiDiscriminatorColumnName];
2446
+ const value = rowData.get(rowIndex, baseMeta.stiDiscriminatorColumnName);
2376
2447
  return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2377
2448
  }
2378
2449
  else {
@@ -2461,7 +2532,7 @@ const fieldMap = {};
2461
2532
  // Generates what a row from the db would look like for a given entity
2462
2533
  function createRowFromEntityData(e, opts = {}) {
2463
2534
  const { preferOriginalData = true } = opts;
2464
- const { row: oldRow, data, originalData } = e.__data;
2535
+ const { rowData, rowIndex, data, originalData } = e.__data;
2465
2536
  const __class = e.constructor.name;
2466
2537
  const { metadata: meta } = e.__data;
2467
2538
  if (!fieldMap[__class]) {
@@ -2486,8 +2557,8 @@ function createRowFromEntityData(e, opts = {}) {
2486
2557
  // reflect what would come from the db if we queried it right now, so use originalData when present
2487
2558
  column.rowValue(preferOriginalData && field.fieldName in originalData ? originalData : data)
2488
2559
  : // `data` is lazy and isn't set until it's accessed, so if the field isn't present there, then we should
2489
- // be safe to pull the raw data out of `row`
2490
- oldRow[column.columnName];
2560
+ // be safe to pull the raw data out of the as-loaded RowData
2561
+ rowData.get(rowIndex, column.columnName);
2491
2562
  row[column.columnName] = value ?? null;
2492
2563
  }
2493
2564
  return row;