@rebasepro/server-postgres 0.9.1-canary.a57c262 → 0.9.1-canary.ad25bc0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1808,17 +1808,12 @@ function parseIdValues(idValue, primaryKeys) {
1808
1808
  /**
1809
1809
  * The primary keys of a collection, as declared by its properties.
1810
1810
  *
1811
- * This is the only tier both sides can read, because it is the only one written
1812
- * in the config: the postgres driver can also infer keys from the Drizzle
1813
- * schema, which the browser never sees and is never sent the admin compiles
1814
- * the collection files into its own bundle rather than being served them. A key
1815
- * that lives only in the Drizzle schema is therefore invisible here, and the
1816
- * server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
1817
- * to add.
1818
- *
1819
- * Returns an empty array when a collection declares none, which callers must
1820
- * treat as "not addressable" rather than defaulting to `id`: guessing a key
1821
- * that is not the real one produces confidently wrong addresses.
1811
+ * The driver can also infer keys from the Drizzle schema, which the browser
1812
+ * cannot see so the server normalizes what it resolved onto the config it
1813
+ * serves (see `stampPrimaryKeys`), and this reads that back. Returns an empty
1814
+ * array when a collection declares none, which callers must treat as "not
1815
+ * addressable" rather than defaulting to `id`: guessing a key that is not the
1816
+ * real one produces confidently wrong addresses.
1822
1817
  */
1823
1818
  function getDeclaredPrimaryKeys(collection) {
1824
1819
  const properties = collection.properties;
@@ -1843,15 +1838,9 @@ function getDeclaredPrimaryKeys(collection) {
1843
1838
  * The postgres driver tries, in order: properties marked `isId`; the primary
1844
1839
  * keys of the Drizzle schema; and finally a column literally named `id`. Only
1845
1840
  * the first and last are visible in a `CollectionConfig`, which is what both
1846
- * sides share.
1847
- *
1848
- * So the two agree except on a collection that declares no `isId` and whose key
1849
- * is known only to Drizzle. There, the driver reads the real key, and this
1850
- * either resolves nothing (reported to the console by the caller) or — if the
1851
- * table happens to have an unrelated `id` property — resolves `id`, which is
1852
- * the wrong key and cannot be detected from here: the addresses look right and
1853
- * route wrong. Only the config can settle it, so the server names both cases
1854
- * at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
1841
+ * sides share — so a collection whose key is *only* known to Drizzle, and is
1842
+ * not named `id`, resolves to nothing here. That is reported rather than
1843
+ * guessed: inventing a key produces addresses that look right and route wrong.
1855
1844
  */
1856
1845
  function resolvePrimaryKeys(collection) {
1857
1846
  const declared = getDeclaredPrimaryKeys(collection);
@@ -2563,7 +2552,7 @@ var buildPropertyCallbacks = (properties) => {
2563
2552
  * Opt out with `disableDefaultPolicies: true` to take full responsibility for
2564
2553
  * the collection's RLS.
2565
2554
  */
2566
- var SERVER_OR_ADMIN_EXPR$1 = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
2555
+ var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
2567
2556
  /** Write operations that must be admin-gated by default on auth collections. */
2568
2557
  var DEFAULT_GUARDED_OPS = [
2569
2558
  "insert",
@@ -2576,7 +2565,7 @@ function isAuthCollection(collection) {
2576
2565
  return auth === true || typeof auth === "object" && auth?.enabled === true;
2577
2566
  }
2578
2567
  /** The property marked as the row id (falls back to `id`). */
2579
- function getIdPropertyName$1(collection) {
2568
+ function getIdPropertyName(collection) {
2580
2569
  for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
2581
2570
  return "id";
2582
2571
  }
@@ -2596,232 +2585,30 @@ function getEffectiveSecurityRules(collection) {
2596
2585
  injected.push({
2597
2586
  name: `${tableName}_default_admin_read`,
2598
2587
  operations: ["select"],
2599
- condition: SERVER_OR_ADMIN_EXPR$1
2588
+ condition: SERVER_OR_ADMIN_EXPR
2600
2589
  });
2601
2590
  injected.push({
2602
2591
  name: `${tableName}_default_admin_write`,
2603
2592
  operations: [...DEFAULT_GUARDED_OPS],
2604
- condition: SERVER_OR_ADMIN_EXPR$1,
2605
- check: SERVER_OR_ADMIN_EXPR$1
2593
+ condition: SERVER_OR_ADMIN_EXPR,
2594
+ check: SERVER_OR_ADMIN_EXPR
2606
2595
  });
2607
2596
  if (isAuthCollection(collection)) {
2608
2597
  injected.push({
2609
2598
  name: `${tableName}_default_self_read`,
2610
2599
  operations: ["select"],
2611
- condition: policy.compare(policy.field(getIdPropertyName$1(collection)), "eq", policy.authUid())
2600
+ condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
2612
2601
  });
2613
2602
  injected.push({
2614
2603
  name: `${tableName}_require_admin_write`,
2615
2604
  mode: "restrictive",
2616
2605
  operations: [...DEFAULT_GUARDED_OPS],
2617
- condition: SERVER_OR_ADMIN_EXPR$1,
2618
- check: SERVER_OR_ADMIN_EXPR$1
2606
+ condition: SERVER_OR_ADMIN_EXPR,
2607
+ check: SERVER_OR_ADMIN_EXPR
2619
2608
  });
2620
2609
  }
2621
2610
  return [...explicit, ...injected];
2622
2611
  }
2623
- //#endregion
2624
- //#region ../common/src/util/junction-policies.ts
2625
- var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
2626
- /**
2627
- * Walk every collection's resolved relations and aggregate the junction tables
2628
- * they declare. Two collections may declare the same junction from opposite
2629
- * sides (posts→tags and tags→posts through `posts_tags`); both become
2630
- * `declaringSides` of one spec, so derived write grants consider both.
2631
- */
2632
- function resolveJunctionSpecs(collections) {
2633
- const specs = /* @__PURE__ */ new Map();
2634
- for (const collection of collections) {
2635
- const resolved = resolveCollectionRelations(collection);
2636
- for (const relation of Object.values(resolved)) {
2637
- if (!relation.through) continue;
2638
- const targetCollection = typeof relation.target === "function" ? relation.target() : void 0;
2639
- if (!targetCollection) continue;
2640
- const rawName = relation.through.table;
2641
- const table = rawName.includes(".") ? rawName.split(".").pop() : rawName;
2642
- const schema = "public";
2643
- const source = {
2644
- collection,
2645
- junctionColumn: relation.through.sourceColumn,
2646
- relation
2647
- };
2648
- const target = {
2649
- collection: targetCollection,
2650
- junctionColumn: relation.through.targetColumn
2651
- };
2652
- const existing = specs.get(table);
2653
- if (!existing) specs.set(table, {
2654
- table,
2655
- schema,
2656
- endpoints: [source, target],
2657
- declaringSides: [source]
2658
- });
2659
- else if (!existing.declaringSides.some((s) => s.collection === collection)) existing.declaringSides.push(source);
2660
- }
2661
- }
2662
- return specs;
2663
- }
2664
- /**
2665
- * A synthetic CollectionConfig standing in for the junction during policy
2666
- * compilation and naming. Its two FK columns carry explicit `columnName`s so
2667
- * `outerField` operands resolve to the exact columns the CREATE TABLE emitted,
2668
- * whatever their casing.
2669
- */
2670
- function getJunctionCollectionConfig(spec) {
2671
- const properties = {};
2672
- for (const endpoint of spec.endpoints) properties[endpoint.junctionColumn] = {
2673
- type: "string",
2674
- columnName: endpoint.junctionColumn
2675
- };
2676
- return {
2677
- slug: spec.table,
2678
- name: spec.table,
2679
- table: spec.table,
2680
- schema: spec.schema,
2681
- properties
2682
- };
2683
- }
2684
- /** The property marked as the row id (falls back to `id`). */
2685
- function getIdPropertyName(collection) {
2686
- for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
2687
- return "id";
2688
- }
2689
- /** `EXISTS (SELECT 1 FROM endpoint WHERE endpoint.pk = junction.fk [AND extra])`. */
2690
- function existsEndpoint(endpoint, extra) {
2691
- const correlation = policy.compare(policy.field(getIdPropertyName(endpoint.collection)), "eq", policy.outerField(endpoint.junctionColumn));
2692
- return policy.existsIn({
2693
- collection: endpoint.collection.slug,
2694
- where: extra ? policy.and(correlation, extra) : correlation
2695
- });
2696
- }
2697
- /**
2698
- * Whether a parent-rule expression keeps its meaning when moved inside the
2699
- * junction's `EXISTS` subquery — and the re-scoped copy if it does.
2700
- *
2701
- * Returns `null` when the rule cannot be embedded faithfully: `raw` SQL
2702
- * anywhere (its `{column}` placeholders would bind to the junction), or an
2703
- * `outerField` inside a nested `existsIn` (it would bind to the junction while
2704
- * the author meant the parent, and no operand can express "the middle scope").
2705
- * Top-level `outerField`s are rewritten to `field`, which is what they meant.
2706
- */
2707
- function embedParentExpression(expr, depth = 0) {
2708
- switch (expr.kind) {
2709
- case "raw": return null;
2710
- case "and":
2711
- case "or": {
2712
- const parts = [];
2713
- for (const child of expr.operands) {
2714
- const embedded = embedParentExpression(child, depth);
2715
- if (!embedded) return null;
2716
- parts.push(embedded);
2717
- }
2718
- return expr.kind === "and" ? policy.and(...parts) : policy.or(...parts);
2719
- }
2720
- case "not": {
2721
- const embedded = embedParentExpression(expr.operand, depth);
2722
- return embedded ? policy.not(embedded) : null;
2723
- }
2724
- case "existsIn": {
2725
- const where = embedParentExpression(expr.where, depth + 1);
2726
- return where ? policy.existsIn({
2727
- collection: expr.collection,
2728
- where
2729
- }) : null;
2730
- }
2731
- case "compare": {
2732
- const left = embedOperand(expr.left, depth);
2733
- const right = embedOperand(expr.right, depth);
2734
- if (!left || !right) return null;
2735
- return {
2736
- ...expr,
2737
- left,
2738
- right
2739
- };
2740
- }
2741
- default: return expr;
2742
- }
2743
- }
2744
- /** Re-scope an operand, or return `null` if its binding cannot be preserved. */
2745
- function embedOperand(operand, depth) {
2746
- if (operand.kind === "outerField") {
2747
- if (depth === 0) return policy.field(operand.name);
2748
- return null;
2749
- }
2750
- return operand;
2751
- }
2752
- /** Does the rule cover the `update` operation? */
2753
- function coversUpdate(rule) {
2754
- return getPolicyOperations(rule).some((op) => op === "update" || op === "all");
2755
- }
2756
- /**
2757
- * The full derived policy set for a junction table: the locked server/admin
2758
- * baseline, the endpoint-visibility read grant, inherited write grants, and
2759
- * inherited restrictive gates. Returns `[]` when every declaring collection set
2760
- * `disableDefaultPolicies` — the junction is then the author's to police, and
2761
- * stays locked (RLS is still enabled) until they write policies for it.
2762
- */
2763
- function getJunctionSecurityRules(spec) {
2764
- if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
2765
- const rules = [];
2766
- rules.push({
2767
- name: `${spec.table}_default_admin_read`,
2768
- operations: ["select"],
2769
- condition: SERVER_OR_ADMIN_EXPR
2770
- });
2771
- rules.push({
2772
- name: `${spec.table}_default_admin_write`,
2773
- operations: [
2774
- "insert",
2775
- "update",
2776
- "delete"
2777
- ],
2778
- condition: SERVER_OR_ADMIN_EXPR,
2779
- check: SERVER_OR_ADMIN_EXPR
2780
- });
2781
- rules.push({
2782
- name: `${spec.table}_default_edge_read`,
2783
- operations: ["select"],
2784
- condition: policy.and(existsEndpoint(spec.endpoints[0]), existsEndpoint(spec.endpoints[1]))
2785
- });
2786
- const writeGrants = [];
2787
- for (const side of spec.declaringSides) {
2788
- const updateRules = ((isPostgresCollectionConfig(side.collection) ? side.collection.securityRules : void 0) ?? []).filter(coversUpdate);
2789
- const permissive = updateRules.filter((r) => r.mode !== "restrictive");
2790
- const restrictive = updateRules.filter((r) => r.mode === "restrictive");
2791
- const embeddedGates = [];
2792
- let gatesEmbeddable = true;
2793
- for (const gate of restrictive) {
2794
- const using = securityRuleToConditions(gate).usingExpr;
2795
- const embedded = using ? embedParentExpression(using) : null;
2796
- if (!embedded) {
2797
- gatesEmbeddable = false;
2798
- break;
2799
- }
2800
- embeddedGates.push(embedded);
2801
- }
2802
- if (!gatesEmbeddable) continue;
2803
- const grants = [];
2804
- for (const rule of permissive) {
2805
- const using = securityRuleToConditions(rule).usingExpr;
2806
- const embedded = using ? embedParentExpression(using) : null;
2807
- if (embedded) grants.push(embedded);
2808
- }
2809
- if (grants.length === 0) continue;
2810
- const condition = embeddedGates.length > 0 ? policy.and(policy.or(...grants), ...embeddedGates) : policy.or(...grants);
2811
- writeGrants.push(existsEndpoint(side, condition));
2812
- }
2813
- if (writeGrants.length > 0) rules.push({
2814
- name: `${spec.table}_default_edge_write`,
2815
- operations: [
2816
- "insert",
2817
- "update",
2818
- "delete"
2819
- ],
2820
- condition: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants),
2821
- check: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants)
2822
- });
2823
- return rules;
2824
- }
2825
2612
  (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2826
2613
  (function(root, factory) {
2827
2614
  if (typeof define === "function" && define.amd) define(factory);
@@ -4106,7 +3893,7 @@ function createPrimaryKeyResolver(options) {
4106
3893
  }
4107
3894
  if (!warned.has(slug)) {
4108
3895
  warned.add(slug);
4109
- console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
3896
+ console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config.`);
4110
3897
  }
4111
3898
  return keys;
4112
3899
  };
@@ -4549,26 +4336,8 @@ function getTableForCollection(collection, registry) {
4549
4336
  if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
4550
4337
  return table;
4551
4338
  }
4552
- /**
4553
- * The key columns a collection's rows are addressed by.
4554
- *
4555
- * Three tiers, in order: properties marked `isId`, the primary keys of the
4556
- * drizzle schema, and finally a column literally named `id`. Only the first is
4557
- * visible to the browser, which is why a key known only to drizzle is reported
4558
- * at boot — see {@link warnOnKeysTheAdminCannotResolve}.
4559
- *
4560
- * Returns `[]` when nothing resolves, rather than throwing. It used to open by
4561
- * resolving the table, which throws when there is none — so the `isId` tier,
4562
- * which needs no table at all, was unreachable for exactly the collections
4563
- * most likely to have no table registered. Every caller that wanted "no keys"
4564
- * to mean "no keys" had to spell that out in a try/catch.
4565
- *
4566
- * Callers that cannot proceed without a key must say so themselves, naming the
4567
- * collection: an empty array here means "this collection has no address", which
4568
- * is a different answer in a notification (broadcast a wildcard) than in a save
4569
- * (fail).
4570
- */
4571
4339
  function getPrimaryKeys(collection, registry) {
4340
+ const table = getTableForCollection(collection, registry);
4572
4341
  if (collection.properties) {
4573
4342
  const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
4574
4343
  fieldName: key,
@@ -4577,8 +4346,6 @@ function getPrimaryKeys(collection, registry) {
4577
4346
  }));
4578
4347
  if (idProps.length > 0) return idProps;
4579
4348
  }
4580
- const table = registry.getTable(getTableName$1(collection));
4581
- if (!table) return [];
4582
4349
  const keys = [];
4583
4350
  for (const [key, colRaw] of Object.entries(table)) {
4584
4351
  const col = colRaw;
@@ -4606,90 +4373,6 @@ function getPrimaryKeys(collection, registry) {
4606
4373
  }
4607
4374
  return keys;
4608
4375
  }
4609
- /**
4610
- * The key columns, for callers that cannot do their job without one.
4611
- *
4612
- * {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
4613
- * collection with no address. Most of this driver, though, is building a WHERE
4614
- * clause and has no meaning without a key — for those, an empty array is not an
4615
- * answer, and indexing `[0]` into it produces `Cannot read properties of
4616
- * undefined` three frames from where the real problem is. This says what is
4617
- * wrong and which collection it is wrong about.
4618
- */
4619
- function requirePrimaryKeys(collection, registry) {
4620
- const keys = getPrimaryKeys(collection, registry);
4621
- if (keys.length === 0) throw new Error(`Collection '${collection.slug}' has no primary key, so its rows cannot be addressed. Mark the key property with \`isId\` in its config, or register a table whose schema declares one.`);
4622
- return keys;
4623
- }
4624
- /**
4625
- * Collections whose key the *browser* cannot resolve, and what it will do
4626
- * instead.
4627
- *
4628
- * The two sides resolve keys from different evidence. This driver reads, in
4629
- * order: properties marked `isId`, the primary keys of the Drizzle schema, then
4630
- * a column literally named `id`. The admin shares the `CollectionConfig` — it
4631
- * compiles the same collection files into its bundle — but never the Drizzle
4632
- * schema, so the middle tier is invisible to it.
4633
- *
4634
- * Nothing can normalize this at runtime: the server does not serve the admin
4635
- * its collections, so a key resolved here cannot be handed over there. The
4636
- * config files are the only thing both sides read, so the fix is an edit to
4637
- * them, and the most this can do is say exactly which edit.
4638
- *
4639
- * Two shapes, and the second is the dangerous one:
4640
- *
4641
- * - No `isId`, no `id` property → the admin resolves no address, warns in the
4642
- * console, and rows cannot be opened or linked.
4643
- * - No `isId`, but an `id` property that is *not* the key → the admin addresses
4644
- * rows by `id` while this driver reads the address as the real key. Nothing
4645
- * errors: the addresses look right and route wrong.
4646
- */
4647
- function findUnresolvableKeyCollections(collections, registry) {
4648
- const findings = [];
4649
- for (const collection of collections) {
4650
- if (getDeclaredPrimaryKeys(collection).length > 0) continue;
4651
- const keys = getPrimaryKeys(collection, registry);
4652
- if (keys.length === 0) continue;
4653
- if (keys.length === 1 && keys[0].fieldName === "id") continue;
4654
- findings.push({
4655
- collection,
4656
- keys,
4657
- shadowedByIdProperty: Boolean(collection.properties?.id)
4658
- });
4659
- }
4660
- return findings;
4661
- }
4662
- /**
4663
- * Report the collections from {@link findUnresolvableKeyCollections} at boot,
4664
- * with the edit that fixes each one.
4665
- *
4666
- * Grouped by failure, not by collection: the shadowed case is a routing bug and
4667
- * the silent case is a missing feature, and they deserve different urgency.
4668
- */
4669
- function warnOnKeysTheAdminCannotResolve(collections, registry) {
4670
- const findings = findUnresolvableKeyCollections(collections, registry);
4671
- if (findings.length === 0) return;
4672
- const edit = (f) => `${f.collection.slug}: mark ${f.keys.map((k) => `\`${k.fieldName}\``).join(" and ")} with \`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
4673
- const shadowed = findings.filter((f) => f.shadowedByIdProperty);
4674
- const silent = findings.filter((f) => !f.shadowedByIdProperty);
4675
- if (shadowed.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema — but they do have a property called `id`. The admin has no way to know `id` is not the key, so it will address rows by it while this server reads the address as the real key: the links look right and route wrong. Nothing will error.\n\n" + shadowed.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
4676
- if (silent.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema, which the admin never sees. It will resolve no address for their rows, so detail links, caching and relations will not work for them:\n\n" + silent.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
4677
- }
4678
- /**
4679
- * The address of a row: derived from the collection's primary keys, because a
4680
- * row does not carry one — it is exactly its columns.
4681
- *
4682
- * Falls back to a literal `id` column, for a row that reached us from somewhere
4683
- * other than this driver. Returns `""` when there is no key and no `id` —
4684
- * callers decide what that means, since "unaddressable" is a different answer
4685
- * in a notification (broadcast a wildcard) than in a save (fail).
4686
- */
4687
- function deriveRowAddress(row, collection, registry) {
4688
- const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
4689
- if (composite && composite.split(":::").some((part) => part !== "")) return composite;
4690
- if (row.id !== void 0 && row.id !== null) return String(row.id);
4691
- return "";
4692
- }
4693
4376
  //#endregion
4694
4377
  //#region src/utils/drizzle-conditions.ts
4695
4378
  /**
@@ -5311,10 +4994,12 @@ function serializeDataToServer(row, properties, collection, registry) {
5311
4994
  continue;
5312
4995
  } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
5313
4996
  const serializedValue = serializePropertyToServer(effectiveValue, property);
4997
+ const pks = getPrimaryKeys(collection, registry);
5314
4998
  inverseRelationUpdates.push({
5315
4999
  relationKey: key,
5316
5000
  relation,
5317
- newValue: serializedValue
5001
+ newValue: serializedValue,
5002
+ currentId: row.id || buildCompositeId(row, pks)
5318
5003
  });
5319
5004
  continue;
5320
5005
  } else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
@@ -5324,11 +5009,15 @@ function serializeDataToServer(row, properties, collection, registry) {
5324
5009
  relation,
5325
5010
  newTargetId: serializedValue
5326
5011
  });
5327
- else inverseRelationUpdates.push({
5328
- relationKey: key,
5329
- relation,
5330
- newValue: serializedValue
5331
- });
5012
+ else {
5013
+ const pks = getPrimaryKeys(collection, registry);
5014
+ inverseRelationUpdates.push({
5015
+ relationKey: key,
5016
+ relation,
5017
+ newValue: serializedValue,
5018
+ currentId: row.id || buildCompositeId(row, pks)
5019
+ });
5020
+ }
5332
5021
  continue;
5333
5022
  } else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
5334
5023
  const serializedValue = serializePropertyToServer(effectiveValue, property);
@@ -5728,63 +5417,6 @@ var RelationService = class {
5728
5417
  this.registry = registry;
5729
5418
  }
5730
5419
  /**
5731
- * One target row, as the {@link RelatedRow} everything here returns.
5732
- *
5733
- * Eight sites built this by hand, which is how the address came to be the
5734
- * target's first key column in all eight — one edit, eight places to miss.
5735
- *
5736
- * `resolveNested` is the one thing they did not agree on, and the
5737
- * disagreement was invisible: the single-parent fetches pass `db` and
5738
- * `registry` to `parseDataFromServer`, so the target's *own* relations get
5739
- * resolved too, while the batch paths deliberately do not — a query per
5740
- * target row is the N+1 the batching exists to avoid. Naming the parameter
5741
- * makes that a decision rather than a difference between two call sites
5742
- * nobody was comparing.
5743
- */
5744
- async toRelatedRow(targetRow, targetCollection, targetPks, options) {
5745
- const values = options?.resolveNested ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry) : await parseDataFromServer(targetRow, targetCollection);
5746
- return {
5747
- id: buildCompositeId(targetRow, targetPks),
5748
- path: targetCollection.slug,
5749
- values
5750
- };
5751
- }
5752
- /**
5753
- * A WHERE matching any of `parentIds`, by the whole key.
5754
- *
5755
- * A single key is an `IN (…)`. A composite one cannot be: matching
5756
- * `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
5757
- * share their first column each receive the other's relations. It becomes
5758
- * an OR of ANDs — one exact address per parent — which Postgres indexes the
5759
- * same way it would a multi-column key lookup.
5760
- */
5761
- parentKeyCondition(parentTable, parentPks, parentIds) {
5762
- const columnFor = (fieldName) => {
5763
- const col = parentTable[fieldName];
5764
- if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
5765
- return col;
5766
- };
5767
- if (parentPks.length === 1) {
5768
- const values = parentIds.map((id) => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
5769
- return inArray(columnFor(parentPks[0].fieldName), values);
5770
- }
5771
- return or(...parentIds.map((id) => {
5772
- const values = parseIdValues(id, parentPks);
5773
- return and(...parentPks.map((pk) => eq(columnFor(pk.fieldName), values[pk.fieldName])));
5774
- }));
5775
- }
5776
- /**
5777
- * Reject a relation that cannot express a composite-keyed parent.
5778
- *
5779
- * `localKey` and `foreignKeyOnTarget` are single column names: one column
5780
- * cannot reference a two-column key, so such a relation has no correct
5781
- * reading. Left alone it would silently match on the first key column and
5782
- * hand a tenant's rows to its neighbour — say so instead.
5783
- */
5784
- assertSingleKeyAddressable(parentCollection, parentPks, via) {
5785
- if (parentPks.length > 1) throw new Error(`Relation on '${parentCollection.slug}' uses '${via}', a single foreign-key column, but '${parentCollection.slug}' is keyed on ${parentPks.map((k) => `'${k.fieldName}'`).join(" + ")}. One column cannot reference a composite key — express this relation with \`joinPath\`, whose \`on.from\`/\`on.to\` take every key column.`);
5786
- }
5787
- /**
5788
5420
  * Fetch rows related to a parent row through a specific relation
5789
5421
  */
5790
5422
  async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
@@ -5803,9 +5435,9 @@ var RelationService = class {
5803
5435
  async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
5804
5436
  const targetCollection = relation.target();
5805
5437
  const targetTable = getTableForCollection(targetCollection, this.registry);
5806
- const idInfo = requirePrimaryKeys(targetCollection, this.registry);
5438
+ const idInfo = getPrimaryKeys(targetCollection, this.registry);
5807
5439
  const idField = targetTable[idInfo[0].fieldName];
5808
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5440
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5809
5441
  const parentIdInfo = parentPks[0];
5810
5442
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
5811
5443
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
@@ -5829,7 +5461,7 @@ var RelationService = class {
5829
5461
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
5830
5462
  currentTable = joinTable;
5831
5463
  }
5832
- const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
5464
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5833
5465
  query = query.where(eq(parentIdField, parsedParentId));
5834
5466
  if (options.limit) query = query.limit(options.limit);
5835
5467
  const results = await query;
@@ -5837,7 +5469,13 @@ var RelationService = class {
5837
5469
  const rows = [];
5838
5470
  for (const row of results) {
5839
5471
  const targetRow = row[targetTableName] || row;
5840
- rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
5472
+ const id = targetRow[idInfo[0].fieldName];
5473
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
5474
+ rows.push({
5475
+ id: id?.toString() || "",
5476
+ path: targetCollection.slug,
5477
+ values: parsedValues
5478
+ });
5841
5479
  }
5842
5480
  return rows;
5843
5481
  }
@@ -5856,7 +5494,13 @@ var RelationService = class {
5856
5494
  const rows = [];
5857
5495
  for (const row of results) {
5858
5496
  const targetRow = row[getTableName$1(targetCollection)] || row;
5859
- rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
5497
+ const id = targetRow[idInfo[0].fieldName];
5498
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
5499
+ rows.push({
5500
+ id: id?.toString() || "",
5501
+ path: targetCollection.slug,
5502
+ values: parsedValues
5503
+ });
5860
5504
  }
5861
5505
  return rows;
5862
5506
  }
@@ -5873,8 +5517,8 @@ var RelationService = class {
5873
5517
  }
5874
5518
  const targetCollection = relation.target();
5875
5519
  const targetTable = getTableForCollection(targetCollection, this.registry);
5876
- const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
5877
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5520
+ const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
5521
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5878
5522
  const parentIdInfo = parentPks[0];
5879
5523
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
5880
5524
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
@@ -5893,10 +5537,9 @@ var RelationService = class {
5893
5537
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
5894
5538
  const targetCollection = relation.target();
5895
5539
  const targetTable = getTableForCollection(targetCollection, this.registry);
5896
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5897
- const targetIdInfo = targetPks[0];
5540
+ const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
5898
5541
  const targetIdField = targetTable[targetIdInfo.fieldName];
5899
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5542
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5900
5543
  const parentIdInfo = parentPks[0];
5901
5544
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
5902
5545
  if (!parentTable) throw new Error("Parent table not found");
@@ -5920,19 +5563,25 @@ var RelationService = class {
5920
5563
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
5921
5564
  currentTable = joinTable;
5922
5565
  }
5923
- query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
5566
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5567
+ query = query.where(inArray(parentIdField, parsedParentIds));
5924
5568
  const results = await query;
5925
5569
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
5926
5570
  const resultMap = /* @__PURE__ */ new Map();
5927
5571
  for (const row of results) {
5928
5572
  const parentRow = row[getTableName$1(parentCollection)] || row;
5929
5573
  const targetRow = row[targetTableName] || row;
5930
- resultMap.set(buildCompositeId(parentRow, parentPks), await this.toRelatedRow(targetRow, targetCollection, targetPks));
5574
+ const parentId = parentRow[parentIdInfo.fieldName];
5575
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5576
+ resultMap.set(String(parentId), {
5577
+ id: String(targetRow[targetIdInfo.fieldName]),
5578
+ path: targetCollection.slug,
5579
+ values: parsedValues
5580
+ });
5931
5581
  }
5932
5582
  return resultMap;
5933
5583
  }
5934
5584
  if (relation.direction === "owning" && relation.localKey) {
5935
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
5936
5585
  const localKeyCol = parentTable[relation.localKey];
5937
5586
  if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
5938
5587
  const fkRows = await this.db.select({
@@ -5961,11 +5610,17 @@ var RelationService = class {
5961
5610
  const resultMap = /* @__PURE__ */ new Map();
5962
5611
  for (const [parentIdStr, fkValue] of parentToFk) {
5963
5612
  const targetRow = targetById.get(String(fkValue));
5964
- if (targetRow) resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
5613
+ if (targetRow) {
5614
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5615
+ resultMap.set(parentIdStr, {
5616
+ id: String(targetRow[targetIdInfo.fieldName]),
5617
+ path: targetCollection.slug,
5618
+ values: parsedValues
5619
+ });
5620
+ }
5965
5621
  }
5966
5622
  return resultMap;
5967
5623
  }
5968
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
5969
5624
  let query = this.db.select().from(targetTable).$dynamic();
5970
5625
  query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
5971
5626
  const results = await query;
@@ -5976,7 +5631,14 @@ var RelationService = class {
5976
5631
  let parentId;
5977
5632
  if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
5978
5633
  else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
5979
- if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
5634
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
5635
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5636
+ resultMap.set(String(parentId), {
5637
+ id: String(targetRow[targetIdInfo.fieldName]),
5638
+ path: targetCollection.slug,
5639
+ values: parsedValues
5640
+ });
5641
+ }
5980
5642
  }
5981
5643
  return resultMap;
5982
5644
  }
@@ -5990,9 +5652,9 @@ var RelationService = class {
5990
5652
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
5991
5653
  const targetCollection = relation.target();
5992
5654
  const targetTable = getTableForCollection(targetCollection, this.registry);
5993
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5994
- const targetIdField = targetTable[targetPks[0].fieldName];
5995
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5655
+ const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
5656
+ const targetIdField = targetTable[targetIdInfo.fieldName];
5657
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5996
5658
  const parentIdInfo = parentPks[0];
5997
5659
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
5998
5660
  if (!parentTable) throw new Error("Parent table not found");
@@ -6014,22 +5676,27 @@ var RelationService = class {
6014
5676
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
6015
5677
  currentTable = joinTable;
6016
5678
  }
6017
- query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
5679
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5680
+ query = query.where(inArray(parentIdField, parsedParentIds));
6018
5681
  const results = await query;
6019
5682
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
6020
5683
  const resultMap = /* @__PURE__ */ new Map();
6021
5684
  for (const row of results) {
6022
5685
  const parentRow = row[getTableName$1(parentCollection)] || row;
6023
5686
  const targetRow = row[targetTableName] || row;
6024
- const parentId = buildCompositeId(parentRow, parentPks);
5687
+ const parentId = String(parentRow[parentIdInfo.fieldName]);
5688
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
6025
5689
  const arr = resultMap.get(parentId) || [];
6026
- arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
5690
+ arr.push({
5691
+ id: String(targetRow[targetIdInfo.fieldName]),
5692
+ path: targetCollection.slug,
5693
+ values: parsedValues
5694
+ });
6027
5695
  resultMap.set(parentId, arr);
6028
5696
  }
6029
5697
  return resultMap;
6030
5698
  }
6031
5699
  if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
6032
- this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
6033
5700
  const junctionTable = this.registry.getTable(relation.through.table);
6034
5701
  if (!junctionTable) {
6035
5702
  logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
@@ -6048,13 +5715,17 @@ var RelationService = class {
6048
5715
  const junctionData = row[relation.through.table] || row;
6049
5716
  const targetData = row[targetTableName] || row;
6050
5717
  const parentId = String(junctionData[relation.through.sourceColumn]);
5718
+ const parsedValues = await parseDataFromServer(targetData, targetCollection);
6051
5719
  const arr = resultMap.get(parentId) || [];
6052
- arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
5720
+ arr.push({
5721
+ id: String(targetData[targetIdInfo.fieldName]),
5722
+ path: targetCollection.slug,
5723
+ values: parsedValues
5724
+ });
6053
5725
  resultMap.set(parentId, arr);
6054
5726
  }
6055
5727
  return resultMap;
6056
5728
  }
6057
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
6058
5729
  let query = this.db.select().from(targetTable).$dynamic();
6059
5730
  query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
6060
5731
  const results = await query;
@@ -6067,9 +5738,14 @@ var RelationService = class {
6067
5738
  else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
6068
5739
  else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
6069
5740
  if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
5741
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
6070
5742
  const key = String(parentId);
6071
5743
  const arr = resultMap.get(key) || [];
6072
- arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
5744
+ arr.push({
5745
+ id: String(targetRow[targetIdInfo.fieldName]),
5746
+ path: targetCollection.slug,
5747
+ values: parsedValues
5748
+ });
6073
5749
  resultMap.set(key, arr);
6074
5750
  }
6075
5751
  }
@@ -6117,12 +5793,12 @@ var RelationService = class {
6117
5793
  logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
6118
5794
  continue;
6119
5795
  }
6120
- const parentPks = requirePrimaryKeys(collection, this.registry);
5796
+ const parentPks = getPrimaryKeys(collection, this.registry);
6121
5797
  const parentIdInfo = parentPks[0];
6122
5798
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6123
5799
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
6124
5800
  if (targetEntityIds.length > 0) {
6125
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5801
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6126
5802
  const targetIdInfo = targetPks[0];
6127
5803
  const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6128
5804
  [sourceJunctionColumn.name]: parsedParentId,
@@ -6142,12 +5818,12 @@ var RelationService = class {
6142
5818
  logger.warn(`Junction columns not found for relation '${key}'`);
6143
5819
  continue;
6144
5820
  }
6145
- const parentPks = requirePrimaryKeys(collection, this.registry);
5821
+ const parentPks = getPrimaryKeys(collection, this.registry);
6146
5822
  const parentIdInfo = parentPks[0];
6147
5823
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6148
5824
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
6149
5825
  if (targetEntityIds.length > 0) {
6150
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5826
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6151
5827
  const targetIdInfo = targetPks[0];
6152
5828
  const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6153
5829
  [sourceJunctionColumn.name]: parsedParentId,
@@ -6158,7 +5834,7 @@ var RelationService = class {
6158
5834
  } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") logger.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
6159
5835
  else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
6160
5836
  const targetTable = getTableForCollection(targetCollection, this.registry);
6161
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5837
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6162
5838
  const targetIdInfo = targetPks[0];
6163
5839
  const targetIdCol = targetTable[targetIdInfo.fieldName];
6164
5840
  const fkCol = targetTable[relation.foreignKeyOnTarget];
@@ -6166,7 +5842,7 @@ var RelationService = class {
6166
5842
  logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
6167
5843
  continue;
6168
5844
  }
6169
- const parentPks = requirePrimaryKeys(collection, this.registry);
5845
+ const parentPks = getPrimaryKeys(collection, this.registry);
6170
5846
  const parentIdInfo = parentPks[0];
6171
5847
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6172
5848
  if (targetEntityIds.length > 0) {
@@ -6186,9 +5862,9 @@ var RelationService = class {
6186
5862
  try {
6187
5863
  const targetCollection = relation.target();
6188
5864
  const targetTable = getTableForCollection(targetCollection, this.registry);
6189
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5865
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6190
5866
  const targetIdInfo = targetPks[0];
6191
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
5867
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6192
5868
  const sourceIdInfo = sourcePks[0];
6193
5869
  if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
6194
5870
  await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
@@ -6269,12 +5945,12 @@ var RelationService = class {
6269
5945
  logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
6270
5946
  return;
6271
5947
  }
6272
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
5948
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6273
5949
  const sourceIdInfo = sourcePks[0];
6274
5950
  const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
6275
5951
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
6276
5952
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
6277
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5953
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6278
5954
  const targetIdInfo = targetPks[0];
6279
5955
  const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6280
5956
  [sourceJunctionColumn.name]: parsedSourceId,
@@ -6282,7 +5958,7 @@ var RelationService = class {
6282
5958
  }));
6283
5959
  if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
6284
5960
  } else if (newValue && !Array.isArray(newValue)) {
6285
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5961
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6286
5962
  const targetIdInfo = targetPks[0];
6287
5963
  const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
6288
5964
  const newLink = {
@@ -6313,12 +5989,12 @@ var RelationService = class {
6313
5989
  logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
6314
5990
  return;
6315
5991
  }
6316
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
5992
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6317
5993
  const sourceIdInfo = sourcePks[0];
6318
5994
  const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
6319
5995
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
6320
5996
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
6321
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5997
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6322
5998
  const targetIdInfo = targetPks[0];
6323
5999
  const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6324
6000
  [sourceJunctionColumn.name]: parsedSourceId,
@@ -6339,12 +6015,12 @@ var RelationService = class {
6339
6015
  const { relation, newTargetId } = upd;
6340
6016
  const targetCollection = relation.target();
6341
6017
  const targetTable = getTableForCollection(targetCollection, this.registry);
6342
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6018
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6343
6019
  const targetIdInfo = targetPks[0];
6344
6020
  const targetIdCol = targetTable[targetIdInfo.fieldName];
6345
6021
  const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
6346
6022
  const parentTable = getTableForCollection(parentCollection, this.registry);
6347
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
6023
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
6348
6024
  const parentIdInfo = parentPks[0];
6349
6025
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
6350
6026
  const parentIdCol = parentTable[parentIdInfo.fieldName];
@@ -6415,7 +6091,7 @@ var RelationService = class {
6415
6091
  logger.warn(`Junction columns not found for relation '${relationKey}'`);
6416
6092
  return;
6417
6093
  }
6418
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6094
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6419
6095
  const targetIdInfo = targetPks[0];
6420
6096
  const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
6421
6097
  const junctionData = {
@@ -6431,100 +6107,6 @@ var RelationService = class {
6431
6107
  }
6432
6108
  };
6433
6109
  //#endregion
6434
- //#region src/services/row-pipeline.ts
6435
- /**
6436
- * Whether a many-relation reaches its target through a junction table.
6437
- *
6438
- * Also used to build the drizzle `with` config, which is why it is exported:
6439
- * the query has to nest one level deeper for a junction, and the row walk has
6440
- * to unwrap that same level back out.
6441
- */
6442
- function isJunctionRelation(relation) {
6443
- if (relation.through) return true;
6444
- if (relation.joinPath && relation.joinPath.length > 1) return true;
6445
- return false;
6446
- }
6447
- /**
6448
- * Reach the target row inside a junction row.
6449
- *
6450
- * A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
6451
- * the foreign keys, and the target nested under one of them. The target is the
6452
- * only object among them, so that is how it is found. A junction row that has
6453
- * not been nested (no `with` on the join) has no object and is returned as-is.
6454
- */
6455
- function unwrapJunctionRow(item) {
6456
- const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
6457
- return nestedKey ? item[nestedKey] : item;
6458
- }
6459
- /** Render one target row in the requested style. */
6460
- function renderTarget(targetRow, targetCollection, style, registry) {
6461
- if (style === "inline") return { ...targetRow };
6462
- const address = relationTargetAddress(targetRow, targetCollection, registry);
6463
- const path = targetCollection.slug;
6464
- return createRelationRefWithData(address, path, {
6465
- id: address,
6466
- path,
6467
- values: normalizeDbValues(targetRow, targetCollection)
6468
- });
6469
- }
6470
- /**
6471
- * The address a relation ref points at.
6472
- *
6473
- * The whole key, not its first column: a composite-keyed target addressed by
6474
- * `tenant_id` alone points at every row that shares it. A target whose key
6475
- * cannot be resolved at all used to throw here — reading `[0]` of an empty
6476
- * array — taking down the parent's fetch over a relation it may not even have
6477
- * asked for. The first column is a guess, but a ref that resolves to nothing
6478
- * beats no rows at all.
6479
- */
6480
- function relationTargetAddress(targetRow, targetCollection, registry) {
6481
- const address = deriveRowAddress(targetRow, targetCollection, registry);
6482
- if (address) return address;
6483
- return String(targetRow[Object.keys(targetRow)[0]] ?? "");
6484
- }
6485
- /**
6486
- * The row the admin renders: every column, with relations as references.
6487
- *
6488
- * Values are normalized (dates, numbers, NaN) because the admin's view-model
6489
- * expects real types. The row's own address is *not* among the columns — it is
6490
- * derived by the consumer from the collection's primary keys.
6491
- */
6492
- function toCmsRow(row, collection, registry) {
6493
- const resolvedRelations = resolveCollectionRelations(collection);
6494
- const normalized = normalizeDbValues(row, collection);
6495
- for (const [key, relation] of Object.entries(resolvedRelations)) {
6496
- const relData = row[relation.relationName || key];
6497
- if (relData === void 0 || relData === null) continue;
6498
- if (relation.cardinality === "many" && Array.isArray(relData)) {
6499
- const targetCollection = relation.target();
6500
- normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
6501
- } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
6502
- }
6503
- return normalized;
6504
- }
6505
- /**
6506
- * The row REST serves: every column under its own name, with the value Postgres
6507
- * returned, and relations inlined as the target's columns.
6508
- *
6509
- * Deliberately not normalized: REST serves what the database holds, and JSON
6510
- * has its own opinions about dates that the admin's view-model does not share.
6511
- *
6512
- * Keyed by the row rather than by the relation list — a REST fetch only loads
6513
- * the relations `include` asked for, so the row is the authority on which are
6514
- * actually there.
6515
- */
6516
- function toRestRow(row, collection, registry) {
6517
- const resolvedRelations = resolveCollectionRelations(collection);
6518
- const flat = {};
6519
- for (const [key, value] of Object.entries(row)) {
6520
- const relation = findRelation(resolvedRelations, key);
6521
- if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
6522
- else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
6523
- else flat[key] = value;
6524
- }
6525
- return flat;
6526
- }
6527
- //#endregion
6528
6110
  //#region src/services/FetchService.ts
6529
6111
  /**
6530
6112
  * Service for handling all row read operations.
@@ -6583,7 +6165,7 @@ var FetchService = class {
6583
6165
  if (!shouldInclude(key)) continue;
6584
6166
  const drizzleRelName = relation.relationName || key;
6585
6167
  if (relation.joinPath && relation.joinPath.length > 0) continue;
6586
- if (relation.cardinality === "many" && isJunctionRelation(relation)) {
6168
+ if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
6587
6169
  const targetFkName = this.getJunctionTargetRelationName(relation, collection);
6588
6170
  if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
6589
6171
  else withConfig[drizzleRelName] = true;
@@ -6592,6 +6174,14 @@ var FetchService = class {
6592
6174
  return withConfig;
6593
6175
  }
6594
6176
  /**
6177
+ * Detect if a many-to-many relation uses a junction table in the Drizzle schema.
6178
+ */
6179
+ isJunctionRelation(relation, _collection) {
6180
+ if (relation.through) return true;
6181
+ if (relation.joinPath && relation.joinPath.length > 1) return true;
6182
+ return false;
6183
+ }
6184
+ /**
6595
6185
  * Get the Drizzle relation name on the junction table that points to the actual target row.
6596
6186
  * For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
6597
6187
  */
@@ -6600,6 +6190,54 @@ var FetchService = class {
6600
6190
  return null;
6601
6191
  }
6602
6192
  /**
6193
+ * Convert a db.query result row (with nested relation objects) to a flat row.
6194
+ * Handles:
6195
+ * - Type normalization (dates, numbers, NaN) via normalizeDbValues
6196
+ * - Converting nested relation objects to { id, path, __type: "relation" } for CMS
6197
+ * - Flattening junction-table many-to-many results
6198
+ *
6199
+ * The row's own address is not among them: it is derived by the consumer
6200
+ * from the collection's primary keys.
6201
+ */
6202
+ drizzleResultToRow(row, collection) {
6203
+ const resolvedRelations = resolveCollectionRelations(collection);
6204
+ const normalizedValues = normalizeDbValues(row, collection);
6205
+ for (const [key, relation] of Object.entries(resolvedRelations)) {
6206
+ const relData = row[relation.relationName || key];
6207
+ if (relData === void 0 || relData === null) continue;
6208
+ if (relation.cardinality === "many" && Array.isArray(relData)) {
6209
+ const targetCollection = relation.target();
6210
+ const targetPath = targetCollection.slug;
6211
+ const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
6212
+ normalizedValues[key] = relData.map((item) => {
6213
+ let targetRow = item;
6214
+ if (this.isJunctionRelation(relation, collection)) {
6215
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
6216
+ if (nestedKey) targetRow = item[nestedKey];
6217
+ }
6218
+ const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
6219
+ return createRelationRefWithData(relId, targetPath, {
6220
+ id: relId,
6221
+ path: targetPath,
6222
+ values: normalizeDbValues(targetRow, targetCollection)
6223
+ });
6224
+ });
6225
+ } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
6226
+ const targetCollection = relation.target();
6227
+ const targetPath = targetCollection.slug;
6228
+ const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
6229
+ const relObj = relData;
6230
+ const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
6231
+ normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
6232
+ id: relId,
6233
+ path: targetPath,
6234
+ values: normalizeDbValues(relObj, targetCollection)
6235
+ });
6236
+ }
6237
+ }
6238
+ return normalizedValues;
6239
+ }
6240
+ /**
6603
6241
  * Post-fetch joinPath relations for a single flat row.
6604
6242
  * joinPath relations cannot be expressed via Drizzle's `with` config,
6605
6243
  * so they must be loaded separately after the primary query.
@@ -6620,6 +6258,31 @@ var FetchService = class {
6620
6258
  await Promise.all(promises);
6621
6259
  }
6622
6260
  /**
6261
+ * Post-fetch joinPath relations for a batch of flat rows.
6262
+ * Uses batch fetching to avoid N+1 queries for list views.
6263
+ */
6264
+ async resolveJoinPathRelationsBatch(rows, collection, collectionPath, idInfo, _databaseId) {
6265
+ if (rows.length === 0) return;
6266
+ const resolvedRelations = resolveCollectionRelations(collection);
6267
+ const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
6268
+ if (joinPathRelations.length === 0) return;
6269
+ for (const [key, relation] of joinPathRelations) try {
6270
+ const rowIds = rows.map((r) => {
6271
+ return parseIdValues(String(r.id), [idInfo])[idInfo.fieldName];
6272
+ });
6273
+ const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
6274
+ for (const row of rows) {
6275
+ const id = parseIdValues(String(row.id), [idInfo])[idInfo.fieldName];
6276
+ const relatedRow = resultMap.get(String(id));
6277
+ if (relatedRow) {
6278
+ if (relation.cardinality === "one") row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
6279
+ }
6280
+ }
6281
+ } catch (e) {
6282
+ logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
6283
+ }
6284
+ }
6285
+ /**
6623
6286
  * Resolves joinPath relations for raw REST rows and directly injects them.
6624
6287
  * Uses RelationService to query the database and maps results back to the flattened objects.
6625
6288
  */
@@ -6630,29 +6293,63 @@ var FetchService = class {
6630
6293
  const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
6631
6294
  const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
6632
6295
  if (joinPathRelations.length === 0) return;
6633
- const parentIdOf = (row) => {
6634
- const address = buildCompositeId(row, idInfoArray);
6635
- return address && address.split(":::").some((part) => part !== "") ? address : void 0;
6636
- };
6296
+ const idInfo = idInfoArray[0];
6637
6297
  for (const [key, relation] of joinPathRelations) try {
6638
- const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
6639
- if (addressable.length === 0) continue;
6640
- const rowIds = addressable.map((r) => parentIdOf(r));
6298
+ const rowIds = rows.map((r) => {
6299
+ return parseIdValues(String(r.id), idInfoArray)[idInfo.fieldName];
6300
+ });
6641
6301
  if (relation.cardinality === "one") {
6642
6302
  const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
6643
- for (const row of addressable) {
6644
- const relatedRow = resultMap.get(String(parentIdOf(row)));
6645
- row[key] = relatedRow ? { ...relatedRow.values } : null;
6303
+ for (const row of rows) {
6304
+ const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
6305
+ const relatedRow = resultMap.get(String(id));
6306
+ if (relatedRow) row[key] = {
6307
+ ...relatedRow.values,
6308
+ id: relatedRow.id
6309
+ };
6310
+ else row[key] = null;
6646
6311
  }
6647
6312
  } else if (relation.cardinality === "many") {
6648
6313
  const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation);
6649
- for (const row of addressable) row[key] = (resultMap.get(String(parentIdOf(row))) || []).map((e) => ({ ...e.values }));
6314
+ for (const row of rows) {
6315
+ const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
6316
+ row[key] = (resultMap.get(String(id)) || []).map((e) => ({
6317
+ ...e.values,
6318
+ id: e.id
6319
+ }));
6320
+ }
6650
6321
  }
6651
6322
  } catch (e) {
6652
6323
  logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
6653
6324
  }
6654
6325
  }
6655
6326
  /**
6327
+ * Convert a db.query result row to a flat REST-style row with populated relations.
6328
+ *
6329
+ * Every column is copied through under its own name, with the value Postgres
6330
+ * returned. This used to open with a synthesized `id` and then skip the key
6331
+ * column, which renamed it (a `sku` primary key was served as `id`, and `sku`
6332
+ * did not appear at all) and restringified it (`42` → `"42"`). Consumers that
6333
+ * need an address derive it from the collection's primary keys.
6334
+ */
6335
+ drizzleResultToRestRow(row, collection) {
6336
+ const flat = {};
6337
+ const resolvedRelations = resolveCollectionRelations(collection);
6338
+ for (const [k, v] of Object.entries(row)) {
6339
+ const relation = findRelation(resolvedRelations, k);
6340
+ if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
6341
+ if (this.isJunctionRelation(relation, collection)) {
6342
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
6343
+ if (nestedKey) return { ...item[nestedKey] };
6344
+ }
6345
+ return { ...item };
6346
+ });
6347
+ else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
6348
+ else flat[k] = v;
6349
+ }
6350
+ return flat;
6351
+ }
6352
+ /**
6656
6353
  * Build db.query-compatible options from standard fetch options.
6657
6354
  * Handles filter, search, orderBy, limit, and cursor-based pagination.
6658
6355
  */
@@ -6722,7 +6419,7 @@ var FetchService = class {
6722
6419
  async fetchOne(collectionPath, id, databaseId) {
6723
6420
  const collection = getCollectionByPath(collectionPath, this.registry);
6724
6421
  const table = getTableForCollection(collection, this.registry);
6725
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6422
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
6726
6423
  const idInfo = idInfoArray[0];
6727
6424
  const idField = table[idInfo.fieldName];
6728
6425
  if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
@@ -6736,7 +6433,7 @@ var FetchService = class {
6736
6433
  with: withConfig
6737
6434
  });
6738
6435
  if (!row) return void 0;
6739
- const flatRow = toCmsRow(row, collection, this.registry);
6436
+ const flatRow = this.drizzleResultToRow(row, collection);
6740
6437
  await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
6741
6438
  return flatRow;
6742
6439
  } catch (e) {
@@ -6778,7 +6475,7 @@ var FetchService = class {
6778
6475
  async fetchRowsWithConditions(collectionPath, options = {}) {
6779
6476
  const collection = getCollectionByPath(collectionPath, this.registry);
6780
6477
  const table = getTableForCollection(collection, this.registry);
6781
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6478
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
6782
6479
  const idInfo = idInfoArray[0];
6783
6480
  const idField = table[idInfo.fieldName];
6784
6481
  if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
@@ -6788,7 +6485,7 @@ var FetchService = class {
6788
6485
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
6789
6486
  if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
6790
6487
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
6791
- return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
6488
+ return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection));
6792
6489
  } catch (e) {
6793
6490
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
6794
6491
  logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
@@ -6849,10 +6546,8 @@ var FetchService = class {
6849
6546
  }
6850
6547
  /**
6851
6548
  * Fallback path used when db.query is unavailable.
6852
- *
6853
- * The primary path runs the results through `toCmsRow`, which maps
6854
- * relations from what drizzle already nested — no query per row. This one
6855
- * has no nesting to read, so it resolves relations itself, in batches.
6549
+ * The primary path uses drizzleResultToRow which handles relation
6550
+ * mapping without N+1 queries.
6856
6551
  *
6857
6552
  * Process raw database results into flat rows with relations.
6858
6553
  */
@@ -6932,7 +6627,10 @@ var FetchService = class {
6932
6627
  const relationKey = pathSegments[i];
6933
6628
  const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
6934
6629
  if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
6935
- if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({ ...row.values }));
6630
+ if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
6631
+ ...row.values,
6632
+ id: row.id
6633
+ }));
6936
6634
  if (i + 1 < pathSegments.length) {
6937
6635
  const nextEntityId = pathSegments[i + 1];
6938
6636
  currentCollection = relation.target();
@@ -6994,7 +6692,7 @@ var FetchService = class {
6994
6692
  if (value === void 0 || value === null) return true;
6995
6693
  const collection = getCollectionByPath(collectionPath, this.registry);
6996
6694
  const table = getTableForCollection(collection, this.registry);
6997
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6695
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
6998
6696
  const idInfo = idInfoArray[0];
6999
6697
  const idField = table[idInfo.fieldName];
7000
6698
  const field = table[fieldName];
@@ -7021,7 +6719,7 @@ var FetchService = class {
7021
6719
  async fetchCollectionForRest(collectionPath, options = {}, include) {
7022
6720
  const collection = getCollectionByPath(collectionPath, this.registry);
7023
6721
  const table = getTableForCollection(collection, this.registry);
7024
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6722
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
7025
6723
  const idInfo = idInfoArray[0];
7026
6724
  const idField = table[idInfo.fieldName];
7027
6725
  const tableName = getTableName(table);
@@ -7029,7 +6727,7 @@ var FetchService = class {
7029
6727
  if (qb && !options.searchString && !options.vectorSearch) try {
7030
6728
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
7031
6729
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
7032
- const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
6730
+ const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection));
7033
6731
  await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
7034
6732
  return restRows;
7035
6733
  } catch (e) {
@@ -7078,7 +6776,7 @@ var FetchService = class {
7078
6776
  async fetchOneForRest(collectionPath, id, include, databaseId) {
7079
6777
  const collection = getCollectionByPath(collectionPath, this.registry);
7080
6778
  const table = getTableForCollection(collection, this.registry);
7081
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6779
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
7082
6780
  const idInfo = idInfoArray[0];
7083
6781
  const idField = table[idInfo.fieldName];
7084
6782
  const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
@@ -7091,7 +6789,7 @@ var FetchService = class {
7091
6789
  ...withConfig ? { with: withConfig } : {}
7092
6790
  });
7093
6791
  if (!row) return null;
7094
- const restRow = toRestRow(row, collection, this.registry);
6792
+ const restRow = this.drizzleResultToRestRow(row, collection);
7095
6793
  await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
7096
6794
  return restRow;
7097
6795
  } catch (e) {
@@ -7136,7 +6834,7 @@ var FetchService = class {
7136
6834
  async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
7137
6835
  const collection = getCollectionByPath(collectionPath, this.registry);
7138
6836
  const table = getTableForCollection(collection, this.registry);
7139
- const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
6837
+ const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
7140
6838
  let vectorMeta;
7141
6839
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
7142
6840
  let query = vectorMeta ? this.db.select({
@@ -8617,8 +8315,8 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8617
8315
  context: contextForCallback
8618
8316
  });
8619
8317
  }
8620
- const savedId = deriveRowAddress(savedRow, resolvedCollection ?? collection, this.registry);
8621
- const savedValues = savedRow;
8318
+ const savedId = savedRow.id;
8319
+ const { id: _savedId, ...savedValues } = savedRow;
8622
8320
  if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
8623
8321
  if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
8624
8322
  collection: resolvedCollection,
@@ -8650,7 +8348,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8650
8348
  }
8651
8349
  if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
8652
8350
  tableName: path,
8653
- id: savedId,
8351
+ id: savedId.toString(),
8654
8352
  action: status === "new" ? "create" : "update",
8655
8353
  values: savedValues,
8656
8354
  previousValues: previousValuesForHistory,
@@ -8658,11 +8356,11 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8658
8356
  });
8659
8357
  if (this._deferNotifications) this._pendingNotifications.push({
8660
8358
  path,
8661
- id: savedId,
8359
+ id: savedId.toString(),
8662
8360
  row: savedRow,
8663
8361
  databaseId: resolvedCollection?.databaseId
8664
8362
  });
8665
- else await this.realtimeService.notifyUpdate(path, savedId, savedRow, resolvedCollection?.databaseId);
8363
+ else await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow, resolvedCollection?.databaseId);
8666
8364
  return savedRow;
8667
8365
  } catch (error) {
8668
8366
  if (globalCallbacks?.afterSaveError || callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
@@ -8742,7 +8440,10 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8742
8440
  }
8743
8441
  async delete({ row, collection }) {
8744
8442
  const targetPath = row.path;
8745
- const targetRow = { ...row.values ?? {} };
8443
+ const targetRow = {
8444
+ id: row.id,
8445
+ ...row.values ?? {}
8446
+ };
8746
8447
  const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
8747
8448
  const contextForCallback = this.buildCallContext();
8748
8449
  if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
@@ -9690,7 +9391,6 @@ var generateSchema = async (collections, stripPolicies = false) => {
9690
9391
  });
9691
9392
  });
9692
9393
  schemaContent += "\n";
9693
- const junctionSpecs = resolveJunctionSpecs(collections);
9694
9394
  for (const collection of collections) {
9695
9395
  const tableName = getTableName$1(collection);
9696
9396
  if (tableName) allTablesToGenerate.set(tableName, { collection });
@@ -9724,17 +9424,9 @@ var generateSchema = async (collections, stripPolicies = false) => {
9724
9424
  schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
9725
9425
  schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
9726
9426
  schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(targetCollection))}.${targetId}, ${refOptions}),\n`;
9727
- schemaContent += "}, (table) => ([\n";
9728
- schemaContent += ` primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] }),\n`;
9729
- const junctionSpec = junctionSpecs.get(baseTableName);
9730
- if (!stripPolicies && junctionSpec) {
9731
- const junctionCollection = getJunctionCollectionConfig(junctionSpec);
9732
- const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName$1(c) === slug);
9733
- getJunctionSecurityRules(junctionSpec).forEach((rule, idx) => {
9734
- schemaContent += generatePolicyCode(junctionCollection, rule, idx, resolveCollection);
9735
- });
9736
- }
9737
- schemaContent += "])).enableRLS();\n\n";
9427
+ schemaContent += "}, (table) => ({\n";
9428
+ schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
9429
+ schemaContent += "}));\n\n";
9738
9430
  } else if (!isJunction) {
9739
9431
  const schema = isPostgresCollectionConfig(collection) ? collection.schema : void 0;
9740
9432
  const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
@@ -10425,7 +10117,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10425
10117
  startAfter: request.startAfter,
10426
10118
  searchString: request.searchString
10427
10119
  }, authContext);
10428
- this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
10120
+ this.sendCollectionUpdate(clientId, subscriptionId, rows);
10429
10121
  } catch (error) {
10430
10122
  const sanitized = sanitizeErrorForClient(error, request.path);
10431
10123
  this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -10526,7 +10218,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10526
10218
  if (subscription.type === "single" && notifyPath === originalPath) if (row && row?._rebase_invalidated) this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);
10527
10219
  else this.sendSingleUpdate(subscription.clientId, subscriptionId, row);
10528
10220
  else if (subscription.type === "collection" && subscription.collectionRequest) {
10529
- if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row, notifyPath);
10221
+ if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
10530
10222
  this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
10531
10223
  }
10532
10224
  } catch (error) {
@@ -10556,7 +10248,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10556
10248
  if (!this._subscriptions.has(subscriptionId)) return;
10557
10249
  try {
10558
10250
  const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
10559
- this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);
10251
+ this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
10560
10252
  } catch (error) {
10561
10253
  const sanitized = sanitizeErrorForClient(error, notifyPath);
10562
10254
  this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -10770,12 +10462,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10770
10462
  }
10771
10463
  return await this.dataService.fetchOne(notifyPath, id);
10772
10464
  }
10773
- sendCollectionUpdate(clientId, subscriptionId, rows, path) {
10465
+ sendCollectionUpdate(clientId, subscriptionId, rows) {
10774
10466
  const message = {
10775
10467
  type: "collection_update",
10776
10468
  subscriptionId,
10777
- rows,
10778
- pks: this.primaryKeysForPath(path)
10469
+ rows
10779
10470
  };
10780
10471
  this.sendMessage(clientId, message);
10781
10472
  }
@@ -10790,33 +10481,16 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10790
10481
  /**
10791
10482
  * Send a lightweight row-level patch to a collection subscriber.
10792
10483
  * The client can merge this into its cached data for instant feedback.
10793
- *
10794
- * The key columns ride along: the patch names a row by address, and the
10795
- * client has to find that row among the ones it cached — which carry
10796
- * columns and no address. The SDK holds no collection config to derive one
10797
- * from, so this is the only place the mapping can come from.
10798
10484
  */
10799
- sendCollectionPatch(clientId, subscriptionId, id, row, notifyPath) {
10485
+ sendCollectionPatch(clientId, subscriptionId, id, row) {
10800
10486
  const message = {
10801
10487
  type: "collection_patch",
10802
10488
  subscriptionId,
10803
10489
  id,
10804
- row,
10805
- pks: this.primaryKeysForPath(notifyPath)
10490
+ row
10806
10491
  };
10807
10492
  this.sendMessage(clientId, message);
10808
10493
  }
10809
- /** The key columns of the collection at `path`, if they can be resolved. */
10810
- primaryKeysForPath(path) {
10811
- try {
10812
- const collection = this.registry.getCollectionByPath(path);
10813
- if (!collection) return void 0;
10814
- const keys = getPrimaryKeys(collection, this.registry);
10815
- return keys.length > 0 ? keys : void 0;
10816
- } catch {
10817
- return;
10818
- }
10819
- }
10820
10494
  sendError(clientId, error, subscriptionId, code) {
10821
10495
  const message = {
10822
10496
  type: "error",
@@ -11064,7 +10738,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
11064
10738
  }
11065
10739
  /** Compute the canonical (possibly composite) id string from a captured row. */
11066
10740
  extractIdFromCdcRow(collection, row) {
11067
- return deriveRowAddress(row, collection, this.registry) || "*";
10741
+ try {
10742
+ const composite = buildCompositeId(row, getPrimaryKeys(collection, this.registry));
10743
+ if (composite && composite !== ":::") return composite;
10744
+ } catch {}
10745
+ if (row.id !== void 0 && row.id !== null) return String(row.id);
10746
+ return "*";
11068
10747
  }
11069
10748
  dedupKey(path, id, databaseId) {
11070
10749
  return `${databaseId ?? ""}::${path}::${id}`;
@@ -20524,33 +20203,6 @@ function formatBytes(bytes) {
20524
20203
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
20525
20204
  }
20526
20205
  //#endregion
20527
- //#region src/collections/buildRegistry.ts
20528
- /**
20529
- * Build the collection registry for a driver.
20530
- *
20531
- * The order matters and is the reason this is one function rather than a run of
20532
- * statements in the bootstrapper. Keys are resolved from the drizzle schema, so
20533
- * anything that inspects them has to run *after* the tables are registered —
20534
- * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
20535
- * collection whose table it cannot look up is one it has nothing to say about.
20536
- * Warned too early, it would skip every collection and report nothing, which
20537
- * reads exactly like having nothing to report.
20538
- */
20539
- function buildCollectionRegistry(schema) {
20540
- const registry = new PostgresCollectionRegistry();
20541
- if (schema.collections) {
20542
- registry.registerMultiple(schema.collections);
20543
- logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
20544
- }
20545
- if (schema.tables) Object.values(schema.tables).forEach((table) => {
20546
- if (isTable(table)) registry.registerTable(table, getTableName(table));
20547
- });
20548
- if (schema.enums) registry.registerEnums(schema.enums);
20549
- if (schema.relations) registry.registerRelations(schema.relations);
20550
- warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
20551
- return registry;
20552
- }
20553
- //#endregion
20554
20206
  //#region src/auth/ensure-tables.ts
20555
20207
  /**
20556
20208
  * Auto-create auth tables if they don't exist.
@@ -22533,14 +22185,21 @@ function createPostgresBootstrapper(pgConfig) {
22533
22185
  logger.info(`🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema "${pgSchemaName}" [${introspectedCollections.map((c) => c.slug).join(", ")}]`);
22534
22186
  }
22535
22187
  const activeCollections = introspectedCollections ?? collections;
22188
+ const registry = new PostgresCollectionRegistry();
22189
+ if (activeCollections) {
22190
+ registry.registerMultiple(activeCollections);
22191
+ logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
22192
+ }
22536
22193
  const schemaTables = introspectedTables ?? pgConfig.schema?.tables;
22537
- const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
22538
- const registry = buildCollectionRegistry({
22539
- collections: activeCollections,
22540
- tables: schemaTables,
22541
- enums: pgConfig.schema?.enums,
22542
- relations: schemaRelations
22194
+ if (schemaTables) Object.values(schemaTables).forEach((table) => {
22195
+ if (isTable(table)) {
22196
+ const tableName = getTableName(table);
22197
+ registry.registerTable(table, tableName);
22198
+ }
22543
22199
  });
22200
+ if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums);
22201
+ const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
22202
+ if (schemaRelations) registry.registerRelations(schemaRelations);
22544
22203
  if (schemaTables) patchPgArrayNullSafety(schemaTables);
22545
22204
  const mergedSchema = {
22546
22205
  ...schemaTables,