@rebasepro/server-postgres 0.9.1-canary.16c42e9 → 0.9.1-canary.1d2d8b5

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.
Files changed (41) hide show
  1. package/dist/PostgresBackendDriver.d.ts +2 -25
  2. package/dist/PostgresBootstrapper.d.ts +0 -10
  3. package/dist/auth/services.d.ts +0 -16
  4. package/dist/connection.d.ts +0 -21
  5. package/dist/data-transformer.d.ts +2 -9
  6. package/dist/index.es.js +548 -1310
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/schema/auth-default-policies.d.ts +10 -0
  9. package/dist/services/FetchService.d.ts +24 -4
  10. package/dist/services/PersistService.d.ts +1 -9
  11. package/dist/services/RelationService.d.ts +1 -34
  12. package/dist/services/collection-helpers.d.ts +14 -79
  13. package/dist/services/dataService.d.ts +1 -3
  14. package/dist/services/index.d.ts +1 -1
  15. package/dist/services/realtimeService.d.ts +0 -7
  16. package/package.json +8 -10
  17. package/src/PostgresBackendDriver.ts +13 -127
  18. package/src/PostgresBootstrapper.ts +25 -62
  19. package/src/auth/ensure-tables.ts +11 -73
  20. package/src/auth/services.ts +19 -49
  21. package/src/connection.ts +1 -61
  22. package/src/data-transformer.ts +9 -11
  23. package/src/databasePoolManager.ts +0 -2
  24. package/src/schema/auth-default-policies.ts +132 -0
  25. package/src/schema/generate-drizzle-schema-logic.ts +29 -24
  26. package/src/schema/generate-postgres-ddl-logic.ts +28 -76
  27. package/src/schema/introspect-db.ts +2 -19
  28. package/src/services/BranchService.ts +10 -42
  29. package/src/services/FetchService.ts +270 -65
  30. package/src/services/PersistService.ts +9 -62
  31. package/src/services/RelationService.ts +94 -153
  32. package/src/services/collection-helpers.ts +47 -164
  33. package/src/services/dataService.ts +2 -3
  34. package/src/services/index.ts +0 -1
  35. package/src/services/realtimeService.ts +19 -40
  36. package/src/utils/drizzle-conditions.ts +0 -13
  37. package/src/websocket.ts +1 -4
  38. package/dist/collections/buildRegistry.d.ts +0 -27
  39. package/dist/services/row-pipeline.d.ts +0 -63
  40. package/src/collections/buildRegistry.ts +0 -59
  41. package/src/services/row-pipeline.ts +0 -215
