@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.
Files changed (112) hide show
  1. package/dist/PostgresBackendDriver.d.ts +49 -2
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
  5. package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
  6. package/dist/backup/backup-service.d.ts +10 -1
  7. package/dist/backup/pg-tools.d.ts +47 -0
  8. package/dist/backup-service-Bww-Lg0s.js +8999 -0
  9. package/dist/backup-service-Bww-Lg0s.js.map +1 -0
  10. package/dist/cli-helpers.d.ts +40 -1
  11. package/dist/connection-BuZ97wsr.js +250 -0
  12. package/dist/connection-BuZ97wsr.js.map +1 -0
  13. package/dist/connection.d.ts +42 -0
  14. package/dist/ensure-collection-policies-DMUdRQdM.js +57 -0
  15. package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
  16. package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
  17. package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
  18. package/dist/index.es.js +1701 -10167
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/policy-CPkCqVTz.js +105 -0
  21. package/dist/policy-CPkCqVTz.js.map +1 -0
  22. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  23. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  24. package/dist/schema/auth-schema.d.ts +83 -144
  25. package/dist/schema/auth-users-columns.d.ts +97 -0
  26. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  27. package/dist/schema/ensure-collection-tables.d.ts +45 -3
  28. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  29. package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -3
  30. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  31. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  32. package/dist/schema/introspect-db-logic.d.ts +94 -5
  33. package/dist/schema/introspect-db-queries.d.ts +119 -0
  34. package/dist/schema/introspect-db-structure.d.ts +263 -0
  35. package/dist/schema/introspect-db-types.d.ts +11 -0
  36. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  37. package/dist/security/rls-enforcement.d.ts +53 -2
  38. package/dist/services/FetchService.d.ts +14 -8
  39. package/dist/services/RelationService.d.ts +24 -1
  40. package/dist/services/channel-bus/index.d.ts +1 -7
  41. package/dist/services/collection-helpers.d.ts +36 -2
  42. package/dist/services/dataService.d.ts +5 -1
  43. package/dist/services/realtimeService.d.ts +25 -21
  44. package/dist/services/row-pipeline.d.ts +1 -1
  45. package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
  46. package/dist/src-C_wvdMnl.js.map +1 -0
  47. package/dist/utils/connection-string.d.ts +29 -0
  48. package/dist/utils/drizzle-conditions.d.ts +5 -4
  49. package/dist/utils/pg-error-utils.d.ts +35 -0
  50. package/dist/websocket-D0TBU3ia.js +530 -0
  51. package/dist/websocket-D0TBU3ia.js.map +1 -0
  52. package/package.json +17 -16
  53. package/src/PostgresAdapter.ts +21 -2
  54. package/src/PostgresBackendDriver.ts +169 -3
  55. package/src/PostgresBootstrapper.ts +233 -35
  56. package/src/auth/ensure-tables.ts +335 -81
  57. package/src/auth/services.ts +24 -2
  58. package/src/backup/backup-cli.ts +41 -2
  59. package/src/backup/backup-service.ts +38 -5
  60. package/src/backup/pg-tools.ts +96 -3
  61. package/src/cli-helpers.ts +92 -9
  62. package/src/cli.ts +219 -56
  63. package/src/collections/validate-relations.ts +136 -14
  64. package/src/connection.ts +73 -0
  65. package/src/data-transformer.ts +22 -6
  66. package/src/databasePoolManager.ts +5 -2
  67. package/src/history/ensure-history-table.ts +7 -0
  68. package/src/schema/auth-schema.ts +30 -19
  69. package/src/schema/auth-users-columns.ts +131 -0
  70. package/src/schema/doctor.ts +7 -5
  71. package/src/schema/ensure-collection-policies.ts +105 -0
  72. package/src/schema/ensure-collection-tables.test.ts +105 -9
  73. package/src/schema/ensure-collection-tables.ts +301 -44
  74. package/src/schema/generate-drizzle-schema-logic.ts +49 -7
  75. package/src/schema/generate-postgres-ddl-logic.ts +474 -27
  76. package/src/schema/generated-schema-staleness.ts +169 -0
  77. package/src/schema/introspect-db-constraints.ts +385 -0
  78. package/src/schema/introspect-db-inference.ts +18 -8
  79. package/src/schema/introspect-db-logic.ts +385 -71
  80. package/src/schema/introspect-db-queries.ts +326 -0
  81. package/src/schema/introspect-db-structure.ts +670 -0
  82. package/src/schema/introspect-db-types.ts +56 -0
  83. package/src/schema/introspect-db.ts +37 -80
  84. package/src/schema/introspect-runtime.test.ts +56 -8
  85. package/src/schema/introspect-runtime.ts +31 -9
  86. package/src/schema/non-sql-collections.test.ts +131 -0
  87. package/src/schema/rls-bootstrap-sql.ts +288 -0
  88. package/src/security/anonymous-grants.test.ts +4 -2
  89. package/src/security/policy-drift.test.ts +11 -3
  90. package/src/security/rls-enforcement.ts +141 -3
  91. package/src/services/BranchService.ts +71 -28
  92. package/src/services/FetchService.ts +100 -107
  93. package/src/services/PersistService.ts +34 -7
  94. package/src/services/RelationService.ts +249 -48
  95. package/src/services/channel-bus/index.ts +0 -9
  96. package/src/services/channel-history.ts +8 -0
  97. package/src/services/channel-presence.ts +6 -0
  98. package/src/services/collection-helpers.ts +69 -3
  99. package/src/services/dataService.ts +5 -1
  100. package/src/services/realtimeService.ts +38 -35
  101. package/src/services/row-pipeline.ts +1 -1
  102. package/src/utils/connection-string.ts +58 -0
  103. package/src/utils/drizzle-conditions.ts +31 -6
  104. package/src/utils/pg-error-utils.ts +65 -0
  105. package/src/websocket.ts +18 -9
  106. package/dist/chunk-DSJWtz9O.js +0 -40
  107. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  108. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  109. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  110. package/dist/src-BbFOPJ1S.js.map +0 -1
  111. package/dist/src-Zqwaw3P5.js.map +0 -1
  112. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The one description of what an auth user table's columns must be.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Three different code paths create `rebase.users`, and before this module they
