@rebasepro/server-postgres 0.13.1-canary.gef9608c → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/PostgresBootstrapper.d.ts +26 -0
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
  5. package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
  6. package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
  7. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
  8. package/dist/cli-helpers.d.ts +56 -0
  9. package/dist/cli-output.d.ts +34 -0
  10. package/dist/data-transformer.d.ts +7 -2
  11. package/dist/data_driver-ULAyJEi9.js +193 -0
  12. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  13. package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
  14. package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
  15. package/dist/{ensure-collection-tables-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
  16. package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
  17. package/dist/index.es.js +1720 -946
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
  20. package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
  21. package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
  22. package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
  23. package/dist/schema/auth-schema.d.ts +102 -0
  24. package/dist/schema/auth-users-columns.d.ts +97 -0
  25. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  26. package/dist/schema/doctor.d.ts +41 -25
  27. package/dist/schema/ensure-collection-policies.d.ts +33 -9
  28. package/dist/schema/ensure-collection-tables.d.ts +60 -6
  29. package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
  30. package/dist/schema/generate-postgres-ddl-logic.d.ts +48 -0
  31. package/dist/schema/introspect-db-inference.d.ts +8 -1
  32. package/dist/schema/introspect-db-logic.d.ts +49 -0
  33. package/dist/schema/introspect-db-project.d.ts +21 -0
  34. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  35. package/dist/schema/search-column.d.ts +248 -0
  36. package/dist/security/policy-drift.d.ts +34 -0
  37. package/dist/security/rls-enforcement.d.ts +61 -5
  38. package/dist/services/FetchService.d.ts +24 -0
  39. package/dist/services/PersistService.d.ts +21 -17
  40. package/dist/services/RelationService.d.ts +9 -57
  41. package/dist/services/RelationWriteService.d.ts +82 -0
  42. package/dist/services/collection-helpers.d.ts +42 -0
  43. package/dist/services/dataService.d.ts +3 -0
  44. package/dist/services/junction-writes.d.ts +82 -0
  45. package/dist/services/realtimeService.d.ts +139 -2
  46. package/dist/services/write-denial.d.ts +36 -0
  47. package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
  48. package/dist/src-DCdn3Val.js.map +1 -0
  49. package/dist/utils/drizzle-conditions.d.ts +124 -2
  50. package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
  51. package/dist/websocket-C8ZqVBiV.js.map +1 -0
  52. package/package.json +8 -7
  53. package/src/PostgresBackendDriver.ts +172 -6
  54. package/src/PostgresBootstrapper.ts +136 -11
  55. package/src/auth/ensure-tables.ts +212 -91
  56. package/src/auth/services.ts +82 -5
  57. package/src/backup/backup-cli.ts +59 -57
  58. package/src/cli-errors.ts +6 -6
  59. package/src/cli-helpers.ts +124 -11
  60. package/src/cli-output.ts +43 -0
  61. package/src/cli.ts +299 -168
  62. package/src/collections/buildRegistry.ts +3 -1
  63. package/src/data-transformer.ts +129 -25
  64. package/src/history/ensure-history-table.ts +9 -2
  65. package/src/schema/auth-schema.ts +17 -1
  66. package/src/schema/auth-users-columns.ts +131 -0
  67. package/src/schema/doctor-cli.ts +14 -65
  68. package/src/schema/doctor-policy-checks.ts +105 -0
  69. package/src/schema/doctor.ts +149 -72
  70. package/src/schema/ensure-collection-policies.ts +99 -6
  71. package/src/schema/ensure-collection-tables.ts +366 -30
  72. package/src/schema/generate-drizzle-schema-logic.ts +146 -66
  73. package/src/schema/generate-drizzle-schema.ts +11 -10
  74. package/src/schema/generate-postgres-ddl-logic.ts +277 -10
  75. package/src/schema/generate-postgres-ddl.ts +38 -14
  76. package/src/schema/generated-schema-staleness.ts +14 -7
  77. package/src/schema/introspect-db-inference.ts +9 -2
  78. package/src/schema/introspect-db-logic.ts +251 -75
  79. package/src/schema/introspect-db-project.ts +78 -0
  80. package/src/schema/introspect-db.ts +42 -25
  81. package/src/schema/introspect-runtime.ts +14 -2
  82. package/src/schema/rls-bootstrap-sql.ts +288 -0
  83. package/src/schema/search-column.ts +643 -0
  84. package/src/security/anonymous-grants.test.ts +4 -2
  85. package/src/security/policy-drift.test.ts +104 -3
  86. package/src/security/policy-drift.ts +129 -7
  87. package/src/security/rls-enforcement.ts +150 -7
  88. package/src/services/BranchService.ts +5 -0
  89. package/src/services/FetchService.ts +243 -22
  90. package/src/services/PersistService.ts +68 -42
  91. package/src/services/RelationService.ts +37 -696
  92. package/src/services/RelationWriteService.ts +653 -0
  93. package/src/services/cdc/trigger-cdc.ts +5 -1
  94. package/src/services/channel-history.ts +14 -0
  95. package/src/services/channel-presence.ts +13 -0
  96. package/src/services/collection-helpers.ts +89 -4
  97. package/src/services/dataService.ts +3 -0
  98. package/src/services/junction-writes.ts +295 -0
  99. package/src/services/pg-notify-listener.ts +1 -1
  100. package/src/services/realtimeService.ts +347 -86
  101. package/src/services/write-denial.ts +55 -0
  102. package/src/utils/drizzle-conditions.ts +433 -35
  103. package/src/utils/pg-error-utils.ts +8 -3
  104. package/src/websocket.ts +113 -16
  105. package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
  106. package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
  107. package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
  108. package/dist/policy-CeA1JcxP.js +0 -105
  109. package/dist/policy-CeA1JcxP.js.map +0 -1
  110. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  111. package/dist/src-CU6WZGYV.js.map +0 -1
  112. package/dist/src-DoU9yPqq.js.map +0 -1
  113. package/dist/websocket-B2LsrINK.js.map +0 -1
  114. 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,28 @@
1
+ /**
2
+ * What the RLS checks concluded.
3
+ *
4
+ * `unchecked` exists because "we could not look" and "we looked and it is fine"
5
+ * used to be reported identically: a collections path that did not resolve made
6
+ * the loader return `[]` (it warns, it does not throw), `checkPolicyDrift`
7
+ * early-returned an empty diff, and the gate printed
8
+ * `✓ RLS policies match your collections` having compared zero policies against
9
+ * zero collections. Any exception at all — a collection file that throws on
10
+ * import, a `pg_policies` read the CI role is not granted, a connection reset —
11
+ * did the same thing through a `warn`, and exited 0.
12
+ */
13
+ export type PolicyCheckStatus = "ok" | "problems" | "unchecked";
14
+ /**
15
+ * The exit code for `rebase doctor --policies`.
16
+ *
17
+ * A gate that could not run has not passed. Only a completed, clean check
18
+ * exits 0 — anything else, including "we never opened a connection", is a
19
+ * failure, or the flag certifies a database nobody looked at.
20
+ */
21
+ export declare function exitCodeForPolicyGate(status: PolicyCheckStatus): 0 | 1;
22
+ /**
23
+ * Policies actually deployed vs the ones the collections describe, plus policy
24
+ * roles this server could never satisfy.
25
+ *
26
+ * Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.
27
+ */
28
+ export declare function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<PolicyCheckStatus>;
@@ -2,7 +2,7 @@ import { CollectionConfig, Property } from "@rebasepro/types";
2
2
  export type IssueSeverity = "error" | "warning" | "info";
3
3
  export interface DoctorIssue {
4
4
  severity: IssueSeverity;
5
- category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated";
5
+ category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated" | "sdk_ungeneratable";
6
6
  table?: string;
7
7
  column?: string;
8
8
  expected?: string;
@@ -10,21 +10,46 @@ export interface DoctorIssue {
10
10
  message: string;
11
11
  fix: string;
12
12
  }
13
+ export interface DoctorPhase {
14
+ passed: boolean;
15
+ issues: DoctorIssue[];
16
+ /**
17
+ * Why this phase never ran, when it did not.
18
+ *
19
+ * A check that did not happen is a third state, not a passing one. While a
20
+ * skipped phase initialised to `{ passed: true, issues: [] }` it rendered as
21
+ * `✅ Collections → Database: In sync` and counted towards
22
+ * `✓ All schemas are in sync!` — so a project whose connection string was
23
+ * spelled `POSTGRES_URL`, or a CI job that never exported one, got two green
24
+ * ticks and exit 0 against a database with no tables in it.
25
+ */
26
+ skipped?: string;
27
+ /**
28
+ * Why this phase had nothing to compare against, when it had nothing.
29
+ *
30
+ * Distinct from `skipped`, which means "the check could not run, and that
31
+ * is probably worth fixing". This one means "the artifact is optional and
32
+ * you have not asked for it": no drift is possible, so the run is still a
33
+ * clean bill of health.
34
+ *
35
+ * It exists because the alternative was a contradiction. The typed-SDK
36
+ * phase returned `{ passed: true }` when `generated/sdk/database.types.ts`
37
+ * did not exist, so a fresh project's report read
38
+ * `✅ Collections → SDK Types: In sync` directly above
39
+ * `ℹ Typed SDK not generated (optional).` — one line calling a file
40
+ * synchronised and the next saying it is absent. "In sync" is a claim about
41
+ * a comparison, and no comparison happened.
42
+ */
43
+ notApplicable?: string;
44
+ }
13
45
  export interface DoctorReport {
14
- collectionsToSchema: {
15
- passed: boolean;
16
- issues: DoctorIssue[];
17
- };
18
- collectionsToSdk: {
19
- passed: boolean;
20
- issues: DoctorIssue[];
21
- };
22
- schemaToDatabase: {
23
- passed: boolean;
24
- issues: DoctorIssue[];
25
- };
46
+ collectionsToSchema: DoctorPhase;
47
+ collectionsToSdk: DoctorPhase;
48
+ schemaToDatabase: DoctorPhase;
26
49
  summary: {
27
50
  passed: number;
51
+ skipped: number;
52
+ notApplicable: number;
28
53
  warnings: number;
29
54
  errors: number;
30
55
  };
@@ -37,18 +62,9 @@ export declare function getExpectedColumnType(prop: Property): string | null;
37
62
  * wrong thing.
38
63
  */
39
64
  export declare function loadCollections(collectionsPath: string): Promise<CollectionConfig[]>;
40
- export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<{
41
- passed: boolean;
42
- issues: DoctorIssue[];
43
- }>;
44
- export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<{
45
- passed: boolean;
46
- issues: DoctorIssue[];
47
- }>;
48
- export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<{
49
- passed: boolean;
50
- issues: DoctorIssue[];
51
- }>;
65
+ export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<DoctorPhase>;
66
+ export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<DoctorPhase>;
67
+ export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<DoctorPhase>;
52
68
  export declare function renderReport(report: DoctorReport): void;
53
69
  export declare function runDoctor(options: {
54
70
  collectionsPath: string;
@@ -43,18 +43,42 @@ export interface PolicyEnsureResult {
43
43
  table: string;
44
44
  reason: string;
45
45
  }[];
46
- /** Tables whose RLS could not be fully applied (fail closed). */
46
+ /**
47
+ * Tables that have RLS on but did not get every policy. They deny — RLS
48
+ * with no matching policy is deny-all — so they are safe but not servable.
49
+ */
47
50
  failures: {
48
51
  table: string;
49
52
  error: string;
50
53
  }[];
54
+ /**
55
+ * Tables RLS could not be enabled on, whose DML grant was withdrawn instead.
56
+ *
57
+ * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`
58
+ * failing was recorded as a `failure` and reported with the same "it stays
59
+ * locked (denies)" wording as a failed policy — but the two are opposites.
60
+ * A policy statement failing leaves RLS on and the table denying. `enableRls`
61
+ * failing leaves RLS *off*, and the schema-wide grant to the user role has
62
+ * already been made by `ensureRlsEnforcement`, so the table is readable and
63
+ * writable by every authenticated request with no row filtering at all.
64
+ */
65
+ unsecured: {
66
+ table: string;
67
+ error: string;
68
+ grantWithdrawn: boolean;
69
+ }[];
70
+ /**
71
+ * Generated policies removed because no current rule produces them.
72
+ *
73
+ * A policy's name embeds a hash of the rule's semantics, so editing a rule
74
+ * does not update a policy — it creates a new one and abandons the old.
75
+ * Postgres ORs permissive policies, so the abandoned one keeps granting:
76
+ * a `USING (true)` tightened to an owner check went on admitting everyone,
77
+ * forever, while the deploy logged success.
78
+ *
79
+ * `db push` reconciles this, and cannot reach a managed tenant's in-cluster
80
+ * database — which is the reason this module exists. So boot has to do it.
81
+ */
82
+ orphansDropped: number;
51
83
  }
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
84
  export declare function ensureCollectionPolicies(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<PolicyEnsureResult>;
@@ -54,9 +54,17 @@ export interface ExistingSchema {
54
54
  * constraint that then fails harmlessly as a duplicate.
55
55
  */
56
56
  constraints?: Set<string>;
57
+ /**
58
+ * `schema.table.column` → that column's comment, for the columns that have
59
+ * one. This is where a generated search column's fingerprint lives, so it
60
+ * is the only evidence that a `search` block has changed since the column
61
+ * was built. Absent is read as "no column is stamped", which plans a stamp
62
+ * and reports nothing as drifted.
63
+ */
64
+ columnComments?: Map<string, string>;
57
65
  }
58
66
  export interface EnsureAction {
59
- kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
67
+ kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index" | "comment-column";
60
68
  /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
61
69
  target: string;
62
70
  sql: string;
@@ -76,6 +84,49 @@ export interface EnsurePlan {
76
84
  * resolving to nothing — which is indistinguishable from having no data.
77
85
  */
78
86
  legacyForeignKeys: LegacyForeignKey[];
87
+ /**
88
+ * Generated search columns whose `search` block has changed since they were
89
+ * built. Reported, never planned into `actions` — see
90
+ * {@link SearchColumnDrift} for why applying it is not this path's call.
91
+ */
92
+ searchDrift: SearchColumnDrift[];
93
+ /**
94
+ * Generated search columns that exist but carry no fingerprint — created
95
+ * before this check existed, or by `search.sql` on an older CLI. The plan
96
+ * stamps them so the *next* change is detectable; whether they match the
97
+ * current block cannot be known, which is what the caller reports.
98
+ */
99
+ searchAdopted: {
100
+ table: string;
101
+ column: string;
102
+ }[];
103
+ }
104
+ /**
105
+ * A generated search column built from a `search` block that has since changed.
106
+ *
107
+ * Reported instead of applied because the two ways to apply it are both worse
108
+ * than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and
109
+ * rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under
110
+ * an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs
111
+ * unattended against live customer data with nobody reading a diff — the same
112
+ * reason it withholds `SET NOT NULL` from an adopted table — so a multi-minute
113
+ * outage is not a decision it may take on its own.
114
+ *
115
+ * Not applying it silently is not an option either: that is the bug this
116
+ * detection exists for. A collection that added a field, flipped `unaccent` or
117
+ * raised a weight kept indexing the *old* set forever, and the only symptom was
118
+ * searches returning nothing for content plainly in the row.
119
+ */
120
+ export interface SearchColumnDrift {
121
+ /** `schema.table`. */
122
+ table: string;
123
+ column: string;
124
+ /** The fingerprint recorded on the column. */
125
+ found: string;
126
+ /** The fingerprint the current `search` block computes. */
127
+ expected: string;
128
+ /** The statements that would rebuild the column, for the operator to run. */
129
+ rebuild: string[];
79
130
  }
80
131
  /** A relation column whose old and new spellings both plausibly apply. */
81
132
  export interface LegacyForeignKey {
@@ -88,14 +139,17 @@ export interface LegacyForeignKey {
88
139
  }
89
140
  export interface EnsureOutcome extends EnsurePlan {
90
141
  /**
91
- * Constraints that could not be added always non-fatal.
142
+ * Actions that could not be applied and are non-fatal by nature.
92
143
  *
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.
144
+ * Two kinds qualify. A foreign key can only fail on data that already
145
+ * violates it, and the column it would police exists either way, so the
146
+ * collection still serves; refusing to boot over one would turn a
147
+ * pre-existing data problem into an outage. A column comment is the search
148
+ * fingerprint, which needs table ownership — losing it costs drift
149
+ * detection on the next boot, not the deployment. Both are reported loudly.
97
150
  */
98
151
  failures: {
152
+ kind: EnsureAction["kind"];
99
153
  target: string;
100
154
  error: string;
101
155
  }[];
@@ -1,2 +1,10 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { CollectionConfig, Property } from "@rebasepro/types";
2
+ /**
3
+ * The Drizzle column declaration a property compiles to, or `null` when the
4
+ * property puts no column on *this* table (an inverse relation, whose column
5
+ * lives on the target). Exported so it can be checked against its DDL twin
6
+ * `getSqlColumnType` directly — the two disagreeing is what left `geopoint`
7
+ * with a database column and no Drizzle key.
8
+ */
9
+ export declare const getDrizzleColumn: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string | null;
2
10
  export declare const generateSchema: (allCollections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
@@ -29,8 +29,56 @@ export declare const generatePolicyStatements: (collection: CollectionConfig, ru
29
29
  */
30
30
  export declare const quoteSqlLiteral: (value: string) => string;
31
31
  export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
32
+ /**
33
+ * Everything a `search` block needs, as a file Rebase applies itself.
34
+ *
35
+ * Search is the one part of the schema Atlas does not own. Two independent
36
+ * reasons, and either alone would be enough:
37
+ *
38
+ * 1. Its free tier refuses to *parse* a desired-state file that so much as
39
+ * contains a function — "functions and procedures are available to
40
+ * logged-in users only". A generated `tsvector` column cannot avoid one:
41
+ * `unaccent` is STABLE and jsonb flattening needs a set-returning function,
42
+ * so both have to be wrapped in an IMMUTABLE helper to be legal in a
43
+ * generated column at all.
44
+ * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs
45
+ * against, so a helper seeded there beforehand is gone by the time the plan
46
+ * is analysed. There is no hook to reinstate it.
47
+ *
48
+ * So the column, its index and its helpers are excluded from Atlas's view
49
+ * (`searchExcludePatterns`) and applied from here — the same arrangement the
50
+ * RLS policies already use, and for the same underlying reason.
51
+ *
52
+ * Ordered as it must run: extensions, then helpers, then the column whose
53
+ * expression calls them, then the index over that column. Every statement is
54
+ * `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and
55
+ * appended to migrations that run against databases at any stage of their
56
+ * life. Empty when nothing opted in — the caller writes no file then.
57
+ *
58
+ * The leading half — extensions and helpers, without the table-shaped
59
+ * statements — is available on its own as {@link searchPrerequisiteStatements},
60
+ * for the dev database Atlas analyses plans against. That database has none of
61
+ * the project's tables, so it wants the functions and nothing else.
62
+ */
63
+ export declare const searchPrerequisiteStatements: (allCollections: CollectionConfig[]) => string[];
64
+ export declare const generatePostgresSearchDdl: (allCollections: CollectionConfig[]) => string;
65
+ /**
66
+ * Glob patterns telling Atlas to leave the search column and its index alone.
67
+ *
68
+ * Without these, a desired state that omits search reads to Atlas as an
69
+ * instruction to drop the column — taking the index and the whole search
70
+ * feature with it on the next push.
71
+ *
72
+ * Fully qualified, `schema.table.object`, matching the include list. The
73
+ * two-part form is what Atlas wants when the connection URL scopes it to one
74
+ * schema and is *silently ignored* otherwise: it reads `posts.search_vector`
75
+ * as a table named `search_vector` in a schema named `posts`, matches nothing,
76
+ * and reports no error for the pattern that never fired.
77
+ */
78
+ export declare const searchExcludePatterns: (allCollections: CollectionConfig[]) => string[];
32
79
  export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
33
80
  includePolicies?: boolean;
81
+ includeSearch?: boolean;
34
82
  }) => Promise<string>;
35
83
  /** The RLS statements one declared collection's table needs, ready to run. */
36
84
  /**
@@ -2,4 +2,11 @@ export interface InferenceResult {
2
2
  propType?: string;
3
3
  extra?: string;
4
4
  }
5
- export declare function inferPropertyFromData(columnName: string, pgDataType: string, currentPropType: string, sampleValues: unknown[], isPk: boolean): InferenceResult;
5
+ export declare function inferPropertyFromData(columnName: string, pgDataType: string, currentPropType: string, sampleValues: unknown[], isPk: boolean,
6
+ /**
7
+ * False when generating for a project without `@rebasepro/admin-types`, where
8
+ * `BaseProperty` declares no `admin` field and the block below would not
9
+ * compile. The type-level inferences (`propType`, `url`, `storage`) are
10
+ * unaffected — only the form-widget hints are dropped.
11
+ */
12
+ emitAdmin?: boolean): InferenceResult;
@@ -177,10 +177,59 @@ export interface GeneratedFile {
177
177
  * have no database to read constraints or row counts from, and they must keep
178
178
  * producing a valid collection.
179
179
  */
180
+ /**
181
+ * Which `defineCollection` — if any — the project being generated into can import.
182
+ *
183
+ * A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`
184
+ * to `Record<string, …>`, and every key-shaped field in the admin block —
185
+ * `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is
186
+ * derived from those keys. Annotated, they accept any string: introspection was
187
+ * emitting a `propertiesOrder` array that nothing checked, so renaming a column and
188
+ * re-introspecting left a stale key that compiled silently. `defineCollection` is
189
+ * the identity function whose `const P` type parameter keeps the keys literal, which
190
+ * is what turns that checking on.
191
+ *
192
+ * There are two of them and they are not interchangeable:
193
+ *
194
+ * - `admin-types` — `@rebasepro/admin-types`. Its index side-effect-imports
195
+ * `augment.ts`, so importing it is also what *declares* the `admin` block. Only a
196
+ * project that depends on the package can resolve it.
197
+ * - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React
198
+ * anywhere in its graph (`scripts/headless-guard` lists it as core). This is the
199
+ * headless flavour.
200
+ * - `annotation` — neither package is declared, so neither import would resolve and
201
+ * the old annotation is the only honest thing to emit. Projects scaffolded before
202
+ * `@rebasepro/common` joined the headless config package land here.
203
+ *
204
+ * The last two emit **no admin block, on the collection or on any property**. That is
205
+ * not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block
206
+ * introspection used to emit was a type error in every headless project it was
207
+ * written into. See `packages/admin-types/src/augment.ts`.
208
+ */
209
+ export type CollectionBuilder = "admin-types" | "common" | "annotation";
210
+ /**
211
+ * The package specifiers the generated files name, spelled once.
212
+ *
213
+ * Written as constants rather than inline in the import templates below because
214
+ * `scripts/headless-guard/check-types.mjs` scans core sources for `from
215
+ * "@rebasepro/admin-types"` and cannot tell a real import from one this module
216
+ * *writes*. It is right to be that blunt — the guard's whole value is that it
217
+ * cannot be reasoned around — so the string simply never appears in that shape
218
+ * here. Inlining them back into the templates re-breaks `check:types-headless`.
219
+ */
220
+ export declare const ADMIN_TYPES_PACKAGE = "@rebasepro/admin-types";
221
+ export declare const COMMON_PACKAGE = "@rebasepro/common";
222
+ export declare const TYPES_PACKAGE = "@rebasepro/types";
180
223
  export interface GenerationContext {
181
224
  metadata?: SchemaMetadata;
182
225
  classifications?: Map<string, TableClassification>;
183
226
  checkFacts?: CheckFactsByTable;
227
+ /**
228
+ * Defaults to `admin-types`, which is what the generator has always emitted.
229
+ * The CLI never relies on the default — `introspect-db.ts` detects the flavour
230
+ * from the target project and passes it. See `detectCollectionBuilder`.
231
+ */
232
+ builder?: CollectionBuilder;
184
233
  }
185
234
  /**
186
235
  * Generate the full TypeScript file content for a single collection.
@@ -0,0 +1,21 @@
1
+ import { type CollectionBuilder } from "./introspect-db-logic";
2
+ /**
3
+ * Which `defineCollection` to generate against, for collections written to `outDir`.
4
+ *
5
+ * **The detection is the package manifests above the output directory**, unioned from
6
+ * `outDir` up to and including the project root — the first ancestor holding a
7
+ * `rebase.json`, or `MAX_LEVELS` up if there is none.
8
+ *
9
+ * That is the rule Node itself applies: a file in `config/collections` resolves a bare
10
+ * specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.
11
+ * Reading the manifests along that same path answers the only question that matters —
12
+ * *will this import resolve in the project I am writing into* — from the state on disk
13
+ * at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s
14
+ * `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory
15
+ * says one was scaffolded, but neither is what the compiler consults, and either can be
16
+ * true of a project whose `config` package does not depend on `@rebasepro/admin-types`.
17
+ *
18
+ * Ambiguity resolves towards the admin panel: a project that declares both packages has
19
+ * a panel, and `@rebasepro/admin-types` is the flavour that keeps the `admin` block.
20
+ */
21
+ export declare function detectCollectionBuilder(outDir: string): CollectionBuilder;