@@ -0,0 +1,132 @@
1
+ import { CollectionConfig, SecurityRule, SecurityOperation, AuthCollectionConfig, PolicyExpression, isPostgresCollectionConfig, policy } from "@rebasepro/types";
2
+ import { getTableName } from "@rebasepro/common";
3
+
4
+ /**
5
+ * Default RLS policies injected by the schema generator.
6
+ *
7
+ * Rebase's enforcement model is unified: authenticated (user-context) requests
8
+ * run under the restricted `rebase_user` role, so Postgres RLS binds *every*
9
+ * statement — reads and writes. A collection's `securityRules` are the whole
10
+ * authorization model. The server context (auth flows, migrations,
11
+ * `dataAsAdmin`) runs as the owner and bypasses RLS.
12
+ *
13
+ * Because RLS default-denies, every collection is **locked by default**: with
14
+ * no rules, only the server context and admins can touch it. The generator
15
+ * injects that safe baseline:
16
+ *
17
+ * **For every collection**
18
+ * 1. A permissive **server-or-admin SELECT** grant.
19
+ * 2. A permissive **server-or-admin write** grant (insert/update/delete).
20
+ *
21
+ * Author `securityRules` are permissive and OR together, so explicit rules only
22
+ * *broaden* access from this locked baseline (e.g. "users read/write their own
23
+ * rows").
24
+ *
25
+ * **For auth collections additionally**
26
+ * 3. A permissive **self SELECT** grant (`id = auth.uid()`), so users can read
27
+ * their own row (profile, session bootstrap) without every app re-declaring
28
+ * it.
29
+ * 4. A **restrictive** admin write gate. Restrictive policies are AND'd with
30
+ * every other policy, so a write is rejected unless the caller is an admin
31
+ * (or the server context) — even if the author also wrote a permissive rule
32
+ * such as "a user may edit their own row". Without this, a permissive owner
33
+ * rule would let a user change their own `roles`.
34
+ *
35
+ * The server context is recognised as `auth.uid() IS NULL` (`policy.serverContext()`)
36
+ * — the built-in flows that run without a user (signup, migrations) set no user
37
+ * GUC — which also lets the owner connection satisfy these policies even under
38
+ * FORCE RLS. A *user* request never reaches that state: an anonymous one carries
39
+ * `ANONYMOUS_USER_ID`, precisely so it cannot pass for the server here.
40
+ *
41
+ * Opt out with `disableDefaultPolicies: true` to take full responsibility for
42
+ * the collection's RLS.
43
+ */
44
+ // Expressed structurally (not as raw SQL) so the admin UI can evaluate it
45
+ // exactly — the framework's most security-critical policies must be reflected
46
+ // precisely, not left as un-evaluable raw clauses. Compiles to
47
+ // `auth.uid() IS NULL OR (string_to_array(auth.roles(), ',') && ARRAY['admin'])`.
48
+ //
49
+ // `serverContext()`, emphatically not `not(authenticated())`: the server arm of
50
+ // this grant must match the server context and nothing else. Anonymous visitors
51
+ // are not signed in either, so a negated `authenticated()` would hand them the
52
+ // server-or-admin grant on every collection's default policy.
53
+ const SERVER_OR_ADMIN_EXPR: PolicyExpression = policy.or(
54
+ policy.serverContext(),
55
+ policy.rolesOverlap(["admin"])
56
+ );
57
+
58
+ /** Write operations that must be admin-gated by default on auth collections. */
59
+ const DEFAULT_GUARDED_OPS: SecurityOperation[] = ["insert", "update", "delete"];
60
+
61
+ /** Whether a collection is flagged as an authentication collection. */
62
+ function isAuthCollection(collection: CollectionConfig): boolean {
63
+ const auth = collection.auth;
64
+ return auth === true || (typeof auth === "object" && (auth as AuthCollectionConfig)?.enabled === true);
65
+ }
66
+
67
+ /** The property marked as the row id (falls back to `id`). */
68
+ function getIdPropertyName(collection: CollectionConfig): string {
69
+ for (const [name, prop] of Object.entries(collection.properties ?? {})) {
70
+ if (prop && typeof prop === "object" && "isId" in prop && (prop as { isId?: unknown }).isId) {
71
+ return name;
72
+ }
73
+ }
74
+ return "id";
75
+ }
76
+
77
+ /**
78
+ * Returns the security rules that should be applied to a collection: the
79
+ * author's explicit `securityRules` plus the framework defaults described in
80
+ * the module doc (baseline server/admin read for all collections; self-read
81
+ * and the admin write gate for auth collections).
82
+ *
83
+ * Collections that opt out via `disableDefaultPolicies` are returned unchanged.
84
+ */
85
+ export function getEffectiveSecurityRules(collection: CollectionConfig): SecurityRule[] {
86
+ const explicit = [...((isPostgresCollectionConfig(collection) ? collection.securityRules : undefined) ?? [])];
87
+
88
+ if (collection.disableDefaultPolicies) {
89
+ return explicit;
90
+ }
91
+
92
+ const tableName = getTableName(collection);
93
+ const injected: SecurityRule[] = [];
94
+
95
+ // Baseline read + write: the server context and admins can always operate.
96
+ // RLS default-denies under the user role, so without these a rule-less
97
+ // collection would be locked to everyone — including the admin studio.
98
+ // Author rules are permissive and broaden access from here.
99
+ injected.push({
100
+ name: `${tableName}_default_admin_read`,
101
+ operations: ["select"],
102
+ condition: SERVER_OR_ADMIN_EXPR
103
+ });
104
+ injected.push({
105
+ name: `${tableName}_default_admin_write`,
106
+ operations: [...DEFAULT_GUARDED_OPS],
107
+ condition: SERVER_OR_ADMIN_EXPR,
108
+ check: SERVER_OR_ADMIN_EXPR
109
+ });
110
+
111
+ if (isAuthCollection(collection)) {
112
+ // Self-read: a user can always read their own row.
113
+ injected.push({
114
+ name: `${tableName}_default_self_read`,
115
+ operations: ["select"],
116
+ condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
117
+ });
118
+
119
+ // Restrictive gate: AND'd with all other policies, so no permissive rule
120
+ // (e.g. an owner "edit your own row" rule) can let a non-admin change
121
+ // privileged columns like `roles`.
122
+ injected.push({
123
+ name: `${tableName}_require_admin_write`,
124
+ mode: "restrictive",
125
+ operations: [...DEFAULT_GUARDED_OPS],
126
+ condition: SERVER_OR_ADMIN_EXPR,
127
+ check: SERVER_OR_ADMIN_EXPR
128
+ });
129
+ }
130
+
131
+ return [...explicit, ...injected];
132
+ }
@@ -1,8 +1,10 @@
1
1
  import { CollectionConfig, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
2
2
  import { getPrimaryKeys } from "../services/collection-helpers";
3
- import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
4
- import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
3
+ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres } from "@rebasepro/common";
4
+ import { toSnakeCase } from "@rebasepro/utils";
5
+ import { createHash } from "crypto";
5
6
  import { logger } from "@rebasepro/server";