7
+ * disagreed about it:
8
+ *
9
+ * | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |
10
+ * |------------------|-----------------------|-------------------------|--------------------------|
11
+ * | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |
12
+ * | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |
13
+ * | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |
14
+ * | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |
15
+ *
16
+ * They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so
17
+ * whichever ran first decided the table and the other two silently no-op'd.
18
+ * Boot order therefore chose the constraints: a managed deploy (collection
19
+ * tables first) got a users table whose `email` was nullable and whose
20
+ * `email_verified` had no default, while the same project pushed from a
21
+ * checkout got the strict one. In the same table, `is_anonymous` came out
22
+ * `NOT NULL DEFAULT false` — because *that* column existed in only one of the
23
+ * three lists, so its owner's definition won by default.
24
+ *
25
+ * A second consequence was drift in the other direction. The scaffold's
26
+ * `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and
27
+ * builds its desired state from the collection alone, so the two columns only
28
+ * auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged
29
+ * drift, and a push run after the server had booted once planned to DROP them.
30
+ *
31
+ * Both problems are the same problem: no single place said what this table is.
32
+ * This is that place. Every creator asks here first and only then falls back to
33
+ * the collection's own property definitions, so a column auth owns has one
34
+ * definition regardless of who gets there first, and a column the developer
35
+ * added to their users collection still behaves like any other field.
36
+ *
37
+ * ## What belongs here
38
+ *
39
+ * Only columns the auth services read or write. A developer's own additions to
40
+ * their users collection (`bio`, `stripe_customer_id`) are ordinary columns and
41
+ * must NOT be listed — they are generated from the collection like every other
42
+ * field, and listing them here would freeze a user's schema.
43
+ *
44
+ * `id` is deliberately absent: its type comes from the collection's id property
45
+ * (uuid / increment / text) and each creator already derives it.
46
+ *
47
+ * Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an
48
+ * older runtime unable to work against a migrated table.
49
+ */
50
+ /**
51
+ * A column auth owns.
52
+ *
53
+ * Structured rather than one SQL string because the same facts are needed in
54
+ * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,
55
+ * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a
56
+ * table that already exists with the wrong shape. Parsing a string back apart
57
+ * for the third of those is how the copies drifted in the first place.
58
+ */
59
+ export interface AuthUsersColumn {
60
+ /** Physical column name. */
61
+ column: string;
62
+ /** Postgres type. */
63
+ type: string;
64
+ /** Default expression, verbatim, or absent for no default. */
65
+ default?: string;
66
+ /** Whether the column is NOT NULL. */
67
+ notNull?: boolean;
68
+ }
69
+ /**
70
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
71
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
72
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
73
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
74
+ * table that already holds a longer row.
75
+ */
76
+ export declare const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[];
77
+ /** Type + inline constraints, as they appear after the column name. */
78
+ export declare function authUsersColumnSql(spec: AuthUsersColumn): string;
79
+ /**
80
+ * The auth-owned definition for a physical column name, or `undefined` when
81
+ * auth does not own it.
82
+ *
83
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
84
+ * that is the only name the three creators agree on: the scaffold's users
85
+ * collection spells the property `displayName` and the column `display_name`.
86
+ */
87
+ export declare function authUsersColumnDefinition(column: string): string | undefined;
88
+ /**
89
+ * Whether a collection is an auth collection, i.e. whether the definitions in
90
+ * this module apply to its table at all.
91
+ *
92
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
93
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
94
+ * that hold looser objects, and both spellings must accept `auth: true` as well
95
+ * as `auth: { enabled: true }`.
96
+ */
97
+ export declare function isAuthCollection(collection: unknown): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Applying a bundle's RLS policies to a database at boot, idempotently.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * {@link ensureCollectionTables} creates the collection *tables* a managed
7
+ * runtime boots against, but a table with row-level security disabled and no
8
+ * policies is not servable: authenticated requests run as the restricted
9
+ * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a
10
+ * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy
11
+ * is denied. The policies live in the collections' `securityRules`; nothing at
12
+ * boot applied them. `rebase db push` does — but it drives Atlas against a
13
+ * local `DATABASE_URL`, and a managed tenant's database is reachable only from
14
+ * inside the cluster, by the runtime that is already connected to it. So the
15
+ * runtime is the only thing that *can* apply them, and this is where it does.
16
+ *
17
+ * ## Why this is safe to run on every boot
18
+ *
19
+ * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once
20
+ * enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by
21
+ * a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds
22
+ * and replaces; it never drops data. (It does not *reconcile* — a policy a
23
+ * previous push left behind under an old name is not removed here; that stays a
24
+ * `db push` / `db migrate` concern, alongside destructive schema changes.)
25
+ *
26
+ * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a
27
+ * table whose policies could not be applied fails **closed** (denies) rather
28
+ * than leaking rows. One collection's policy failing (e.g. a rule that
29
+ * references a table a real migration has not created yet) must not crash-loop
30
+ * the whole deployment and take the other collections' working routes down with
31
+ * it. Failures are reported loudly and per-table so the operator can see
32
+ * exactly which collection is not yet servable and why.
33
+ */
34
+ import { type CollectionConfig } from "@rebasepro/types";
35
+ import { type Queryable } from "./ensure-collection-tables";
36
+ export interface PolicyEnsureResult {
37
+ /** `CREATE POLICY` statements that ran successfully. */
38
+ policiesApplied: number;
39
+ /** Tables that had RLS enabled. */
40
+ tablesSecured: number;
41
+ /** Declared tables absent from the database — left to a real migration. */
42
+ skipped: {
43
+ table: string;
44
+ reason: string;
45
+ }[];
46
+ /** Tables whose RLS could not be fully applied (fail closed). */
47
+ failures: {
48
+ table: string;
49
+ error: string;
50
+ }[];
51
+ }
52
+ /**
53
+ * Bring the declared collections' RLS policies up to date. Returns what it did.
54
+ *
55
+ * Only tables that already exist are touched: the boot-time table creator runs
56
+ * first, so anything still missing is a table this additive path is not allowed
57
+ * to create (a junction, or a relation left to a migration). Enabling RLS on a
58
+ * non-existent table would error, so those are recorded as skipped, not failed.
59
+ */
60
+ export declare function ensureCollectionPolicies(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<PolicyEnsureResult>;
@@ -46,9 +46,17 @@ export interface ExistingSchema {
46
46
  tables: Map<string, Set<string>>;
47
47
  /** `schema.typename` of every enum type that already exists. */
48
48
  enums: Set<string>;
49
+ /**
50
+ * `schema.table.constraint` of every constraint that already exists.
51
+ *
52
+ * Optional so a caller that only cares about tables can still build one by
53
+ * hand; absent is read as "none known", which at worst re-attempts a
54
+ * constraint that then fails harmlessly as a duplicate.
55
+ */
56
+ constraints?: Set<string>;
49
57
  }
50
58
  export interface EnsureAction {
51
- kind: "create-enum" | "create-table" | "add-column";
59
+ kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
52
60
  /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
53
61
  target: string;
54
62
  sql: string;
@@ -57,6 +65,40 @@ export interface EnsurePlan {
57
65
  actions: EnsureAction[];
58
66
  /** Every statement, in dependency order. Empty when the schema is current. */
59
67
  statements: string[];
68
+ /**
69
+ * Relation columns this plan is about to create where the table already
70
+ * carries the same column under its pre-singularization name.
71
+ *
72
+ * The reason this is reported rather than silently handled: the ensure is
73
+ * additive, so it would add `category_id` beside a populated
74
+ * `categorie_id` and the relation would then read the new, empty one. No
75
+ * statement fails, no table is missing, and the only symptom is relations
76
+ * resolving to nothing — which is indistinguishable from having no data.
77
+ */
78
+ legacyForeignKeys: LegacyForeignKey[];
79
+ }
80
+ /** A relation column whose old and new spellings both plausibly apply. */
81
+ export interface LegacyForeignKey {
82
+ /** `schema.table`. */
83
+ table: string;
84
+ /** The name the current rule derives, and what this plan would create. */
85
+ expected: string;
86
+ /** The name the old rule derived, which the table already has. */
87
+ legacy: string;
88
+ }
89
+ export interface EnsureOutcome extends EnsurePlan {
90
+ /**
91
+ * Constraints that could not be added — always non-fatal.
92
+ *
93
+ * A foreign key can only fail on data that already violates it, and the
94
+ * column it would police exists either way, so the collection still serves.
95
+ * Refusing to boot over one would turn a pre-existing data problem into an
96
+ * outage. Reported loudly instead.
97
+ */
98
+ failures: {
99
+ target: string;
100
+ error: string;
101
+ }[];
60
102
  }
61
103
  /**
62
104
  * Decide what to add. Pure — the caller supplies what exists and runs the result.
@@ -65,7 +107,7 @@ export interface EnsurePlan {
65
107
  * that reference them, tables before the columns added to other tables (a new
66
108
  * table may be the target of a relation), and nothing is emitted twice.
67
109
  */
68
- export declare function planCollectionSchemaEnsure(collections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
110
+ export declare function planCollectionSchemaEnsure(allCollections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
69
111
  /** Read what the database has, for the schemas the collections live in. */
70
112
  export declare function readExistingSchema(client: Queryable, schemas: string[]): Promise<ExistingSchema>;
71
113
  /**
@@ -76,4 +118,4 @@ export declare function readExistingSchema(client: Queryable, schemas: string[])
76
118
  * cannot be added, say) should not roll back the tables that were created fine.
77
119
  * The error is surfaced with the statement that caused it.
78
120
  */
79
- export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsurePlan>;
121
+ export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsureOutcome>;
@@ -1,2 +1,2 @@
1
1
  import { CollectionConfig } from "@rebasepro/types";
2
- export declare const generateSchema: (collections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
2
+ export declare const generateSchema: (allCollections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
@@ -1,8 +1,142 @@
1
- import { CollectionConfig, Property } from "@rebasepro/types";
1
+ import { CollectionConfig, Property, SecurityRule } from "@rebasepro/types";
2
2
  export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
3
+ export declare const getPrimaryKeyProp: (collection: CollectionConfig) => {
4
+ name: string;
5
+ type: "string" | "number";
6
+ isUuid: boolean;
7
+ };
8
+ export declare const isNumericId: (collection: CollectionConfig) => boolean;
9
+ export declare const getPrimaryKeyName: (collection: CollectionConfig) => string;
3
10
  export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
11
+ type ResolveCollection = (slug: string) => CollectionConfig | undefined;
12
+ /**
13
+ * The individual SQL statements a single security rule compiles to: a
14
+ * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
15
+ * statement (terminated by `;`, no trailing newline).
16
+ *
17
+ * This is the primitive the boot-time RLS applier runs one statement at a time
18
+ * (the runtime's DB handle speaks the extended query protocol, which forbids
19
+ * multiple commands in one execute), while `db push` writes the joined string.
20
+ */
21
+ export declare const generatePolicyStatements: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection) => string[];
22
+ /**
23
+ * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
24
+ * quote). Enum labels come straight from user-authored collection config, so a
25
+ * label like `it's` closes the literal early and the whole generated file stops
26
+ * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
27
+ * because ensure-collection-tables already imports from this module — the
28
+ * reverse would be a cycle.
29
+ */
30
+ export declare const quoteSqlLiteral: (value: string) => string;
4
31
  export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
