@rebasepro/server-postgres 0.9.1-canary.742f831 → 0.9.1-canary.7dddf96

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
@@ -71,51 +71,16 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
71
71
  var connection_exports = /* @__PURE__ */ __exportAll({
72
72
  createDirectDatabaseConnection: () => createDirectDatabaseConnection,
73
73
  createPostgresDatabaseConnection: () => createPostgresDatabaseConnection,
74
- createReadReplicaConnection: () => createReadReplicaConnection,
75
- guardPoolAgainstDirtyRelease: () => guardPoolAgainstDirtyRelease
74
+ createReadReplicaConnection: () => createReadReplicaConnection
76
75
  });
77
76
  var DEFAULT_POOL = {
78
77
  max: 20,
79
78
  idleTimeoutMillis: 3e4,
80
79
  connectionTimeoutMillis: 1e4,
81
- queryTimeout: 6e4,
80
+ queryTimeout: 3e4,
82
81
  statementTimeout: 3e4,
83
82
  keepAlive: true
84
83
  };
85
- /** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */
86
- var TX_IDLE = "I";
87
- /**
88
- * Destroy pool clients that are released while still inside a transaction.
89
- *
90
- * pg-pool returns a client to the idle list whenever `release()` is called
91
- * without an error — even if the connection is still mid-transaction (status
92
- * `T`/`E`). That happens in practice: drizzle's pool transaction releases in
93
- * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
94
- * (e.g. it was queued behind a statement that hit the client-side
95
- * query_timeout), the client goes back dirty. The next checkout then runs
96
- * its statements inside the zombie transaction — with the previous request's
97
- * `app.*` RLS GUCs still applied, which turns unrelated queries into
98
- * RLS-scoped ones (observed in production as registration failing with
99
- * SQLSTATE 42501 under a leaked anonymous context).
100
- *
101
- * pg-pool emits `release` before it consults its private `_expired` set, so
102
- * marking the client expired here makes `_release()` destroy it instead of
103
- * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
104
- * private APIs — feature-detect and fall back to loud logging so an upstream
105
- * change degrades to observability, never to silent corruption.
106
- */
107
- function guardPoolAgainstDirtyRelease(pool, label) {
108
- pool.on("release", (err, client) => {
109
- if (err) return;
110
- const txStatus = client?._txStatus;
111
- if (typeof txStatus !== "string" || txStatus === TX_IDLE) return;
112
- const expired = pool._expired;
113
- if (expired && typeof expired.add === "function" && typeof expired.has === "function" && client && typeof client === "object") {
114
- expired.add(client);
115
- logger.error(`[${label}] Client released back to the pool while still in a transaction (status '${txStatus}') — destroying it so the open transaction and its session state (RLS GUCs) cannot leak into the next request.`);
116
- } else logger.error(`[${label}] Client released mid-transaction (status '${txStatus}') but the pool's internal expiry set is unavailable (pg-pool internals changed?). The connection may leak its open transaction into subsequent requests.`);
117
- });
118
- }
119
84
  /**
120
85
  * Create a Drizzle-backed Postgres connection with a production-grade
121
86
  * connection pool.
@@ -147,7 +112,6 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
147
112
  logger.error("[pg-pool] Unexpected pool error", { detail: err.message });
148
113
  if (err.message.includes("ETIMEDOUT")) logger.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
149
114
  });
150
- guardPoolAgainstDirtyRelease(pool, "pg-pool");
151
115
  return {
152
116
  db: schema ? drizzle(pool, { schema }) : drizzle(pool),
153
117
  pool,
@@ -180,7 +144,6 @@ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
180
144
  pool.on("error", (err) => {
181
145
  logger.error("[pg-direct-pool] Unexpected pool error", { detail: err.message });
182
146
  });
183
- guardPoolAgainstDirtyRelease(pool, "pg-direct-pool");
184
147
  return {
185
148
  db: schema ? drizzle(pool, { schema }) : drizzle(pool),
186
149
  pool,
@@ -210,7 +173,6 @@ function createReadReplicaConnection(connectionString, schema, poolConfig) {
210
173
  pool.on("error", (err) => {
211
174
  logger.error("[pg-replica-pool] Unexpected pool error", { detail: err.message });
212
175
  });
213
- guardPoolAgainstDirtyRelease(pool, "pg-replica-pool");
214
176
  return {
215
177
  db: schema ? drizzle(pool, { schema }) : drizzle(pool),
216
178
  pool,
@@ -4587,26 +4549,8 @@ function getTableForCollection(collection, registry) {
4587
4549
  if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
4588
4550
  return table;
4589
4551
  }
4590
- /**
4591
- * The key columns a collection's rows are addressed by.
4592
- *
4593
- * Three tiers, in order: properties marked `isId`, the primary keys of the
4594
- * drizzle schema, and finally a column literally named `id`. Only the first is
4595
- * visible to the browser, which is why a key known only to drizzle is reported
4596
- * at boot — see {@link warnOnKeysTheAdminCannotResolve}.
4597
- *
4598
- * Returns `[]` when nothing resolves, rather than throwing. It used to open by
4599
- * resolving the table, which throws when there is none — so the `isId` tier,
4600
- * which needs no table at all, was unreachable for exactly the collections
4601
- * most likely to have no table registered. Every caller that wanted "no keys"
4602
- * to mean "no keys" had to spell that out in a try/catch.
4603
- *
4604
- * Callers that cannot proceed without a key must say so themselves, naming the
4605
- * collection: an empty array here means "this collection has no address", which
4606
- * is a different answer in a notification (broadcast a wildcard) than in a save
4607
- * (fail).
4608
- */
4609
4552
  function getPrimaryKeys(collection, registry) {
4553
+ const table = getTableForCollection(collection, registry);
4610
4554
  if (collection.properties) {
4611
4555
  const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
4612
4556
  fieldName: key,
@@ -4615,8 +4559,6 @@ function getPrimaryKeys(collection, registry) {
4615
4559
  }));
4616
4560
  if (idProps.length > 0) return idProps;
4617
4561
  }
4618
- const table = registry.getTable(getTableName$1(collection));
4619
- if (!table) return [];
4620
4562
  const keys = [];
