@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g06dbe5b
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/PostgresBackendDriver.d.ts +49 -2
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +40 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-CBEOeYqa.js} +1118 -220
- package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-Bww-Lg0s.js +8999 -0
- package/dist/backup-service-Bww-Lg0s.js.map +1 -0
- package/dist/cli-helpers.d.ts +96 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js +57 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js.map +1 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js +800 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
- package/dist/index.es.js +2460 -10233
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +185 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +23 -25
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +99 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +185 -3
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +38 -8
- package/dist/services/RelationService.d.ts +54 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +62 -2
- package/dist/services/dataService.d.ts +8 -1
- package/dist/services/realtimeService.d.ts +131 -21
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +129 -6
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D1qbmLZ2.js +587 -0
- package/dist/websocket-D1qbmLZ2.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +176 -6
- package/src/PostgresBootstrapper.ts +245 -37
- package/src/auth/ensure-tables.ts +357 -81
- package/src/auth/services.ts +93 -7
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +199 -10
- package/src/cli.ts +276 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +129 -20
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +46 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +12 -63
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +135 -76
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +572 -42
- package/src/schema/generate-drizzle-schema-logic.ts +169 -43
- package/src/schema/generate-postgres-ddl-logic.ts +670 -29
- package/src/schema/generate-postgres-ddl.ts +25 -2
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +446 -100
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +11 -3
- package/src/security/rls-enforcement.ts +149 -6
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +265 -122
- package/src/services/PersistService.ts +57 -7
- package/src/services/RelationService.ts +358 -114
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +123 -4
- package/src/services/dataService.ts +8 -1
- package/src/services/realtimeService.ts +346 -81
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +396 -29
- package/src/utils/pg-error-utils.ts +73 -3
- package/src/websocket.ts +130 -24
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -37,7 +37,7 @@ import { ensureHistoryTableExists } from "./history/ensure-history-table";
|
|
|
37
37
|
import { patchPgArrayNullSafety } from "./utils/pg-array-null-patch";
|
|
38
38
|
import { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from "./schema/introspect-runtime";
|
|
39
39
|
import { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from "./schema/dynamic-tables";
|
|
40
|
-
import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
|
|
40
|
+
import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
|
|
41
41
|
import { provisionTriggerCdc, type CdcTableRef } from "./services/cdc/trigger-cdc";
|
|
42
42
|
import { collectJunctionLinks } from "./services/cdc/junction-tables";
|
|
43
43
|
import { createChannelBus, resolveChannelBusSetting } from "./services/channel-bus";
|
|
@@ -102,6 +102,40 @@ export interface PostgresDriverInternals {
|
|
|
102
102
|
|
|
103
103
|
// Re-export from shared CLI error utilities
|
|
104
104
|
import { isEconnrefused } from "./cli-errors";
|
|
105
|
+
import { classifyConnectFailure } from "./utils/pg-error-utils";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Which table name the boot-time drift check should look for, for one collection.
|
|
109
|
+
*
|
|
110
|
+
* A declared `table` IS the table name, not a hint to be second-guessed. This
|
|
111
|
+
* used to ask the registry whether it had indexed the declared name and fall
|
|
112
|
+
* back to the SLUG when it had not — but "the registry does not know this table"
|
|
113
|
+
* is exactly the condition the drift check exists to report, so the fallback
|
|
114
|
+
* fired precisely when it was most harmful.
|
|
115
|
+
*
|
|
116
|
+
* A collection with `slug: "usage-daily", table: "usage_daily"` was reported as
|
|
117
|
+
* missing table `usage-daily`: a name that does not exist, should never exist,
|
|
118
|
+
* and that nobody can find by looking. Worse, the remediation the caller prints
|
|
119
|
+
* says to run `rebase db push` — which would then CREATE that invented table
|
|
120
|
+
* beside the correct one, the same "second copy" hazard the misplaced-schema
|
|
121
|
+
* branch further down exists to prevent. Seen in production, where a correctly
|
|
122
|
+
* migrated database reported drift on every boot.
|
|
123
|
+
*
|
|
124
|
+
* The slug is used only when nothing was declared, which is the config shape
|
|
125
|
+
* where the slug genuinely is the table name.
|
|
126
|
+
*
|
|
127
|
+
* Exported for its own test: the caller needs a live pool and a real database,
|
|
128
|
+
* and this is the part that was wrong.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveDriftCheckName(
|
|
131
|
+
col: CollectionConfig,
|
|
132
|
+
registeredTableNames: string[]
|
|
133
|
+
): string {
|
|
134
|
+
const declaredTable = isRelationalCollectionConfig(col) ? col.table : undefined;
|
|
135
|
+
return declaredTable
|
|
136
|
+
?? registeredTableNames.find((k) => k === col.slug)
|
|
137
|
+
?? col.slug;
|
|
138
|
+
}
|
|
105
139
|
|
|
106
140
|
/**
|
|
107
141
|
* Default PostgreSQL bootstrapper.
|
|
@@ -260,9 +294,45 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
260
294
|
throw new Error(`Cannot connect to PostgreSQL at ${hostInfo}: connection refused. Is the database running?`);
|
|
261
295
|
}
|
|
262
296
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
297
|
+
/*
|
|
298
|
+
* Everything else.
|
|
299
|
+
*
|
|
300
|
+
* Two problems with what used to happen here. First, the logged
|
|
301
|
+
* error was Drizzle's wrapper — `Failed query: SELECT 1` and a
|
|
302
|
+
* stack through drizzle internals — while the sentence that
|
|
303
|
+
* actually says what is wrong ("password authentication failed
|
|
304
|
+
* for user …", "database … does not exist") sits in `.cause`
|
|
305
|
+
* and was never printed. A developer with a typo in their
|
|
306
|
+
* DATABASE_URL got two walls of stack trace and no cause.
|
|
307
|
+
*
|
|
308
|
+
* Second, "continuing… the pool may recover" is only true for
|
|
309
|
+
* a transient fault. A wrong password or a missing database is
|
|
310
|
+
* settled: the next query fails the same way, so the process
|
|
311
|
+
* died seconds later anyway — after printing a reassurance.
|
|
312
|
+
* Those now fail here, where the message can be about the
|
|
313
|
+
* cause rather than about whichever query ran next.
|
|
314
|
+
*/
|
|
315
|
+
const { fatal, reason, code } = classifyConnectFailure(err);
|
|
316
|
+
const detail = code ? ` [${code}]` : "";
|
|
317
|
+
|
|
318
|
+
if (fatal) {
|
|
319
|
+
logger.error(
|
|
320
|
+
`\n` +
|
|
321
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
322
|
+
` ❌ PostgreSQL refused the connection\n` +
|
|
323
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
324
|
+
`\n` +
|
|
325
|
+
` ${reason}${detail}\n` +
|
|
326
|
+
`\n` +
|
|
327
|
+
` The server is reachable, so this is the credentials or the\n` +
|
|
328
|
+
` database name in DATABASE_URL — check them in your .env.\n` +
|
|
329
|
+
`\n` +
|
|
330
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
|
|
331
|
+
);
|
|
332
|
+
throw new Error(`PostgreSQL refused the connection: ${reason}${detail}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
logger.error(`❌ Failed to connect to PostgreSQL: ${reason}${detail}`, { error: err });
|
|
266
336
|
logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
267
337
|
}
|
|
268
338
|
|
|
@@ -294,20 +364,33 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
294
364
|
// bypasses RLS. Detect the posture and, when privileged, provision
|
|
295
365
|
// the restricted `rebase_user` role and route authenticated
|
|
296
366
|
// requests (reads AND writes) through it. The server context (base
|
|
297
|
-
// driver /
|
|
298
|
-
// and bypasses.
|
|
367
|
+
// driver / auth flows / `rebase.sql`) stays on the owner connection
|
|
368
|
+
// and bypasses; `rebase.dataAsAdmin` does NOT — it is scoped with
|
|
369
|
+
// `withAuth({ uid: "service", roles: ["admin"] })` at boot, so it
|
|
370
|
+
// runs as `rebase_user` and its policies are evaluated.
|
|
371
|
+
// Default-on: a privileged connection that cannot be
|
|
299
372
|
// isolated fails the boot rather than serving unenforced requests.
|
|
300
373
|
{
|
|
301
374
|
const runSql: RawSqlRunner = async (text) => {
|
|
302
375
|
const res = await schemaAwareDb.execute(sql.raw(text));
|
|
303
376
|
return (res.rows ?? []) as Record<string, unknown>[];
|
|
304
377
|
};
|
|
378
|
+
// Said before anything else touches the schema: if the role and
|
|
379
|
+
// a schema share a name, unqualified SQL from any tool that does
|
|
380
|
+
// not pin `search_path` has been landing in the wrong place, and
|
|
381
|
+
// that is worth knowing before reading the drift report below.
|
|
382
|
+
await warnOnRoleSchemaCollision(runSql);
|
|
383
|
+
|
|
305
384
|
const posture = await detectConnectionPosture(runSql);
|
|
306
385
|
if (posture.privileged) {
|
|
307
386
|
const collectionSchemas = registry.getCollections()
|
|
308
387
|
.map((c) => (c as { schema?: string }).schema)
|
|
309
388
|
.filter((s): s is string => typeof s === "string");
|
|
310
|
-
|
|
389
|
+
// `auth` is deliberately absent: the RLS helpers moved into
|
|
390
|
+
// `rebase`, and granting USAGE on a schema Rebase does not
|
|
391
|
+
// own would, on a Supabase database, hand the end-user role
|
|
392
|
+
// access to theirs.
|
|
393
|
+
await ensureAppRole(runSql, ["public", "rebase", ...collectionSchemas]);
|
|
311
394
|
driver.rlsUserRole = REBASE_USER_ROLE;
|
|
312
395
|
realtimeService.rlsUserRole = REBASE_USER_ROLE;
|
|
313
396
|
logger.info(`🔐 RLS enforcement active: authenticated requests run as "${REBASE_USER_ROLE}" (connection "${posture.role}" bypasses RLS: ${posture.superuser ? "superuser" : posture.bypassRLS ? "BYPASSRLS" : "table owner"})`);
|
|
@@ -336,6 +419,11 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
336
419
|
// a rule that reads as "signed in only" but is true for every
|
|
337
420
|
// caller grants the data away rather than hiding it.
|
|
338
421
|
warnOnAnonymousGrants(registry.getCollections() as never);
|
|
422
|
+
|
|
423
|
+
// Raw policy SQL written against the pre-1.0 helper schema. It
|
|
424
|
+
// is rewritten on compile, so this is the only place the project
|
|
425
|
+
// is ever told the spelling moved.
|
|
426
|
+
warnOnLegacyRlsFunctions(registry.getCollections() as never);
|
|
339
427
|
}
|
|
340
428
|
|
|
341
429
|
// Ensure branch metadata table exists when branching is available
|
|
@@ -487,22 +575,28 @@ table: link.table });
|
|
|
487
575
|
try {
|
|
488
576
|
const registeredCollections = registry.getCollections();
|
|
489
577
|
if (registeredCollections.length > 0) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
578
|
+
// Deliberately unfiltered by schema: a table that is missing
|
|
579
|
+
// from where the collection says it lives is very often
|
|
580
|
+
// sitting in another schema entirely (see the misplaced-table
|
|
581
|
+
// report below), and knowing *which* is the whole answer.
|
|
494
582
|
const result = await schemaAwareDb.execute(sql.raw(`
|
|
495
583
|
SELECT table_name, table_schema
|
|
496
584
|
FROM information_schema.tables
|
|
497
|
-
WHERE table_schema IN (
|
|
585
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
498
586
|
AND table_type = 'BASE TABLE'
|
|
499
587
|
`));
|
|
588
|
+
const tablesByName = new Map<string, string[]>();
|
|
589
|
+
for (const row of result.rows as Array<{ table_name: string; table_schema: string }>) {
|
|
590
|
+
const schemas = tablesByName.get(row.table_name) ?? [];
|
|
591
|
+
schemas.push(row.table_schema);
|
|
592
|
+
tablesByName.set(row.table_name, schemas);
|
|
593
|
+
}
|
|
500
594
|
const dbTables = new Set(
|
|
501
595
|
(result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>
|
|
502
596
|
r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
|
|
503
597
|
)
|
|
504
598
|
);
|
|
505
|
-
const missing: Array<{ slug: string; table: string }> = [];
|
|
599
|
+
const missing: Array<{ slug: string; table: string; foundIn: string[] }> = [];
|
|
506
600
|
for (const col of registeredCollections) {
|
|
507
601
|
// Auth owns its table and creates it later in this same
|
|
508
602
|
// boot (initializeAuth → ensureAuthTablesExist), so it is
|
|
@@ -513,41 +607,72 @@ table: link.table });
|
|
|
513
607
|
if ((col as { auth?: { enabled?: boolean } }).auth?.enabled) continue;
|
|
514
608
|
|
|
515
609
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
516
|
-
const
|
|
517
|
-
const tableName = registry.hasTableForCollection(
|
|
518
|
-
declaredTable ?? col.slug
|
|
519
|
-
)
|
|
520
|
-
? (declaredTable ?? col.slug)
|
|
521
|
-
: col.slug;
|
|
522
|
-
// Resolve the actual table name the registry stored
|
|
523
|
-
const resolvedTable = registry.getTableNames().find((k) =>
|
|
524
|
-
k === tableName ||
|
|
525
|
-
k === col.slug
|
|
526
|
-
);
|
|
527
|
-
const checkName = resolvedTable ?? tableName;
|
|
610
|
+
const checkName = resolveDriftCheckName(col, registry.getTableNames());
|
|
528
611
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
529
612
|
if (!dbTables.has(fullCheckName)) {
|
|
530
613
|
// Report what was actually looked up: an unqualified
|
|
531
614
|
// "users" sends people hunting for public.users.
|
|
532
615
|
missing.push({ slug: col.slug,
|
|
533
|
-
table: fullCheckName
|
|
616
|
+
table: fullCheckName,
|
|
617
|
+
foundIn: (tablesByName.get(checkName) ?? []).filter(s => s !== schemaName) });
|
|
534
618
|
}
|
|
535
619
|
}
|
|
536
620
|
if (missing.length > 0) {
|
|
537
621
|
const lines = missing.map(
|
|
538
|
-
m => ` • collection "${m.slug}" → table "${m.table}"`
|
|
622
|
+
m => ` • collection "${m.slug}" → table "${m.table}"` +
|
|
623
|
+
(m.foundIn.length > 0
|
|
624
|
+
? ` — but a table of that name exists in ${m.foundIn.map(s => `"${s}"`).join(", ")}`
|
|
625
|
+
: "")
|
|
539
626
|
);
|
|
627
|
+
// A table that exists under the same name in another
|
|
628
|
+
// schema is not ordinary drift: it is almost always this
|
|
629
|
+
// framework's own `search_path` hazard. Postgres resolves
|
|
630
|
+
// unqualified SQL through `"$user", public`, and this
|
|
631
|
+
// project creates a schema named `rebase` while every
|
|
632
|
+
// template names the database role `rebase` too — so an
|
|
633
|
+
// unqualified CREATE TABLE (a hand-written migration, the
|
|
634
|
+
// SQL editor, drizzle-kit) lands in `rebase`, and the
|
|
635
|
+
// runtime, which now pins `search_path=public`, cannot see
|
|
636
|
+
// it. Say so, because "missing table" sends people to
|
|
637
|
+
// re-run a push that will create a *second* copy.
|
|
638
|
+
const misplaced = missing.filter(m => m.foundIn.length > 0);
|
|
639
|
+
const misplacedHelp = misplaced.length === 0 ? [] : [
|
|
640
|
+
" Those tables exist — in the wrong schema. Unqualified SQL run by a",
|
|
641
|
+
" role whose name matches a schema (`rebase` is both, by default)",
|
|
642
|
+
" resolves through search_path's \"$user\" and creates there. Move them:",
|
|
643
|
+
...misplaced.map(m =>
|
|
644
|
+
` ALTER TABLE "${m.foundIn[0]}"."${m.table.split(".").pop()}" SET SCHEMA "${m.table.includes(".") ? m.table.split(".")[0] : "public"}";`
|
|
645
|
+
),
|
|
646
|
+
" and qualify the SQL that created them, or pin search_path in DATABASE_URL",
|
|
647
|
+
" (`?options=-c%20search_path%3Dpublic`).",
|
|
648
|
+
""
|
|
649
|
+
];
|
|
650
|
+
// This runtime creates collection tables (and their RLS)
|
|
651
|
+
// at boot unless REBASE_MIGRATE_ON_BOOT=none, so a drift
|
|
652
|
+
// this late means that step was disabled, could not run,
|
|
653
|
+
// or failed — the guidance names a path for both a managed
|
|
654
|
+
// tenant (whose in-cluster database `pnpm db:push` cannot
|
|
655
|
+
// reach) and a self-host. Naming only the pnpm scripts, as
|
|
656
|
+
// this once did, told a managed operator to run a command
|
|
657
|
+
// that structurally cannot touch their database.
|
|
540
658
|
logger.warn([
|
|
541
659
|
"",
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
"
|
|
545
|
-
...
|
|
546
|
-
"
|
|
547
|
-
"
|
|
548
|
-
"
|
|
549
|
-
"
|
|
550
|
-
"
|
|
660
|
+
"⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:",
|
|
661
|
+
...lines,
|
|
662
|
+
"",
|
|
663
|
+
...misplacedHelp,
|
|
664
|
+
" This runtime applies the collection schema at boot unless",
|
|
665
|
+
" REBASE_MIGRATE_ON_BOOT=none. Check the \"Collection schema\" / \"policies\"",
|
|
666
|
+
" log lines above — this drift means that step was off, skipped, or failed.",
|
|
667
|
+
" • Managed cloud: redeploy with REBASE_MIGRATE_ON_BOOT unset or",
|
|
668
|
+
" \"ensure\"; the runtime applies the schema to the tenant DB.",
|
|
669
|
+
" If the log above says the driver does not implement collection-table",
|
|
670
|
+
" creation, THIS driver is too old to do it. A driver is installed from",
|
|
671
|
+
" your bundle's dependencies, not supplied by the platform image, so a",
|
|
672
|
+
" newer runtime will not update it: bump \"@rebasepro/server-postgres\"",
|
|
673
|
+
" in your project's package.json and redeploy.",
|
|
674
|
+
" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)",
|
|
675
|
+
" against DATABASE_URL.",
|
|
551
676
|
""
|
|
552
677
|
].join("\n"));
|
|
553
678
|
}
|
|
@@ -711,7 +836,90 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
|
|
|
711
836
|
collections as Parameters<typeof ensureCollectionTables>[1],
|
|
712
837
|
log
|
|
713
838
|
);
|
|
714
|
-
|
|
839
|
+
for (const failure of plan.failures) {
|
|
840
|
+
logger.warn(
|
|
841
|
+
failure.kind === "comment-column"
|
|
842
|
+
// The stamp records which `search` block the generated
|
|
843
|
+
// column was built from. Without it the next boot cannot
|
|
844
|
+
// tell a changed block from an unchanged one and adopts
|
|
845
|
+
// the column again instead of refusing.
|
|
846
|
+
? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, ` +
|
|
847
|
+
`but a later change to the \`search\` block will not be detected: ${failure.error}`
|
|
848
|
+
: `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the ` +
|
|
849
|
+
`collection still serves, but rows are not policed by this constraint: ${failure.error}`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return { applied: plan.actions.length - plan.failures.length };
|
|
853
|
+
},
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the
|
|
857
|
+
* `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned
|
|
858
|
+
* database serves data instead of denying every user-context read.
|
|
859
|
+
*
|
|
860
|
+
* The companion to {@link ensureCollectionSchema}: that creates the
|
|
861
|
+
* tables, this makes them servable. Boot runs it *after* auth
|
|
862
|
+
* initialization, because the generated policies call `auth.uid()` /
|
|
863
|
+
* `auth.roles()`, and `CREATE POLICY` validates those functions exist.
|
|
864
|
+
*
|
|
865
|
+
* Runs through the same drizzle handle, one statement at a time (that
|
|
866
|
+
* handle speaks the extended query protocol, which rejects multi-command
|
|
867
|
+
* strings). Failures are per-table and non-fatal: a table left un-policed
|
|
868
|
+
* stays RLS-enabled, so it denies rather than leaks.
|
|
869
|
+
*/
|
|
870
|
+
async ensureCollectionPolicies(
|
|
871
|
+
collections: unknown[],
|
|
872
|
+
driverResult: InitializedDriver,
|
|
873
|
+
log?: (message: string) => void
|
|
874
|
+
): Promise<{ applied: number }> {
|
|
875
|
+
const internals = driverResult.internals as PostgresDriverInternals;
|
|
876
|
+
const { ensureCollectionPolicies } = await import("./schema/ensure-collection-policies");
|
|
877
|
+
const queryable = {
|
|
878
|
+
async query<T>(text: string): Promise<{ rows: T[] }> {
|
|
879
|
+
const result = await internals.db.execute(sql.raw(text));
|
|
880
|
+
const rows = (result as unknown as { rows?: T[] }).rows;
|
|
881
|
+
return { rows: rows ?? (Array.isArray(result) ? (result as T[]) : []) };
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const outcome = await ensureCollectionPolicies(
|
|
885
|
+
queryable,
|
|
886
|
+
collections as CollectionConfig[],
|
|
887
|
+
log
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
for (const skip of outcome.skipped) {
|
|
891
|
+
logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
|
|
892
|
+
}
|
|
893
|
+
for (const failure of outcome.failures) {
|
|
894
|
+
logger.warn(
|
|
895
|
+
`🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Retire the pre-1.0 `auth` schema now that the policies above no
|
|
900
|
+
// longer call into it. Deliberately after, and deliberately quiet:
|
|
901
|
+
// Postgres refuses to drop a function an RLS policy still
|
|
902
|
+
// references, so on a database where some table has not been
|
|
903
|
+
// recompiled yet this is expected to fail and succeed on a later
|
|
904
|
+
// boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it
|
|
905
|
+
// off a Supabase `auth` schema.
|
|
906
|
+
try {
|
|
907
|
+
const { dropLegacyAuthSchema } = await import("./schema/rls-bootstrap-sql");
|
|
908
|
+
await dropLegacyAuthSchema(
|
|
909
|
+
async (text) => {
|
|
910
|
+
const res = await internals.db.execute(sql.raw(text));
|
|
911
|
+
return (res.rows ?? []) as Record<string, unknown>[];
|
|
912
|
+
},
|
|
913
|
+
{ info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
914
|
+
);
|
|
915
|
+
} catch (err) {
|
|
916
|
+
logger.info(
|
|
917
|
+
"Left the legacy `auth` schema in place: " +
|
|
918
|
+
(err instanceof Error ? err.message : String(err))
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return { applied: outcome.policiesApplied };
|
|
715
923
|
},
|
|
716
924
|
|
|
717
925
|
getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {
|