5
- export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
32
+ export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
6
33
  includePolicies?: boolean;
7
34
  }) => Promise<string>;
8
- export declare const generatePostgresPoliciesDdl: (collections: CollectionConfig[]) => string;
35
+ /** The RLS statements one declared collection's table needs, ready to run. */
36
+ /**
37
+ * A foreign key, as both its parts and the statement that creates it.
38
+ *
39
+ * `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`, so a caller applying
40
+ * these has to skip by name — hence the name is a field and not only a substring
41
+ * of the SQL.
42
+ */
43
+ export interface ForeignKeyPlan {
44
+ constraintName: string;
45
+ schema: string;
46
+ /** Bare table name, no schema prefix. */
47
+ table: string;
48
+ column: string;
49
+ targetSchema: string;
50
+ targetTable: string;
51
+ targetColumn: string;
52
+ sql: string;
53
+ }
54
+ /** A column a `relation` or `reference` property owns on its own table. */
55
+ export interface RelationalColumnPlan {
56
+ schema: string;
57
+ /** Bare table name, no schema prefix. */
58
+ table: string;
59
+ column: string;
60
+ /** Postgres type, exactly as the DDL generator declares it. */
61
+ type: string;
62
+ /** Absent when the target collection is not part of this bundle. */
63
+ foreignKey?: ForeignKeyPlan;
64
+ /**
65
+ * What this column would have been called before `generateForeignKeyName`
66
+ * learned to singularize — set only when the two differ and the column name
67
+ * is the derived default rather than one the author wrote.
68
+ *
69
+ * Carried so the boot-time ensure can notice a database provisioned under
70
+ * the old rule. It is never used to name anything.
71
+ */
72
+ legacyColumn?: string;
73
+ }
74
+ /** The table behind a many-to-many `through` relation. */
75
+ export interface JunctionTablePlan {
76
+ schema: string;
77
+ /** Bare table name, no schema prefix. */
78
+ table: string;
79
+ columns: {
80
+ name: string;
81
+ type: string;
82
+ legacyName?: string;
83
+ }[];
84
+ /** Both endpoint columns plus the composite primary key. */
85
+ createTable: string;
86
+ foreignKeys: ForeignKeyPlan[];
87
+ }
88
+ /**
89
+ * The FK columns the declared collections own — one entry per `relation`
90
+ * (`belongsTo` side) or `reference` property.
91
+ *
92
+ * Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
93
+ * create the same columns with the same names, types and constraints. Before
94
+ * this it skipped them outright, which was survivable only because `db push`
95
+ * always followed; on a managed tenant nothing follows, so a table arrived
96
+ * without the column its own collection reads and wrote 400 on every insert.
97
+ *
98
+ * A relation whose target is not in the bundle yields no column at all (the
99
+ * generator returns early on an unresolvable target); a `reference` whose target
100
+ * is unknown yields the column without a constraint. Both mirror the generator
101
+ * exactly — a divergence here is a schema fork between boot and `db push`.
102
+ */
103
+ export declare const planRelationalColumns: (allCollections: CollectionConfig[]) => RelationalColumnPlan[];
104
+ /**
105
+ * The junction tables a bundle's many-to-many relations imply.
106
+ *
107
+ * Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
108
+ * comes from, so a table created here always has policies planned for it — a
109
+ * junction with row-level security left off is readable and writable by every
110
+ * signed-in user, which is why the two must ship together.
111
+ */
112
+ export declare const planJunctionTables: (allCollections: CollectionConfig[]) => JunctionTablePlan[];
113
+ export interface CollectionPolicyPlan {
114
+ /** The table's schema (e.g. `public`, `rebase`). */
115
+ schema: string;
116
+ /** The bare table name, no schema prefix. */
117
+ table: string;
118
+ /** `schema.table` — matches the keys `readExistingSchema` returns. */
119
+ qualified: string;
120
+ /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */
121
+ enableRls: string;
122
+ /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */
123
+ policyStatements: string[];
124
+ }
125
+ /**
126
+ * The per-table RLS plan for the *declared* collections, as executable
127
+ * statements — what the managed runtime applies at boot so a freshly
128
+ * provisioned tenant database serves data instead of 401ing every read.
129
+ *
130
+ * Mirrors {@link generatePostgresPoliciesDdl} exactly (same
131
+ * `generatePolicyStatements`, same enable-RLS, same effective rules, same
132
+ * derived junction rules), so boot and `db push` produce identical policies from
133
+ * identical collections.
134
+ *
135
+ * Junction tables are included, and have to be: boot creates them now
136
+ * ({@link planJunctionTables}), and a junction with RLS left off is readable and
137
+ * writable by every signed-in user. A junction whose table is still absent is
138
+ * skipped by the applier, not planned away here.
139
+ */
140
+ export declare const planCollectionPolicies: (allCollections: CollectionConfig[]) => CollectionPolicyPlan[];
141
+ export declare const generatePostgresPoliciesDdl: (allCollections: CollectionConfig[]) => string;
142
+ export {};
@@ -0,0 +1,39 @@
1
+ import { CollectionConfig } from "@rebasepro/types";
2
+ /**
3
+ * Notice a generated Drizzle schema that a *library upgrade* invalidated.
4
+ *
5
+ * `rebase dev` already watches `config/collections` and warns when a collection
6
+ * file changes. That covers drift the developer caused. It cannot cover this one,
7
+ * because nothing the developer owns changed: 0.13 derives `category_id` where
8
+ * 0.12 derived `categorie_id`, from the same unedited collection. The watcher
9
+ * never fires, and `backend/src/schema.generated.ts` quietly stops describing the
10
+ * schema the runtime expects.
11
+ *
12
+ * The consequence is not cosmetic. Boot-ensure renames the column in the
13
+ * database, then relation validation reads the stale module and refuses to
14
+ * start — on that boot and every boot after it, because the rename is already
15
+ * applied and will not be attempted again.
16
+ *
17
+ * Deliberately narrow: this answers "does the generated schema name a foreign key
18
+ * the way the previous rule did", not "is this file what we would generate now".
19
+ * The wide question would report every whitespace change in the generator as a
20
+ * fatal staleness, and a check that cries wolf gets switched off.
21
+ */
22
+ /** One column the generated schema names under the pre-0.13 rule. */
23
+ export interface LegacyForeignKeyName {
24
+ /** Table whose column declaration is stale. */
25
+ table: string;
26
+ /** The name the generated schema declares. */
27
+ legacy: string;
28
+ /** The name this release derives, and which the database now carries. */
29
+ current: string;
30
+ /** `<collection>.<relation>` that derives it, for the message. */
31
+ relation: string;
32
+ }
33
+ /**
34
+ * @param generatedSource contents of `backend/src/schema.generated.ts`
35
+ * @param collections the project's collections, as this release reads them
36
+ */
37
+ export declare function findLegacyForeignKeyNames(generatedSource: string, collections: CollectionConfig[]): LegacyForeignKeyName[];
38
+ /** One-line summary for a log or a CLI notice. */
39
+ export declare function describeLegacyForeignKeyNames(found: LegacyForeignKeyName[]): string;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Reading validation rules out of CHECK constraints.
3
+ *
4
+ * A CHECK constraint is the schema author stating a rule the database already
5
+ * enforces. Introspection has never read them, so a generated form let the user
6
+ * type a value the database was always going to reject — the rule was written
7
+ * down, in the schema, and the UI asked anyway.
8
+ *
9
+ * Everything here parses the *normalized* text `pg_get_constraintdef` produces,
10
+ * not what the author typed: Postgres re-renders the expression from its parse
11
+ * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as
12
+ * `CHECK ((price > (0)::numeric))`. That normalization is what makes a
13
+ * string-level parser tractable — the input is generated, and the shapes are
14
+ * few.
15
+ *
16
+ * Constraints this cannot read are skipped in full. A partially-understood
17
+ * constraint is worse than an unread one: it would produce validation that
18
+ * *narrows* differently from the database, so a value the UI accepts still
19
+ * fails on write, or — worse — a value the database allows is refused in the
20
+ * form with no way to see why.
21
+ *
22
+ * Pure module: no I/O, no logging.
23
+ */
24
+ import type { CheckConstraintRow } from "./introspect-db-logic";
25
+ /** What a table's CHECK constraints say about one column. */
26
+ export interface ColumnCheckFacts {
27
+ /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */
28
+ enumValues?: string[];
29
+ /** `x >= n` */
30
+ min?: number;
31
+ /** `x <= n` */
32
+ max?: number;
33
+ /** `x > n` */
34
+ moreThan?: number;
35
+ /** `x < n` */
36
+ lessThan?: number;
37
+ /** `length(x) >= n` */
38
+ lengthMin?: number;
39
+ /** `length(x) <= n` */
40
+ lengthMax?: number;
41
+ }
42
+ /** Per-table, per-column facts. */
43
+ export type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;
44
+ /** Removes one layer of wrapping parentheses, repeatedly, when balanced. */
45
+ export declare function unwrapParens(input: string): string;
46
+ /** Drops trailing `::type` casts, including array and quoted forms. */
47
+ export declare function stripCasts(input: string): string;
48
+ /**
49
+ * Reads one constraint definition into per-column facts.
50
+ *
51
+ * Returns an empty map for anything not understood — including a constraint
52
+ * that is understood but spans two columns (`start_date < end_date`), which no
53
+ * per-property validation rule can express.
54
+ */
55
+ export declare function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts>;
56
+ /** Merges every readable CHECK in the schema into per-table, per-column facts. */
57
+ export declare function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable;
@@ -1,5 +1,10 @@
1
+ import { mapPgType } from "./introspect-db-types";
2
+ import type { CheckFactsByTable } from "./introspect-db-constraints";
3
+ import type { TableClassification } from "./introspect-db-structure";
1
4
  export interface TableRow {
2
5
  table_name: string;
6
+ /** True for the parent of a partitioned table (`relkind = 'p'`). */
7
+ is_partitioned?: boolean;
3
8
  }