7
+ import { getEffectiveSecurityRules } from "./auth-default-policies";
6
8
  // --- Helper Functions ---
7
9
 
8
10
  /**
@@ -313,6 +315,22 @@ const wrapSql = (clause: string): string => `sql\`${clause}\``;
313
315
  /**
314
316
  * Generates a deterministic hash based on the rule configuration.
315
317
  */
318
+ const getPolicyNameHash = (rule: SecurityRule): string => {
319
+ const data = JSON.stringify({
320
+ a: rule.access,
321
+ m: rule.mode,
322
+ op: rule.operation,
323
+ ops: rule.operations?.slice().sort(),
324
+ own: rule.ownerField,
325
+ rol: rule.roles?.slice().sort(),
326
+ pg: rule.pgRoles?.slice().sort(),
327
+ u: rule.using,
328
+ w: rule.withCheck,
329
+ c: rule.condition,
330
+ ch: rule.check
331
+ });
332
+ return createHash("sha1").update(data).digest("hex").substring(0, 7);
333
+ };
316
334
 
317
335
  /**
318
336
  * Generates Drizzle pgPolicy() calls from a declarative SecurityRule definition.
@@ -333,11 +351,15 @@ const generatePolicyCode = (collection: CollectionConfig, rule: SecurityRule, in
333
351
  ? rule.operations
334
352
  : [rule.operation ?? "all"];
335
353
 
336
- const policyNames = getPolicyNamesForRule(rule, tableName);
354
+ const ruleHash = getPolicyNameHash(rule);
337
355
 
338
356
  // Generate one pgPolicy per operation
339
357
  return ops.map((op, opIdx) => {
340
- return generateSinglePolicyCode(collection, rule, op, policyNames[opIdx], resolveCollection);
358
+ const policyName = rule.name
359
+ ? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
360
+ : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
361
+
362
+ return generateSinglePolicyCode(collection, rule, op, policyName, resolveCollection);
341
363
  }).join("");
342
364
  };
343
365
 
@@ -545,10 +567,6 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
545
567
  });
546
568
  schemaContent += "\n";
547
569
 
548
- // Junction policy derivation needs every declaring side of each junction,
549
- // not just the first relation that reached it in the walk below.
550
- const junctionSpecs = resolveJunctionSpecs(collections);
551
-
552
570
  // 2. Identify all tables (collections and junction tables only)
553
571
  for (const collection of collections) {
554
572
  const tableName = getTableName(collection);
@@ -605,22 +623,9 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
605
623
  schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
606
624
  schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
607
625
  schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
608
- schemaContent += "}, (table) => ([\n";
609
- schemaContent += ` primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] }),\n`;
610
-
611
- // Junctions are generated tables like any other: locked by default,
612
- // with derived policies (reads follow the endpoints, writes follow
613
- // the declaring side's update rules). RLS is enabled regardless of
614
- // policy stripping — a bare junction must default-deny, not fail open.
615
- const junctionSpec = junctionSpecs.get(baseTableName);
616
- if (!stripPolicies && junctionSpec) {
617
- const junctionCollection = getJunctionCollectionConfig(junctionSpec);
618
- const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
619
- getJunctionSecurityRules(junctionSpec).forEach((rule: SecurityRule, idx: number) => {
620
- schemaContent += generatePolicyCode(junctionCollection, rule, idx, resolveCollection);
621
- });
622
- }
623
- schemaContent += "])).enableRLS();\n\n";
626
+ schemaContent += "}, (table) => ({\n";
627
+ schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
628
+ schemaContent += "}));\n\n";
624
629
  } else if (!isJunction) {
625
630
  const schema = isPostgresCollectionConfig(collection) ? collection.schema : undefined;
626
631
  const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
@@ -1,6 +1,8 @@
1
1
  import { CollectionConfig, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
2
- import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
3
- import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
2
+ import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres } from "@rebasepro/common";
3
+ import { toSnakeCase } from "@rebasepro/utils";
4
+ import { createHash } from "crypto";
5
+ import { getEffectiveSecurityRules } from "./auth-default-policies";
4
6
 
5
7
  // --- Helper Functions ---
6
8
 
@@ -42,6 +44,22 @@ const isIdProperty = (propName: string, prop: Property, collection: CollectionCo
42
44
  return !hasExplicitId && propName === "id";
43
45
  };
44
46
 
47
+ const getPolicyNameHash = (rule: SecurityRule): string => {
48
+ const data = JSON.stringify({
49
+ a: rule.access,
50
+ m: rule.mode,
51
+ op: rule.operation,
52
+ ops: rule.operations?.slice().sort(),
53
+ own: rule.ownerField,
54
+ rol: rule.roles?.slice().sort(),
55
+ pg: rule.pgRoles?.slice().sort(),
56
+ u: rule.using,
57
+ w: rule.withCheck,
58
+ c: rule.condition,
59
+ ch: rule.check
60
+ });
61
+ return createHash("sha1").update(data).digest("hex").substring(0, 7);
62
+ };
45
63
 
46
64
  type ResolveCollection = (slug: string) => CollectionConfig | undefined;
47
65
 
@@ -51,10 +69,14 @@ const generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, res
51
69
  ? rule.operations
52
70
  : [rule.operation ?? "all"];
53
71
 
54
- const policyNames = getPolicyNamesForRule(rule, tableName);
72
+ const ruleHash = getPolicyNameHash(rule);
55
73
 
56
74
  return ops.map((op, opIdx) => {
57
- return generateSinglePolicyDdl(collection, rule, op, policyNames[opIdx], resolveCollection);
75
+ const policyName = rule.name
76
+ ? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
77
+ : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
78
+
79
+ return generateSinglePolicyDdl(collection, rule, op, policyName, resolveCollection);
58
80
  }).join("");
59
81
  };
60
82
 
@@ -226,10 +248,6 @@ export const generatePostgresDdl = async (
226
248
  });
227
249
  if (ddl.endsWith(";\n")) ddl += "\n";
228
250
 
229
- // Junction policy derivation needs every declaring side of each junction,
230
- // not just the first relation that reached it in the walk below.
231
- const junctionSpecs = resolveJunctionSpecs(collections);
232
-
233
251
  const allTablesToGenerate = new Map<string, {
234
252
  collection: CollectionConfig,
235
253
  isJunction?: boolean,
@@ -265,11 +283,6 @@ export const generatePostgresDdl = async (
265
283
 
266
284
  // 3. Generate tables
267
285
  const fkStatements: string[] = [];
268
- // Policies are emitted after every CREATE TABLE, like the FK constraints:
269
- // a policy may reference other tables (a junction's derived policies always
270
- // reference both endpoints; `policy.existsIn` references a join table), and
271
- // CREATE POLICY validates those relations at creation time.
272
- const policyStatements: string[] = [];
273
286
  for (const [tableName, {
274
287
  collection,
275
288
  isJunction,
@@ -302,25 +315,6 @@ export const generatePostgresDdl = async (
302
315
 
303
316
  fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${sourceColumn}_fkey" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
304
317
  fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${baseTableName}_${targetColumn}_fkey" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
305
-
306
- if (options.includePolicies) {
307
- // Junction tables are generated tables like any other: locked by
308
- // default, with derived policies — reads follow the endpoints'
309
- // visibility, writes follow the declaring side's update rules.
310
- // Without this they were the one kind of generated table with no
311
- // RLS at all, readable and writable by every signed-in user.
312
- ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
313
- ddl += `\n`;
314
-
315
- const spec = junctionSpecs.get(baseTableName);
316
- if (spec) {
317
- const junctionCollection = getJunctionCollectionConfig(spec);
318
- const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
319
- getJunctionSecurityRules(spec).forEach((rule: SecurityRule) => {
320
- policyStatements.push(generatePolicyDdl(junctionCollection, rule, resolveCollection));
321
- });
322
- }
323
- }
324
318
  } else if (!isJunction) {
325
319
  ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
326
320
  const columns: string[] = [];
@@ -442,8 +436,9 @@ export const generatePostgresDdl = async (
442
436
  if (securityRules.length > 0) {
443
437
  const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
444
438
  securityRules.forEach((rule: SecurityRule) => {
445
- policyStatements.push(generatePolicyDdl(collection, rule, resolveCollection));
439
+ ddl += generatePolicyDdl(collection, rule, resolveCollection);
446
440
  });
441
+ ddl += "\n";
447
442
  }
448
443
  }
449
444
  }
@@ -454,12 +449,6 @@ export const generatePostgresDdl = async (
454
449
  ddl += fkStatements.join("\n") + "\n\n";
455
450
  }
456
451
 
457
- if (policyStatements.length > 0) {
458
- ddl += "-- Row Level Security Policies\n";
459
- ddl += policyStatements.join("");
460
- ddl += "\n";
461
- }
462
-
463
452
  return ddl;
464
453
  };
465
454
 
@@ -489,50 +478,13 @@ export const generatePostgresPoliciesDdl = (collections: CollectionConfig[]): st
489
478
  const securityRules = getEffectiveSecurityRules(collection);
490
479
  if (securityRules.length > 0) {
491
480
  const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
492
- const injectedNames = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));
493
-
494
481
  securityRules.forEach((rule: SecurityRule) => {
495
- // Say which policies the author did not write. They are permissive,
496
- // so they OR with the declared rules and widen the final ACL beyond
497
- // what `securityRules` reads like — and re-appear after any manual
498
- // DROP, because a push asserts the declared state.
499
- if (rule.name && injectedNames.has(rule.name)) {
500
- ddl += `-- Injected by Rebase (not from this collection's securityRules).\n`;
501
- ddl += `-- Set \`disableDefaultPolicies: true\` on "${collection.slug}" to drop these and own its RLS outright.\n`;
502
- }
503
482
  ddl += generatePolicyDdl(collection, rule, resolveCollection);
504
483
  });
505
484
  ddl += "\n";
506
485
  }
507
486
  }
508
487
 
509
- // Junction tables are generated from `through` relations, not declared as
510
- // collections, so the walk above never sees them. They get the same
511
- // treatment as any generated table: locked by default, with derived
512
- // policies — reads follow the endpoints, writes follow the declaring
513
- // side's update rules.
514
- const junctionSpecs = resolveJunctionSpecs(collections);
515
- for (const spec of junctionSpecs.values()) {
516
- ddl += `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;\n`;
517
- ddl += `\n`;
518
-
519
- const junctionRules = getJunctionSecurityRules(spec);
520
- if (junctionRules.length === 0) continue;
521
-
522
- const junctionCollection = getJunctionCollectionConfig(spec);
523
- const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
524
- const declaringSlugs = spec.declaringSides.map(s => s.collection.slug).join('", "');
525
-
526
- ddl += `-- Derived by Rebase for the junction "${spec.table}" (no collection declares it).\n`;
527
- ddl += `-- Reads require both endpoint rows to be visible; writes follow the update\n`;
528
- ddl += `-- rules of "${declaringSlugs}". Set \`disableDefaultPolicies: true\` on the\n`;
529
- ddl += `-- declaring collection(s) to drop these and police the junction yourself.\n`;
530
- junctionRules.forEach((rule: SecurityRule) => {
531
- ddl += generatePolicyDdl(junctionCollection, rule, resolveCollection);
532
- });
533
- ddl += "\n";
534
- }
535
-
536
488
  return ddl;
537
489
  };
538
490
 
@@ -30,7 +30,6 @@ async function main() {
30
30
  "--force": Boolean,
31
31
  "--schema": String,
32
32
  "--data-inference": Boolean,
33
- "--no-data-inference": Boolean,
34
33
  "-o": "--output",
35
34
  "-c": "--collections",
36
35
  "-f": "--force"
@@ -167,14 +166,8 @@ async function main() {
167
166
  logger.info(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
168
167
 
169
168
  let runDataInference = false;
170
- if (args["--no-data-inference"]) {
171
- runDataInference = false;
172
- } else if (args["--data-inference"] !== undefined) {
169
+ if (args["--data-inference"] !== undefined) {
173
170
  runDataInference = args["--data-inference"];
174
- } else if (!process.stdin.isTTY) {
175
- // No terminal to answer the question below (scaffolding scripts, CI,
176
- // `rebase init --introspect`) — asking would hang forever.
177
- logger.info(chalk.gray("Skipping data inference (non-interactive run; pass --data-inference to enable)."));
178
171
  } else {
179
172
  const rl = readline.createInterface({
180
173
  input: process.stdin,
@@ -243,17 +236,7 @@ async function main() {
243
236
  const merged = mergeIndexContent(existing, generatedFiles);
244
237
  fs.writeFileSync(indexPath, merged, "utf-8");
245
238
  } else {
246
- // --force replaces collections derived from the database, but the
247
- // directory can also hold hand-written ones with no table in the
248
- // introspected schema (the auth users collection lives in
249
- // "rebase"). The backend discovers the whole directory, so an
250
- // index listing only introspected tables would silently drop them
251
- // from the admin UI while the API still served them.
252
- const siblings = fs.readdirSync(outDir)
253
- .filter(f => f.endsWith(".ts") && f !== "index.ts")
254
- .map(f => f.replace(/\.ts$/, ""));
255
- const allFiles = [...new Set([...generatedFiles, ...siblings])];
256
- const indexContent = generateIndexContent(allFiles);
239
+ const indexContent = generateIndexContent(generatedFiles);
257
240
  fs.writeFileSync(indexPath, indexContent, "utf-8");
258
241
  }
259
242
  logger.info(chalk.green(` ✓ ${indexPath}`));
@@ -11,45 +11,10 @@ import { sql } from "drizzle-orm";
11
11
  import { BranchInfo } from "@rebasepro/types";
12
12
  import { DrizzleClient } from "../interfaces";
13
13
  import { DatabasePoolManager } from "../databasePoolManager";
14
- import { extractPgError, extractCauseMessage } from "../utils/pg-error-utils";
15
14
 
16
15
  /** Internal prefix applied to branch database names to avoid collisions. */
