@rebasepro/server-postgres 0.9.1-canary.97e305f → 0.9.1-canary.a57c262

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.
@@ -9,7 +9,6 @@ import { PostgresBackendDriver } from "./PostgresBackendDriver";
9
9
  import { RealtimeService } from "./services/realtimeService";
10
10
  import { DatabasePoolManager } from "./databasePoolManager";
11
11
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
12
- import { type CdcTableRef } from "./services/cdc/trigger-cdc";
13
12
  export interface PostgresDriverConfig {
14
13
  connectionString?: string;
15
14
  adminConnectionString?: string;
@@ -37,15 +36,6 @@ export interface PostgresDriverInternals {
37
36
  realtimeService: RealtimeService;
38
37
  driver: PostgresBackendDriver;
39
38
  poolManager?: DatabasePoolManager;
40
- /**
41
- * Attach CDC triggers to tables that did not exist when the driver
42
- * bootstrapped. Only set when database-level capture is actually active.
43
- *
44
- * Auth owns its own tables and creates them later in boot, so at driver
45
- * bootstrap they are legitimately missing and get skipped; without this
46
- * they would stay uninstrumented until the next restart.
47
- */
48
- provisionCdcForTables?: (tables: CdcTableRef[]) => Promise<void>;
49
39
  }
50
40
  /**
51
41
  * Default PostgreSQL bootstrapper.
@@ -19,22 +19,6 @@ export declare class UserService implements UserRepository {
19
19
  private userIdentitiesTable;
20
20
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
21
21
  private getQualifiedUsersTableName;
22
- /**
23
- * Run a privileged auth write with an explicitly cleared RLS context.
24
- *
25
- * The auth services run on the base/owner connection, which by design
26
- * carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
27
- * in the default policies applies. That NULL is normally guaranteed by
28
- * `set_config(..., is_local = true)` resetting at transaction end — but a
29
- * GUC that survives on a pooled connection (or a connection role that
30
- * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
31
- * turns the trusted write into an RLS-scoped one and denies it with
32
- * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
33
- * single chokepoint, makes the server context deterministic instead of
34
- * trusting whatever state the pool hands us. `auth.uid()` reads '' as
35
- * NULL via NULLIF, so '' is the server context.
36
- */
37
- private withServerContext;
38
22
  private mapRowToUser;
39
23
  private mapPayload;
40
24
  createUser(data: CreateUserData): Promise<UserData>;
@@ -19,27 +19,6 @@ export interface PostgresPoolConfig {
19
19
  /** Enable TCP keep-alive (default: true) */
20
20
  keepAlive?: boolean;
21
21
  }
22
- /**
23
- * Destroy pool clients that are released while still inside a transaction.
24
- *
25
- * pg-pool returns a client to the idle list whenever `release()` is called
26
- * without an error — even if the connection is still mid-transaction (status
27
- * `T`/`E`). That happens in practice: drizzle's pool transaction releases in
28
- * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
29
- * (e.g. it was queued behind a statement that hit the client-side
30
- * query_timeout), the client goes back dirty. The next checkout then runs
31
- * its statements inside the zombie transaction — with the previous request's
32
- * `app.*` RLS GUCs still applied, which turns unrelated queries into
33
- * RLS-scoped ones (observed in production as registration failing with
34
- * SQLSTATE 42501 under a leaked anonymous context).
35
- *
36
- * pg-pool emits `release` before it consults its private `_expired` set, so
37
- * marking the client expired here makes `_release()` destroy it instead of
38
- * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
39
- * private APIs — feature-detect and fall back to loud logging so an upstream
40
- * change degrades to observability, never to silent corruption.
41
- */
42
- export declare function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void;
43
22
  /**
44
23
  * Create a Drizzle-backed Postgres connection with a production-grade
45
24
  * connection pool.
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,
@@ -5266,7 +5228,6 @@ var DrizzleConditionBuilder = class {
5266
5228
  static buildVectorSearchConditions(table, vectorSearch) {
5267
5229
  const column = table[vectorSearch.property];
5268
5230
  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
5231
  const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
5271
5232
  const distanceFn = vectorSearch.distance || "cosine";
5272
5233
  let operator;
@@ -6495,56 +6456,9 @@ function unwrapJunctionRow(item) {
6495
6456
  const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
6496
6457
  return nestedKey ? item[nestedKey] : item;
6497
6458
  }
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
6459
  /** 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
6460
  function renderTarget(targetRow, targetCollection, style, registry) {
6547
- if (style === "inline") return stripExcluded(coerceDeclaredNumbers({ ...targetRow }, targetCollection), targetCollection);
6461
+ if (style === "inline") return { ...targetRow };
6548
6462
  const address = relationTargetAddress(targetRow, targetCollection, registry);
6549
6463
  const path = targetCollection.slug;
6550
6464
  return createRelationRefWithData(address, path, {
@@ -6586,17 +6500,14 @@ function toCmsRow(row, collection, registry) {
6586
6500
  normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
6587
6501
  } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
6588
6502
  }
6589
- return stripExcluded(normalized, collection);
6503
+ return normalized;
6590
6504
  }
6591
6505
  /**
6592
6506
  * The row REST serves: every column under its own name, with the value Postgres
6593
6507
  * returned, and relations inlined as the target's columns.
6594
6508
  *
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.
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.
6600
6511
  *
6601
6512
  * Keyed by the row rather than by the relation list — a REST fetch only loads
6602
6513
  * the relations `include` asked for, so the row is the authority on which are
@@ -6609,9 +6520,9 @@ function toRestRow(row, collection, registry) {
6609
6520
  const relation = findRelation(resolvedRelations, key);
6610
6521
  if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
6611
6522
  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]);
6523
+ else flat[key] = value;
6613
6524
  }
6614
- return stripExcluded(flat, collection);
6525
+ return flat;
6615
6526
  }
6616
6527
  //#endregion
6617
6528
  //#region src/services/FetchService.ts
@@ -7714,7 +7625,7 @@ var PersistService = class {
7714
7625
  } catch (error) {
7715
7626
  throw this.toUserFriendlyError(error, collection.slug);
7716
7627
  }
7717
- const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId);
7628
+ const finalEntity = await this.fetchService.fetchOne(collection.slug, savedId, databaseId);
7718
7629
  if (!finalEntity) throw new Error("Could not fetch row after save.");
7719
7630
  return finalEntity;
7720
7631
  }
@@ -8633,14 +8544,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8633
8544
  let updatedValues = values;
8634
8545
  const contextForCallback = this.buildCallContext();
8635
8546
  let previousValuesForHistory;
8636
- if (status === "existing" && id) try {
8637
- const existing = await this.dataService.getFetchService().fetchOneForRest(path, id, void 0, resolvedCollection?.databaseId);
8547
+ if (status === "existing" && id) {
8548
+ const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
8638
8549
  if (existing) {
8639
8550
  const { id: _existingId, ...existingValues } = existing;
8640
8551
  previousValuesForHistory = existingValues;
8641
8552
  }
8642
- } catch (err) {
8643
- logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
8644
8553
  }
8645
8554
  if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
8646
8555
  if (globalCallbacks?.beforeSave) {
@@ -9253,7 +9162,6 @@ var DatabasePoolManager = class {
9253
9162
  pool.on("error", (err) => {
9254
9163
  logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
9255
9164
  });
9256
- guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
9257
9165
  this.pools.set(databaseName, pool);
9258
9166
  return pool;
9259
9167
  }
@@ -20814,22 +20722,14 @@ async function ensureAuthTablesExist(db, collection) {
20814
20722
  $$ LANGUAGE sql STABLE
20815
20723
  `);
20816
20724
  });
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
- `);
20725
+ await db.execute(sql`
20726
+ ALTER TABLE ${sql.raw(usersTableName)}
20727
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
20728
+ `);
20729
+ await db.execute(sql`
20730
+ ALTER TABLE ${sql.raw(usersTableName)}
20731
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
20732
+ `);
20833
20733
  try {
20834
20734
  if ((await db.execute(sql`
20835
20735
  SELECT EXISTS (
@@ -20900,34 +20800,6 @@ async function ensureAuthTablesExist(db, collection) {
20900
20800
  CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
20901
20801
  ON ${sql.raw(recoveryCodesTableName)}(user_id)
20902
20802
  `);
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
20803
  logger.info("✅ Auth tables ready");
20932
20804
  } catch (error) {
20933
20805
  logger.error("❌ Failed to create auth tables", { error });
@@ -20975,31 +20847,6 @@ var UserService = class {
20975
20847
  const name = getTableName(this.usersTable);
20976
20848
  return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
20977
20849
  }
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
20850
  mapRowToUser(row) {
21004
20851
  if (!row) return row;
21005
20852
  const id = row.id ?? row.uid;
@@ -21097,7 +20944,7 @@ var UserService = class {
21097
20944
  }
21098
20945
  async createUser(data) {
21099
20946
  const payload = this.mapPayload(data);
21100
- const [row] = await this.withServerContext(async (db) => await db.insert(this.usersTable).values(payload).returning());
20947
+ const [row] = await this.db.insert(this.usersTable).values(payload).returning();
21101
20948
  return this.mapRowToUser(row);
21102
20949
  }
21103
20950
  async getUserById(id) {
@@ -21136,12 +20983,12 @@ var UserService = class {
21136
20983
  }));
21137
20984
  }
21138
20985
  async linkUserIdentity(userId, provider, providerId, profileData) {
21139
- await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
20986
+ await this.db.insert(this.userIdentitiesTable).values({
21140
20987
  userId,
21141
20988
  provider,
21142
20989
  providerId,
21143
20990
  profileData: profileData || null
21144
- }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));
20991
+ }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
21145
20992
  }
21146
20993
  async updateUser(id, data) {
21147
20994
  const idCol = getColumn(this.usersTable, "id");
@@ -21149,13 +20996,13 @@ var UserService = class {
21149
20996
  const payload = this.mapPayload(data);
21150
20997
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21151
20998
  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());
20999
+ const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
21153
21000
  return row ? this.mapRowToUser(row) : null;
21154
21001
  }
21155
21002
  async deleteUser(id) {
21156
21003
  const idCol = getColumn(this.usersTable, "id");
21157
21004
  if (!idCol) return;
21158
- await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));
21005
+ await this.db.delete(this.usersTable).where(eq(idCol, id));
21159
21006
  }
21160
21007
  async listUsers() {
21161
21008
  return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
@@ -21209,10 +21056,10 @@ var UserService = class {
21209
21056
  if (!idCol) return;
21210
21057
  const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
21211
21058
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21212
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21059
+ await this.db.update(this.usersTable).set({
21213
21060
  [passwordHashColKey]: passwordHash,
21214
21061
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21215
- }).where(eq(idCol, id)));
21062
+ }).where(eq(idCol, id));
21216
21063
  }
21217
21064
  /**
21218
21065
  * Set email verification status
@@ -21223,11 +21070,11 @@ var UserService = class {
21223
21070
  const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
21224
21071
  const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
21225
21072
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21226
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21073
+ await this.db.update(this.usersTable).set({
21227
21074
  [emailVerifiedColKey]: verified,
21228
21075
  [emailVerificationTokenColKey]: null,
21229
21076
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21230
- }).where(eq(idCol, id)));
21077
+ }).where(eq(idCol, id));
21231
21078
  }
21232
21079
  /**
21233
21080
  * Set email verification token
@@ -21238,11 +21085,11 @@ var UserService = class {
21238
21085
  const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
21239
21086
  const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
21240
21087
  const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
21241
- await this.withServerContext(async (db) => db.update(this.usersTable).set({
21088
+ await this.db.update(this.usersTable).set({
21242
21089
  [emailVerificationTokenColKey]: token,
21243
21090
  [emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
21244
21091
  [updatedAtColKey]: /* @__PURE__ */ new Date()
21245
- }).where(eq(idCol, id)));
21092
+ }).where(eq(idCol, id));
21246
21093
  }