4
9
  export interface TableColumn {
5
10
  table_name: string;
@@ -9,6 +14,18 @@ export interface TableColumn {
9
14
  is_nullable: string;
10
15
  column_default: string | null;
11
16
  atttypmod: number | null;
17
+ /** 1-based position in the table, as declared. */
18
+ ordinal_position?: number;
19
+ /** `"ALWAYS"` for a generated column, `"NEVER"` otherwise. */
20
+ is_generated?: string;
21
+ /** `"YES"` for an identity column. */
22
+ is_identity?: string;
23
+ /** `"ALWAYS"` or `"BY DEFAULT"` on an identity column. */
24
+ identity_generation?: string | null;
25
+ /** The declared `varchar(n)` / `char(n)` bound, if any. */
26
+ character_maximum_length?: number | null;
27
+ numeric_precision?: number | null;
28
+ numeric_scale?: number | null;
12
29
  }
13
30
  export interface EnumValue {
14
31
  enum_name: string;
@@ -24,6 +41,53 @@ export interface ForeignKeyRow {
24
41
  column_name: string;
25
42
  foreign_table_name: string;
26
43
  foreign_column_name: string;
44
+ /** Name of the FK constraint — the only way to tell composite keys apart. */
45
+ constraint_name?: string;
46
+ /** 1-based position of this column within its constraint. */
47
+ ordinal?: number;
48
+ /** `"CASCADE"`, `"RESTRICT"`, `"SET NULL"`, `"SET DEFAULT"`, `"NO ACTION"`. */
49
+ delete_rule?: string;
50
+ }
51
+ /** A unique constraint or unique index, as an ordered column list. */
52
+ export interface UniqueConstraintRow {
53
+ table_name: string;
54
+ constraint_name: string;
55
+ column_names: string[];
56
+ }
57
+ /** A CHECK constraint, as `pg_get_constraintdef` renders it. */
58
+ export interface CheckConstraintRow {
59
+ table_name: string;
60
+ constraint_name: string;
61
+ definition: string;
62
+ }
63
+ /** A `COMMENT ON TABLE` (null `column_name`) or `COMMENT ON COLUMN`. */
64
+ export interface CommentRow {
65
+ table_name: string;
66
+ column_name: string | null;
67
+ comment: string;
68
+ }
69
+ /**
70
+ * Everything one introspection run reads from the database.
71
+ *
72
+ * Passed around as one value so a new signal means a new field here rather than
73
+ * a new parameter on every function between the query and the generator — the
74
+ * shape `generateCollectionFile` had grown to seven positional arguments by.
75
+ */
76
+ export interface SchemaMetadata {
77
+ schema: string;
78
+ tables: TableRow[];
79
+ columns: TableColumn[];
80
+ enumValues: EnumValue[];
81
+ pks: PrimaryKeyRow[];
82
+ fks: ForeignKeyRow[];
83
+ uniques: UniqueConstraintRow[];
84
+ checks: CheckConstraintRow[];
85
+ comments: CommentRow[];
86
+ /**
87
+ * Row counts for the tables that needed one, capped — see `countRowsUpTo`.
88
+ * Absent for every table introspection never had a reason to count.
89
+ */
90
+ rowCounts: Record<string, number>;
27
91
  }
28
92
  export interface TableMeta {
29
93
  name: string;
@@ -38,12 +102,23 @@ export declare function singularize(word: string): string;
38
102
  */
39
103
  export declare function toCollectionVarName(tableName: string): string;
40
104
  export declare function getIconForTable(tableName: string): string;
41
- /**
42
- * Map a PostgreSQL data type to a Rebase property type.
43
- */
44
- export declare function mapPgType(dataType: string): string;
105
+ export { mapPgType };
45
106
  export declare function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]>;
46
107
  export declare function buildTablesMap(tables: TableRow[], columns: TableColumn[], pks: PrimaryKeyRow[], fks: ForeignKeyRow[]): Map<string, TableMeta>;
108
+ /**
109
+ * Join tables, identified by column name.
110
+ *
111
+ * Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,
112
+ * which asks the database instead: two single-column keys, unique together, no
113
+ * payload column, nothing referencing the table. This rule folds away
114
+ * `northwind.order_details` — which has the key shape and carries unit price,
115
+ * quantity and discount — because it recognises `id`, `created_at` and
116
+ * `updated_at` by name and calls everything else a foreign key.
117
+ *
118
+ * Still used by `./introspect-runtime`, which builds collections in memory from
119
+ * a narrower set of catalog queries and has no unique-constraint or row-count
120
+ * data to reason with.
121
+ */
47
122
  export declare function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string>;
48
123
  /**
49
124
  * Property metadata used to compute display priority.
@@ -93,11 +168,25 @@ export interface GeneratedFile {
93
168
  fileName: string;
94
169
  content: string;
95
170
  }
171
+ /**
172
+ * The structural analysis a run can hand the generator.
173
+ *
174
+ * Optional in full, and the generator degrades to exactly its previous output
175
+ * without it. That is not politeness towards old callers: three existing test
176
+ * suites and the `rebase init` scaffold path build a `TableMeta` by hand and
177
+ * have no database to read constraints or row counts from, and they must keep
178
+ * producing a valid collection.
179
+ */
180
+ export interface GenerationContext {
181
+ metadata?: SchemaMetadata;
182
+ classifications?: Map<string, TableClassification>;
183
+ checkFacts?: CheckFactsByTable;
184
+ }
96
185
  /**
97
186
  * Generate the full TypeScript file content for a single collection.
98
187
  * Pure function — no I/O.
99
188
  */
100
- export declare function generateCollectionFile(tableName: string, meta: TableMeta, allFks: ForeignKeyRow[], joinTables: Set<string>, tablesMap: Map<string, TableMeta>, enumMap: Map<string, string[]>, sampleData?: Record<string, unknown>[]): string;
189
+ export declare function generateCollectionFile(tableName: string, meta: TableMeta, allFks: ForeignKeyRow[], joinTables: Set<string>, tablesMap: Map<string, TableMeta>, enumMap: Map<string, string[]>, sampleData?: Record<string, unknown>[], context?: GenerationContext): string;
101
190
  /**
102
191
  * Generate the content for an index.ts file that re-exports all collections.
103
192
  */