@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g18cfeb7
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 +21 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
- package/dist/auth-users-columns-Do9mw5Y5.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 +40 -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/ensure-collection-policies-DMUdRQdM.js +57 -0
- package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
- package/dist/index.es.js +1701 -10167
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CPkCqVTz.js +105 -0
- package/dist/policy-CPkCqVTz.js.map +1 -0
- 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 +83 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +45 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -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/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +14 -8
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +5 -1
- package/dist/services/realtimeService.d.ts +25 -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 +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D0TBU3ia.js +530 -0
- package/dist/websocket-D0TBU3ia.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +169 -3
- package/src/PostgresBootstrapper.ts +233 -35
- package/src/auth/ensure-tables.ts +335 -81
- package/src/auth/services.ts +24 -2
- 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 +92 -9
- package/src/cli.ts +219 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +22 -6
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- 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 +301 -44
- package/src/schema/generate-drizzle-schema-logic.ts +49 -7
- package/src/schema/generate-postgres-ddl-logic.ts +474 -27
- 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 +385 -71
- 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/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +11 -3
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +100 -107
- package/src/services/PersistService.ts +34 -7
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +5 -1
- package/src/services/realtimeService.ts +38 -35
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- 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
|
|
|
@@ -302,12 +372,22 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
302
372
|
const res = await schemaAwareDb.execute(sql.raw(text));
|
|
303
373
|
return (res.rows ?? []) as Record<string, unknown>[];
|
|
304
374
|
};
|
|
375
|
+
// Said before anything else touches the schema: if the role and
|
|
376
|
+
// a schema share a name, unqualified SQL from any tool that does
|
|
377
|
+
// not pin `search_path` has been landing in the wrong place, and
|
|
378
|
+
// that is worth knowing before reading the drift report below.
|
|
379
|
+
await warnOnRoleSchemaCollision(runSql);
|
|
380
|
+
|
|
305
381
|
const posture = await detectConnectionPosture(runSql);
|
|
306
382
|
if (posture.privileged) {
|
|
307
383
|
const collectionSchemas = registry.getCollections()
|
|
308
384
|
.map((c) => (c as { schema?: string }).schema)
|
|
309
385
|
.filter((s): s is string => typeof s === "string");
|
|
310
|
-
|
|
386
|
+
// `auth` is deliberately absent: the RLS helpers moved into
|
|
387
|
+
// `rebase`, and granting USAGE on a schema Rebase does not
|
|
388
|
+
// own would, on a Supabase database, hand the end-user role
|
|
389
|
+
// access to theirs.
|
|
390
|
+
await ensureAppRole(runSql, ["public", "rebase", ...collectionSchemas]);
|
|
311
391
|
driver.rlsUserRole = REBASE_USER_ROLE;
|
|
312
392
|
realtimeService.rlsUserRole = REBASE_USER_ROLE;
|
|
313
393
|
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 +416,11 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
336
416
|
// a rule that reads as "signed in only" but is true for every
|
|
337
417
|
// caller grants the data away rather than hiding it.
|
|
338
418
|
warnOnAnonymousGrants(registry.getCollections() as never);
|
|
419
|
+
|
|
420
|
+
// Raw policy SQL written against the pre-1.0 helper schema. It
|
|
421
|
+
// is rewritten on compile, so this is the only place the project
|
|
422
|
+
// is ever told the spelling moved.
|
|
423
|
+
warnOnLegacyRlsFunctions(registry.getCollections() as never);
|
|
339
424
|
}
|
|
340
425
|
|
|
341
426
|
// Ensure branch metadata table exists when branching is available
|
|
@@ -487,22 +572,28 @@ table: link.table });
|
|
|
487
572
|
try {
|
|
488
573
|
const registeredCollections = registry.getCollections();
|
|
489
574
|
if (registeredCollections.length > 0) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
575
|
+
// Deliberately unfiltered by schema: a table that is missing
|
|
576
|
+
// from where the collection says it lives is very often
|
|
577
|
+
// sitting in another schema entirely (see the misplaced-table
|
|
578
|
+
// report below), and knowing *which* is the whole answer.
|
|
494
579
|
const result = await schemaAwareDb.execute(sql.raw(`
|
|
495
580
|
SELECT table_name, table_schema
|
|
496
581
|
FROM information_schema.tables
|
|
497
|
-
WHERE table_schema IN (
|
|
582
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
498
583
|
AND table_type = 'BASE TABLE'
|
|
499
584
|
`));
|
|
585
|
+
const tablesByName = new Map<string, string[]>();
|
|
586
|
+
for (const row of result.rows as Array<{ table_name: string; table_schema: string }>) {
|
|
587
|
+
const schemas = tablesByName.get(row.table_name) ?? [];
|
|
588
|
+
schemas.push(row.table_schema);
|
|
589
|
+
tablesByName.set(row.table_name, schemas);
|
|
590
|
+
}
|
|
500
591
|
const dbTables = new Set(
|
|
501
592
|
(result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>
|
|
502
593
|
r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
|
|
503
594
|
)
|
|
504
595
|
);
|
|
505
|
-
const missing: Array<{ slug: string; table: string }> = [];
|
|
596
|
+
const missing: Array<{ slug: string; table: string; foundIn: string[] }> = [];
|
|
506
597
|
for (const col of registeredCollections) {
|
|
507
598
|
// Auth owns its table and creates it later in this same
|
|
508
599
|
// boot (initializeAuth → ensureAuthTablesExist), so it is
|
|
@@ -513,41 +604,72 @@ table: link.table });
|
|
|
513
604
|
if ((col as { auth?: { enabled?: boolean } }).auth?.enabled) continue;
|
|
514
605
|
|
|
515
606
|
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;
|
|
607
|
+
const checkName = resolveDriftCheckName(col, registry.getTableNames());
|
|
528
608
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
529
609
|
if (!dbTables.has(fullCheckName)) {
|
|
530
610
|
// Report what was actually looked up: an unqualified
|
|
531
611
|
// "users" sends people hunting for public.users.
|
|
532
612
|
missing.push({ slug: col.slug,
|
|
533
|
-
table: fullCheckName
|
|
613
|
+
table: fullCheckName,
|
|
614
|
+
foundIn: (tablesByName.get(checkName) ?? []).filter(s => s !== schemaName) });
|
|
534
615
|
}
|
|
535
616
|
}
|
|
536
617
|
if (missing.length > 0) {
|
|
537
618
|
const lines = missing.map(
|
|
538
|
-
m => ` • collection "${m.slug}" → table "${m.table}"`
|
|
619
|
+
m => ` • collection "${m.slug}" → table "${m.table}"` +
|
|
620
|
+
(m.foundIn.length > 0
|
|
621
|
+
? ` — but a table of that name exists in ${m.foundIn.map(s => `"${s}"`).join(", ")}`
|
|
622
|
+
: "")
|
|
539
623
|
);
|
|
624
|
+
// A table that exists under the same name in another
|
|
625
|
+
// schema is not ordinary drift: it is almost always this
|
|
626
|
+
// framework's own `search_path` hazard. Postgres resolves
|
|
627
|
+
// unqualified SQL through `"$user", public`, and this
|
|
628
|
+
// project creates a schema named `rebase` while every
|
|
629
|
+
// template names the database role `rebase` too — so an
|
|
630
|
+
// unqualified CREATE TABLE (a hand-written migration, the
|
|
631
|
+
// SQL editor, drizzle-kit) lands in `rebase`, and the
|
|
632
|
+
// runtime, which now pins `search_path=public`, cannot see
|
|
633
|
+
// it. Say so, because "missing table" sends people to
|
|
634
|
+
// re-run a push that will create a *second* copy.
|
|
635
|
+
const misplaced = missing.filter(m => m.foundIn.length > 0);
|
|
636
|
+
const misplacedHelp = misplaced.length === 0 ? [] : [
|
|
637
|
+
" Those tables exist — in the wrong schema. Unqualified SQL run by a",
|
|
638
|
+
" role whose name matches a schema (`rebase` is both, by default)",
|
|
639
|
+
" resolves through search_path's \"$user\" and creates there. Move them:",
|
|
640
|
+
...misplaced.map(m =>
|
|
641
|
+
` ALTER TABLE "${m.foundIn[0]}"."${m.table.split(".").pop()}" SET SCHEMA "${m.table.includes(".") ? m.table.split(".")[0] : "public"}";`
|
|
642
|
+
),
|
|
643
|
+
" and qualify the SQL that created them, or pin search_path in DATABASE_URL",
|
|
644
|
+
" (`?options=-c%20search_path%3Dpublic`).",
|
|
645
|
+
""
|
|
646
|
+
];
|
|
647
|
+
// This runtime creates collection tables (and their RLS)
|
|
648
|
+
// at boot unless REBASE_MIGRATE_ON_BOOT=none, so a drift
|
|
649
|
+
// this late means that step was disabled, could not run,
|
|
650
|
+
// or failed — the guidance names a path for both a managed
|
|
651
|
+
// tenant (whose in-cluster database `pnpm db:push` cannot
|
|
652
|
+
// reach) and a self-host. Naming only the pnpm scripts, as
|
|
653
|
+
// this once did, told a managed operator to run a command
|
|
654
|
+
// that structurally cannot touch their database.
|
|
540
655
|
logger.warn([
|
|
541
656
|
"",
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
"
|
|
545
|
-
...
|
|
546
|
-
"
|
|
547
|
-
"
|
|
548
|
-
"
|
|
549
|
-
"
|
|
550
|
-
"
|
|
657
|
+
"⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:",
|
|
658
|
+
...lines,
|
|
659
|
+
"",
|
|
660
|
+
...misplacedHelp,
|
|
661
|
+
" This runtime applies the collection schema at boot unless",
|
|
662
|
+
" REBASE_MIGRATE_ON_BOOT=none. Check the \"Collection schema\" / \"policies\"",
|
|
663
|
+
" log lines above — this drift means that step was off, skipped, or failed.",
|
|
664
|
+
" • Managed cloud: redeploy with REBASE_MIGRATE_ON_BOOT unset or",
|
|
665
|
+
" \"ensure\"; the runtime applies the schema to the tenant DB.",
|
|
666
|
+
" If the log above says the driver does not implement collection-table",
|
|
667
|
+
" creation, THIS driver is too old to do it. A driver is installed from",
|
|
668
|
+
" your bundle's dependencies, not supplied by the platform image, so a",
|
|
669
|
+
" newer runtime will not update it: bump \"@rebasepro/server-postgres\"",
|
|
670
|
+
" in your project's package.json and redeploy.",
|
|
671
|
+
" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)",
|
|
672
|
+
" against DATABASE_URL.",
|
|
551
673
|
""
|
|
552
674
|
].join("\n"));
|
|
553
675
|
}
|
|
@@ -711,7 +833,83 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
|
|
|
711
833
|
collections as Parameters<typeof ensureCollectionTables>[1],
|
|
712
834
|
log
|
|
713
835
|
);
|
|
714
|
-
|
|
836
|
+
for (const failure of plan.failures) {
|
|
837
|
+
logger.warn(
|
|
838
|
+
`🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the ` +
|
|
839
|
+
`collection still serves, but rows are not policed by this constraint: ${failure.error}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return { applied: plan.actions.length - plan.failures.length };
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the
|
|
847
|
+
* `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned
|
|
848
|
+
* database serves data instead of denying every user-context read.
|
|
849
|
+
*
|
|
850
|
+
* The companion to {@link ensureCollectionSchema}: that creates the
|
|
851
|
+
* tables, this makes them servable. Boot runs it *after* auth
|
|
852
|
+
* initialization, because the generated policies call `auth.uid()` /
|
|
853
|
+
* `auth.roles()`, and `CREATE POLICY` validates those functions exist.
|
|
854
|
+
*
|
|
855
|
+
* Runs through the same drizzle handle, one statement at a time (that
|
|
856
|
+
* handle speaks the extended query protocol, which rejects multi-command
|
|
857
|
+
* strings). Failures are per-table and non-fatal: a table left un-policed
|
|
858
|
+
* stays RLS-enabled, so it denies rather than leaks.
|
|
859
|
+
*/
|
|
860
|
+
async ensureCollectionPolicies(
|
|
861
|
+
collections: unknown[],
|
|
862
|
+
driverResult: InitializedDriver,
|
|
863
|
+
log?: (message: string) => void
|
|
864
|
+
): Promise<{ applied: number }> {
|
|
865
|
+
const internals = driverResult.internals as PostgresDriverInternals;
|
|
866
|
+
const { ensureCollectionPolicies } = await import("./schema/ensure-collection-policies");
|
|
867
|
+
const queryable = {
|
|
868
|
+
async query<T>(text: string): Promise<{ rows: T[] }> {
|
|
869
|
+
const result = await internals.db.execute(sql.raw(text));
|
|
870
|
+
const rows = (result as unknown as { rows?: T[] }).rows;
|
|
871
|
+
return { rows: rows ?? (Array.isArray(result) ? (result as T[]) : []) };
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const outcome = await ensureCollectionPolicies(
|
|
875
|
+
queryable,
|
|
876
|
+
collections as CollectionConfig[],
|
|
877
|
+
log
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
for (const skip of outcome.skipped) {
|
|
881
|
+
logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
|
|
882
|
+
}
|
|
883
|
+
for (const failure of outcome.failures) {
|
|
884
|
+
logger.warn(
|
|
885
|
+
`🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Retire the pre-1.0 `auth` schema now that the policies above no
|
|
890
|
+
// longer call into it. Deliberately after, and deliberately quiet:
|
|
891
|
+
// Postgres refuses to drop a function an RLS policy still
|
|
892
|
+
// references, so on a database where some table has not been
|
|
893
|
+
// recompiled yet this is expected to fail and succeed on a later
|
|
894
|
+
// boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it
|
|
895
|
+
// off a Supabase `auth` schema.
|
|
896
|
+
try {
|
|
897
|
+
const { dropLegacyAuthSchema } = await import("./schema/rls-bootstrap-sql");
|
|
898
|
+
await dropLegacyAuthSchema(
|
|
899
|
+
async (text) => {
|
|
900
|
+
const res = await internals.db.execute(sql.raw(text));
|
|
901
|
+
return (res.rows ?? []) as Record<string, unknown>[];
|
|
902
|
+
},
|
|
903
|
+
{ info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
904
|
+
);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
logger.info(
|
|
907
|
+
"Left the legacy `auth` schema in place: " +
|
|
908
|
+
(err instanceof Error ? err.message : String(err))
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return { applied: outcome.policiesApplied };
|
|
715
913
|
},
|
|
716
914
|
|
|
717
915
|
getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {
|