21247
21094
  /**
21248
21095
  * Find user by email verification token
@@ -21287,22 +21134,22 @@ var UserService = class {
21287
21134
  async setUserRoles(userId, roleIds) {
21288
21135
  const usersTableName = this.getQualifiedUsersTableName();
21289
21136
  const rolesArray = `{${roleIds.join(",")}}`;
21290
- await this.withServerContext(async (db) => db.execute(sql`
21137
+ await this.db.execute(sql`
21291
21138
  UPDATE ${sql.raw(usersTableName)}
21292
21139
  SET roles = ${rolesArray}::text[], updated_at = NOW()
21293
21140
  WHERE id = ${userId}
21294
- `));
21141
+ `);
21295
21142
  }
21296
21143
  /**
21297
21144
  * Assign a specific role to new user (appends if not present)
21298
21145
  */
21299
21146
  async assignDefaultRole(userId, roleId) {
21300
21147
  const usersTableName = this.getQualifiedUsersTableName();
21301
- await this.withServerContext(async (db) => db.execute(sql`
21148
+ await this.db.execute(sql`
21302
21149
  UPDATE ${sql.raw(usersTableName)}
21303
21150
  SET roles = array_append(roles, ${roleId}), updated_at = NOW()
21304
21151
  WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
21305
- `));
21152
+ `);
21306
21153
  }
21307
21154
  /**
21308
21155
  * Get user with their roles
@@ -22772,7 +22619,6 @@ function createPostgresBootstrapper(pgConfig) {
22772
22619
  const wantsCdc = cdcMode !== "off";
22773
22620
  const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
22774
22621
  let cdcEnabled = false;
22775
- let provisionCdcForTables;
22776
22622
  if (wantsCdc && !directUrl) {
22777
22623
  const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
22778
22624
  if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
@@ -22789,9 +22635,6 @@ function createPostgresBootstrapper(pgConfig) {
22789
22635
  })).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
22790
22636
  await realtimeService.enableCdc(directUrl);
22791
22637
  cdcEnabled = true;
22792
- provisionCdcForTables = async (tables) => {
22793
- await provisionTriggerCdc(cdcRunSql, tables);
22794
- };
22795
22638
  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
22639
  } catch (err) {
22797
22640
  if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
@@ -22816,14 +22659,13 @@ function createPostgresBootstrapper(pgConfig) {
22816
22659
  const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
22817
22660
  const missing = [];
22818
22661
  for (const col of registeredCollections) {
22819
- if (col.auth?.enabled) continue;
22820
22662
  const schemaName = "schema" in col && col.schema ? col.schema : "public";
22821
22663
  const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
22822
22664
  const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
22823
22665
  const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
22824
22666
  if (!dbTables.has(fullCheckName)) missing.push({
22825
22667
  slug: col.slug,
22826
- table: fullCheckName
22668
+ table: checkName
22827
22669
  });
22828
22670
  }
22829
22671
  if (missing.length > 0) {
@@ -22857,8 +22699,7 @@ function createPostgresBootstrapper(pgConfig) {
22857
22699
  registry,
22858
22700
  realtimeService,
22859
22701
  driver,
22860
- poolManager,
22861
- provisionCdcForTables
22702
+ poolManager
22862
22703
  }
22863
22704
  };
22864
22705
  },
@@ -22870,18 +22711,6 @@ function createPostgresBootstrapper(pgConfig) {
22870
22711
  const registry = internals.registry;
22871
22712
  const authCollection = authConfig.collection;
22872
22713
  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
22714
  let emailService;
22886
22715
  if (authConfig.email) emailService = createEmailService(authConfig.email);
22887
22716
  const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
@@ -22952,6 +22781,6 @@ function createPostgresAdapter(pgConfig) {
22952
22781
  };
22953
22782
  }
22954
22783
  //#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 };
22784
+ 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
22785
 
22957
22786
  //# sourceMappingURL=index.es.js.map