4621
4563
  for (const [key, colRaw] of Object.entries(table)) {
4622
4564
  const col = colRaw;
@@ -4645,21 +4587,6 @@ function getPrimaryKeys(collection, registry) {
4645
4587
  return keys;
4646
4588
  }
4647
4589
  /**
4648
- * The key columns, for callers that cannot do their job without one.
4649
- *
4650
- * {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
4651
- * collection with no address. Most of this driver, though, is building a WHERE
4652
- * clause and has no meaning without a key — for those, an empty array is not an
4653
- * answer, and indexing `[0]` into it produces `Cannot read properties of
4654
- * undefined` three frames from where the real problem is. This says what is
4655
- * wrong and which collection it is wrong about.
4656
- */
4657
- function requirePrimaryKeys(collection, registry) {
4658
- const keys = getPrimaryKeys(collection, registry);
4659
- 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.`);
4660
- return keys;
4661
- }
4662
- /**
4663
4590
  * Collections whose key the *browser* cannot resolve, and what it will do
4664
4591
  * instead.
4665
4592
  *
@@ -4686,7 +4613,12 @@ function findUnresolvableKeyCollections(collections, registry) {
4686
4613
  const findings = [];
4687
4614
  for (const collection of collections) {
4688
4615
  if (getDeclaredPrimaryKeys(collection).length > 0) continue;
4689
- const keys = getPrimaryKeys(collection, registry);
4616
+ let keys;
4617
+ try {
4618
+ keys = getPrimaryKeys(collection, registry);
4619
+ } catch {
4620
+ continue;
4621
+ }
4690
4622
  if (keys.length === 0) continue;
4691
4623
  if (keys.length === 1 && keys[0].fieldName === "id") continue;
4692
4624
  findings.push({
@@ -4717,14 +4649,19 @@ function warnOnKeysTheAdminCannotResolve(collections, registry) {
4717
4649
  * The address of a row: derived from the collection's primary keys, because a
4718
4650
  * row does not carry one — it is exactly its columns.
4719
4651
  *
4720
- * Falls back to a literal `id` column, for a row that reached us from somewhere
4721
- * other than this driver. Returns `""` when there is no key and no `id` —
4722
- * callers decide what that means, since "unaddressable" is a different answer
4723
- * in a notification (broadcast a wildcard) than in a save (fail).
4652
+ * Falls back to a literal `id` column, which covers the two cases where the
4653
+ * keys cannot be resolved: a row that reached us from somewhere other than the
4654
+ * postgres driver, and a collection whose table the registry cannot look up
4655
+ * (`getPrimaryKeys` throws for an unregistered table rather than returning
4656
+ * nothing). Returns `""` when there is no key and no `id` — callers decide what
4657
+ * that means, since "unaddressable" is a different answer in a notification
4658
+ * (broadcast a wildcard) than in a save (fail).
4724
4659
  */
4725
4660
  function deriveRowAddress(row, collection, registry) {
4726
- const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
4727
- if (composite && composite.split(":::").some((part) => part !== "")) return composite;
4661
+ try {
4662
+ const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
4663
+ if (composite && composite.split(":::").some((part) => part !== "")) return composite;
4664
+ } catch {}
4728
4665
  if (row.id !== void 0 && row.id !== null) return String(row.id);
4729
4666
  return "";
4730
4667
  }
@@ -5266,7 +5203,6 @@ var DrizzleConditionBuilder = class {
5266
5203
  static buildVectorSearchConditions(table, vectorSearch) {
5267
5204
  const column = table[vectorSearch.property];
5268
5205
  if (!column) throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
5269
- if (!Array.isArray(vectorSearch.vector) || vectorSearch.vector.length === 0 || !vectorSearch.vector.every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error("Vector search requires a non-empty array of finite numbers");
5270
5206
  const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
5271
5207
  const distanceFn = vectorSearch.distance || "cosine";
5272
5208
  let operator;
@@ -5767,63 +5703,6 @@ var RelationService = class {
5767
5703
  this.registry = registry;
5768
5704
  }
5769
5705
  /**
5770
- * One target row, as the {@link RelatedRow} everything here returns.
5771
- *
5772
- * Eight sites built this by hand, which is how the address came to be the
5773
- * target's first key column in all eight — one edit, eight places to miss.
5774
- *
5775
- * `resolveNested` is the one thing they did not agree on, and the
5776
- * disagreement was invisible: the single-parent fetches pass `db` and
5777
- * `registry` to `parseDataFromServer`, so the target's *own* relations get
5778
- * resolved too, while the batch paths deliberately do not — a query per
5779
- * target row is the N+1 the batching exists to avoid. Naming the parameter
5780
- * makes that a decision rather than a difference between two call sites
5781
- * nobody was comparing.
5782
- */
5783
- async toRelatedRow(targetRow, targetCollection, targetPks, options) {
5784
- const values = options?.resolveNested ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry) : await parseDataFromServer(targetRow, targetCollection);
5785
- return {
5786
- id: buildCompositeId(targetRow, targetPks),
5787
- path: targetCollection.slug,
5788
- values
5789
- };
5790
- }
5791
- /**
5792
- * A WHERE matching any of `parentIds`, by the whole key.
5793
- *
5794
- * A single key is an `IN (…)`. A composite one cannot be: matching
5795
- * `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
5796
- * share their first column each receive the other's relations. It becomes
5797
- * an OR of ANDs — one exact address per parent — which Postgres indexes the
5798
- * same way it would a multi-column key lookup.
5799
- */
5800
- parentKeyCondition(parentTable, parentPks, parentIds) {
5801
- const columnFor = (fieldName) => {
5802
- const col = parentTable[fieldName];
5803
- if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
5804
- return col;
5805
- };
5806
- if (parentPks.length === 1) {
5807
- const values = parentIds.map((id) => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
5808
- return inArray(columnFor(parentPks[0].fieldName), values);
5809
- }
5810
- return or(...parentIds.map((id) => {
5811
- const values = parseIdValues(id, parentPks);
5812
- return and(...parentPks.map((pk) => eq(columnFor(pk.fieldName), values[pk.fieldName])));
5813
- }));
5814
- }
5815
- /**
5816
- * Reject a relation that cannot express a composite-keyed parent.
5817
- *
5818
- * `localKey` and `foreignKeyOnTarget` are single column names: one column
5819
- * cannot reference a two-column key, so such a relation has no correct
5820
- * reading. Left alone it would silently match on the first key column and
5821
- * hand a tenant's rows to its neighbour — say so instead.
5822
- */
5823
- assertSingleKeyAddressable(parentCollection, parentPks, via) {
5824
- 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.`);
5825
- }
5826
- /**
5827
5706
  * Fetch rows related to a parent row through a specific relation
5828
5707
  */
5829
5708
  async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
@@ -5842,9 +5721,9 @@ var RelationService = class {
5842
5721
  async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
5843
5722
  const targetCollection = relation.target();
5844
5723
  const targetTable = getTableForCollection(targetCollection, this.registry);
5845
- const idInfo = requirePrimaryKeys(targetCollection, this.registry);
5724
+ const idInfo = getPrimaryKeys(targetCollection, this.registry);
5846
5725
  const idField = targetTable[idInfo[0].fieldName];
5847
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5726
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5848
5727
  const parentIdInfo = parentPks[0];
5849
5728
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
5850
5729
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
@@ -5868,7 +5747,7 @@ var RelationService = class {
5868
5747
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
5869
5748
  currentTable = joinTable;
5870
5749
  }
5871
- const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
5750
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5872
5751
  query = query.where(eq(parentIdField, parsedParentId));
5873
5752
  if (options.limit) query = query.limit(options.limit);
5874
5753
  const results = await query;
@@ -5876,7 +5755,12 @@ var RelationService = class {
5876
5755
  const rows = [];
5877
5756
  for (const row of results) {
5878
5757
  const targetRow = row[targetTableName] || row;
5879
- rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
5758
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
5759
+ rows.push({
5760
+ id: buildCompositeId(targetRow, idInfo),
5761
+ path: targetCollection.slug,
5762
+ values: parsedValues
5763
+ });
5880
5764
  }
5881
5765
  return rows;
5882
5766
  }
@@ -5895,7 +5779,12 @@ var RelationService = class {
5895
5779
  const rows = [];
5896
5780
  for (const row of results) {
5897
5781
  const targetRow = row[getTableName$1(targetCollection)] || row;
5898
- rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
5782
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
5783
+ rows.push({
5784
+ id: buildCompositeId(targetRow, idInfo),
5785
+ path: targetCollection.slug,
5786
+ values: parsedValues
5787
+ });
5899
5788
  }
5900
5789
  return rows;
5901
5790
  }
@@ -5912,8 +5801,8 @@ var RelationService = class {
5912
5801
  }
5913
5802
  const targetCollection = relation.target();
5914
5803
  const targetTable = getTableForCollection(targetCollection, this.registry);
5915
- const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
5916
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5804
+ const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
5805
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5917
5806
  const parentIdInfo = parentPks[0];
5918
5807
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
5919
5808
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
@@ -5932,10 +5821,10 @@ var RelationService = class {
5932
5821
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
5933
5822
  const targetCollection = relation.target();
5934
5823
  const targetTable = getTableForCollection(targetCollection, this.registry);
5935
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5824
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
5936
5825
  const targetIdInfo = targetPks[0];
5937
5826
  const targetIdField = targetTable[targetIdInfo.fieldName];
5938
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5827
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
5939
5828
  const parentIdInfo = parentPks[0];
5940
5829
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
5941
5830
  if (!parentTable) throw new Error("Parent table not found");
@@ -5959,19 +5848,25 @@ var RelationService = class {
5959
5848
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
5960
5849
  currentTable = joinTable;
5961
5850
  }
5962
- query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
5851
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5852
+ query = query.where(inArray(parentIdField, parsedParentIds));
5963
5853
  const results = await query;
5964
5854
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
5965
5855
  const resultMap = /* @__PURE__ */ new Map();
5966
5856
  for (const row of results) {
5967
5857
  const parentRow = row[getTableName$1(parentCollection)] || row;
5968
5858
  const targetRow = row[targetTableName] || row;
5969
- resultMap.set(buildCompositeId(parentRow, parentPks), await this.toRelatedRow(targetRow, targetCollection, targetPks));
5859
+ const parentId = parentRow[parentIdInfo.fieldName];
5860
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5861
+ resultMap.set(String(parentId), {
5862
+ id: buildCompositeId(targetRow, targetPks),
5863
+ path: targetCollection.slug,
5864
+ values: parsedValues
5865
+ });
5970
5866
  }
5971
5867
  return resultMap;
5972
5868
  }
5973
5869
  if (relation.direction === "owning" && relation.localKey) {
5974
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
5975
5870
  const localKeyCol = parentTable[relation.localKey];
5976
5871
  if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
5977
5872
  const fkRows = await this.db.select({
@@ -6000,11 +5895,17 @@ var RelationService = class {
6000
5895
  const resultMap = /* @__PURE__ */ new Map();
6001
5896
  for (const [parentIdStr, fkValue] of parentToFk) {
6002
5897
  const targetRow = targetById.get(String(fkValue));
6003
- if (targetRow) resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
5898
+ if (targetRow) {
5899
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5900
+ resultMap.set(parentIdStr, {
5901
+ id: buildCompositeId(targetRow, targetPks),
5902
+ path: targetCollection.slug,
5903
+ values: parsedValues
5904
+ });
5905
+ }
6004
5906
  }
6005
5907
  return resultMap;
6006
5908
  }
6007
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
6008
5909
  let query = this.db.select().from(targetTable).$dynamic();
6009
5910
  query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
6010
5911
  const results = await query;
@@ -6015,7 +5916,14 @@ var RelationService = class {
6015
5916
  let parentId;
6016
5917
  if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
6017
5918
  else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
6018
- if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
5919
+ if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
5920
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
5921
+ resultMap.set(String(parentId), {
5922
+ id: buildCompositeId(targetRow, targetPks),
5923
+ path: targetCollection.slug,
5924
+ values: parsedValues
5925
+ });
5926
+ }
6019
5927
  }
6020
5928
  return resultMap;
6021
5929
  }
@@ -6029,9 +5937,9 @@ var RelationService = class {
6029
5937
  const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
6030
5938
  const targetCollection = relation.target();
6031
5939
  const targetTable = getTableForCollection(targetCollection, this.registry);
6032
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
5940
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6033
5941
  const targetIdField = targetTable[targetPks[0].fieldName];
6034
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
5942
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
6035
5943
  const parentIdInfo = parentPks[0];
6036
5944
  const parentTable = this.registry.getTable(getTableName$1(parentCollection));
6037
5945
  if (!parentTable) throw new Error("Parent table not found");
@@ -6053,22 +5961,27 @@ var RelationService = class {
6053
5961
  query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
6054
5962
  currentTable = joinTable;
6055
5963
  }
6056
- query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
5964
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
5965
+ query = query.where(inArray(parentIdField, parsedParentIds));
6057
5966
  const results = await query;
6058
5967
  const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
6059
5968
  const resultMap = /* @__PURE__ */ new Map();
6060
5969
  for (const row of results) {
6061
5970
  const parentRow = row[getTableName$1(parentCollection)] || row;
6062
5971
  const targetRow = row[targetTableName] || row;
6063
- const parentId = buildCompositeId(parentRow, parentPks);
5972
+ const parentId = String(parentRow[parentIdInfo.fieldName]);
5973
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
6064
5974
  const arr = resultMap.get(parentId) || [];
6065
- arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
5975
+ arr.push({
5976
+ id: buildCompositeId(targetRow, targetPks),
5977
+ path: targetCollection.slug,
5978
+ values: parsedValues
5979
+ });
6066
5980
  resultMap.set(parentId, arr);
6067
5981
  }
6068
5982
  return resultMap;
6069
5983
  }
6070
5984
  if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
6071
- this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
6072
5985
  const junctionTable = this.registry.getTable(relation.through.table);
6073
5986
  if (!junctionTable) {
6074
5987
  logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
@@ -6087,13 +6000,17 @@ var RelationService = class {
6087
6000
  const junctionData = row[relation.through.table] || row;
6088
6001
  const targetData = row[targetTableName] || row;
6089
6002
  const parentId = String(junctionData[relation.through.sourceColumn]);
6003
+ const parsedValues = await parseDataFromServer(targetData, targetCollection);
6090
6004
  const arr = resultMap.get(parentId) || [];
6091
- arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
6005
+ arr.push({
6006
+ id: buildCompositeId(targetData, targetPks),
6007
+ path: targetCollection.slug,
6008
+ values: parsedValues
6009
+ });
6092
6010
  resultMap.set(parentId, arr);
6093
6011
  }
6094
6012
  return resultMap;
6095
6013
  }
6096
- this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
6097
6014
  let query = this.db.select().from(targetTable).$dynamic();
6098
6015
  query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
6099
6016
  const results = await query;
@@ -6106,9 +6023,14 @@ var RelationService = class {
6106
6023
  else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
6107
6024
  else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
6108
6025
  if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
6026
+ const parsedValues = await parseDataFromServer(targetRow, targetCollection);
6109
6027
  const key = String(parentId);
6110
6028
  const arr = resultMap.get(key) || [];
6111
- arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
6029
+ arr.push({
6030
+ id: buildCompositeId(targetRow, targetPks),
6031
+ path: targetCollection.slug,
6032
+ values: parsedValues
6033
+ });
6112
6034
  resultMap.set(key, arr);
6113
6035
  }
6114
6036
  }
@@ -6156,12 +6078,12 @@ var RelationService = class {
6156
6078
  logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
6157
6079
  continue;
6158
6080
  }
6159
- const parentPks = requirePrimaryKeys(collection, this.registry);
6081
+ const parentPks = getPrimaryKeys(collection, this.registry);
6160
6082
  const parentIdInfo = parentPks[0];
6161
6083
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6162
6084
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
6163
6085
  if (targetEntityIds.length > 0) {
6164
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6086
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6165
6087
  const targetIdInfo = targetPks[0];
6166
6088
  const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6167
6089
  [sourceJunctionColumn.name]: parsedParentId,
@@ -6181,12 +6103,12 @@ var RelationService = class {
6181
6103
  logger.warn(`Junction columns not found for relation '${key}'`);
6182
6104
  continue;
6183
6105
  }
6184
- const parentPks = requirePrimaryKeys(collection, this.registry);
6106
+ const parentPks = getPrimaryKeys(collection, this.registry);
6185
6107
  const parentIdInfo = parentPks[0];
6186
6108
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6187
6109
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
6188
6110
  if (targetEntityIds.length > 0) {
6189
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6111
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6190
6112
  const targetIdInfo = targetPks[0];
6191
6113
  const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6192
6114
  [sourceJunctionColumn.name]: parsedParentId,
@@ -6197,7 +6119,7 @@ var RelationService = class {
6197
6119
  } 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.`);
6198
6120
  else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
6199
6121
  const targetTable = getTableForCollection(targetCollection, this.registry);
6200
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6122
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6201
6123
  const targetIdInfo = targetPks[0];
6202
6124
  const targetIdCol = targetTable[targetIdInfo.fieldName];
6203
6125
  const fkCol = targetTable[relation.foreignKeyOnTarget];
@@ -6205,7 +6127,7 @@ var RelationService = class {
6205
6127
  logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
6206
6128
  continue;
6207
6129
  }
6208
- const parentPks = requirePrimaryKeys(collection, this.registry);
6130
+ const parentPks = getPrimaryKeys(collection, this.registry);
6209
6131
  const parentIdInfo = parentPks[0];
6210
6132
  const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
6211
6133
  if (targetEntityIds.length > 0) {
@@ -6225,9 +6147,9 @@ var RelationService = class {
6225
6147
  try {
6226
6148
  const targetCollection = relation.target();
6227
6149
  const targetTable = getTableForCollection(targetCollection, this.registry);
6228
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6150
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6229
6151
  const targetIdInfo = targetPks[0];
6230
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
6152
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6231
6153
  const sourceIdInfo = sourcePks[0];
6232
6154
  if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
6233
6155
  await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
@@ -6308,12 +6230,12 @@ var RelationService = class {
6308
6230
  logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
6309
6231
  return;
6310
6232
  }
6311
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
6233
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6312
6234
  const sourceIdInfo = sourcePks[0];
6313
6235
  const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
6314
6236
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
6315
6237
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
6316
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6238
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6317
6239
  const targetIdInfo = targetPks[0];
6318
6240
  const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6319
6241
  [sourceJunctionColumn.name]: parsedSourceId,
@@ -6321,7 +6243,7 @@ var RelationService = class {
6321
6243
  }));
6322
6244
  if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
6323
6245
  } else if (newValue && !Array.isArray(newValue)) {
6324
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6246
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6325
6247
  const targetIdInfo = targetPks[0];
6326
6248
  const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
6327
6249
  const newLink = {
@@ -6352,12 +6274,12 @@ var RelationService = class {
6352
6274
  logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
6353
6275
  return;
6354
6276
  }
6355
- const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
6277
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
6356
6278
  const sourceIdInfo = sourcePks[0];
6357
6279
  const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
6358
6280
  await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
6359
6281
  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
6360
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6282
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6361
6283
  const targetIdInfo = targetPks[0];
6362
6284
  const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
6363
6285
  [sourceJunctionColumn.name]: parsedSourceId,
@@ -6378,12 +6300,12 @@ var RelationService = class {
6378
6300
  const { relation, newTargetId } = upd;
6379
6301
  const targetCollection = relation.target();
6380
6302
  const targetTable = getTableForCollection(targetCollection, this.registry);
6381
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6303
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6382
6304
  const targetIdInfo = targetPks[0];
6383
6305
  const targetIdCol = targetTable[targetIdInfo.fieldName];
6384
6306
  const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
6385
6307
  const parentTable = getTableForCollection(parentCollection, this.registry);
6386
- const parentPks = requirePrimaryKeys(parentCollection, this.registry);
6308
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
6387
6309
  const parentIdInfo = parentPks[0];
6388
6310
  const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
6389
6311
  const parentIdCol = parentTable[parentIdInfo.fieldName];
@@ -6454,7 +6376,7 @@ var RelationService = class {
6454
6376
  logger.warn(`Junction columns not found for relation '${relationKey}'`);
6455
6377
  return;
6456
6378
  }
6457
- const targetPks = requirePrimaryKeys(targetCollection, this.registry);
6379
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
6458
6380
  const targetIdInfo = targetPks[0];
6459
6381
  const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
6460
6382
  const junctionData = {
@@ -6470,150 +6392,6 @@ var RelationService = class {
6470
6392
  }
6471
6393
  };
6472
6394
  //#endregion
6473
- //#region src/services/row-pipeline.ts
6474
- /**
6475
- * Whether a many-relation reaches its target through a junction table.
6476
- *
6477
- * Also used to build the drizzle `with` config, which is why it is exported:
6478
- * the query has to nest one level deeper for a junction, and the row walk has
6479
- * to unwrap that same level back out.
6480
- */
6481
- function isJunctionRelation(relation) {
6482
- if (relation.through) return true;
6483
- if (relation.joinPath && relation.joinPath.length > 1) return true;
6484
- return false;
6485
- }
6486
- /**
6487
- * Reach the target row inside a junction row.
6488
- *
6489
- * A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
6490
- * the foreign keys, and the target nested under one of them. The target is the
6491
- * only object among them, so that is how it is found. A junction row that has
6492
- * not been nested (no `with` on the join) has no object and is returned as-is.
6493
- */
6494
- function unwrapJunctionRow(item) {
6495
- const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
6496
- return nestedKey ? item[nestedKey] : item;
6497
- }
6498
- /**
6499
- * Give back the number a `number` property was declared to be.
6500
- *
6501
- * Postgres returns NUMERIC as a string and drizzle keeps it that way, since a
6502
- * numeric can hold more precision than a double. But the collection declared
6503
- * this property `number` and the OpenAPI spec this server publishes says
6504
- * `type: number`, so serving `"9.99"` breaks its own contract — and breaks it
6505
- * asymmetrically, because a create answers with the number and the read that
6506
- * follows answers with the string. Any client that multiplies a price works
6507
- * until the first refresh.
6508
- *
6509
- * Declaring a property `number` already accepts double precision — that is what
6510
- * the admin has always parsed it to. Columns REST serves that no property
6511
- * declares are left exactly as the database returned them.
6512
- */
6513
- function coerceDeclaredNumber(value, property) {
6514
- if (property?.type !== "number" || typeof value !== "string") return value;
6515
- const parsed = parseFloat(value);
6516
- return isNaN(parsed) ? null : parsed;
6517
- }
6518
- /** Apply {@link coerceDeclaredNumber} across a row, leaving every other column alone. */
6519
- function coerceDeclaredNumbers(row, collection) {
6520
- const properties = collection.properties;
6521
- if (!properties) return row;
6522
- const out = {};
6523
- for (const [key, value] of Object.entries(row)) out[key] = coerceDeclaredNumber(value, properties[key]);
6524
- return out;
6525
- }
6526
- /** Render one target row in the requested style. */
6527
- /**
6528
- * Drop every column the collection marked `excludeFromApi`.
6529
- *
6530
- * Password hashes and verification tokens have to be readable server-side but
6531
- * must never reach a client — and "never" has to mean every exit from this
6532
- * pipeline, including relation targets, or a secret leaks through whichever
6533
- * path was overlooked. Keyed by both the property name and its column name,
6534
- * since a row can arrive keyed either way depending on the caller.
6535
- */
6536
- function stripExcluded(row, collection) {
6537
- const properties = collection.properties;
6538
- if (!properties) return row;
6539
- for (const [key, property] of Object.entries(properties)) {
6540
- if (!property?.excludeFromApi) continue;
6541
- delete row[key];
6542
- if (property.columnName) delete row[property.columnName];
6543
- }
6544
- return row;
6545
- }
6546
- function renderTarget(targetRow, targetCollection, style, registry) {
6547
- if (style === "inline") return stripExcluded(coerceDeclaredNumbers({ ...targetRow }, targetCollection), targetCollection);
6548
- const address = relationTargetAddress(targetRow, targetCollection, registry);
6549
- const path = targetCollection.slug;
6550
- return createRelationRefWithData(address, path, {
6551
- id: address,
6552
- path,
6553
- values: normalizeDbValues(targetRow, targetCollection)
6554
- });
6555
- }
6556
- /**
6557
- * The address a relation ref points at.
6558
- *
6559
- * The whole key, not its first column: a composite-keyed target addressed by
6560
- * `tenant_id` alone points at every row that shares it. A target whose key
6561
- * cannot be resolved at all used to throw here — reading `[0]` of an empty
6562
- * array — taking down the parent's fetch over a relation it may not even have
6563
- * asked for. The first column is a guess, but a ref that resolves to nothing
6564
- * beats no rows at all.
6565
- */
6566
- function relationTargetAddress(targetRow, targetCollection, registry) {
6567
- const address = deriveRowAddress(targetRow, targetCollection, registry);
6568
- if (address) return address;
6569
- return String(targetRow[Object.keys(targetRow)[0]] ?? "");
6570
- }
6571
- /**
6572
- * The row the admin renders: every column, with relations as references.
6573
- *
6574
- * Values are normalized (dates, numbers, NaN) because the admin's view-model
6575
- * expects real types. The row's own address is *not* among the columns — it is
6576
- * derived by the consumer from the collection's primary keys.
6577
- */
6578
- function toCmsRow(row, collection, registry) {
6579
- const resolvedRelations = resolveCollectionRelations(collection);
6580
- const normalized = normalizeDbValues(row, collection);
6581
- for (const [key, relation] of Object.entries(resolvedRelations)) {
6582
- const relData = row[relation.relationName || key];
6583
- if (relData === void 0 || relData === null) continue;
6584
- if (relation.cardinality === "many" && Array.isArray(relData)) {
6585
- const targetCollection = relation.target();
6586
- normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
6587
- } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
6588
- }
6589
- return stripExcluded(normalized, collection);
6590
- }
6591
- /**
6592
- * The row REST serves: every column under its own name, with the value Postgres
6593
- * returned, and relations inlined as the target's columns.
6594
- *
6595
- * Values are the ones the database returned, except where that contradicts the
6596
- * declared type: a `number` property is served as a number (see
6597
- * {@link coerceDeclaredNumber}). Dates stay as the database returned them —
6598
- * JSON has its own opinions about dates that the admin's view-model does not
6599
- * share.
6600
- *
6601
- * Keyed by the row rather than by the relation list — a REST fetch only loads
6602
- * the relations `include` asked for, so the row is the authority on which are
6603
- * actually there.
6604
- */
6605
- function toRestRow(row, collection, registry) {
6606
- const resolvedRelations = resolveCollectionRelations(collection);
6607
- const flat = {};
6608
- for (const [key, value] of Object.entries(row)) {
6609
- const relation = findRelation(resolvedRelations, key);
6610
- if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
6611
- else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
6612
- else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
6613
- }
6614
- return stripExcluded(flat, collection);
6615
- }
6616
- //#endregion
6617
6395
  //#region src/services/FetchService.ts
6618
6396
  /**
6619
6397
  * Service for handling all row read operations.
@@ -6672,7 +6450,7 @@ var FetchService = class {
6672
6450
  if (!shouldInclude(key)) continue;
6673
6451
  const drizzleRelName = relation.relationName || key;
6674
6452
  if (relation.joinPath && relation.joinPath.length > 0) continue;
6675
- if (relation.cardinality === "many" && isJunctionRelation(relation)) {
6453
+ if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
6676
6454
  const targetFkName = this.getJunctionTargetRelationName(relation, collection);
6677
6455
  if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
6678
6456
  else withConfig[drizzleRelName] = true;
@@ -6681,6 +6459,14 @@ var FetchService = class {
6681
6459
  return withConfig;
6682
6460
  }
6683
6461
  /**
6462
+ * Detect if a many-to-many relation uses a junction table in the Drizzle schema.
6463
+ */
6464
+ isJunctionRelation(relation, _collection) {
6465
+ if (relation.through) return true;
6466
+ if (relation.joinPath && relation.joinPath.length > 1) return true;
6467
+ return false;
6468
+ }
6469
+ /**
6684
6470
  * Get the Drizzle relation name on the junction table that points to the actual target row.
6685
6471
  * For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
6686
6472
  */
@@ -6689,6 +6475,68 @@ var FetchService = class {
6689
6475
  return null;
6690
6476
  }
6691
6477
  /**
6478
+ * The address a relation ref points at.
6479
+ *
6480
+ * The whole key, not its first column: a composite-keyed target addressed
6481
+ * by `tenant_id` alone points at every row that shares it. And a target
6482
+ * whose key cannot be resolved at all used to throw here — reading
6483
+ * `targetPks[0]` of an empty array — taking down the parent's fetch over a
6484
+ * relation it may not even have asked for; the first column is a guess, but
6485
+ * a ref that resolves to nothing beats no rows at all.
6486
+ */
6487
+ relationTargetAddress(targetRow, targetCollection) {
6488
+ const address = deriveRowAddress(targetRow, targetCollection, this.registry);
6489
+ if (address) return address;
6490
+ const firstColumn = targetRow[Object.keys(targetRow)[0]];
6491
+ return String(firstColumn ?? "");
6492
+ }
6493
+ /**
6494
+ * Convert a db.query result row (with nested relation objects) to a flat row.
6495
+ * Handles:
6496
+ * - Type normalization (dates, numbers, NaN) via normalizeDbValues
6497
+ * - Converting nested relation objects to { id, path, __type: "relation" } for CMS
6498
+ * - Flattening junction-table many-to-many results
6499
+ *
6500
+ * The row's own address is not among them: it is derived by the consumer
6501
+ * from the collection's primary keys.
6502
+ */
6503
+ drizzleResultToRow(row, collection) {
6504
+ const resolvedRelations = resolveCollectionRelations(collection);
6505
+ const normalizedValues = normalizeDbValues(row, collection);
6506
+ for (const [key, relation] of Object.entries(resolvedRelations)) {
6507
+ const relData = row[relation.relationName || key];
6508
+ if (relData === void 0 || relData === null) continue;
6509
+ if (relation.cardinality === "many" && Array.isArray(relData)) {
6510
+ const targetCollection = relation.target();
6511
+ const targetPath = targetCollection.slug;
6512
+ normalizedValues[key] = relData.map((item) => {
6513
+ let targetRow = item;
6514
+ if (this.isJunctionRelation(relation, collection)) {
6515
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
6516
+ if (nestedKey) targetRow = item[nestedKey];
6517
+ }
6518
+ const relId = this.relationTargetAddress(targetRow, targetCollection);
6519
+ return createRelationRefWithData(relId, targetPath, {
6520
+ id: relId,
6521
+ path: targetPath,
6522
+ values: normalizeDbValues(targetRow, targetCollection)
6523
+ });
6524
+ });
6525
+ } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
6526
+ const targetCollection = relation.target();
6527
+ const targetPath = targetCollection.slug;
6528
+ const relObj = relData;
6529
+ const relId = this.relationTargetAddress(relObj, targetCollection);
6530
+ normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
6531
+ id: relId,
6532
+ path: targetPath,
6533
+ values: normalizeDbValues(relObj, targetCollection)
6534
+ });
6535
+ }
6536
+ }
6537
+ return normalizedValues;
6538
+ }
6539
+ /**
6692
6540
  * Post-fetch joinPath relations for a single flat row.
6693
6541
  * joinPath relations cannot be expressed via Drizzle's `with` config,
6694
6542
  * so they must be loaded separately after the primary query.
@@ -6719,10 +6567,8 @@ var FetchService = class {
6719
6567
  const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
6720
6568
  const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
6721
6569
  if (joinPathRelations.length === 0) return;
6722
- const parentIdOf = (row) => {
6723
- const address = buildCompositeId(row, idInfoArray);
6724
- return address && address.split(":::").some((part) => part !== "") ? address : void 0;
6725
- };
6570
+ const idInfo = idInfoArray[0];
6571
+ const parentIdOf = (row) => row[idInfo.fieldName];
6726
6572
  for (const [key, relation] of joinPathRelations) try {
6727
6573
  const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
6728
6574
  if (addressable.length === 0) continue;
@@ -6742,6 +6588,32 @@ var FetchService = class {
6742
6588
  }
6743
6589
  }
6744
6590
  /**
6591
+ * Convert a db.query result row to a flat REST-style row with populated relations.
6592
+ *
6593
+ * Every column is copied through under its own name, with the value Postgres
6594
+ * returned. This used to open with a synthesized `id` and then skip the key
6595
+ * column, which renamed it (a `sku` primary key was served as `id`, and `sku`
6596
+ * did not appear at all) and restringified it (`42` → `"42"`). Consumers that
6597
+ * need an address derive it from the collection's primary keys.
6598
+ */
6599
+ drizzleResultToRestRow(row, collection) {
6600
+ const flat = {};
6601
+ const resolvedRelations = resolveCollectionRelations(collection);
6602
+ for (const [k, v] of Object.entries(row)) {
6603
+ const relation = findRelation(resolvedRelations, k);
6604
+ if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
6605
+ if (this.isJunctionRelation(relation, collection)) {
6606
+ const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
6607
+ if (nestedKey) return { ...item[nestedKey] };
6608
+ }
6609
+ return { ...item };
6610
+ });
6611
+ else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
6612
+ else flat[k] = v;
6613
+ }
6614
+ return flat;
6615
+ }
6616
+ /**
6745
6617
  * Build db.query-compatible options from standard fetch options.
6746
6618
  * Handles filter, search, orderBy, limit, and cursor-based pagination.
6747
6619
  */
@@ -6811,7 +6683,7 @@ var FetchService = class {
6811
6683
  async fetchOne(collectionPath, id, databaseId) {
6812
6684
  const collection = getCollectionByPath(collectionPath, this.registry);
6813
6685
  const table = getTableForCollection(collection, this.registry);
6814
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6686
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
6815
6687
  const idInfo = idInfoArray[0];
6816
6688
  const idField = table[idInfo.fieldName];
6817
6689
  if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
@@ -6825,7 +6697,7 @@ var FetchService = class {
6825
6697
  with: withConfig
6826
6698
  });
6827
6699
  if (!row) return void 0;
6828
- const flatRow = toCmsRow(row, collection, this.registry);
6700
+ const flatRow = this.drizzleResultToRow(row, collection);
6829
6701
  await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
6830
6702
  return flatRow;
6831
6703
  } catch (e) {
@@ -6867,7 +6739,7 @@ var FetchService = class {
6867
6739
  async fetchRowsWithConditions(collectionPath, options = {}) {
6868
6740
  const collection = getCollectionByPath(collectionPath, this.registry);
6869
6741
  const table = getTableForCollection(collection, this.registry);
6870
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6742
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
6871
6743
  const idInfo = idInfoArray[0];
6872
6744
  const idField = table[idInfo.fieldName];
6873
6745
  if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
@@ -6877,7 +6749,7 @@ var FetchService = class {
6877
6749
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
6878
6750
  if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
6879
6751
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
6880
- return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
6752
+ return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection));
6881
6753
  } catch (e) {
6882
6754
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
6883
6755
  logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
@@ -6938,10 +6810,8 @@ var FetchService = class {
6938
6810
  }
6939
6811
  /**
6940
6812
  * Fallback path used when db.query is unavailable.
6941
- *
6942
- * The primary path runs the results through `toCmsRow`, which maps
6943
- * relations from what drizzle already nested — no query per row. This one
6944
- * has no nesting to read, so it resolves relations itself, in batches.
6813
+ * The primary path uses drizzleResultToRow which handles relation
6814
+ * mapping without N+1 queries.
6945
6815
  *
6946
6816
  * Process raw database results into flat rows with relations.
6947
6817
  */
@@ -7083,7 +6953,7 @@ var FetchService = class {
7083
6953
  if (value === void 0 || value === null) return true;
7084
6954
  const collection = getCollectionByPath(collectionPath, this.registry);
7085
6955
  const table = getTableForCollection(collection, this.registry);
7086
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6956
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
7087
6957
  const idInfo = idInfoArray[0];
7088
6958
  const idField = table[idInfo.fieldName];
7089
6959
  const field = table[fieldName];
@@ -7110,7 +6980,7 @@ var FetchService = class {
7110
6980
  async fetchCollectionForRest(collectionPath, options = {}, include) {
7111
6981
  const collection = getCollectionByPath(collectionPath, this.registry);
7112
6982
  const table = getTableForCollection(collection, this.registry);
7113
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
6983
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
7114
6984
  const idInfo = idInfoArray[0];
7115
6985
  const idField = table[idInfo.fieldName];
7116
6986
  const tableName = getTableName(table);
@@ -7118,7 +6988,7 @@ var FetchService = class {
7118
6988
  if (qb && !options.searchString && !options.vectorSearch) try {
7119
6989
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
7120
6990
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
7121
- const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
6991
+ const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection));
7122
6992
  await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
7123
6993
  return restRows;
7124
6994
  } catch (e) {
@@ -7167,7 +7037,7 @@ var FetchService = class {
7167
7037
  async fetchOneForRest(collectionPath, id, include, databaseId) {
7168
7038
  const collection = getCollectionByPath(collectionPath, this.registry);
7169
7039
  const table = getTableForCollection(collection, this.registry);
7170
- const idInfoArray = requirePrimaryKeys(collection, this.registry);
7040
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
7171
7041
  const idInfo = idInfoArray[0];
7172
7042
  const idField = table[idInfo.fieldName];
7173
7043
  const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
@@ -7180,7 +7050,7 @@ var FetchService = class {
7180
7050
  ...withConfig ? { with: withConfig } : {}
7181
7051
  });
7182
7052
  if (!row) return null;
7183
- const restRow = toRestRow(row, collection, this.registry);
7053
+ const restRow = this.drizzleResultToRestRow(row, collection);
7184
7054
  await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
7185
7055
  return restRow;
7186
7056
  } catch (e) {
@@ -7225,7 +7095,7 @@ var FetchService = class {
7225
7095
  async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
7226
7096
  const collection = getCollectionByPath(collectionPath, this.registry);
7227
7097
  const table = getTableForCollection(collection, this.registry);
7228
- const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
7098
+ const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
7229
7099
  let vectorMeta;
7230
7100
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
7231
7101
  let query = vectorMeta ? this.db.select({
@@ -7714,7 +7584,7 @@ var PersistService = class {
7714
7584
  } catch (error) {
7715
7585
  throw this.toUserFriendlyError(error, collection.slug);
7716
7586
  }
7717
- const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId);
7587
+ const finalEntity = await this.fetchService.fetchOne(collection.slug, savedId, databaseId);
7718
7588
  if (!finalEntity) throw new Error("Could not fetch row after save.");
7719
7589
  return finalEntity;
7720
7590
  }
@@ -8633,14 +8503,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8633
8503
  let updatedValues = values;
8634
8504
  const contextForCallback = this.buildCallContext();
8635
8505
  let previousValuesForHistory;
8636
- if (status === "existing" && id) try {
8637
- const existing = await this.dataService.getFetchService().fetchOneForRest(path, id, void 0, resolvedCollection?.databaseId);
8506
+ if (status === "existing" && id) {
8507
+ const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
8638
8508
  if (existing) {
8639
8509
  const { id: _existingId, ...existingValues } = existing;
8640
8510
  previousValuesForHistory = existingValues;
8641
8511
  }
8642
- } catch (err) {
8643
- logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
8644
8512
  }
8645
8513
  if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
8646
8514
  if (globalCallbacks?.beforeSave) {
@@ -9253,7 +9121,6 @@ var DatabasePoolManager = class {
9253
9121
  pool.on("error", (err) => {
9254
9122
  logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
9255
9123
  });
9256
- guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
9257
9124
  this.pools.set(databaseName, pool);
9258
9125
  return pool;
9259
9126
  }
@@ -20814,22 +20681,14 @@ async function ensureAuthTablesExist(db, collection) {
20814
20681
  $$ LANGUAGE sql STABLE
20815
20682
  `);
20816
20683
  });
20817
- for (const columnDef of [
20818
- "display_name VARCHAR(255)",
20819
- "photo_url VARCHAR(500)",
20820
- "roles TEXT[] DEFAULT '{}' NOT NULL",
20821
- "password_hash VARCHAR(255)",
20822
- "email_verified BOOLEAN DEFAULT FALSE NOT NULL",
20823
- "email_verification_token VARCHAR(255)",
20824
- "email_verification_sent_at TIMESTAMP WITH TIME ZONE",
20825
- "is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
20826
- "metadata JSONB DEFAULT '{}' NOT NULL",
20827
- "created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
20828
- "updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
20829
- ]) await db.execute(sql`
20830
- ALTER TABLE ${sql.raw(usersTableName)}
20831
- ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
20832
- `);
20684
+ await db.execute(sql`
20685
+ ALTER TABLE ${sql.raw(usersTableName)}
20686
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
20687
+ `);
20688
+ await db.execute(sql`
20689
+ ALTER TABLE ${sql.raw(usersTableName)}
20690
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
20691
+ `);
20833
20692
  try {
20834
20693
  if ((await db.execute(sql`
20835
20694
  SELECT EXISTS (
@@ -20900,34 +20759,6 @@ async function ensureAuthTablesExist(db, collection) {
20900
20759
  CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
20901
20760
  ON ${sql.raw(recoveryCodesTableName)}(user_id)
20902
20761
  `);
20903
- try {
20904
- const authTablePairs = [
20905
- [usersSchema, resolvedTable],
20906
- [authSchema, "user_identities"],
20907
- [authSchema, "refresh_tokens"],
20908
- [authSchema, "password_reset_tokens"],
20909
- [authSchema, "app_config"],
20910
- [authSchema, "mfa_factors"],
20911
- [authSchema, "mfa_challenges"],
20912
- [authSchema, "recovery_codes"]
20913
- ];
20914
- for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
20915
- SELECT 1
20916
- FROM pg_class c
20917
- JOIN pg_namespace n ON n.oid = c.relnamespace
20918
- WHERE n.nspname = ${schemaName}
20919
- AND c.relname = ${tableName}
20920
- AND c.relforcerowsecurity
20921
- `)).rows.length > 0) {
20922
- await db.execute(sql`
20923
- ALTER TABLE ${sql.raw(`"${schemaName}"."${tableName}"`)}
20924
- NO FORCE ROW LEVEL SECURITY
20925
- `);
20926
- logger.warn(`🔧 Cleared stale FORCE ROW LEVEL SECURITY on "${schemaName}"."${tableName}" (legacy RLS model — it binds the owner connection and breaks privileged auth writes)`);
20927
- }
20928
- } catch (rlsReconcileError) {
20929
- logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
20930
- }
20931
20762
  logger.info("✅ Auth tables ready");
20932
20763
  } catch (error) {
20933
20764
  logger.error("❌ Failed to create auth tables", { error });
@@ -20975,31 +20806,6 @@ var UserService = class {
20975
20806
  const name = getTableName(this.usersTable);
20976
20807
  return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
20977
20808
  }
20978
- /**
20979
- * Run a privileged auth write with an explicitly cleared RLS context.
20980
- *
20981
- * The auth services run on the base/owner connection, which by design
20982
- * carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
20983
- * in the default policies applies. That NULL is normally guaranteed by
20984
- * `set_config(..., is_local = true)` resetting at transaction end — but a
20985
- * GUC that survives on a pooled connection (or a connection role that
20986
- * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
20987
- * turns the trusted write into an RLS-scoped one and denies it with
20988
- * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
20989
- * single chokepoint, makes the server context deterministic instead of
20990
- * trusting whatever state the pool hands us. `auth.uid()` reads '' as
20991
- * NULL via NULLIF, so '' is the server context.
20992
- */
20993
- async withServerContext(fn) {
20994
- return await this.db.transaction(async (tx) => {
20995
- await tx.execute(sql`
20996
- SELECT set_config('app.user_id', '', true),
20997
- set_config('app.user_roles', '', true),
20998
- set_config('app.jwt', '', true)
20999
- `);
21000
- return await fn(tx);
21001
- });
21002
- }
21003
20809
  mapRowToUser(row) {
21004
20810
  if (!row) return row;
21005
20811
  const id = row.id ?? row.uid;
@@ -21097,7 +20903,7 @@ var UserService = class {
21097
20903
  }
21098
20904
  async createUser(data) {
21099
20905
  const payload = this.mapPayload(data);
21100
- const [row] = await this.withServerContext(async (db) => await db.insert(this.usersTable).values(payload).returning());
20906
+ const [row] = await this.db.insert(this.usersTable).values(payload).returning();
21101
20907
  return this.mapRowToUser(row);
21102
20908
  }
21103
20909
  async getUserById(id) {
@@ -21136,12 +20942,12 @@ var UserService = class {
21136
20942
  }));
21137
20943
  }
21138
20944
  async linkUserIdentity(userId, provider, providerId, profileData) {
21139
- await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
20945
+ await this.db.insert(this.userIdentitiesTable).values({
21140
20946
  userId,
21141
20947
  provider,
21142
20948
  providerId,
21143
20949
  profileData: profileData || null
21144
- }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));
20950
+ }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
21145
20951
  }
21146
20952
  async updateUser(id, data) {
21147
20953
  const idCol = getColumn(this.usersTable, "id");
@@ -21149,13 +20955,13 @@ var UserService = class {
21149
20955
  const payload = this.mapPayload(data);
21150
20956
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21151
20957
  payload[updatedAtKey] = /* @__PURE__ */ new Date();
21152
- const [row] = await this.withServerContext(async (db) => await db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning());
20958
+ const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
21153
20959
  return row ? this.mapRowToUser(row) : null;
21154
20960
  }
21155
20961
  async deleteUser(id) {
21156
20962
  const idCol = getColumn(this.usersTable, "id");
21157
20963
  if (!idCol) return;
21158
- await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));
20964
+ await this.db.delete(this.usersTable).where(eq(idCol, id));
21159
20965
  }
21160
20966
  async listUsers() {
21161
20967
  return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
@@ -21209,10 +21015,10 @@ var UserService = class {
21209
21015
  if (!idCol) return;
21210
21016
  const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
21211
21017
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21212
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21018
+ await this.db.update(this.usersTable).set({
21213
21019
  [passwordHashColKey]: passwordHash,
21214
21020
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21215
- }).where(eq(idCol, id)));
21021
+ }).where(eq(idCol, id));
21216
21022
  }
21217
21023
  /**
21218
21024
  * Set email verification status
@@ -21223,11 +21029,11 @@ var UserService = class {
21223
21029
  const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
21224
21030
  const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
21225
21031
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21226
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21032
+ await this.db.update(this.usersTable).set({
21227
21033
  [emailVerifiedColKey]: verified,
21228
21034
  [emailVerificationTokenColKey]: null,
21229
21035
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21230
- }).where(eq(idCol, id)));
21036
+ }).where(eq(idCol, id));
21231
21037
  }
21232
21038
  /**
21233
21039
  * Set email verification token
@@ -21238,11 +21044,11 @@ var UserService = class {
21238
21044
  const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
21239
21045
  const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
21240
21046
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21241
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21047
+ await this.db.update(this.usersTable).set({
21242
21048
  [emailVerificationTokenColKey]: token,
21243
21049
  [emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
21244
21050
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21245
- }).where(eq(idCol, id)));
21051
+ }).where(eq(idCol, id));
21246
21052
  }
21247
21053
  /**
21248
21054
  * Find user by email verification token
@@ -21287,22 +21093,22 @@ var UserService = class {
21287
21093
  async setUserRoles(userId, roleIds) {
21288
21094
  const usersTableName = this.getQualifiedUsersTableName();
21289
21095
  const rolesArray = `{${roleIds.join(",")}}`;
21290
- await this.withServerContext(async (db) => db.execute(sql`
21096
+ await this.db.execute(sql`
21291
21097
  UPDATE ${sql.raw(usersTableName)}
21292
21098
  SET roles = ${rolesArray}::text[], updated_at = NOW()
21293
21099
  WHERE id = ${userId}
21294
- `));
21100
+ `);
21295
21101
  }
21296
21102
  /**
21297
21103
  * Assign a specific role to new user (appends if not present)
21298
21104
  */
21299
21105
  async assignDefaultRole(userId, roleId) {
21300
21106
  const usersTableName = this.getQualifiedUsersTableName();
21301
- await this.withServerContext(async (db) => db.execute(sql`
21107
+ await this.db.execute(sql`
21302
21108
  UPDATE ${sql.raw(usersTableName)}
21303
21109
  SET roles = array_append(roles, ${roleId}), updated_at = NOW()
21304
21110
  WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
21305
- `));
21111
+ `);
21306
21112
  }
21307
21113
  /**
21308
21114
  * Get user with their roles
@@ -22772,7 +22578,6 @@ function createPostgresBootstrapper(pgConfig) {
22772
22578
  const wantsCdc = cdcMode !== "off";
22773
22579
  const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
22774
22580
  let cdcEnabled = false;
22775
- let provisionCdcForTables;
22776
22581
  if (wantsCdc && !directUrl) {
22777
22582
  const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
22778
22583
  if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
@@ -22789,9 +22594,6 @@ function createPostgresBootstrapper(pgConfig) {
22789
22594
  })).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
22790
22595
  await realtimeService.enableCdc(directUrl);
22791
22596
  cdcEnabled = true;
22792
- provisionCdcForTables = async (tables) => {
22793
- await provisionTriggerCdc(cdcRunSql, tables);
22794
- };
22795
22597
  logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
22796
22598
  } catch (err) {
22797
22599
  if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
@@ -22816,14 +22618,13 @@ function createPostgresBootstrapper(pgConfig) {
22816
22618
  const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
22817
22619
  const missing = [];
22818
22620
  for (const col of registeredCollections) {
22819
- if (col.auth?.enabled) continue;
22820
22621
  const schemaName = "schema" in col && col.schema ? col.schema : "public";
22821
22622
  const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
22822
22623
  const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
22823
22624
  const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
22824
22625
  if (!dbTables.has(fullCheckName)) missing.push({
22825
22626
  slug: col.slug,
22826
- table: fullCheckName
22627
+ table: checkName
22827
22628
  });
22828
22629
  }
22829
22630
  if (missing.length > 0) {
@@ -22857,8 +22658,7 @@ function createPostgresBootstrapper(pgConfig) {
22857
22658
  registry,
22858
22659
  realtimeService,
22859
22660
  driver,
22860
- poolManager,
22861
- provisionCdcForTables
22661
+ poolManager
22862
22662
  }
22863
22663
  };
22864
22664
  },
@@ -22870,18 +22670,6 @@ function createPostgresBootstrapper(pgConfig) {
22870
22670
  const registry = internals.registry;
22871
22671
  const authCollection = authConfig.collection;
22872
22672
  await ensureAuthTablesExist(db, authCollection);
22873
- if (authCollection && internals.provisionCdcForTables) {
22874
- const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
22875
- const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
22876
- if (authTable) try {
22877
- await internals.provisionCdcForTables([{
22878
- schema: authSchema,
22879
- table: authTable
22880
- }]);
22881
- } catch (err) {
22882
- logger.warn(`⚠️ [CDC] Could not attach change-capture to the auth table "${authSchema}.${authTable}" — writes to it won't emit database-level events.`, { detail: err instanceof Error ? err.message : String(err) });
22883
- }
22884
- }
22885
22673
  let emailService;
22886
22674
  if (authConfig.email) emailService = createEmailService(authConfig.email);
22887
22675
  const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
@@ -22952,6 +22740,6 @@ function createPostgresAdapter(pgConfig) {
22952
22740
  };
22953
22741
  }
22954
22742
  //#endregion
22955
- export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, guardPoolAgainstDirtyRelease, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, withDatabaseName };
22743
+ export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, withDatabaseName };
22956
22744
 
22957
22745
  //# sourceMappingURL=index.es.js.map