17
16
  const BRANCH_DB_PREFIX = "rb_";
18
17
 
19
- /** `duplicate_database` — the target database name is already taken. */
20
- const PG_DUPLICATE_DATABASE = "42P04";
21
-
22
- /** `object_in_use` — the database still has connections attached. */
23
- const PG_OBJECT_IN_USE = "55006";
24
-
25
- /**
26
- * Describe a failed branch DDL statement in terms a user can act on.
27
- *
28
- * Drizzle reports failures as `Failed query: <sql> params:` and hides the real
29
- * PostgreSQL error in the `cause` chain, so matching on `err.message` never sees
30
- * the actual problem. Match on the PG error code instead — it survives wrapping
31
- * and, unlike the message text, is not locale-dependent.
32
- */
33
- function describeBranchDdlError(err: unknown, fallbackContext: string): Error {
34
- const pgError = extractPgError(err);
35
-
36
- if (pgError?.code === PG_DUPLICATE_DATABASE) {
37
- return new Error(`Database "${fallbackContext}" already exists on the server. Choose a different branch name.`);
38
- }
39
- if (pgError?.code === PG_OBJECT_IN_USE) {
40
- return new Error(
41
- `Cannot complete the operation: the database "${fallbackContext}" has active connections. ` +
42
- "Close other clients or connections and try again."
43
- );
44
- }
45
-
46
- // Unknown failure: surface the real PG message rather than the Drizzle
47
- // wrapper, which would otherwise show the raw SQL and no reason at all.
48
- const detail = pgError?.message ?? extractCauseMessage(err);
49
- if (detail) return new Error(detail);
50
- return err instanceof Error ? err : new Error(String(err));
51
- }
52
-
53
18
  /** Fully-qualified metadata table in the rebase schema. */
