@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.
- package/dist/PostgresBootstrapper.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -16
- package/dist/connection.d.ts +0 -21
- package/dist/index.es.js +37 -208
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/services/row-pipeline.d.ts +2 -5
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +5 -18
- package/src/PostgresBootstrapper.ts +2 -51
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/connection.ts +1 -61
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/doctor.ts +20 -45
- package/src/schema/introspect-db.ts +1 -11
- package/src/services/PersistService.ts +2 -9
- package/src/services/row-pipeline.ts +7 -70
- package/src/utils/drizzle-conditions.ts +0 -13
|
@@ -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.
|
package/dist/auth/services.d.ts
CHANGED
|
@@ -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>;
|
package/dist/connection.d.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
*
|
|
6596
|
-
*
|
|
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] =
|
|
6523
|
+
else flat[key] = value;
|
|
6613
6524
|
}
|
|
6614
|
-
return
|
|
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.
|
|
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)
|
|
8637
|
-
const existing = await this.dataService.
|
|
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
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
20820
|
-
|
|
20821
|
-
|
|
20822
|
-
|
|
20823
|
-
|
|
20824
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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,
|
|
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
|