@rebasepro/server-postgres 0.23.0 → 0.24.0
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/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +84 -12
- package/dist/auth/services.d.ts +72 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
- package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
- package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +12 -12
- package/dist/cli.js +91 -37
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +3415 -917
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +33 -21
- package/dist/schema/destructive-sql.d.ts +20 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -3
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +1 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +37 -11
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +5 -4
- package/dist/services/RelationService.d.ts +18 -0
- package/dist/services/cdc/CdcListener.d.ts +9 -4
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +11 -1
- package/dist/services/pg-notify-listener.d.ts +88 -6
- package/dist/services/realtimeService.d.ts +159 -45
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +6 -3
- package/dist/utils/drizzle-conditions.d.ts +42 -2
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +50 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import { d as extractPgError, u as extractCauseMessage } from "./cli-errors-
|
|
4
|
+
import { d as extractPgError, u as extractCauseMessage } from "./cli-errors-DsA-K9uP.js";
|
|
5
5
|
import { sql } from "drizzle-orm";
|
|
6
6
|
import { revokeInternalTableSql } from "@rebasepro/common";
|
|
7
7
|
//#region src/services/BranchService.ts
|
|
@@ -106,9 +106,20 @@ function toBranchDbName(name) {
|
|
|
106
106
|
var BranchService = class {
|
|
107
107
|
db;
|
|
108
108
|
poolManager;
|
|
109
|
-
|
|
109
|
+
home;
|
|
110
|
+
/**
|
|
111
|
+
* @param db A connection on the server the branches are made on. Branch
|
|
112
|
+
* DDL runs here.
|
|
113
|
+
* @param home Where branches are kept. Without one, the database the
|
|
114
|
+
* pool manager's connection string names, and `db`'s registry.
|
|
115
|
+
*/
|
|
116
|
+
constructor(db, poolManager, home) {
|
|
110
117
|
this.db = db;
|
|
111
118
|
this.poolManager = poolManager;
|
|
119
|
+
this.home = home ?? {
|
|
120
|
+
database: async () => poolManager.defaultDatabaseName,
|
|
121
|
+
registry: async () => db
|
|
122
|
+
};
|
|
112
123
|
}
|
|
113
124
|
/**
|
|
114
125
|
* Refuse a branch mutation the connected server cannot honour.
|
|
@@ -130,8 +141,9 @@ var BranchService = class {
|
|
|
130
141
|
* Idempotent — safe to call on every startup.
|
|
131
142
|
*/
|
|
132
143
|
async ensureBranchMetadataTable() {
|
|
133
|
-
await this.
|
|
134
|
-
await
|
|
144
|
+
const registry = await this.home.registry();
|
|
145
|
+
await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
146
|
+
await registry.execute(sql.raw(`
|
|
135
147
|
CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (
|
|
136
148
|
name TEXT PRIMARY KEY,
|
|
137
149
|
db_name TEXT NOT NULL UNIQUE,
|
|
@@ -140,7 +152,7 @@ var BranchService = class {
|
|
|
140
152
|
metadata JSONB DEFAULT '{}'
|
|
141
153
|
);
|
|
142
154
|
`));
|
|
143
|
-
await
|
|
155
|
+
await registry.execute(sql.raw(revokeInternalTableSql("rebase", "branches")));
|
|
144
156
|
}
|
|
145
157
|
/**
|
|
146
158
|
* Create a new branch database by templating the source database.
|
|
@@ -155,8 +167,8 @@ var BranchService = class {
|
|
|
155
167
|
await this.assertBranchingSupported();
|
|
156
168
|
if (options?.source) validateIdentifier(options.source, "source database name");
|
|
157
169
|
const dbName = toBranchDbName(name);
|
|
158
|
-
const sourceDb = options?.source || this.
|
|
159
|
-
if ((await this.
|
|
170
|
+
const sourceDb = options?.source || await this.home.database();
|
|
171
|
+
if ((await (await this.home.registry()).execute(sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`)).rows.length > 0) throw new Error(`Branch "${name}" already exists.`);
|
|
160
172
|
await this.poolManager.disconnectDatabase(sourceDb);
|
|
161
173
|
if (options?.force) {
|
|
162
174
|
if (await this.terminateConnections(sourceDb) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
@@ -170,10 +182,11 @@ var BranchService = class {
|
|
|
170
182
|
throw describeBranchDdlError(err, dbName);
|
|
171
183
|
}
|
|
172
184
|
const now = /* @__PURE__ */ new Date();
|
|
173
|
-
await this.
|
|
185
|
+
await (await this.home.registry()).execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
|
|
174
186
|
VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`);
|
|
175
187
|
return {
|
|
176
188
|
name,
|
|
189
|
+
database: dbName,
|
|
177
190
|
parentDatabase: sourceDb,
|
|
178
191
|
createdAt: now
|
|
179
192
|
};
|
|
@@ -185,11 +198,12 @@ var BranchService = class {
|
|
|
185
198
|
async deleteBranch(name, options) {
|
|
186
199
|
await this.assertBranchingSupported();
|
|
187
200
|
assertValidBranchName(name);
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
const mainDatabase = await this.home.database();
|
|
202
|
+
if (toBranchDbName(name) === mainDatabase) throw new Error("Cannot delete the main database.");
|
|
203
|
+
const existingRows = (await (await this.home.registry()).execute(sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`)).rows;
|
|
190
204
|
if (existingRows.length === 0) throw new Error(`Branch "${name}" not found.`);
|
|
191
205
|
const dbName = existingRows[0].db_name;
|
|
192
|
-
if (dbName ===
|
|
206
|
+
if (dbName === mainDatabase) throw new Error("Cannot delete the main database.");
|
|
193
207
|
await this.poolManager.disconnectDatabase(dbName);
|
|
194
208
|
if (options?.force) {
|
|
195
209
|
if (await this.terminateConnections(dbName) > 0) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
@@ -201,7 +215,7 @@ var BranchService = class {
|
|
|
201
215
|
if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${name}": the database has active connections.\n` + await this.describeBlockingConnections(dbName));
|
|
202
216
|
throw describeBranchDdlError(err, dbName);
|
|
203
217
|
}
|
|
204
|
-
await this.
|
|
218
|
+
await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
|
|
205
219
|
}
|
|
206
220
|
/**
|
|
207
221
|
* Who is holding the database open, in words a developer can act on.
|
|
@@ -268,7 +282,7 @@ var BranchService = class {
|
|
|
268
282
|
*/
|
|
269
283
|
async pruneCandidates() {
|
|
270
284
|
return {
|
|
271
|
-
rows: (await this.
|
|
285
|
+
rows: (await (await this.home.registry()).execute(sql.raw(`
|
|
272
286
|
SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at
|
|
273
287
|
`))).rows.map((row) => ({
|
|
274
288
|
name: row.name,
|
|
@@ -282,7 +296,7 @@ var BranchService = class {
|
|
|
282
296
|
}
|
|
283
297
|
/** Remove a metadata row whose database is already gone. */
|
|
284
298
|
async forgetBranchRow(name) {
|
|
285
|
-
await this.
|
|
299
|
+
await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
|
|
286
300
|
}
|
|
287
301
|
/**
|
|
288
302
|
* Drop a database by name, with no metadata row required.
|
|
@@ -295,7 +309,7 @@ var BranchService = class {
|
|
|
295
309
|
*/
|
|
296
310
|
async dropDatabase(dbName) {
|
|
297
311
|
validateIdentifier(dbName, "database name");
|
|
298
|
-
if (dbName === this.
|
|
312
|
+
if (dbName === await this.home.database()) throw new Error("Cannot delete the main database.");
|
|
299
313
|
await this.poolManager.disconnectDatabase(dbName);
|
|
300
314
|
try {
|
|
301
315
|
await this.db.execute(sql.raw(`DROP DATABASE "${dbName.replace(/"/g, "\"\"")}"`));
|
|
@@ -308,9 +322,10 @@ var BranchService = class {
|
|
|
308
322
|
* Optionally fetches database sizes from pg_database.
|
|
309
323
|
*/
|
|
310
324
|
async listBranches() {
|
|
311
|
-
return (await this.
|
|
325
|
+
return (await (await this.home.registry()).execute(sql.raw(`
|
|
312
326
|
SELECT
|
|
313
327
|
b.name,
|
|
328
|
+
b.db_name,
|
|
314
329
|
b.parent_db,
|
|
315
330
|
b.created_at,
|
|
316
331
|
pg_database_size(b.db_name) as size_bytes
|
|
@@ -319,6 +334,7 @@ var BranchService = class {
|
|
|
319
334
|
ORDER BY b.created_at DESC
|
|
320
335
|
`))).rows.map((row) => ({
|
|
321
336
|
name: row.name,
|
|
337
|
+
database: row.db_name,
|
|
322
338
|
parentDatabase: row.parent_db,
|
|
323
339
|
createdAt: new Date(row.created_at),
|
|
324
340
|
sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : void 0
|
|
@@ -329,7 +345,7 @@ var BranchService = class {
|
|
|
329
345
|
*/
|
|
330
346
|
async getBranchInfo(name) {
|
|
331
347
|
assertValidBranchName(name);
|
|
332
|
-
const rows = (await this.
|
|
348
|
+
const rows = (await (await this.home.registry()).execute(sql`
|
|
333
349
|
SELECT
|
|
334
350
|
b.name,
|
|
335
351
|
b.db_name,
|
|
@@ -348,6 +364,7 @@ var BranchService = class {
|
|
|
348
364
|
} catch {}
|
|
349
365
|
return {
|
|
350
366
|
name: row.name,
|
|
367
|
+
database: row.db_name,
|
|
351
368
|
parentDatabase: row.parent_db,
|
|
352
369
|
createdAt: new Date(row.created_at),
|
|
353
370
|
sizeBytes
|
|
@@ -357,4 +374,4 @@ var BranchService = class {
|
|
|
357
374
|
//#endregion
|
|
358
375
|
export { BranchService_exports as n, BranchingUnsupportedError as r, BranchService as t };
|
|
359
376
|
|
|
360
|
-
//# sourceMappingURL=BranchService-
|
|
377
|
+
//# sourceMappingURL=BranchService-BRt78gfa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchService-BRt78gfa.js","names":[],"sources":["../src/services/BranchService.ts"],"sourcesContent":["/**\n * BranchService\n *\n * Manages database branching by creating/deleting PostgreSQL databases\n * using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the\n * `rebase.branches` table in the default (main) database, following the\n * same `rebase` schema convention used by entity_history, auth, etc.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { BranchInfo } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { DrizzleClient } from \"../interfaces\";\nimport { BranchRow } from \"../branch-prune\";\nimport { DatabasePoolManager } from \"../databasePoolManager\";\nimport { extractPgError, extractCauseMessage } from \"../utils/pg-error-utils\";\n\n/** Internal prefix applied to branch database names to avoid collisions. */\nconst BRANCH_DB_PREFIX = \"rb_\";\n\n/** `duplicate_database` — the target database name is already taken. */\nconst PG_DUPLICATE_DATABASE = \"42P04\";\n\n/** `object_in_use` — the database still has connections attached. */\nconst PG_OBJECT_IN_USE = \"55006\";\n\n/**\n * Describe a failed branch DDL statement in terms a user can act on.\n *\n * Drizzle reports failures as `Failed query: <sql> params:` and hides the real\n * PostgreSQL error in the `cause` chain, so matching on `err.message` never sees\n * the actual problem. Match on the PG error code instead — it survives wrapping\n * and, unlike the message text, is not locale-dependent.\n */\nfunction describeBranchDdlError(err: unknown, fallbackContext: string): Error {\n const pgError = extractPgError(err);\n\n if (pgError?.code === PG_DUPLICATE_DATABASE) {\n return new Error(`Database \"${fallbackContext}\" already exists on the server. Choose a different branch name.`);\n }\n if (pgError?.code === PG_OBJECT_IN_USE) {\n return new Error(\n `Cannot complete the operation: the database \"${fallbackContext}\" has active connections. ` +\n \"Close other clients or connections and try again.\"\n );\n }\n\n // Unknown failure: surface the real PG message rather than the Drizzle\n // wrapper, which would otherwise show the raw SQL and no reason at all.\n const detail = pgError?.message ?? extractCauseMessage(err);\n if (detail) return new Error(detail);\n return err instanceof Error ? err : new Error(String(err));\n}\n\n/** Fully-qualified metadata table in the rebase schema. */\nconst BRANCHES_TABLE = \"rebase.branches\";\n\n/**\n * The connected server cannot branch, so nothing was done.\n *\n * A refusal, not a fault: typed so the transports can forward it intact.\n * `websocket.ts` answers it with its own code and message rather than letting\n * it fall into the generic handler, which suppresses the text in production —\n * and the text is the only part that says what to do instead.\n */\nexport class BranchingUnsupportedError extends Error {\n /** Wire code, forwarded verbatim to the client. */\n readonly code = \"BRANCHING_UNSUPPORTED\";\n\n constructor(message: string) {\n super(message);\n this.name = \"BranchingUnsupportedError\";\n }\n}\n\n/**\n * What to say when the server serves one database.\n *\n * Deliberately the same argument the CLI's `refuseBranchOnManagedDatabase`\n * makes, because it is the same refusal — it just used to be available only to\n * whoever came in through argv. One paragraph, because this one has to fit in a\n * toast as well as a terminal.\n */\nconst BRANCHING_UNSUPPORTED_MESSAGE =\n \"Branching does not work on the managed development database. It copies a database with \"\n + \"CREATE DATABASE ... TEMPLATE, and the managed database is PGlite, which serves exactly one — \"\n + \"the copy would be the original, and every write you meant to sandbox would land in it. \"\n + \"Branching needs a real Postgres: run `rebase dev --docker`, or uncomment DATABASE_URL in .env \"\n + \"to point at your own.\";\n\n/**\n * Validate that a user-provided identifier only contains safe characters.\n * Throws if the value contains characters outside [a-zA-Z0-9_-].\n */\nfunction validateIdentifier(value: string, label: string): void {\n if (!/^[a-zA-Z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);\n }\n}\n\n/**\n * Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it\n * silently. The prefix comes out of the same budget.\n */\nconst MAX_BRANCH_NAME_LENGTH = 63 - BRANCH_DB_PREFIX.length;\n\n/**\n * Check a user-provided branch name, and otherwise leave it exactly as given.\n *\n * This used to strip everything outside [a-zA-Z0-9_], so `my-feature` was\n * quietly created as `myfeature`: the name you typed was not the name `list`\n * gave back. Nothing ever needed that. Every identifier this service builds is\n * double-quoted (see `CREATE DATABASE` below), which is what makes a hyphen\n * safe, and `validateIdentifier` has always accepted hyphens for the `--from`\n * source database — the two disagreed about the same character class.\n *\n * Refusing a name we cannot represent is better than representing a different\n * one, which is also why the length is checked here rather than left to\n * Postgres, whose answer to an over-long identifier is a silent rename.\n */\nfunction assertValidBranchName(name: string): void {\n validateIdentifier(name, \"branch name\");\n if (name.length > MAX_BRANCH_NAME_LENGTH) {\n throw new Error(\n `Branch name \"${name}\" is too long: ${name.length} characters, maximum ${MAX_BRANCH_NAME_LENGTH}. ` +\n \"Postgres truncates identifiers past 63 bytes, which would give the branch a name you did not choose.\"\n );\n }\n}\n\n/**\n * Convert a user-facing branch name to the actual PostgreSQL database name.\n */\nfunction toBranchDbName(name: string): string {\n assertValidBranchName(name);\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/**\n * Where a server's branches are kept: the main database — what a branch is\n * copied from unless told otherwise, and what may never be dropped — and the\n * connection whose `rebase.branches` records them.\n */\nexport interface BranchHome {\n /** The main database's name. */\n database(): Promise<string>;\n /**\n * A connection on the database the registry is in. Asked for at each use:\n * a pool to the main database is closed before it is copied.\n */\n registry(): Promise<DrizzleClient>;\n}\n\nexport class BranchService {\n private readonly home: BranchHome;\n\n /**\n * @param db A connection on the server the branches are made on. Branch\n * DDL runs here.\n * @param home Where branches are kept. Without one, the database the\n * pool manager's connection string names, and `db`'s registry.\n */\n constructor(\n private db: DrizzleClient,\n private poolManager: DatabasePoolManager,\n home?: BranchHome\n ) {\n this.home = home ?? {\n database: async () => poolManager.defaultDatabaseName,\n registry: async () => db\n };\n }\n\n /**\n * Refuse a branch mutation the connected server cannot honour.\n *\n * The check lives here rather than in the CLI's argv parsing because the\n * CLI is not the only caller: Studio reaches `createBranch` over the\n * websocket, and before this it got \"Branch created successfully\" and a\n * detail pane promising isolation for a database that is the parent.\n *\n * Reads only, and only mutations are guarded: `listBranches` on a managed\n * database is an honest empty list, and refusing it would turn the Branches\n * pane into a failure with a \"Try again\" that can never succeed.\n */\n private async assertBranchingSupported(): Promise<void> {\n if (await this.poolManager.servesOneDatabase()) {\n throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);\n }\n }\n\n /**\n * Ensure the `rebase.branches` metadata table exists in the default database.\n * Idempotent — safe to call on every startup.\n */\n async ensureBranchMetadataTable(): Promise<void> {\n // Create the rebase schema (idempotent — may already exist from auth/history init)\n const registry = await this.home.registry();\n await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await registry.execute(sql.raw(`\n CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (\n name TEXT PRIMARY KEY,\n db_name TEXT NOT NULL UNIQUE,\n parent_db TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB DEFAULT '{}'\n );\n `));\n\n // Not a collection, so no RLS — and it names every branch database on\n // this server. The driver's schema-wide grant reaches it, so revoke.\n await registry.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"branches\")));\n }\n\n /**\n * Create a new branch database by templating the source database.\n *\n * Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy\n * of both schema and data.\n *\n * @param name User-facing branch name (e.g., \"feature_auth\")\n * @param options.source Source database to clone; defaults to the main database.\n */\n async createBranch(\n name: string,\n options?: { source?: string; force?: boolean }\n ): Promise<BranchInfo> {\n await this.assertBranchingSupported();\n\n if (options?.source) {\n validateIdentifier(options.source, \"source database name\");\n }\n\n const dbName = toBranchDbName(name);\n const sourceDb = options?.source || await this.home.database();\n\n // Check if branch already exists\n const existing = await (await this.home.registry()).execute(\n sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`\n );\n if ((existing.rows as unknown[]).length > 0) {\n throw new Error(`Branch \"${name}\" already exists.`);\n }\n\n // Disconnect any idle pools to the source DB so TEMPLATE works.\n // CREATE DATABASE ... TEMPLATE requires no other connections to the template.\n //\n // This only reaches pools inside *this* process. The CLI runs as its own\n // process, so a `rebase dev` holding the database open is untouched by\n // it — which is why the failure below has to name what is connected, and\n // why `--force` exists.\n await this.poolManager.disconnectDatabase(sourceDb);\n\n if (options?.force) {\n const closed = await this.terminateConnections(sourceDb);\n if (closed > 0) {\n // Postgres reports the backend as gone before its slot is\n // actually free, and CREATE DATABASE then fails on a connection\n // that no longer exists. One short settle beats a confusing retry.\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n\n // Create the database using the source as a template.\n // Note: Identifiers must be double-quoted, not parameterized.\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n const safeSourceDb = sourceDb.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(\n sql.raw(`CREATE DATABASE \"${safeDbName}\" TEMPLATE \"${safeSourceDb}\"`)\n );\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n // The template — not the new database — is the one still in use.\n throw new Error(\n `Cannot create branch: the source database \"${sourceDb}\" has active connections.\\n`\n + await this.describeBlockingConnections(sourceDb)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Record metadata in the registry — asked for again, as copying the\n // main database closed any pool on it.\n const now = new Date();\n await (await this.home.registry()).execute(\n sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)\n VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`\n );\n\n return {\n name,\n database: dbName,\n parentDatabase: sourceDb,\n createdAt: now\n };\n }\n\n /**\n * Delete a branch database and remove its metadata.\n * Cannot delete the main/default database.\n */\n async deleteBranch(name: string, options?: { force?: boolean }): Promise<void> {\n await this.assertBranchingSupported();\n\n assertValidBranchName(name);\n\n // Safety, first pass: a request that would target the default database\n // is refused before any metadata is read, so the answer costs nothing\n // and cannot depend on what a row happens to say.\n const mainDatabase = await this.home.database();\n if (toBranchDbName(name) === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Verify the branch exists, and take the database name from the row\n // rather than deriving it again. A branch created before names stopped\n // being stripped is recorded as `rb_myfeature` while `my-feature` now\n // derives `rb_my-feature`; re-deriving would drop a database this row\n // never named — or, if that name happened to exist, somebody else's.\n // The stored value is the only one that is true by construction.\n const existing = await (await this.home.registry()).execute(\n sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`\n );\n const existingRows = existing.rows as Record<string, unknown>[];\n if (existingRows.length === 0) {\n throw new Error(`Branch \"${name}\" not found.`);\n }\n const dbName = existingRows[0].db_name as string;\n\n // Safety, second pass: the first checked a name we derived, this checks\n // the one we are about to drop. They differ for any row written under\n // the old scheme, and it is this one that governs.\n if (dbName === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Disconnect any pools to this branch before dropping. Same limit as\n // `createBranch`: in-process pools only.\n await this.poolManager.disconnectDatabase(dbName);\n\n if (options?.force) {\n const closed = await this.terminateConnections(dbName);\n if (closed > 0) await new Promise((resolve) => setTimeout(resolve, 250));\n }\n\n // Drop the database\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${safeDbName}\"`));\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n throw new Error(\n `Cannot delete branch \"${name}\": the database has active connections.\\n`\n + await this.describeBlockingConnections(dbName)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Remove metadata\n await (await this.home.registry()).execute(\n sql`DELETE FROM rebase.branches WHERE name = ${name}`\n );\n }\n\n /**\n * Who is holding the database open, in words a developer can act on.\n *\n * `CREATE DATABASE ... TEMPLATE` and `DROP DATABASE` both require that no\n * other session is connected, and the one thing the old message could not\n * say is the only thing worth knowing: *which* session. \"Close other\n * clients or connections and try again\" is advice you cannot follow when\n * you do not know there is a `rebase dev` in another terminal — and that is\n * the common case, because wanting a branch and running the app are the\n * same moment.\n *\n * `application_name` is the useful column: node-postgres sends none by\n * default, so a Rebase process shows as `(unnamed)` while DBeaver, pgAdmin\n * and psql all name themselves. Counting by it separates \"my own dev\n * server\" from \"the GUI I forgot about\" without any guessing.\n *\n * Best-effort by construction: this runs while reporting a failure, and a\n * diagnostic that throws would replace a real error with its own.\n */\n private async describeBlockingConnections(dbName: string): Promise<string> {\n try {\n const result = await this.db.execute(sql`\n SELECT coalesce(nullif(application_name, ''), '(unnamed)') AS app,\n count(*)::int AS n\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n GROUP BY 1\n ORDER BY 2 DESC, 1\n `);\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) {\n // The blocker went away between the failure and this query, or\n // it is a session Postgres does not attribute to this database.\n return \" Retry — whatever was connected has since disconnected.\";\n }\n\n const listed = rows\n .map((row) => ` ${row.n} × ${String(row.app)}`)\n .join(\"\\n\");\n\n return ` Connected right now:\\n${listed}\\n`\n + \" A running `rebase dev` is the usual one — stop it, or re-run with --force to\\n\"\n + \" disconnect them for you.\";\n } catch {\n return \" Close other clients and connections, or re-run with --force.\";\n }\n }\n\n /**\n * Disconnect every other session on a database, and say how many.\n *\n * `pg_terminate_backend` rather than `pg_cancel_backend`: cancelling a\n * query leaves the session connected, and a connected session is exactly\n * what blocks the operation.\n *\n * The current backend is excluded — terminating the connection running this\n * statement would abort the command that asked for it.\n */\n private async terminateConnections(dbName: string): Promise<number> {\n const result = await this.db.execute(sql`\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n `);\n\n return (result.rows as unknown[]).length;\n }\n\n /**\n * Everything prune needs to decide, in two queries.\n *\n * Reads the rows and the server's database list rather than joining them,\n * so the *disagreements* between the two are visible — a row whose database\n * is gone and a database with no row are the two things prune exists to\n * find, and a join hides both.\n */\n async pruneCandidates(): Promise<{ rows: BranchRow[]; databases: string[] }> {\n const rowResult = await (await this.home.registry()).execute(sql.raw(`\n SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at\n `));\n const rows = (rowResult.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n dbName: row.db_name as string,\n createdAt: new Date(row.created_at as string)\n }));\n\n const dbResult = await this.db.execute(sql`\n SELECT datname FROM pg_database WHERE NOT datistemplate\n `);\n const databases = (dbResult.rows as Record<string, unknown>[]).map((row) => String(row.datname));\n\n return { rows, databases };\n }\n\n /** Remove a metadata row whose database is already gone. */\n async forgetBranchRow(name: string): Promise<void> {\n await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);\n }\n\n /**\n * Drop a database by name, with no metadata row required.\n *\n * `deleteBranch` deliberately takes its database name from the row, because\n * the row is the only value that is true by construction. Prune has the\n * opposite job — the cases it handles are exactly the ones where row and\n * database disagree — so it needs to name a database directly. Guarded the\n * same way regardless: the main database is never droppable.\n */\n async dropDatabase(dbName: string): Promise<void> {\n validateIdentifier(dbName, \"database name\");\n if (dbName === await this.home.database()) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n await this.poolManager.disconnectDatabase(dbName);\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${dbName.replace(/\"/g, '\"\"')}\"`));\n } catch (err) {\n throw describeBranchDdlError(err, dbName);\n }\n }\n\n /**\n * List all branches recorded in the metadata table.\n * Optionally fetches database sizes from pg_database.\n */\n async listBranches(): Promise<BranchInfo[]> {\n const result = await (await this.home.registry()).execute(sql.raw(`\n SELECT \n b.name,\n b.db_name,\n b.parent_db,\n b.created_at,\n pg_database_size(b.db_name) as size_bytes\n FROM ${BRANCHES_TABLE} b\n JOIN pg_database d ON d.datname = b.db_name\n ORDER BY b.created_at DESC\n `));\n\n return (result.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined\n }));\n }\n\n /**\n * Get info about a specific branch.\n */\n async getBranchInfo(name: string): Promise<BranchInfo | undefined> {\n assertValidBranchName(name);\n\n const result = await (await this.home.registry()).execute(sql`\n SELECT\n b.name,\n b.db_name,\n b.parent_db,\n b.created_at\n FROM rebase.branches b\n WHERE b.name = ${name}\n `);\n\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) return undefined;\n\n const row = rows[0];\n\n // Attempt to get size — may fail if the DB was externally dropped.\n // Same reason as `deleteBranch`: the size of a re-derived name is the\n // size of some other database, or of nothing.\n let sizeBytes: number | undefined;\n try {\n const dbName = row.db_name as string;\n const sizeResult = await this.db.execute(\n sql`SELECT pg_database_size(${dbName}) as size_bytes`\n );\n const sizeRows = sizeResult.rows as Record<string, unknown>[];\n if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {\n sizeBytes = Number(sizeRows[0].size_bytes);\n }\n } catch {\n // Database might not exist anymore\n }\n\n return {\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { BranchService } from "./services/BranchService.js";
|
|
|
3
3
|
import { RealtimeService, type SubscriptionAuthContext } from "./services/realtimeService.js";
|
|
4
4
|
import { DatabasePoolManager } from "./databasePoolManager.js";
|
|
5
5
|
import { DrizzleClient } from "./interfaces.js";
|
|
6
|
-
import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
|
|
6
|
+
import { DatabaseAdmin, SQLAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, SqlScriptResult, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
|
|
7
7
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry.js";
|
|
8
8
|
import { HistoryService } from "./history/HistoryService.js";
|
|
9
9
|
/**
|
|
@@ -20,6 +20,16 @@ import { HistoryService } from "./history/HistoryService.js";
|
|
|
20
20
|
* not be fixed alone and left disagreeing with the rest.
|
|
21
21
|
*/
|
|
22
22
|
export declare function isRoleSwitchingOptedOut(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The main database, when this process serves a branch of it.
|
|
25
|
+
*
|
|
26
|
+
* `rebase dev` on a checkout switched to a branch connects to the branch, and
|
|
27
|
+
* names the database it was branched from in `REBASE_BRANCH_PARENT_DATABASE`.
|
|
28
|
+
* Branches are kept there — the registry, and the default source of a new
|
|
29
|
+
* one — as they are when the main database itself is served: a branch's own
|
|
30
|
+
* `rebase.branches` is a snapshot, taken before its row was written.
|
|
31
|
+
*/
|
|
32
|
+
export declare function branchParentDatabase(): string | undefined;
|
|
23
33
|
/**
|
|
24
34
|
* The role a statement will actually have run as, given the role it asked for.
|
|
25
35
|
*
|
|
@@ -31,6 +41,17 @@ export declare function isRoleSwitchingOptedOut(): boolean;
|
|
|
31
41
|
export declare function effectiveSqlRole(requestedRole?: string): string;
|
|
32
42
|
/** How {@link effectiveSqlRole} names "whatever role the connection holds". */
|
|
33
43
|
export declare const CONNECTION_OWNER = "<connection owner>";
|
|
44
|
+
/**
|
|
45
|
+
* The role a console statement ran as, given the role it asked for — said to
|
|
46
|
+
* the console with its rows, so it can say when that is not the role picked.
|
|
47
|
+
*
|
|
48
|
+
* The requested role whenever role switching is on: a statement that cannot
|
|
49
|
+
* assume its role is refused, never run as anyone else. With
|
|
50
|
+
* `DISABLE_DB_ROLE_SWITCHING`, every statement runs as the connection owner,
|
|
51
|
+
* named by asking a session of its own on the same database — a role the
|
|
52
|
+
* session already held is then still the one requested.
|
|
53
|
+
*/
|
|
54
|
+
export declare function sqlRoleRanAs(admin: Pick<SQLAdmin, "executeSql">, requestedRole: string, database: string | undefined): Promise<string>;
|
|
34
55
|
/**
|
|
35
56
|
* A statement named a database role the connection cannot assume.
|
|
36
57
|
*
|
|
@@ -223,27 +244,43 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
223
244
|
* collection is readable by its own members in the first place.
|
|
224
245
|
*/
|
|
225
246
|
private resolveCallerTenants;
|
|
247
|
+
/**
|
|
248
|
+
* Where an update reads the row it addresses before writing it.
|
|
249
|
+
*
|
|
250
|
+
* The path itself, except through a many-to-many relation: there the path
|
|
251
|
+
* names the parent's set, and an update may *link* a row that exists but is
|
|
252
|
+
* not in that set yet (the persistence layer attaches it). Read through the
|
|
253
|
+
* path, such a row is absent, so it is read from its own collection.
|
|
254
|
+
*/
|
|
255
|
+
private storedRowPath;
|
|
226
256
|
/**
|
|
227
257
|
* The stored row an upsert's key names, if this caller can address it —
|
|
228
258
|
* with its address and the values to update it with.
|
|
229
259
|
*
|
|
230
|
-
* Read as the caller reads: their policies and `beforeQuery` scope
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
260
|
+
* Read as the caller reads: their policies and `beforeQuery` scope. On the
|
|
261
|
+
* trusted plane for its fields only, like {@link resolveCallerTenants}:
|
|
262
|
+
* the key columns are the server's to see.
|
|
263
|
+
*
|
|
264
|
+
* Soft-deleted rows are read too, and a key one of them holds is refused
|
|
265
|
+
* with `ROW_IN_TRASH` (see {@link rowInTrashError}). Hidden, it was not
|
|
266
|
+
* found here, the write went down the create pipeline, and the INSERT's
|
|
267
|
+
* conflict branch wrote into the trashed row: `201 Created`, hooks told
|
|
268
|
+
* "new", a second `create` in history, and the row still in the trash.
|
|
234
269
|
*
|
|
235
270
|
* `undefined` when the key is incomplete (the write is a plain insert,
|
|
236
271
|
* which is the persistence layer's rule too), when a key value is not a
|
|
237
272
|
* plain value, or when no such row is visible. None of those can write a
|
|
238
273
|
* stored row wrongly: the statement's own conflict branch sets only what
|
|
239
|
-
* the caller sent,
|
|
274
|
+
* the caller sent, only on a row in their scope, and never on one in the
|
|
275
|
+
* trash.
|
|
240
276
|
*
|
|
241
277
|
* The values drop the key and the conflict target, which the stored row
|
|
242
278
|
* already has: a natural-key upsert carrying a key the caller invented
|
|
243
279
|
* must not move the row onto it.
|
|
244
280
|
*/
|
|
245
281
|
private findUpsertTarget;
|
|
246
|
-
save<M extends Record<string, unknown>>(
|
|
282
|
+
save<M extends Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
|
|
283
|
+
private saveInFrame;
|
|
247
284
|
/**
|
|
248
285
|
* Write many rows through the same pipeline as {@link save}.
|
|
249
286
|
*
|
|
@@ -318,11 +355,25 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
318
355
|
* body that cannot work never costs a rollback.
|
|
319
356
|
*/
|
|
320
357
|
batchWrite<M extends Record<string, unknown>>({ operations }: BatchWriteProps<M>): Promise<(Record<string, unknown> | null)[]>;
|
|
321
|
-
delete<M extends Record<string, unknown>>(
|
|
358
|
+
delete<M extends Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;
|
|
359
|
+
private deleteInFrame;
|
|
322
360
|
deleteAll(path: string): Promise<void>;
|
|
323
361
|
updateRelationPivot({ path, targetId, pivot }: UpdateRelationPivotProps): Promise<void>;
|
|
324
362
|
checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
325
363
|
count<M extends Record<string, unknown>>({ path, collection, filter, logical, searchString, vectorSearch, withDeleted }: FetchCollectionProps<M>): Promise<number>;
|
|
364
|
+
/** The answer to {@link appDatabaseName}, once it has been given. */
|
|
365
|
+
private appDatabase?;
|
|
366
|
+
/**
|
|
367
|
+
* The database this driver's own connection is on: the app's, which every
|
|
368
|
+
* data request and the CMS read and write.
|
|
369
|
+
*
|
|
370
|
+
* Asked of the connection rather than read off the admin connection
|
|
371
|
+
* string, which can name another database — the server's `postgres`, or
|
|
372
|
+
* the main database while this process runs on a branch. The SQL console
|
|
373
|
+
* preselects this name, a statement sent to it runs on this connection,
|
|
374
|
+
* and branching copies it by default.
|
|
375
|
+
*/
|
|
376
|
+
appDatabaseName(): Promise<string>;
|
|
326
377
|
private getTargetDb;
|
|
327
378
|
/**
|
|
328
379
|
* Build one statement, binding `$n` placeholders as real parameters.
|
|
@@ -338,6 +389,26 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
338
389
|
params?: unknown[];
|
|
339
390
|
isolateSession?: boolean;
|
|
340
391
|
}): Promise<Record<string, unknown>[]>;
|
|
392
|
+
/**
|
|
393
|
+
* Run a script a person wrote — the Studio console — and describe its
|
|
394
|
+
* result. See `SQLAdmin.runSqlScript` and `services/sql-script.ts`.
|
|
395
|
+
*/
|
|
396
|
+
runSqlScript(sqlText: string, options?: {
|
|
397
|
+
database?: string;
|
|
398
|
+
role?: string;
|
|
399
|
+
}): Promise<SqlScriptResult>;
|
|
400
|
+
/**
|
|
401
|
+
* `SET ROLE` for the rest of a session of a script's own — not `SET LOCAL`
|
|
402
|
+
* inside a transaction, which a `COMMIT` in the script ends, and every
|
|
403
|
+
* statement after it ran as the connection owner. The session is reset
|
|
404
|
+
* after the script, so the role does not outlive it.
|
|
405
|
+
*
|
|
406
|
+
* The same refusals as {@link executeSqlOn}: a role the session already
|
|
407
|
+
* has is no switch; `DISABLE_DB_ROLE_SWITCHING` runs as the owner on
|
|
408
|
+
* purpose; and a connection that may not `SET ROLE` refuses, never falls
|
|
409
|
+
* back to the owner.
|
|
410
|
+
*/
|
|
411
|
+
private assumeSessionRole;
|
|
341
412
|
/**
|
|
342
413
|
* Run `fn` on a session of its own, and put that session back the way it
|
|
343
414
|
* was handed out before anything else can use it.
|
|
@@ -350,10 +421,11 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
350
421
|
* connection in N, until a restart. `SET SESSION AUTHORIZATION DEFAULT`
|
|
351
422
|
* clears the role as well, which `RESET ALL` does not.
|
|
352
423
|
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
424
|
+
* On a pool, `role` is assumed for the whole session, and a transaction
|
|
425
|
+
* the SQL leaves open is rolled back and the run refused — see
|
|
426
|
+
* `onOwnSession`. `fn` is then handed no role: there is nothing left to
|
|
427
|
+
* switch. A handle that is already one session (PGlite in process, a
|
|
428
|
+
* single client) is reset in place, and `fn` switches the role itself.
|
|
357
429
|
*/
|
|
358
430
|
private onIsolatedSession;
|
|
359
431
|
private executeSqlOn;
|
package/dist/auth/services.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { type RebasePgTable } from "../types.js";
|
|
3
|
-
import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData,
|
|
4
|
-
export type { Role };
|
|
3
|
+
import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, AccountAccessState, PendingEmailChange } from "@rebasepro/server";
|
|
5
4
|
export interface AuthSchemaTables {
|
|
6
5
|
users: RebasePgTable;
|
|
7
6
|
refreshTokens: RebasePgTable;
|
|
@@ -38,6 +37,8 @@ export declare class UserService implements UserRepository {
|
|
|
38
37
|
private db;
|
|
39
38
|
private usersTable;
|
|
40
39
|
private userIdentitiesTable;
|
|
40
|
+
/** Read by `getAccountAccessState`, to say whether one sign-in is still live. */
|
|
41
|
+
private refreshTokensTable;
|
|
41
42
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
42
43
|
private getQualifiedUsersTableName;
|
|
43
44
|
/**
|
|
@@ -100,10 +101,6 @@ export declare class UserService implements UserRepository {
|
|
|
100
101
|
* Find user by email verification token
|
|
101
102
|
*/
|
|
102
103
|
getUserByVerificationToken(token: string): Promise<UserData | null>;
|
|
103
|
-
/**
|
|
104
|
-
* Get roles for a user from database (inline TEXT[] column)
|
|
105
|
-
*/
|
|
106
|
-
getUserRoles(uid: string): Promise<Role[]>;
|
|
107
104
|
/**
|
|
108
105
|
* Get role IDs for a user
|
|
109
106
|
*/
|
|
@@ -116,12 +113,53 @@ export declare class UserService implements UserRepository {
|
|
|
116
113
|
* Assign a specific role to new user (appends if not present)
|
|
117
114
|
*/
|
|
118
115
|
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* One read of what an access token is judged against: whether the account
|
|
118
|
+
* exists, its roles, and its revocation watermark. See
|
|
119
|
+
* `TokenRepository.getAccountAccessState`.
|
|
120
|
+
*
|
|
121
|
+
* The watermark is read only where the table declares it, as
|
|
122
|
+
* `RefreshTokenService.getTokensValidAfter` reads it — a table without the
|
|
123
|
+
* column has no watermark rather than an error on every request.
|
|
124
|
+
*/
|
|
125
|
+
getAccountAccessState(uid: string, sessionId?: string): Promise<AccountAccessState | null>;
|
|
126
|
+
/**
|
|
127
|
+
* Whether the users table has `disabled_at`. `ensureAuthTablesExist` adds
|
|
128
|
+
* it to every auth table at boot, but the app's own drizzle object may
|
|
129
|
+
* predate it, so the database is asked — once — rather than the object.
|
|
130
|
+
*/
|
|
131
|
+
private disabledColumn?;
|
|
132
|
+
private hasDisabledColumn;
|
|
133
|
+
/**
|
|
134
|
+
* Whether the users table has the address-change columns. Added at boot to
|
|
135
|
+
* every auth table, but the app's drizzle object never declares them, so
|
|
136
|
+
* the database is asked — once — as for `disabled_at`.
|
|
137
|
+
*/
|
|
138
|
+
private emailChangeColumns?;
|
|
139
|
+
private hasEmailChangeColumns;
|
|
140
|
+
private requireEmailChangeColumns;
|
|
141
|
+
/** @see UserRepository.setPendingEmailChange */
|
|
142
|
+
setPendingEmailChange(uid: string, change: {
|
|
143
|
+
email: string;
|
|
144
|
+
tokenHash: string;
|
|
145
|
+
} | null): Promise<void>;
|
|
146
|
+
/** @see UserRepository.getPendingEmailChange */
|
|
147
|
+
getPendingEmailChange(uid: string): Promise<PendingEmailChange | null>;
|
|
148
|
+
/** @see UserRepository.findPendingEmailChange */
|
|
149
|
+
findPendingEmailChange(tokenHash: string): Promise<{
|
|
150
|
+
user: UserData;
|
|
151
|
+
change: PendingEmailChange;
|
|
152
|
+
} | null>;
|
|
153
|
+
/** @see UserRepository.applyPendingEmailChange — one write, held to the token. */
|
|
154
|
+
applyPendingEmailChange(uid: string, tokenHash: string): Promise<UserData | null>;
|
|
155
|
+
/** @see UserRepository.setUserDisabled */
|
|
156
|
+
setUserDisabled(uid: string, disabled: boolean): Promise<void>;
|
|
119
157
|
/**
|
|
120
158
|
* Get user with their roles
|
|
121
159
|
*/
|
|
122
160
|
getUserWithRoles(uid: string): Promise<{
|
|
123
161
|
user: UserData;
|
|
124
|
-
roles:
|
|
162
|
+
roles: string[];
|
|
125
163
|
} | null>;
|
|
126
164
|
}
|
|
127
165
|
export declare class RefreshTokenService {
|
|
@@ -191,6 +229,12 @@ export declare class RefreshTokenService {
|
|
|
191
229
|
deleteAllForUser(uid: string): Promise<void>;
|
|
192
230
|
listForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
193
231
|
deleteById(id: string, uid: string): Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Every token past its expiry, whoever holds it. `prune` clears a user's
|
|
234
|
+
* expired rows when that user refreshes; a session nobody comes back to
|
|
235
|
+
* is only ever cleared here.
|
|
236
|
+
*/
|
|
237
|
+
deleteExpired(): Promise<void>;
|
|
194
238
|
}
|
|
195
239
|
/**
|
|
196
240
|
* Password reset token service
|
|
@@ -235,6 +279,8 @@ export declare class MagicLinkTokenService {
|
|
|
235
279
|
private getQualifiedTableName;
|
|
236
280
|
createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
237
281
|
findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
282
|
+
/** Links and email codes past their expiry, used or not. */
|
|
283
|
+
deleteExpired(): Promise<void>;
|
|
238
284
|
markAsUsed(tokenHash: string): Promise<void>;
|
|
239
285
|
}
|
|
240
286
|
/**
|
|
@@ -262,6 +308,10 @@ export declare class PostgresTokenRepository implements TokenRepository {
|
|
|
262
308
|
findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
|
|
263
309
|
markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
|
|
264
310
|
deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
|
|
311
|
+
/**
|
|
312
|
+
* @see TokenRepository.deleteExpiredTokens — reset links, magic links and
|
|
313
|
+
* email codes, and refresh tokens, each past its own expiry.
|
|
314
|
+
*/
|
|
265
315
|
deleteExpiredTokens(): Promise<void>;
|
|
266
316
|
createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
267
317
|
findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
@@ -292,19 +342,25 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
292
342
|
setEmailVerified(id: string, verified: boolean): Promise<void>;
|
|
293
343
|
setVerificationToken(id: string, token: string | null): Promise<void>;
|
|
294
344
|
getUserByVerificationToken(token: string): Promise<UserData | null>;
|
|
295
|
-
getUserRoles(uid: string): Promise<RoleData[]>;
|
|
296
345
|
getUserRoleIds(uid: string): Promise<string[]>;
|
|
297
346
|
setUserRoles(uid: string, roleIds: string[]): Promise<void>;
|
|
298
347
|
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
299
348
|
getUserWithRoles(uid: string): Promise<{
|
|
300
349
|
user: UserData;
|
|
301
|
-
roles:
|
|
350
|
+
roles: string[];
|
|
351
|
+
} | null>;
|
|
352
|
+
getAccountAccessState(uid: string, sessionId?: string): Promise<AccountAccessState | null>;
|
|
353
|
+
setUserDisabled(uid: string, disabled: boolean): Promise<void>;
|
|
354
|
+
setPendingEmailChange(uid: string, change: {
|
|
355
|
+
email: string;
|
|
356
|
+
tokenHash: string;
|
|
357
|
+
} | null): Promise<void>;
|
|
358
|
+
getPendingEmailChange(uid: string): Promise<PendingEmailChange | null>;
|
|
359
|
+
findPendingEmailChange(tokenHash: string): Promise<{
|
|
360
|
+
user: UserData;
|
|
361
|
+
change: PendingEmailChange;
|
|
302
362
|
} | null>;
|
|
303
|
-
|
|
304
|
-
listRoles(): Promise<RoleData[]>;
|
|
305
|
-
createRole(_data: CreateRoleData): Promise<RoleData>;
|
|
306
|
-
updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
|
|
307
|
-
deleteRole(_id: string): Promise<void>;
|
|
363
|
+
applyPendingEmailChange(uid: string, tokenHash: string): Promise<UserData | null>;
|
|
308
364
|
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<boolean>;
|
|
309
365
|
markRefreshTokenRotated(tokenHash: string): Promise<void>;
|
|
310
366
|
revokeRefreshTokenSession(sessionId: string): Promise<void>;
|
|
@@ -383,6 +439,8 @@ export declare class MfaService implements MfaRepository {
|
|
|
383
439
|
* share a single increment.
|
|
384
440
|
*/
|
|
385
441
|
recordMfaChallengeAttempt(challengeId: string): Promise<number>;
|
|
442
|
+
/** Challenges past their five minutes, answered or not. */
|
|
443
|
+
deleteExpiredChallenges(): Promise<void>;
|
|
386
444
|
verifyMfaChallenge(challengeId: string): Promise<void>;
|
|
387
445
|
createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
|
|
388
446
|
useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
|
|
@@ -62,6 +62,22 @@ var AUTH_USERS_COLUMNS = [
|
|
|
62
62
|
column: "tokens_valid_after",
|
|
63
63
|
type: "TIMESTAMP WITH TIME ZONE"
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
column: "disabled_at",
|
|
67
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
column: "pending_email",
|
|
71
|
+
type: "TEXT"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
column: "email_change_token",
|
|
75
|
+
type: "TEXT"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
column: "email_change_sent_at",
|
|
79
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
80
|
+
},
|
|
65
81
|
{
|
|
66
82
|
column: "created_at",
|
|
67
83
|
type: "TIMESTAMP WITH TIME ZONE",
|
|
@@ -113,4 +129,4 @@ function isAuthCollection(collection) {
|
|
|
113
129
|
//#endregion
|
|
114
130
|
export { isAuthCollection as i, authUsersColumnDefinition as n, authUsersColumnSql as r, AUTH_USERS_COLUMNS as t };
|
|
115
131
|
|
|
116
|
-
//# sourceMappingURL=auth-users-columns-
|
|
132
|
+
//# sourceMappingURL=auth-users-columns-C72EMoDJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-users-columns-
|
|
1
|
+
{"version":3,"file":"auth-users-columns-C72EMoDJ.js","names":[],"sources":["../src/schema/auth-users-columns.ts"],"sourcesContent":["/**\n * The one description of what an auth user table's columns must be.\n *\n * ## Why this exists\n *\n * Three different code paths create `rebase.users`, and before this module they\n * disagreed about it:\n *\n * | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |\n * |------------------|-----------------------|-------------------------|--------------------------|\n * | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |\n * | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |\n * | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |\n * | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |\n *\n * They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so\n * whichever ran first decided the table and the other two silently no-op'd.\n * Boot order therefore chose the constraints: a managed deploy (collection\n * tables first) got a users table whose `email` was nullable and whose\n * `email_verified` had no default, while the same project pushed from a\n * checkout got the strict one. In the same table, `is_anonymous` came out\n * `NOT NULL DEFAULT false` — because *that* column existed in only one of the\n * three lists, so its owner's definition won by default.\n *\n * A second consequence was drift in the other direction. The scaffold's\n * `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and\n * builds its desired state from the collection alone, so the two columns only\n * auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged\n * drift, and a push run after the server had booted once planned to DROP them.\n *\n * Both problems are the same problem: no single place said what this table is.\n * This is that place. Every creator asks here first and only then falls back to\n * the collection's own property definitions, so a column auth owns has one\n * definition regardless of who gets there first, and a column the developer\n * added to their users collection still behaves like any other field.\n *\n * ## What belongs here\n *\n * Only columns the auth services read or write. A developer's own additions to\n * their users collection (`bio`, `stripe_customer_id`) are ordinary columns and\n * must NOT be listed — they are generated from the collection like every other\n * field, and listing them here would freeze a user's schema.\n *\n * `id` is deliberately absent: its type comes from the collection's id property\n * (uuid / increment / text) and each creator already derives it.\n *\n * Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an\n * older runtime unable to work against a migrated table.\n */\n\n/**\n * A column auth owns.\n *\n * Structured rather than one SQL string because the same facts are needed in\n * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,\n * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a\n * table that already exists with the wrong shape. Parsing a string back apart\n * for the third of those is how the copies drifted in the first place.\n */\nexport interface AuthUsersColumn {\n /** Physical column name. */\n column: string;\n /** Postgres type. */\n type: string;\n /** Default expression, verbatim, or absent for no default. */\n default?: string;\n /** Whether the column is NOT NULL. */\n notNull?: boolean;\n}\n\n/**\n * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it\n * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.\n * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added\n * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted\n * table that already holds a longer row.\n */\nexport const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[] = [\n { column: \"email\", type: \"TEXT\", notNull: true },\n { column: \"display_name\", type: \"TEXT\" },\n { column: \"photo_url\", type: \"TEXT\" },\n { column: \"roles\", type: \"TEXT[]\", default: \"'{}'\", notNull: true },\n { column: \"password_hash\", type: \"TEXT\" },\n { column: \"email_verified\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"email_verification_token\", type: \"TEXT\" },\n { column: \"email_verification_sent_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"is_anonymous\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"metadata\", type: \"JSONB\", default: \"'{}'\", notNull: true },\n { column: \"tokens_valid_after\", type: \"TIMESTAMP WITH TIME ZONE\" },\n // When an administrator switched the account off; NULL while it is on.\n { column: \"disabled_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n // A self-service address change waiting for its link: the address, the\n // hash of the token mailed to it, and when (it lives 24 hours).\n { column: \"pending_email\", type: \"TEXT\" },\n { column: \"email_change_token\", type: \"TEXT\" },\n { column: \"email_change_sent_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"created_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true },\n { column: \"updated_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true }\n];\n\nconst BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map(c => [c.column, c]));\n\n/** Type + inline constraints, as they appear after the column name. */\nexport function authUsersColumnSql(spec: AuthUsersColumn): string {\n return [\n spec.type,\n spec.default !== undefined ? `DEFAULT ${spec.default}` : \"\",\n spec.notNull ? \"NOT NULL\" : \"\"\n ].filter(Boolean).join(\" \");\n}\n\n/**\n * The auth-owned definition for a physical column name, or `undefined` when\n * auth does not own it.\n *\n * Callers pass the RESOLVED column name (after `columnName` mapping), because\n * that is the only name the three creators agree on: the scaffold's users\n * collection spells the property `displayName` and the column `display_name`.\n */\nexport function authUsersColumnDefinition(column: string): string | undefined {\n const spec = BY_COLUMN.get(column);\n return spec ? authUsersColumnSql(spec) : undefined;\n}\n\n/**\n * Whether a collection is an auth collection, i.e. whether the definitions in\n * this module apply to its table at all.\n *\n * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:\n * that one takes a `CollectionConfig`, this one is called from DDL code paths\n * that hold looser objects, and both spellings must accept `auth: true` as well\n * as `auth: { enabled: true }`.\n */\nexport function isAuthCollection(collection: unknown): boolean {\n const auth = (collection as { auth?: unknown } | undefined)?.auth;\n if (auth === true) return true;\n return typeof auth === \"object\" && auth !== null && (auth as { enabled?: unknown }).enabled === true;\n}\n"],"mappings":";;;;;;;;;;AA6EA,IAAa,qBAAiD;CAC1D;EAAE,QAAQ;EAAS,MAAM;EAAQ,SAAS;CAAK;CAC/C;EAAE,QAAQ;EAAgB,MAAM;CAAO;CACvC;EAAE,QAAQ;EAAa,MAAM;CAAO;CACpC;EAAE,QAAQ;EAAS,MAAM;EAAU,SAAS;EAAQ,SAAS;CAAK;CAClE;EAAE,QAAQ;EAAiB,MAAM;CAAO;CACxC;EAAE,QAAQ;EAAkB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC7E;EAAE,QAAQ;EAA4B,MAAM;CAAO;CACnD;EAAE,QAAQ;EAA8B,MAAM;CAA2B;CACzE;EAAE,QAAQ;EAAgB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC3E;EAAE,QAAQ;EAAY,MAAM;EAAS,SAAS;EAAQ,SAAS;CAAK;CACpE;EAAE,QAAQ;EAAsB,MAAM;CAA2B;CAEjE;EAAE,QAAQ;EAAe,MAAM;CAA2B;CAG1D;EAAE,QAAQ;EAAiB,MAAM;CAAO;CACxC;EAAE,QAAQ;EAAsB,MAAM;CAAO;CAC7C;EAAE,QAAQ;EAAwB,MAAM;CAA2B;CACnE;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;CAC1F;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;AAC9F;AAEA,IAAM,YAAY,IAAI,IAAI,mBAAmB,KAAI,MAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;;AAGpE,SAAgB,mBAAmB,MAA+B;CAC9D,OAAO;EACH,KAAK;EACL,KAAK,YAAY,KAAA,IAAY,WAAW,KAAK,YAAY;EACzD,KAAK,UAAU,aAAa;CAChC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AAC9B;;;;;;;;;AAUA,SAAgB,0BAA0B,QAAoC;CAC1E,MAAM,OAAO,UAAU,IAAI,MAAM;CACjC,OAAO,OAAO,mBAAmB,IAAI,IAAI,KAAA;AAC7C;;;;;;;;;;AAWA,SAAgB,iBAAiB,YAA8B;CAC3D,MAAM,OAAQ,YAA+C;CAC7D,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,OAAO,SAAS,YAAY,SAAS,QAAS,KAA+B,YAAY;AACpG"}
|