54
19
  const BRANCHES_TABLE = "rebase.branches";
55
20
 
@@ -144,15 +109,18 @@ export class BranchService {
144
109
  sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`)
145
110
  );
146
111
  } catch (err) {
147
- const pgError = extractPgError(err);
148
- if (pgError?.code === PG_OBJECT_IN_USE) {
149
- // The template not the new database is the one still in use.
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ if (msg.includes("already exists")) {
114
+ throw new Error(`Database "${dbName}" already exists on the server. Choose a different branch name.`);
115
+ }
116
+ // If template fails due to active connections, provide a helpful error
117
+ if (msg.includes("being accessed by other users")) {
150
118
  throw new Error(
151
119
  `Cannot create branch: the source database "${sourceDb}" has active connections. ` +
152
120
  "Close other clients or connections and try again."
153
121
  );
154
122
  }
155
- throw describeBranchDdlError(err, dbName);
123
+ throw err;
156
124
  }
157
125
 
158
126
  // Record metadata in the default database
@@ -198,14 +166,14 @@ export class BranchService {
198
166
  try {
199
167
  await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
200
168
  } catch (err) {
201
- const pgError = extractPgError(err);
202
- if (pgError?.code === PG_OBJECT_IN_USE) {
169
+ const msg = err instanceof Error ? err.message : String(err);
170
+ if (msg.includes("being accessed by other users")) {
203
171
  throw new Error(
204
172
  `Cannot delete branch "${sanitizedName}": the database has active connections. ` +
205
173
  "Close other clients and try again."
206
174
  );
207
175
  }
208
- throw describeBranchDdlError(err, dbName);
176
+ throw err;
209
177
  }
210
178
 
211
179
  // Remove metadata