@rebasepro/server-postgres 0.23.0 → 0.24.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 (108) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
  2. package/dist/BranchService-BRt78gfa.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +84 -12
  4. package/dist/auth/services.d.ts +72 -14
  5. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  6. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  7. package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
  8. package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
  9. package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
  10. package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
  11. package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
  12. package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
  13. package/dist/cli-errors.d.ts +42 -0
  14. package/dist/cli-helpers.d.ts +12 -12
  15. package/dist/cli.js +91 -37
  16. package/dist/cli.js.map +1 -1
  17. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
  18. package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
  19. package/dist/data-transformer.d.ts +0 -8
  20. package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
  21. package/dist/doctor-C-sYWbmt.js.map +1 -0
  22. package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
  23. package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
  24. package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
  25. package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
  26. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
  27. package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
  28. package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
  29. package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
  30. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
  31. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
  33. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
  34. package/dist/generated-sql.d.ts +28 -0
  35. package/dist/index.es.js +3415 -917
  36. package/dist/index.es.js.map +1 -1
  37. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
  38. package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
  39. package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
  40. package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
  41. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
  42. package/dist/plan-schema-DU9exq6C.js.map +1 -0
  43. package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
  44. package/dist/policy-drift-B-J2hhm0.js.map +1 -0
  45. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
  46. package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
  47. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
  48. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
  49. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
  50. package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
  51. package/dist/schema/auth-schema.d.ts +170 -0
  52. package/dist/schema/classify-change.d.ts +29 -1
  53. package/dist/schema/column-plan-helpers.d.ts +33 -21
  54. package/dist/schema/destructive-sql.d.ts +20 -1
  55. package/dist/schema/doctor-cli.js +4 -4
  56. package/dist/schema/doctor.d.ts +34 -1
  57. package/dist/schema/ensure-collection-policies.d.ts +22 -0
  58. package/dist/schema/generate-drizzle-schema.js +1 -1
  59. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  60. package/dist/schema/generate-postgres-ddl.js +1 -1
  61. package/dist/schema/generate-schema-commit.d.ts +12 -0
  62. package/dist/schema/introspect-db-logic.d.ts +31 -0
  63. package/dist/schema/introspect-db-queries.d.ts +1 -1
  64. package/dist/schema/introspect-db-search.d.ts +22 -0
  65. package/dist/schema/introspect-db-storage.d.ts +83 -0
  66. package/dist/schema/introspect-db.js +15 -3
  67. package/dist/schema/introspect-db.js.map +1 -1
  68. package/dist/schema/plan/diff-plan.d.ts +1 -1
  69. package/dist/schema/plan/plan-schema.d.ts +16 -8
  70. package/dist/schema/plan/render-ddl.d.ts +6 -0
  71. package/dist/schema/plan/types.d.ts +37 -11
  72. package/dist/search-column-BM-GV6vH.js +442 -0
  73. package/dist/search-column-BM-GV6vH.js.map +1 -0
  74. package/dist/security/policy-drift.d.ts +1 -1
  75. package/dist/security/rls-enforcement.d.ts +7 -0
  76. package/dist/services/BranchService.d.ts +22 -1
  77. package/dist/services/FetchService.d.ts +5 -4
  78. package/dist/services/RelationService.d.ts +18 -0
  79. package/dist/services/cdc/CdcListener.d.ts +9 -4
  80. package/dist/services/cdc/identity-columns.d.ts +19 -0
  81. package/dist/services/cdc/trigger-cdc.d.ts +45 -7
  82. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  83. package/dist/services/channel-history.d.ts +17 -1
  84. package/dist/services/collection-helpers.d.ts +11 -1
  85. package/dist/services/pg-notify-listener.d.ts +88 -6
  86. package/dist/services/realtimeService.d.ts +159 -45
  87. package/dist/services/socket-liveness.d.ts +45 -0
  88. package/dist/services/soft-delete.d.ts +12 -0
  89. package/dist/services/sql-script.d.ts +71 -0
  90. package/dist/services/write-depth.d.ts +16 -0
  91. package/dist/services/write-transaction-scope.d.ts +6 -3
  92. package/dist/utils/drizzle-conditions.d.ts +42 -2
  93. package/dist/utils/sql-redaction.d.ts +21 -0
  94. package/dist/websocket.d.ts +50 -18
  95. package/package.json +7 -7
  96. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  97. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  98. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  99. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  100. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  101. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  102. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  103. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  104. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  105. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  106. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  107. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  108. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
@@ -31,7 +31,7 @@ type ResolveCollection = (slug: string) => CollectionConfig | undefined;
31
31
  */
32
32
  export declare const generatePolicyStatements: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection) => string[];
33
33
  /**
34
- * `drizzle/schema.sql` — the desired state Atlas diffs the database against.
34
+ * `.rebase/sql/schema.sql` — the desired state Atlas diffs the database against.
35
35
  *
36
36
  * Synchronous. It was `async` with nothing to await, which made every caller
37
37
  * (the writer script, the doctor, the live editor, two gates) carry a promise
@@ -39,10 +39,10 @@ export declare const generatePolicyStatements: (collection: CollectionConfig, ru
39
39
  * `[object Promise]`.
40
40
  */
41
41
  export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: DdlRenderOptions) => string;
42
- /** `drizzle/policies.sql` — the RLS policies. Atlas has no model for a policy. */
42
+ /** `.rebase/sql/policies.sql` — the RLS policies. Atlas has no model for a policy. */
43
43
  export declare const generatePostgresPoliciesDdl: (allCollections: CollectionConfig[]) => string;
44
44
  /**
45
- * `drizzle/search.sql` — everything a `search` block needs, as a file Rebase
45
+ * `.rebase/sql/search.sql` — everything a `search` block needs, as a file Rebase
46
46
  * applies itself.
47
47
  *
48
48
  * Search is one of the parts of the schema Atlas does not own, for two
@@ -70,7 +70,7 @@ export declare const generatePostgresSearchDdl: (allCollections: CollectionConfi
70
70
  */
71
71
  export declare const searchExcludePatterns: (allCollections: CollectionConfig[]) => string[];
72
72
  /**
73
- * `drizzle/vector.sql` — the `vector` columns, their ANN indexes and, when a
73
+ * `.rebase/sql/vector.sql` — the `vector` columns, their ANN indexes and, when a
74
74
  * database declared it, the extension.
75
75
  *
76
76
  * Vector is out of Atlas's hands for a narrower reason than search: not that
@@ -86,7 +86,7 @@ export declare const generatePostgresVectorDdl: (allCollections: CollectionConfi
86
86
  /** @see searchExcludePatterns */
87
87
  export declare const vectorExcludePatterns: (allCollections: CollectionConfig[]) => string[];
88
88
  /**
89
- * `drizzle/triggers.sql` — the `BEFORE UPDATE` triggers behind
89
+ * `.rebase/sql/triggers.sql` — the `BEFORE UPDATE` triggers behind
90
90
  * `autoValue: "on_update"`, and the one function they share.
91
91
  *
92
92
  * A trigger is a function plus a binding, so it is carved out of Atlas's view
@@ -1,6 +1,6 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "../generate-postgres-ddl-logic-Bt2d2mRH.js";
3
+ import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "../generate-postgres-ddl-logic-BJsLaVNX.js";
4
4
  import { n as outError, t as out } from "../cli-output-CNdMql-L.js";
5
5
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
6
6
  import { declaredDatabaseExtensions } from "@rebasepro/types";
@@ -72,6 +72,12 @@ export interface SchemaCommitInput {
72
72
  * a statement that applies and one that is rejected.
73
73
  */
74
74
  existing?: ExistingSchema;
75
+ /**
76
+ * Plan a commit of the source alone — "Edit source only". An unapplicable
77
+ * change is not refused: it is committed with no statements, under a
78
+ * message that names what the database keeps.
79
+ */
80
+ sourceOnly?: boolean;
75
81
  }
76
82
  export interface SchemaCommit {
77
83
  /** Every file the commit writes, source and generated alike. */
@@ -123,6 +129,12 @@ export declare class SchemaCommitError extends Error {
123
129
  * where there is no database to describe.
124
130
  */
125
131
  export declare function additiveStatements(before: CollectionConfig[], after: CollectionConfig[], existing?: ExistingSchema, options?: EnsureOptions): string[];
132
+ /**
133
+ * The commit of an "Edit source only": what changed in the source, and what
134
+ * the database keeps — `chore(schema): remove sku from products (source only —
135
+ * column products.sku kept)`. The body lists what each change leaves behind.
136
+ */
137
+ export declare function sourceOnlyCommitMessage(classified: ClassifiedChanges): string;
126
138
  /** A commit message that says what changed rather than that something did. */
127
139
  export declare function commitMessage(classified: ClassifiedChanges): string;
128
140
  /**
@@ -91,6 +91,24 @@ export interface SchemaMetadata {
91
91
  */
92
92
  rowCounts: Record<string, number>;
93
93
  }
94
+ /**
95
+ * Why a table keyed on several columns is not generated as a collection, or
96
+ * `undefined` when it is.
97
+ *
98
+ * Several `isId` properties are one composite key, and the planner builds that
99
+ * table under the same `PRIMARY KEY (a, b)` — so such a table round-trips when
100
+ * two things hold:
101
+ *
102
+ * - **Every key column can carry `isId`,** which exists on string and number
103
+ * properties only. Read from the column's SQL type, not a data sample: a
104
+ * `line_no` holding only 1s is not a boolean. A key with a column of another
105
+ * type — pagila's `payment` is keyed on a timestamp too — can be marked on
106
+ * some of its columns only, and those would be read as the whole key.
107
+ * - **Nothing points at it.** A foreign key into it, or a junction, is a link
108
+ * into a composite key, which the planner refuses: the relation has one
109
+ * column to point with.
110
+ */
111
+ export declare function compositeKeySkipReason(meta: TableMeta, allFks: ForeignKeyRow[]): string | undefined;
94
112
  export interface TableMeta {
95
113
  name: string;
96
114
  columns: TableColumn[];
@@ -232,6 +250,19 @@ export interface GenerationContext {
232
250
  * from the target project and passes it. See `detectCollectionBuilder`.
233
251
  */
234
252
  builder?: CollectionBuilder;
253
+ /**
254
+ * Collects one line per column the planner will not reproduce exactly —
255
+ * what `rebase db push` would do to it, and how to avoid that when there is
256
+ * a way. The CLI prints them; the same lines are written at the top of the
257
+ * generated file.
258
+ */
259
+ notes?: string[];
260
+ /**
261
+ * Tables this run does not generate a collection for, so nothing may point
262
+ * at them: a relation whose target file does not exist is an import that
263
+ * does not resolve.
264
+ */
265
+ skippedTables?: ReadonlySet<string>;
235
266
  }
236
267
  /**
237
268
  * Generate the full TypeScript file content for a single collection.
@@ -71,7 +71,7 @@ export declare const TABLES_QUERY = "\n SELECT c.relname AS table_name,\n
71
71
  * errors. `character_maximum_length` is the declared `varchar(n)` bound —
72
72
  * already enforced by the database, and free to surface as validation.
73
73
  */
74
- export declare const COLUMNS_QUERY = "\n SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n c.ordinal_position,\n c.is_generated,\n c.is_identity,\n c.identity_generation,\n c.character_maximum_length,\n c.numeric_precision,\n c.numeric_scale,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1\n ORDER BY c.table_name, c.ordinal_position\n";
74
+ export declare const COLUMNS_QUERY = "\n SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n c.ordinal_position,\n c.is_generated,\n c.generation_expression,\n c.is_identity,\n c.identity_generation,\n c.character_maximum_length,\n c.numeric_precision,\n c.numeric_scale,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1\n ORDER BY c.table_name, c.ordinal_position\n";
75
75
  export declare const ENUMS_QUERY = "\n SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder\n";
76
76
  /**
77
77
  * Primary key columns, in key order.
@@ -0,0 +1,22 @@
1
+ import type { TableColumn } from "./introspect-db-logic.js";
2
+ /** What introspection makes of a table's search columns. */
3
+ export interface ReconstructedSearch {
4
+ /** The generated columns that belong to the search block — never emitted as properties. */
5
+ columns: Set<string>;
6
+ /** The `search: { … }` source, when the stamp confirmed it. */
7
+ block?: string;
8
+ note?: string;
9
+ }
10
+ /**
11
+ * The search block a table's stamped generated columns were built from, or a
12
+ * note saying why it could not be read back.
13
+ */
14
+ export declare function reconstructSearchBlock(input: {
15
+ schema: string;
16
+ table: string;
17
+ columns: TableColumn[];
18
+ /** column → `COMMENT ON COLUMN`. */
19
+ comments: Map<string, string>;
20
+ /** column → the property key it is generated under. */
21
+ keyByColumn: Map<string, string>;
22
+ }): ReconstructedSearch;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The property keys a column needs for the planner to build that same column.
3
+ *
4
+ * Introspection writes collections, and `rebase db push` plans them back into
5
+ * DDL. The two used to read a column differently: introspection wrote
6
+ * `type: "number"` for an `integer`, and the planner reads an unqualified
7
+ * number as `NUMERIC`; `validation.max: 40` for a `varchar(40)`, and the
8
+ * planner reads an unqualified string as `TEXT`; a `time` as a plain `date`,
9
+ * which the planner makes a `timestamptz` — a change Postgres refuses to cast.
10
+ * So adopting a database — introspect, then push, the documented path — planned
11
+ * a type change on nearly every column, and some it could not even dry-run.
12
+ *
13
+ * Each function here answers one question about one column: what to write so
14
+ * that `planSchema` produces exactly what the catalogue holds. Where no
15
+ * property can say it, the answer is a note naming the column and what a push
16
+ * would do to it, so the reader decides before anything runs.
17
+ *
18
+ * Pure: catalogue rows in, keys and notes out.
19
+ */
20
+ import type { ForeignKeyRow, TableColumn } from "./introspect-db-logic.js";
21
+ /** Keys to write on a property, as `key: value` source fragments, plus what could not be said. */
22
+ export interface ColumnStorage {
23
+ /** e.g. `columnType: "varchar"`, `precision: 10`. */
24
+ keys: string[];
25
+ /** Why the planner will not reproduce this column exactly, when it will not. */
26
+ note?: string;
27
+ }
28
+ /**
29
+ * The name Postgres gives a `SERIAL` column's sequence — `ChooseRelationName`
30
+ * with the `seq` label: `<table>_<column>_seq`, and when that is over 63 bytes
31
+ * the longer of the two names is shortened, one character at a time, until it
32
+ * fits.
33
+ */
34
+ export declare function serialSequenceName(table: string, column: string): string;
35
+ /**
36
+ * The `isId` (and `columnType`) a single-column primary key needs.
37
+ *
38
+ * `isId: "increment"` is an identity column, and only an INTEGER one. A serial
39
+ * key — an integer with a `nextval` default — is `SERIAL`/`BIGSERIAL`, declared
40
+ * through `columnType` beside `isId: "manual"`. A uuid key is `"uuid"` only when
41
+ * the database generates it; a key with no default is `"manual"`, which keeps
42
+ * the planner from inventing one.
43
+ */
44
+ export declare function primaryKeyStorage(col: TableColumn, propType: string): ColumnStorage;
45
+ /**
46
+ * The keys a non-key column needs, for the property type introspection chose.
47
+ *
48
+ * `enumTypeName` is the type the planner would name for this column
49
+ * (`<table>_<column>`, cut to 63 bytes); an enum column whose type is called
50
+ * anything else cannot be declared, and the note says how to rename it.
51
+ */
52
+ export declare function columnStorage(col: TableColumn, propType: string, context: {
53
+ isEnumColumn: boolean;
54
+ enumTypeName: string;
55
+ enumTypeShared: boolean;
56
+ }): ColumnStorage;
57
+ /**
58
+ * The `columnType` an array column needs, or a note when its element type is
59
+ * not one the planner stores natively.
60
+ *
61
+ * Only `text[]`, `integer[]`, `boolean[]` and `numeric[]` are. Reading
62
+ * `bigint[]` as `integer[]` — what this used to do — planned a narrowing.
63
+ */
64
+ export declare function arrayStorage(col: TableColumn): {
65
+ columnType?: string;
66
+ innerType: string;
67
+ note?: string;
68
+ };
69
+ /**
70
+ * A column default as a `defaultValue`, when it is a literal a property can carry.
71
+ *
72
+ * `undefined` when there is no default to carry, or when the default is one the
73
+ * generator expresses another way (`now()` is `autoValue`, a key's default is
74
+ * its `isId`). `{ note }` when there is a default and no property can say it —
75
+ * a push would plan dropping it.
76
+ */
77
+ export declare function literalDefaultOf(col: TableColumn, propType: string): {
78
+ source: string;
79
+ } | {
80
+ note: string;
81
+ } | undefined;
82
+ /** `ON DELETE` as the relation's `onDelete`, when it is not the default the planner would choose. */
83
+ export declare function onDeleteOf(fk: ForeignKeyRow, required: boolean): string | undefined;
@@ -1,9 +1,10 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { a as __toESM, t as require___vite_browser_external } from "../__vite-browser-external-BnuHet1e.js";
4
- import { _ as isUsableFileName, a as generateCollectionFile, f as classifyTables, g as countRowsUpTo, i as buildTablesMap, l as mergeIndexContent, o as generateIndexContent, p as lookupCandidates, r as buildEnumMap, u as safeHostFromUrl, v as quoteRelation, y as readSchemaMetadata } from "../introspect-db-logic-WMuAfxvw.js";
4
+ import { i as readSchemaMetadata, n as isUsableFileName, r as quoteRelation, t as countRowsUpTo } from "../introspect-db-queries-C_Q5VgQw.js";
5
5
  import { n as outError, r as outWarn, t as out } from "../cli-output-CNdMql-L.js";
6
6
  import { t as chalk } from "../source-Br7L7GOI.js";
7
+ import { a as compositeKeySkipReason, d as safeHostFromUrl, i as buildTablesMap, m as lookupCandidates, o as generateCollectionFile, p as classifyTables, r as buildEnumMap, s as generateIndexContent, u as mergeIndexContent } from "../introspect-db-logic-kCETE8TY.js";
7
8
  import { t as require_arg } from "../arg-Dni7MzLB.js";
8
9
  import pg from "pg";
9
10
  import fs from "fs";
@@ -485,7 +486,10 @@ async function main() {
485
486
  if (runDataInference) out(chalk.gray("Sampling database rows for data inference..."));
486
487
  const generatedFiles = [];
487
488
  const skippedFiles = [];
488
- const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
489
+ const compositeKeyed = new Map(Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName)).map(([tableName, meta]) => [tableName, compositeKeySkipReason(meta, fks)]).filter((entry) => entry[1] !== void 0));
490
+ for (const [tableName, reason] of compositeKeyed) outWarn(chalk.yellow(`⚠ Skipping table ${JSON.stringify(tableName)}: it is keyed on (${tablesMap.get(tableName)?.pks.join(", ")}), and ${reason}. \`rebase db push\` leaves a table that is not a collection alone.`));
491
+ const notes = [];
492
+ const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName) && !compositeKeyed.has(tableName));
489
493
  const BATCH_SIZE = 10;
490
494
  for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
491
495
  const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
@@ -511,13 +515,21 @@ async function main() {
511
515
  metadata,
512
516
  classifications,
513
517
  checkFacts,
514
- builder
518
+ builder,
519
+ notes,
520
+ skippedTables: new Set(compositeKeyed.keys())
515
521
  });
516
522
  fs.writeFileSync(filePath, fileContent, "utf-8");
517
523
  generatedFiles.push(tableName);
518
524
  out(chalk.green(` ✓ ${filePath}`));
519
525
  }));
520
526
  }
527
+ if (notes.length > 0) {
528
+ outWarn("");
529
+ outWarn(chalk.yellow(`⚠ ${notes.length} column(s) will not round-trip exactly — \`rebase db push\` would change them:`));
530
+ for (const note of [...notes].sort()) outWarn(chalk.gray(` - ${note}`));
531
+ outWarn(chalk.gray(" Read these before the first push. Run `rebase db push --dry-run` to see the plan."));
532
+ }
521
533
  if (generatedFiles.length > 0) {
522
534
  const indexPath = path.join(outDir, "index.ts");
523
535
  if (fs.existsSync(indexPath) && !force) {
@@ -1 +1 @@
1
- {"version":3,"file":"introspect-db.js","names":[],"sources":["../../src/schema/introspect-db-project.ts","../../src/schema/introspect-db-constraints.ts","../../src/schema/introspect-db.ts"],"sourcesContent":["/**\n * What the project being introspected into can actually import.\n *\n * The generator is pure — `introspect-db-logic.ts` reads no files — so the one\n * question it cannot answer for itself lives here: which `defineCollection`, if\n * any, will resolve from the directory the collections are written to.\n */\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { ADMIN_TYPES_PACKAGE, COMMON_PACKAGE, type CollectionBuilder } from \"./introspect-db-logic\";\n\n/** How far up to look before giving up, if no project root announces itself. */\nconst MAX_LEVELS = 8;\n\n/** Every dependency name a manifest declares, in any of the three fields. */\nfunction declaredDependencies(manifestPath: string): Set<string> {\n const names = new Set<string>();\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, \"utf-8\"));\n } catch {\n // A malformed or unreadable package.json is not this function's problem\n // to report — it answers \"can this import resolve\", and the answer is no.\n return names;\n }\n if (typeof parsed !== \"object\" || parsed === null) return names;\n const manifest = parsed as Record<string, unknown>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"]) {\n const block = manifest[field];\n if (typeof block === \"object\" && block !== null) {\n for (const name of Object.keys(block)) names.add(name);\n }\n }\n return names;\n}\n\n/**\n * Which `defineCollection` to generate against, for collections written to `outDir`.\n *\n * **The detection is the package manifests above the output directory**, unioned from\n * `outDir` up to and including the project root — the first ancestor holding a\n * `rebase.json`, or `MAX_LEVELS` up if there is none.\n *\n * That is the rule Node itself applies: a file in `config/collections` resolves a bare\n * specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.\n * Reading the manifests along that same path answers the only question that matters —\n * *will this import resolve in the project I am writing into* — from the state on disk\n * at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s\n * `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory\n * says one was scaffolded, but neither is what the compiler consults, and either can be\n * true of a project whose `config` package does not depend on `@rebasepro/cms-types`.\n *\n * Ambiguity resolves towards the admin panel: a project that declares both packages has\n * a panel, and `@rebasepro/cms-types` is the flavour that keeps the `admin` block.\n */\nexport function detectCollectionBuilder(outDir: string): CollectionBuilder {\n let dir = path.resolve(outDir);\n const declared = new Set<string>();\n\n for (let level = 0; level < MAX_LEVELS; level++) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n for (const name of declaredDependencies(manifest)) declared.add(name);\n }\n // The project root, and the last level worth reading: anything above it\n // belongs to whatever the project happens to be checked out inside.\n if (fs.existsSync(path.join(dir, \"rebase.json\"))) break;\n\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n if (declared.has(ADMIN_TYPES_PACKAGE)) return \"admin-types\";\n if (declared.has(COMMON_PACKAGE)) return \"common\";\n return \"annotation\";\n}\n","/**\n * Reading validation rules out of CHECK constraints.\n *\n * A CHECK constraint is the schema author stating a rule the database already\n * enforces. Introspection has never read them, so a generated form let the user\n * type a value the database was always going to reject — the rule was written\n * down, in the schema, and the UI asked anyway.\n *\n * Everything here parses the *normalized* text `pg_get_constraintdef` produces,\n * not what the author typed: Postgres re-renders the expression from its parse\n * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as\n * `CHECK ((price > (0)::numeric))`. That normalization is what makes a\n * string-level parser tractable — the input is generated, and the shapes are\n * few.\n *\n * Constraints this cannot read are skipped in full. A partially-understood\n * constraint is worse than an unread one: it would produce validation that\n * *narrows* differently from the database, so a value the UI accepts still\n * fails on write, or — worse — a value the database allows is refused in the\n * form with no way to see why.\n *\n * Pure module: no I/O, no logging.\n */\nimport type { CheckConstraintRow } from \"./introspect-db-logic\";\n\n/** What a table's CHECK constraints say about one column. */\nexport interface ColumnCheckFacts {\n /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */\n enumValues?: string[];\n /** `x >= n` */\n min?: number;\n /** `x <= n` */\n max?: number;\n /** `x > n` */\n moreThan?: number;\n /** `x < n` */\n lessThan?: number;\n /** `length(x) >= n` */\n lengthMin?: number;\n /** `length(x) <= n` */\n lengthMax?: number;\n}\n\n/** Per-table, per-column facts. */\nexport type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;\n\n/** The length functions whose argument is the column being constrained. */\nconst LENGTH_FUNCTIONS = new Set([\"length\", \"char_length\", \"character_length\", \"octet_length\"]);\n\n/**\n * Splits on a separator that appears at paren depth 0 and outside string\n * literals.\n *\n * Depth tracking is the whole job: `ARRAY['a', 'b']` contains a comma that must\n * not split the enclosing argument list, and `'it''s'` contains a quote that\n * must not end the literal.\n */\nfunction splitTopLevel(input: string, separator: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let inString = false;\n let current = \"\";\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n\n if (inString) {\n current += ch;\n if (ch === \"'\") {\n // '' inside a literal is an escaped quote, not the end of it.\n if (input[i + 1] === \"'\") {\n current += input[++i];\n } else {\n inString = false;\n }\n }\n continue;\n }\n\n if (ch === \"'\") {\n inString = true;\n current += ch;\n continue;\n }\n if (ch === \"(\" || ch === \"[\") depth++;\n if (ch === \")\" || ch === \"]\") depth--;\n\n if (depth === 0 && input.startsWith(separator, i)) {\n // A word separator must not match inside an identifier: \"AND\" is a\n // separator in `a AND b` but not in `brand = 1`.\n const before = input[i - 1];\n const after = input[i + separator.length];\n const wordish = /[A-Za-z]/.test(separator[0]);\n const boundedOk = !wordish || ((before === undefined || !/[A-Za-z0-9_]/.test(before)) &&\n (after === undefined || !/[A-Za-z0-9_]/.test(after)));\n if (boundedOk) {\n parts.push(current);\n current = \"\";\n i += separator.length - 1;\n continue;\n }\n }\n\n current += ch;\n }\n\n parts.push(current);\n return parts.map((p) => p.trim()).filter((p) => p.length > 0);\n}\n\n/** Removes one layer of wrapping parentheses, repeatedly, when balanced. */\nexport function unwrapParens(input: string): string {\n let s = input.trim();\n while (s.startsWith(\"(\") && s.endsWith(\")\")) {\n let depth = 0;\n let balanced = true;\n let inString = false;\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (inString) {\n if (ch === \"'\") {\n if (s[i + 1] === \"'\") i++;\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n // The opening paren closed before the end, so the outer pair is\n // not a wrapper: `(a) AND (b)`.\n if (depth === 0 && i < s.length - 1) { balanced = false; break; }\n }\n }\n if (!balanced) break;\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Drops trailing `::type` casts, including array and quoted forms. */\nexport function stripCasts(input: string): string {\n let s = unwrapParens(input);\n let previous: string;\n do {\n previous = s;\n s = unwrapParens(\n s.replace(/::\\s*\"?[A-Za-z_][A-Za-z0-9_ ]*\"?(\\s*\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?(\\s*\\[\\s*\\])*\\s*$/, \"\").trim()\n );\n } while (s !== previous);\n return s;\n}\n\n/** A bare, unquoted column reference — anything else is an expression. */\nfunction asColumnName(input: string): string | null {\n const s = stripCasts(input);\n if (/^[a-z_][a-z0-9_$]*$/i.test(s)) return s;\n // A quoted identifier keeps its case: \"userId\".\n const quoted = s.match(/^\"([^\"]+)\"$/);\n return quoted ? quoted[1] : null;\n}\n\n/** `length(col)` and friends → the column they measure. */\nfunction asLengthOfColumn(input: string): string | null {\n const s = stripCasts(input);\n const match = s.match(/^([a-z_]+)\\s*\\((.*)\\)$/is);\n if (!match || !LENGTH_FUNCTIONS.has(match[1].toLowerCase())) return null;\n return asColumnName(match[2]);\n}\n\n/** A numeric literal, after casts are removed. Rejects anything else. */\nfunction asNumber(input: string): number | null {\n const s = stripCasts(input).replace(/^'(.*)'$/s, \"$1\");\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(s)) return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** A single-quoted string literal, with `''` unescaped. */\nfunction asStringLiteral(input: string): string | null {\n const s = stripCasts(input);\n if (!s.startsWith(\"'\") || !s.endsWith(\"'\") || s.length < 2) return null;\n const body = s.slice(1, -1);\n // Any unescaped quote inside means this was not one literal.\n if (/(^|[^'])'($|[^'])/.test(body)) return null;\n return body.replace(/''/g, \"'\");\n}\n\n/** The element list of `ARRAY[…]`, or of a parenthesised `IN (…)` list. */\nfunction asValueList(input: string): string[] | null {\n const s = stripCasts(input);\n const arrayMatch = s.match(/^ARRAY\\s*\\[(.*)\\]$/is);\n const body = arrayMatch ? arrayMatch[1] : (s.startsWith(\"(\") && s.endsWith(\")\") ? s.slice(1, -1) : null);\n if (body === null) return null;\n if (body.trim() === \"\") return null;\n\n const values: string[] = [];\n for (const element of splitTopLevel(body, \",\")) {\n const literal = asStringLiteral(element);\n if (literal !== null) {\n values.push(literal);\n continue;\n }\n const numeric = asNumber(element);\n if (numeric !== null) {\n values.push(String(numeric));\n continue;\n }\n return null; // A non-literal element means this is not an enumeration.\n }\n return values;\n}\n\n/** One understood comparison, already resolved to the column it constrains. */\ninterface CheckTerm {\n column: string;\n apply: (facts: ColumnCheckFacts) => void;\n}\n\n/**\n * Parses one conjunct. Returns null when the shape is not one of the few this\n * understands, which is the signal to abandon the whole constraint.\n */\nfunction parseTerm(rawTerm: string): CheckTerm | null {\n const term = unwrapParens(rawTerm);\n\n // ── Membership: `x = ANY (ARRAY[…])`, `x IN (…)` ──────────────────\n const anyMatch = term.match(/^(.*?)\\s*=\\s*ANY\\s*(\\(.*\\))$/is);\n const inMatch = anyMatch ? null : term.match(/^(.*?)\\s+IN\\s*(\\(.*\\))$/is);\n const membership = anyMatch ?? inMatch;\n if (membership) {\n const column = asColumnName(membership[1]);\n const values = asValueList(membership[2]);\n if (!column || !values || values.length === 0) return null;\n return {\n column,\n apply: (facts) => {\n // Two membership checks on one column mean both must hold.\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => values.includes(v))\n : values;\n }\n };\n }\n\n // ── Comparison: `x >= n`, `length(x) <= n`, `x = 'literal'` ───────\n // Longest operators first so `>=` is not read as `>`.\n for (const operator of [\">=\", \"<=\", \"<>\", \"!=\", \"=\", \">\", \"<\"] as const) {\n const parts = splitTopLevel(term, operator);\n if (parts.length !== 2) continue;\n const [rawLeft, rawRight] = parts;\n\n const lengthColumn = asLengthOfColumn(rawLeft);\n if (lengthColumn) {\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound, bound); } };\n case \">\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound + 1, bound + 1); } };\n case \"<=\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound, bound); } };\n case \"<\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound - 1, bound - 1); } };\n case \"=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = bound; f.lengthMax = bound; } };\n default: return null;\n }\n }\n\n const column = asColumnName(rawLeft);\n if (!column) continue;\n\n if (operator === \"=\") {\n const literal = asStringLiteral(rawRight);\n if (literal !== null) {\n return {\n column,\n apply: (facts) => {\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => v === literal)\n : [literal];\n }\n };\n }\n // `x = 5` pins a number. Expressed as both bounds rather than as a\n // one-value enum, which for a number would render as a dropdown.\n const pinned = asNumber(rawRight);\n if (pinned === null) return null;\n return { column, apply: (f) => { f.min = pinned; f.max = pinned; } };\n }\n\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column, apply: (f) => { f.min = Math.max(f.min ?? bound, bound); } };\n case \">\": return { column, apply: (f) => { f.moreThan = Math.max(f.moreThan ?? bound, bound); } };\n case \"<=\": return { column, apply: (f) => { f.max = Math.min(f.max ?? bound, bound); } };\n case \"<\": return { column, apply: (f) => { f.lessThan = Math.min(f.lessThan ?? bound, bound); } };\n default: return null; // `<>` / `!=` exclude rather than bound.\n }\n }\n\n return null;\n}\n\n/**\n * Reads one constraint definition into per-column facts.\n *\n * Returns an empty map for anything not understood — including a constraint\n * that is understood but spans two columns (`start_date < end_date`), which no\n * per-property validation rule can express.\n */\nexport function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts> {\n const result = new Map<string, ColumnCheckFacts>();\n\n const body = definition\n .trim()\n .replace(/\\s+NOT\\s+VALID\\s*$/i, \"\")\n .replace(/^CHECK\\s*/i, \"\")\n .trim();\n if (body === definition.trim()) return result; // Not a CHECK definition.\n\n const expression = unwrapParens(body);\n if (!expression) return result;\n\n // Only conjunctions. A disjunction (`a OR b`) does not narrow a column: each\n // branch may allow what the other forbids, so no single bound follows.\n if (splitTopLevel(expression, \" OR \").length > 1) return result;\n\n const terms = splitTopLevel(expression, \" AND \");\n const parsed: CheckTerm[] = [];\n for (const term of terms) {\n const one = parseTerm(term);\n if (!one) return result; // One unreadable conjunct voids the constraint.\n parsed.push(one);\n }\n\n // A CHECK naming two columns constrains their *relationship*; keeping the\n // half that mentions one column would assert something the database does not.\n const columns = new Set(parsed.map((t) => t.column));\n if (columns.size !== 1) return result;\n\n const facts: ColumnCheckFacts = {};\n for (const term of parsed) term.apply(facts);\n if (Object.keys(facts).length === 0) return result;\n\n result.set(parsed[0].column, facts);\n return result;\n}\n\n/** Merges every readable CHECK in the schema into per-table, per-column facts. */\nexport function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable {\n const byTable: CheckFactsByTable = new Map();\n\n for (const row of rows) {\n const parsed = parseCheckDefinition(row.definition);\n if (parsed.size === 0) continue;\n\n let table = byTable.get(row.table_name);\n if (!table) {\n table = new Map();\n byTable.set(row.table_name, table);\n }\n\n for (const [column, facts] of parsed) {\n const existing = table.get(column);\n if (!existing) {\n table.set(column, facts);\n continue;\n }\n // Separate constraints on the same column all hold at once.\n if (facts.enumValues) {\n existing.enumValues = existing.enumValues\n ? existing.enumValues.filter((v) => facts.enumValues!.includes(v))\n : facts.enumValues;\n }\n if (facts.min !== undefined) existing.min = Math.max(existing.min ?? facts.min, facts.min);\n if (facts.max !== undefined) existing.max = Math.min(existing.max ?? facts.max, facts.max);\n if (facts.moreThan !== undefined) existing.moreThan = Math.max(existing.moreThan ?? facts.moreThan, facts.moreThan);\n if (facts.lessThan !== undefined) existing.lessThan = Math.min(existing.lessThan ?? facts.lessThan, facts.lessThan);\n if (facts.lengthMin !== undefined) existing.lengthMin = Math.max(existing.lengthMin ?? facts.lengthMin, facts.lengthMin);\n if (facts.lengthMax !== undefined) existing.lengthMax = Math.min(existing.lengthMax ?? facts.lengthMax, facts.lengthMax);\n }\n }\n\n return byTable;\n}\n","import chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport pg from \"pg\";\nimport arg from \"arg\";\nimport * as dotenv from \"dotenv\";\nimport readline from \"readline\";\n\nimport {\n buildTablesMap,\n buildEnumMap,\n generateCollectionFile,\n generateIndexContent,\n mergeIndexContent,\n safeHostFromUrl\n} from \"./introspect-db-logic\";\nimport { detectCollectionBuilder } from \"./introspect-db-project\";\nimport { countRowsUpTo, isUsableFileName, quoteRelation, readSchemaMetadata } from \"./introspect-db-queries\";\nimport { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from \"./introspect-db-structure\";\nimport { parseCheckConstraints } from \"./introspect-db-constraints\";\nimport { out, outWarn, outError } from \"../cli-output\";\n\nasync function main() {\n const args = arg(\n {\n \"--output\": String,\n \"--collections\": String,\n \"--force\": Boolean,\n \"--schema\": String,\n \"--data-inference\": Boolean,\n \"--no-data-inference\": Boolean,\n \"-o\": \"--output\",\n \"-c\": \"--collections\",\n \"-f\": \"--force\"\n },\n { permissive: true }\n );\n\n const cwd = process.cwd();\n const isBackendDir = path.basename(cwd) === \"backend\";\n const defaultOutDir = isBackendDir\n ? path.resolve(cwd, \"..\", \"config\", \"collections\")\n : path.resolve(cwd, \"config\", \"collections\");\n\n const outDir = args[\"--output\"] || args[\"--collections\"] || defaultOutDir;\n const force = args[\"--force\"] || false;\n const pgSchema = args[\"--schema\"] || \"public\";\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true });\n }\n\n // Load env\n const envPaths = [\n process.env.DOTENV_CONFIG_PATH,\n path.resolve(process.cwd(), \".env\"),\n path.resolve(process.cwd(), \"../.env\"),\n path.resolve(process.cwd(), \"../../.env\")\n ].filter(Boolean) as string[];\n\n for (const p of envPaths) {\n if (fs.existsSync(p)) {\n dotenv.config({ path: p, quiet: true });\n break;\n }\n }\n\n const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;\n if (!databaseUrl) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n\n const client = new pg.Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n } catch (err) {\n outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));\n outError(chalk.gray(\" Check your DATABASE_URL and ensure the database is reachable.\"));\n process.exit(1);\n }\n\n // Log the host portion safely — handle URLs without \"@\"\n const hostPart = safeHostFromUrl(databaseUrl);\n out(chalk.gray(`Connected to database: ${hostPart}`));\n out(chalk.gray(`Introspecting schema '${pgSchema}'...`));\n\n try {\n const metadata = await readSchemaMetadata(client, pgSchema);\n const enumMap = buildEnumMap(metadata.enumValues);\n const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);\n const fks = metadata.fks;\n\n // Only tables that could structurally be a code list are counted, and\n // each count stops at the threshold — see `countRowsUpTo`. Introspection\n // runs against a database it does not own, so \"cheap on a table of any\n // size\" is a requirement, not an optimization.\n for (const table of lookupCandidates(metadata, tablesMap)) {\n try {\n metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);\n } catch (err) {\n // A table this run cannot read is simply not classified as a code\n // list; everything else about it still generates.\n out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));\n }\n }\n\n const classifications = classifyTables(metadata, tablesMap);\n const checkFacts = parseCheckConstraints(metadata.checks);\n\n // Which builder the generated files may import — read from the manifests\n // above `outDir`, because a project without `@rebasepro/cms-types` cannot\n // resolve that import and has no `admin` block to write into either.\n const builder = detectCollectionBuilder(outDir);\n if (builder === \"annotation\") {\n outWarn(chalk.yellow(\n \"⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above \" +\n `${outDir}.`\n ));\n outWarn(chalk.gray(\n \" Generating plain type annotations, which widen the property keys to `string`: \" +\n \"`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the \" +\n \"config package to turn that checking on.\"\n ));\n }\n const joinTables = new Set(\n Array.from(classifications.values())\n .filter((c) => c.role === \"junction\")\n .map((c) => c.table)\n );\n\n const roleCount = (role: string) =>\n Array.from(classifications.values()).filter((c) => c.role === role).length;\n\n out(chalk.blue(`Found ${tablesMap.size} tables.`));\n out(chalk.gray(\n ` ${roleCount(\"entity\")} entities, ${joinTables.size} join tables (folded into relations), ` +\n `${roleCount(\"lookup\")} code lists, ${roleCount(\"owned-child\")} owned by another table (hidden from navigation).`\n ));\n\n let runDataInference = false;\n if (args[\"--no-data-inference\"]) {\n runDataInference = false;\n } else if (args[\"--data-inference\"] !== undefined) {\n runDataInference = args[\"--data-inference\"];\n } else if (!process.stdin.isTTY) {\n // No terminal to answer the question below (scaffolding scripts, CI,\n // `rebase init --introspect`) — asking would hang forever.\n out(chalk.gray(\"Skipping data inference (non-interactive run; pass --data-inference to enable).\"));\n } else {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n });\n const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow(\"? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) \"), resolve));\n runDataInference = answer.trim().toLowerCase() === \"y\";\n rl.close();\n }\n\n if (runDataInference) {\n out(chalk.gray(\"Sampling database rows for data inference...\"));\n }\n\n // Generate Collections\n const generatedFiles: string[] = [];\n const skippedFiles: string[] = [];\n\n const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));\n\n const BATCH_SIZE = 10;\n for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {\n const batch = tablesToProcess.slice(i, i + BATCH_SIZE);\n\n await Promise.all(batch.map(async ([tableName, meta]) => {\n // ── The name has to be usable as a filename ────────────────\n //\n // `tableName` came out of `pg_class` in a database this tool\n // did not create — adopting somebody else's database is the\n // whole point of introspection — and it is about to be joined\n // onto a path. Postgres will happily hold a table called\n // `../../../etc/cron.d/x`, and `path.join` resolves it: the\n // generator would write a TypeScript file wherever that\n // pointed, as whoever ran the command.\n //\n // Skipped rather than sanitised. A file named something other\n // than its table is a collection that does not round-trip, and\n // silently renaming it would be a worse surprise than one line\n // saying which table was left out.\n if (!isUsableFileName(tableName)) {\n outWarn(chalk.yellow(\n `⚠ Skipping table ${JSON.stringify(tableName)}: its name cannot be used as a ` +\n \"filename. Rename the table, or write this collection by hand.\"\n ));\n skippedFiles.push(tableName);\n return;\n }\n\n // ── File overwrite protection ──────────────────────────────\n const filePath = path.join(outDir, `${tableName}.ts`);\n if (fs.existsSync(filePath) && !force) {\n skippedFiles.push(tableName);\n return;\n }\n\n let sampleData: Record<string, unknown>[] | undefined = undefined;\n if (runDataInference) {\n try {\n // Quoted by `quoteRelation`, which doubles an embedded\n // quote. Wrapping a catalogue name in `\"` is not\n // escaping it — see that function.\n const { rows } = await client.query(`SELECT * FROM ${quoteRelation(pgSchema, tableName)} LIMIT 100`);\n sampleData = rows;\n } catch (err) {\n outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n\n const fileContent = generateCollectionFile(\n tableName,\n meta,\n fks,\n joinTables,\n tablesMap,\n enumMap,\n sampleData,\n { metadata, classifications, checkFacts, builder }\n );\n\n fs.writeFileSync(filePath, fileContent, \"utf-8\");\n generatedFiles.push(tableName);\n out(chalk.green(` ✓ ${filePath}`));\n }));\n }\n\n // Generate index.ts (sorted alphabetically for deterministic output)\n if (generatedFiles.length > 0) {\n const indexPath = path.join(outDir, \"index.ts\");\n\n if (fs.existsSync(indexPath) && !force) {\n // Merge: read existing index, add new exports that don't already exist\n const existing = fs.readFileSync(indexPath, \"utf-8\");\n const merged = mergeIndexContent(existing, generatedFiles);\n fs.writeFileSync(indexPath, merged, \"utf-8\");\n } else {\n // --force replaces collections derived from the database, but the\n // directory can also hold hand-written ones with no table in the\n // introspected schema (the auth users collection lives in\n // \"rebase\"). The backend discovers the whole directory, so an\n // index listing only introspected tables would silently drop them\n // from the admin UI while the API still served them.\n const siblings = fs.readdirSync(outDir)\n .filter(f => f.endsWith(\".ts\") && f !== \"index.ts\")\n .map(f => f.replace(/\\.ts$/, \"\"));\n const allFiles = [...new Set([...generatedFiles, ...siblings])];\n const indexContent = generateIndexContent(allFiles);\n fs.writeFileSync(indexPath, indexContent, \"utf-8\");\n }\n out(chalk.green(` ✓ ${indexPath}`));\n }\n\n out(\"\");\n if (skippedFiles.length > 0) {\n out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(\", \")}`));\n out(chalk.gray(\" Use --force to overwrite existing files.\"));\n out(\"\");\n }\n out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));\n out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));\n out(\"\");\n\n } catch (e) {\n outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));\n process.exit(1);\n } finally {\n await client.end();\n }\n}\n\nmain().catch((err) => {\n outError(String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"introspect-db.js","names":[],"sources":["../../src/schema/introspect-db-project.ts","../../src/schema/introspect-db-constraints.ts","../../src/schema/introspect-db.ts"],"sourcesContent":["/**\n * What the project being introspected into can actually import.\n *\n * The generator is pure — `introspect-db-logic.ts` reads no files — so the one\n * question it cannot answer for itself lives here: which `defineCollection`, if\n * any, will resolve from the directory the collections are written to.\n */\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { ADMIN_TYPES_PACKAGE, COMMON_PACKAGE, type CollectionBuilder } from \"./introspect-db-logic\";\n\n/** How far up to look before giving up, if no project root announces itself. */\nconst MAX_LEVELS = 8;\n\n/** Every dependency name a manifest declares, in any of the three fields. */\nfunction declaredDependencies(manifestPath: string): Set<string> {\n const names = new Set<string>();\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, \"utf-8\"));\n } catch {\n // A malformed or unreadable package.json is not this function's problem\n // to report — it answers \"can this import resolve\", and the answer is no.\n return names;\n }\n if (typeof parsed !== \"object\" || parsed === null) return names;\n const manifest = parsed as Record<string, unknown>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"]) {\n const block = manifest[field];\n if (typeof block === \"object\" && block !== null) {\n for (const name of Object.keys(block)) names.add(name);\n }\n }\n return names;\n}\n\n/**\n * Which `defineCollection` to generate against, for collections written to `outDir`.\n *\n * **The detection is the package manifests above the output directory**, unioned from\n * `outDir` up to and including the project root — the first ancestor holding a\n * `rebase.json`, or `MAX_LEVELS` up if there is none.\n *\n * That is the rule Node itself applies: a file in `config/collections` resolves a bare\n * specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.\n * Reading the manifests along that same path answers the only question that matters —\n * *will this import resolve in the project I am writing into* — from the state on disk\n * at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s\n * `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory\n * says one was scaffolded, but neither is what the compiler consults, and either can be\n * true of a project whose `config` package does not depend on `@rebasepro/cms-types`.\n *\n * Ambiguity resolves towards the admin panel: a project that declares both packages has\n * a panel, and `@rebasepro/cms-types` is the flavour that keeps the `admin` block.\n */\nexport function detectCollectionBuilder(outDir: string): CollectionBuilder {\n let dir = path.resolve(outDir);\n const declared = new Set<string>();\n\n for (let level = 0; level < MAX_LEVELS; level++) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n for (const name of declaredDependencies(manifest)) declared.add(name);\n }\n // The project root, and the last level worth reading: anything above it\n // belongs to whatever the project happens to be checked out inside.\n if (fs.existsSync(path.join(dir, \"rebase.json\"))) break;\n\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n if (declared.has(ADMIN_TYPES_PACKAGE)) return \"admin-types\";\n if (declared.has(COMMON_PACKAGE)) return \"common\";\n return \"annotation\";\n}\n","/**\n * Reading validation rules out of CHECK constraints.\n *\n * A CHECK constraint is the schema author stating a rule the database already\n * enforces. Introspection has never read them, so a generated form let the user\n * type a value the database was always going to reject — the rule was written\n * down, in the schema, and the UI asked anyway.\n *\n * Everything here parses the *normalized* text `pg_get_constraintdef` produces,\n * not what the author typed: Postgres re-renders the expression from its parse\n * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as\n * `CHECK ((price > (0)::numeric))`. That normalization is what makes a\n * string-level parser tractable — the input is generated, and the shapes are\n * few.\n *\n * Constraints this cannot read are skipped in full. A partially-understood\n * constraint is worse than an unread one: it would produce validation that\n * *narrows* differently from the database, so a value the UI accepts still\n * fails on write, or — worse — a value the database allows is refused in the\n * form with no way to see why.\n *\n * Pure module: no I/O, no logging.\n */\nimport type { CheckConstraintRow } from \"./introspect-db-logic\";\n\n/** What a table's CHECK constraints say about one column. */\nexport interface ColumnCheckFacts {\n /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */\n enumValues?: string[];\n /** `x >= n` */\n min?: number;\n /** `x <= n` */\n max?: number;\n /** `x > n` */\n moreThan?: number;\n /** `x < n` */\n lessThan?: number;\n /** `length(x) >= n` */\n lengthMin?: number;\n /** `length(x) <= n` */\n lengthMax?: number;\n}\n\n/** Per-table, per-column facts. */\nexport type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;\n\n/** The length functions whose argument is the column being constrained. */\nconst LENGTH_FUNCTIONS = new Set([\"length\", \"char_length\", \"character_length\", \"octet_length\"]);\n\n/**\n * Splits on a separator that appears at paren depth 0 and outside string\n * literals.\n *\n * Depth tracking is the whole job: `ARRAY['a', 'b']` contains a comma that must\n * not split the enclosing argument list, and `'it''s'` contains a quote that\n * must not end the literal.\n */\nfunction splitTopLevel(input: string, separator: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let inString = false;\n let current = \"\";\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n\n if (inString) {\n current += ch;\n if (ch === \"'\") {\n // '' inside a literal is an escaped quote, not the end of it.\n if (input[i + 1] === \"'\") {\n current += input[++i];\n } else {\n inString = false;\n }\n }\n continue;\n }\n\n if (ch === \"'\") {\n inString = true;\n current += ch;\n continue;\n }\n if (ch === \"(\" || ch === \"[\") depth++;\n if (ch === \")\" || ch === \"]\") depth--;\n\n if (depth === 0 && input.startsWith(separator, i)) {\n // A word separator must not match inside an identifier: \"AND\" is a\n // separator in `a AND b` but not in `brand = 1`.\n const before = input[i - 1];\n const after = input[i + separator.length];\n const wordish = /[A-Za-z]/.test(separator[0]);\n const boundedOk = !wordish || ((before === undefined || !/[A-Za-z0-9_]/.test(before)) &&\n (after === undefined || !/[A-Za-z0-9_]/.test(after)));\n if (boundedOk) {\n parts.push(current);\n current = \"\";\n i += separator.length - 1;\n continue;\n }\n }\n\n current += ch;\n }\n\n parts.push(current);\n return parts.map((p) => p.trim()).filter((p) => p.length > 0);\n}\n\n/** Removes one layer of wrapping parentheses, repeatedly, when balanced. */\nexport function unwrapParens(input: string): string {\n let s = input.trim();\n while (s.startsWith(\"(\") && s.endsWith(\")\")) {\n let depth = 0;\n let balanced = true;\n let inString = false;\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (inString) {\n if (ch === \"'\") {\n if (s[i + 1] === \"'\") i++;\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n // The opening paren closed before the end, so the outer pair is\n // not a wrapper: `(a) AND (b)`.\n if (depth === 0 && i < s.length - 1) { balanced = false; break; }\n }\n }\n if (!balanced) break;\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Drops trailing `::type` casts, including array and quoted forms. */\nexport function stripCasts(input: string): string {\n let s = unwrapParens(input);\n let previous: string;\n do {\n previous = s;\n s = unwrapParens(\n s.replace(/::\\s*\"?[A-Za-z_][A-Za-z0-9_ ]*\"?(\\s*\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?(\\s*\\[\\s*\\])*\\s*$/, \"\").trim()\n );\n } while (s !== previous);\n return s;\n}\n\n/** A bare, unquoted column reference — anything else is an expression. */\nfunction asColumnName(input: string): string | null {\n const s = stripCasts(input);\n if (/^[a-z_][a-z0-9_$]*$/i.test(s)) return s;\n // A quoted identifier keeps its case: \"userId\".\n const quoted = s.match(/^\"([^\"]+)\"$/);\n return quoted ? quoted[1] : null;\n}\n\n/** `length(col)` and friends → the column they measure. */\nfunction asLengthOfColumn(input: string): string | null {\n const s = stripCasts(input);\n const match = s.match(/^([a-z_]+)\\s*\\((.*)\\)$/is);\n if (!match || !LENGTH_FUNCTIONS.has(match[1].toLowerCase())) return null;\n return asColumnName(match[2]);\n}\n\n/** A numeric literal, after casts are removed. Rejects anything else. */\nfunction asNumber(input: string): number | null {\n const s = stripCasts(input).replace(/^'(.*)'$/s, \"$1\");\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(s)) return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** A single-quoted string literal, with `''` unescaped. */\nfunction asStringLiteral(input: string): string | null {\n const s = stripCasts(input);\n if (!s.startsWith(\"'\") || !s.endsWith(\"'\") || s.length < 2) return null;\n const body = s.slice(1, -1);\n // Any unescaped quote inside means this was not one literal.\n if (/(^|[^'])'($|[^'])/.test(body)) return null;\n return body.replace(/''/g, \"'\");\n}\n\n/** The element list of `ARRAY[…]`, or of a parenthesised `IN (…)` list. */\nfunction asValueList(input: string): string[] | null {\n const s = stripCasts(input);\n const arrayMatch = s.match(/^ARRAY\\s*\\[(.*)\\]$/is);\n const body = arrayMatch ? arrayMatch[1] : (s.startsWith(\"(\") && s.endsWith(\")\") ? s.slice(1, -1) : null);\n if (body === null) return null;\n if (body.trim() === \"\") return null;\n\n const values: string[] = [];\n for (const element of splitTopLevel(body, \",\")) {\n const literal = asStringLiteral(element);\n if (literal !== null) {\n values.push(literal);\n continue;\n }\n const numeric = asNumber(element);\n if (numeric !== null) {\n values.push(String(numeric));\n continue;\n }\n return null; // A non-literal element means this is not an enumeration.\n }\n return values;\n}\n\n/** One understood comparison, already resolved to the column it constrains. */\ninterface CheckTerm {\n column: string;\n apply: (facts: ColumnCheckFacts) => void;\n}\n\n/**\n * Parses one conjunct. Returns null when the shape is not one of the few this\n * understands, which is the signal to abandon the whole constraint.\n */\nfunction parseTerm(rawTerm: string): CheckTerm | null {\n const term = unwrapParens(rawTerm);\n\n // ── Membership: `x = ANY (ARRAY[…])`, `x IN (…)` ──────────────────\n const anyMatch = term.match(/^(.*?)\\s*=\\s*ANY\\s*(\\(.*\\))$/is);\n const inMatch = anyMatch ? null : term.match(/^(.*?)\\s+IN\\s*(\\(.*\\))$/is);\n const membership = anyMatch ?? inMatch;\n if (membership) {\n const column = asColumnName(membership[1]);\n const values = asValueList(membership[2]);\n if (!column || !values || values.length === 0) return null;\n return {\n column,\n apply: (facts) => {\n // Two membership checks on one column mean both must hold.\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => values.includes(v))\n : values;\n }\n };\n }\n\n // ── Comparison: `x >= n`, `length(x) <= n`, `x = 'literal'` ───────\n // Longest operators first so `>=` is not read as `>`.\n for (const operator of [\">=\", \"<=\", \"<>\", \"!=\", \"=\", \">\", \"<\"] as const) {\n const parts = splitTopLevel(term, operator);\n if (parts.length !== 2) continue;\n const [rawLeft, rawRight] = parts;\n\n const lengthColumn = asLengthOfColumn(rawLeft);\n if (lengthColumn) {\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound, bound); } };\n case \">\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound + 1, bound + 1); } };\n case \"<=\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound, bound); } };\n case \"<\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound - 1, bound - 1); } };\n case \"=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = bound; f.lengthMax = bound; } };\n default: return null;\n }\n }\n\n const column = asColumnName(rawLeft);\n if (!column) continue;\n\n if (operator === \"=\") {\n const literal = asStringLiteral(rawRight);\n if (literal !== null) {\n return {\n column,\n apply: (facts) => {\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => v === literal)\n : [literal];\n }\n };\n }\n // `x = 5` pins a number. Expressed as both bounds rather than as a\n // one-value enum, which for a number would render as a dropdown.\n const pinned = asNumber(rawRight);\n if (pinned === null) return null;\n return { column, apply: (f) => { f.min = pinned; f.max = pinned; } };\n }\n\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column, apply: (f) => { f.min = Math.max(f.min ?? bound, bound); } };\n case \">\": return { column, apply: (f) => { f.moreThan = Math.max(f.moreThan ?? bound, bound); } };\n case \"<=\": return { column, apply: (f) => { f.max = Math.min(f.max ?? bound, bound); } };\n case \"<\": return { column, apply: (f) => { f.lessThan = Math.min(f.lessThan ?? bound, bound); } };\n default: return null; // `<>` / `!=` exclude rather than bound.\n }\n }\n\n return null;\n}\n\n/**\n * Reads one constraint definition into per-column facts.\n *\n * Returns an empty map for anything not understood — including a constraint\n * that is understood but spans two columns (`start_date < end_date`), which no\n * per-property validation rule can express.\n */\nexport function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts> {\n const result = new Map<string, ColumnCheckFacts>();\n\n const body = definition\n .trim()\n .replace(/\\s+NOT\\s+VALID\\s*$/i, \"\")\n .replace(/^CHECK\\s*/i, \"\")\n .trim();\n if (body === definition.trim()) return result; // Not a CHECK definition.\n\n const expression = unwrapParens(body);\n if (!expression) return result;\n\n // Only conjunctions. A disjunction (`a OR b`) does not narrow a column: each\n // branch may allow what the other forbids, so no single bound follows.\n if (splitTopLevel(expression, \" OR \").length > 1) return result;\n\n const terms = splitTopLevel(expression, \" AND \");\n const parsed: CheckTerm[] = [];\n for (const term of terms) {\n const one = parseTerm(term);\n if (!one) return result; // One unreadable conjunct voids the constraint.\n parsed.push(one);\n }\n\n // A CHECK naming two columns constrains their *relationship*; keeping the\n // half that mentions one column would assert something the database does not.\n const columns = new Set(parsed.map((t) => t.column));\n if (columns.size !== 1) return result;\n\n const facts: ColumnCheckFacts = {};\n for (const term of parsed) term.apply(facts);\n if (Object.keys(facts).length === 0) return result;\n\n result.set(parsed[0].column, facts);\n return result;\n}\n\n/** Merges every readable CHECK in the schema into per-table, per-column facts. */\nexport function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable {\n const byTable: CheckFactsByTable = new Map();\n\n for (const row of rows) {\n const parsed = parseCheckDefinition(row.definition);\n if (parsed.size === 0) continue;\n\n let table = byTable.get(row.table_name);\n if (!table) {\n table = new Map();\n byTable.set(row.table_name, table);\n }\n\n for (const [column, facts] of parsed) {\n const existing = table.get(column);\n if (!existing) {\n table.set(column, facts);\n continue;\n }\n // Separate constraints on the same column all hold at once.\n if (facts.enumValues) {\n existing.enumValues = existing.enumValues\n ? existing.enumValues.filter((v) => facts.enumValues!.includes(v))\n : facts.enumValues;\n }\n if (facts.min !== undefined) existing.min = Math.max(existing.min ?? facts.min, facts.min);\n if (facts.max !== undefined) existing.max = Math.min(existing.max ?? facts.max, facts.max);\n if (facts.moreThan !== undefined) existing.moreThan = Math.max(existing.moreThan ?? facts.moreThan, facts.moreThan);\n if (facts.lessThan !== undefined) existing.lessThan = Math.min(existing.lessThan ?? facts.lessThan, facts.lessThan);\n if (facts.lengthMin !== undefined) existing.lengthMin = Math.max(existing.lengthMin ?? facts.lengthMin, facts.lengthMin);\n if (facts.lengthMax !== undefined) existing.lengthMax = Math.min(existing.lengthMax ?? facts.lengthMax, facts.lengthMax);\n }\n }\n\n return byTable;\n}\n","import chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport pg from \"pg\";\nimport arg from \"arg\";\nimport * as dotenv from \"dotenv\";\nimport readline from \"readline\";\n\nimport {\n buildTablesMap,\n buildEnumMap,\n compositeKeySkipReason,\n generateCollectionFile,\n generateIndexContent,\n mergeIndexContent,\n safeHostFromUrl\n} from \"./introspect-db-logic\";\nimport { detectCollectionBuilder } from \"./introspect-db-project\";\nimport { countRowsUpTo, isUsableFileName, quoteRelation, readSchemaMetadata } from \"./introspect-db-queries\";\nimport { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from \"./introspect-db-structure\";\nimport { parseCheckConstraints } from \"./introspect-db-constraints\";\nimport { out, outWarn, outError } from \"../cli-output\";\n\nasync function main() {\n const args = arg(\n {\n \"--output\": String,\n \"--collections\": String,\n \"--force\": Boolean,\n \"--schema\": String,\n \"--data-inference\": Boolean,\n \"--no-data-inference\": Boolean,\n \"-o\": \"--output\",\n \"-c\": \"--collections\",\n \"-f\": \"--force\"\n },\n { permissive: true }\n );\n\n const cwd = process.cwd();\n const isBackendDir = path.basename(cwd) === \"backend\";\n const defaultOutDir = isBackendDir\n ? path.resolve(cwd, \"..\", \"config\", \"collections\")\n : path.resolve(cwd, \"config\", \"collections\");\n\n const outDir = args[\"--output\"] || args[\"--collections\"] || defaultOutDir;\n const force = args[\"--force\"] || false;\n const pgSchema = args[\"--schema\"] || \"public\";\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true });\n }\n\n // Load env\n const envPaths = [\n process.env.DOTENV_CONFIG_PATH,\n path.resolve(process.cwd(), \".env\"),\n path.resolve(process.cwd(), \"../.env\"),\n path.resolve(process.cwd(), \"../../.env\")\n ].filter(Boolean) as string[];\n\n for (const p of envPaths) {\n if (fs.existsSync(p)) {\n dotenv.config({ path: p, quiet: true });\n break;\n }\n }\n\n const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;\n if (!databaseUrl) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n\n const client = new pg.Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n } catch (err) {\n outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));\n outError(chalk.gray(\" Check your DATABASE_URL and ensure the database is reachable.\"));\n process.exit(1);\n }\n\n // Log the host portion safely — handle URLs without \"@\"\n const hostPart = safeHostFromUrl(databaseUrl);\n out(chalk.gray(`Connected to database: ${hostPart}`));\n out(chalk.gray(`Introspecting schema '${pgSchema}'...`));\n\n try {\n const metadata = await readSchemaMetadata(client, pgSchema);\n const enumMap = buildEnumMap(metadata.enumValues);\n const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);\n const fks = metadata.fks;\n\n // Only tables that could structurally be a code list are counted, and\n // each count stops at the threshold — see `countRowsUpTo`. Introspection\n // runs against a database it does not own, so \"cheap on a table of any\n // size\" is a requirement, not an optimization.\n for (const table of lookupCandidates(metadata, tablesMap)) {\n try {\n metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);\n } catch (err) {\n // A table this run cannot read is simply not classified as a code\n // list; everything else about it still generates.\n out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));\n }\n }\n\n const classifications = classifyTables(metadata, tablesMap);\n const checkFacts = parseCheckConstraints(metadata.checks);\n\n // Which builder the generated files may import — read from the manifests\n // above `outDir`, because a project without `@rebasepro/cms-types` cannot\n // resolve that import and has no `admin` block to write into either.\n const builder = detectCollectionBuilder(outDir);\n if (builder === \"annotation\") {\n outWarn(chalk.yellow(\n \"⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above \" +\n `${outDir}.`\n ));\n outWarn(chalk.gray(\n \" Generating plain type annotations, which widen the property keys to `string`: \" +\n \"`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the \" +\n \"config package to turn that checking on.\"\n ));\n }\n const joinTables = new Set(\n Array.from(classifications.values())\n .filter((c) => c.role === \"junction\")\n .map((c) => c.table)\n );\n\n const roleCount = (role: string) =>\n Array.from(classifications.values()).filter((c) => c.role === role).length;\n\n out(chalk.blue(`Found ${tablesMap.size} tables.`));\n out(chalk.gray(\n ` ${roleCount(\"entity\")} entities, ${joinTables.size} join tables (folded into relations), ` +\n `${roleCount(\"lookup\")} code lists, ${roleCount(\"owned-child\")} owned by another table (hidden from navigation).`\n ));\n\n let runDataInference = false;\n if (args[\"--no-data-inference\"]) {\n runDataInference = false;\n } else if (args[\"--data-inference\"] !== undefined) {\n runDataInference = args[\"--data-inference\"];\n } else if (!process.stdin.isTTY) {\n // No terminal to answer the question below (scaffolding scripts, CI,\n // `rebase init --introspect`) — asking would hang forever.\n out(chalk.gray(\"Skipping data inference (non-interactive run; pass --data-inference to enable).\"));\n } else {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n });\n const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow(\"? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) \"), resolve));\n runDataInference = answer.trim().toLowerCase() === \"y\";\n rl.close();\n }\n\n if (runDataInference) {\n out(chalk.gray(\"Sampling database rows for data inference...\"));\n }\n\n // Generate Collections\n const generatedFiles: string[] = [];\n const skippedFiles: string[] = [];\n\n // A table keyed on more than one column is generated with `isId` on\n // every key column — one composite key, which the planner builds the\n // same way — unless that cannot round-trip: a key column no `isId` can\n // mark, or a link pointing at the table (see `compositeKeySkipReason`).\n // Those are left out, with the reason. Generating one anyway produced a\n // file that read the table by an `id` column it does not have, and a\n // push that planned that column as its new primary key.\n const compositeKeyed = new Map(\n Array.from(tablesMap.entries())\n .filter(([tableName]) => !joinTables.has(tableName))\n .map(([tableName, meta]) => [tableName, compositeKeySkipReason(meta, fks)] as const)\n .filter((entry): entry is readonly [string, string] => entry[1] !== undefined)\n );\n for (const [tableName, reason] of compositeKeyed) {\n outWarn(chalk.yellow(\n `⚠ Skipping table ${JSON.stringify(tableName)}: it is keyed on ` +\n `(${tablesMap.get(tableName)?.pks.join(\", \")}), and ${reason}. ` +\n \"`rebase db push` leaves a table that is not a collection alone.\"\n ));\n }\n const notes: string[] = [];\n\n const tablesToProcess = Array.from(tablesMap.entries())\n .filter(([tableName]) => !joinTables.has(tableName) && !compositeKeyed.has(tableName));\n\n const BATCH_SIZE = 10;\n for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {\n const batch = tablesToProcess.slice(i, i + BATCH_SIZE);\n\n await Promise.all(batch.map(async ([tableName, meta]) => {\n // ── The name has to be usable as a filename ────────────────\n //\n // `tableName` came out of `pg_class` in a database this tool\n // did not create — adopting somebody else's database is the\n // whole point of introspection — and it is about to be joined\n // onto a path. Postgres will happily hold a table called\n // `../../../etc/cron.d/x`, and `path.join` resolves it: the\n // generator would write a TypeScript file wherever that\n // pointed, as whoever ran the command.\n //\n // Skipped rather than sanitised. A file named something other\n // than its table is a collection that does not round-trip, and\n // silently renaming it would be a worse surprise than one line\n // saying which table was left out.\n if (!isUsableFileName(tableName)) {\n outWarn(chalk.yellow(\n `⚠ Skipping table ${JSON.stringify(tableName)}: its name cannot be used as a ` +\n \"filename. Rename the table, or write this collection by hand.\"\n ));\n skippedFiles.push(tableName);\n return;\n }\n\n // ── File overwrite protection ──────────────────────────────\n const filePath = path.join(outDir, `${tableName}.ts`);\n if (fs.existsSync(filePath) && !force) {\n skippedFiles.push(tableName);\n return;\n }\n\n let sampleData: Record<string, unknown>[] | undefined = undefined;\n if (runDataInference) {\n try {\n // Quoted by `quoteRelation`, which doubles an embedded\n // quote. Wrapping a catalogue name in `\"` is not\n // escaping it — see that function.\n const { rows } = await client.query(`SELECT * FROM ${quoteRelation(pgSchema, tableName)} LIMIT 100`);\n sampleData = rows;\n } catch (err) {\n outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n\n const fileContent = generateCollectionFile(\n tableName,\n meta,\n fks,\n joinTables,\n tablesMap,\n enumMap,\n sampleData,\n { metadata, classifications, checkFacts, builder, notes, skippedTables: new Set(compositeKeyed.keys()) }\n );\n\n fs.writeFileSync(filePath, fileContent, \"utf-8\");\n generatedFiles.push(tableName);\n out(chalk.green(` ✓ ${filePath}`));\n }));\n }\n\n // Every column the planner will not reproduce exactly, before anyone\n // pushes. Each line is also at the top of its file.\n if (notes.length > 0) {\n outWarn(\"\");\n outWarn(chalk.yellow(`⚠ ${notes.length} column(s) will not round-trip exactly — \\`rebase db push\\` would change them:`));\n for (const note of [...notes].sort()) outWarn(chalk.gray(` - ${note}`));\n outWarn(chalk.gray(\" Read these before the first push. Run `rebase db push --dry-run` to see the plan.\"));\n }\n\n // Generate index.ts (sorted alphabetically for deterministic output)\n if (generatedFiles.length > 0) {\n const indexPath = path.join(outDir, \"index.ts\");\n\n if (fs.existsSync(indexPath) && !force) {\n // Merge: read existing index, add new exports that don't already exist\n const existing = fs.readFileSync(indexPath, \"utf-8\");\n const merged = mergeIndexContent(existing, generatedFiles);\n fs.writeFileSync(indexPath, merged, \"utf-8\");\n } else {\n // --force replaces collections derived from the database, but the\n // directory can also hold hand-written ones with no table in the\n // introspected schema (the auth users collection lives in\n // \"rebase\"). The backend discovers the whole directory, so an\n // index listing only introspected tables would silently drop them\n // from the admin UI while the API still served them.\n const siblings = fs.readdirSync(outDir)\n .filter(f => f.endsWith(\".ts\") && f !== \"index.ts\")\n .map(f => f.replace(/\\.ts$/, \"\"));\n const allFiles = [...new Set([...generatedFiles, ...siblings])];\n const indexContent = generateIndexContent(allFiles);\n fs.writeFileSync(indexPath, indexContent, \"utf-8\");\n }\n out(chalk.green(` ✓ ${indexPath}`));\n }\n\n out(\"\");\n if (skippedFiles.length > 0) {\n out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(\", \")}`));\n out(chalk.gray(\" Use --force to overwrite existing files.\"));\n out(\"\");\n }\n out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));\n out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));\n out(\"\");\n\n } catch (e) {\n outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));\n process.exit(1);\n } finally {\n await client.end();\n }\n}\n\nmain().catch((err) => {\n outError(String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -102,7 +102,7 @@ export interface DiffOptions {
102
102
  constraints?: ConstraintPolicy;
103
103
  }
104
104
  export interface EnsureAction {
105
- kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index" | "comment-column" | "add-enum-value" | "set-not-null" | "drop-not-null" | "set-default" | "create-trigger";
105
+ kind: "create-enum" | "create-table" | "enable-rls" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index" | "comment-column" | "add-enum-value" | "set-not-null" | "drop-not-null" | "set-default" | "create-trigger";
106
106
  /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
107
107
  target: string;
108
108
  sql: string;
@@ -12,7 +12,7 @@
12
12
  * imported would not be comparable with itself.
13
13
  *
14
14
  * It **throws** rather than guessing. A configuration no renderer can honour —
15
- * an empty enum, two `isId` properties, `isId: "cuid"`, an unknown
15
+ * an empty enum, `isId: "cuid"`, a foreign key into a composite key, an unknown
16
16
  * `columnType`, a relation to a collection that is not in the bundle, a
17
17
  * `search` block on a collection Postgres does not store — used to fail
18
18
  * differently in each of the three emitters, or not at all, and the failure
@@ -65,8 +65,17 @@ export declare const defaultBelongsToOnDelete: (required: boolean | undefined) =
65
65
  * One exception: a `serial` key owns a sequence, and a column pointing at it is
66
66
  * the plain integer of the same width — `SERIAL` there would give every
67
67
  * referencing column a sequence of its own.
68
+ *
69
+ * @param via the link asking, named if the collection's key is composite and so
70
+ * cannot be pointed at — see `getPrimaryKeyProp`.
71
+ */
72
+ export declare const primaryKeyPgType: (collection: CollectionConfig, via: string) => PgType;
73
+ /**
74
+ * The DEFAULT of the implicit `id TEXT PRIMARY KEY` a collection that declares
75
+ * no key gets: a uuid, as text. The same generator `isId: "uuid"` uses, cast to
76
+ * the column the key has always been.
68
77
  */
69
- export declare const primaryKeyPgType: (collection: CollectionConfig) => PgType;
78
+ export declare const IMPLICIT_ID_DEFAULT = "gen_random_uuid()::text";
70
79
  /**
71
80
  * The Postgres type a property's column has.
72
81
  *
@@ -79,12 +88,11 @@ export declare const columnPgType: (propName: string, prop: Property, collection
79
88
  /**
80
89
  * One security rule, compiled to the clauses a policy is made of.
81
90
  *
82
- * The desugaring (`access` / `ownerField` / `roles` / structured condition /
83
- * raw SQL → `PolicyExpression`) and the SQL compilation are `@rebasepro/common`'s,
84
- * which is what the client-side evaluator uses too — so the UI, the DDL and the
85
- * database agree about who can read a row. What lives here is only the shape:
86
- * which operations a rule expands to, which clauses each operation takes, and
87
- * the deny-all fallback for a clause that compiled to nothing.
91
+ * The compilation is `compileRulePolicies` in `@rebasepro/common`, which the
92
+ * Studio's RLS editor also compares the live database against — so the UI, the
93
+ * DDL and the database agree about who can read a row. What this adds is only
94
+ * the planner's bookkeeping: which rule a policy came from, and whether Rebase
95
+ * injected it.
88
96
  */
89
97
  export declare const compileSecurityRule: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection, injected: boolean, ruleKey?: string) => PolicyPlan[];
90
98
  export declare function planSchema(allCollections: CollectionConfig[], options?: PlanOptions): SchemaPlan;
@@ -25,6 +25,12 @@ export declare const renderPgType: (type: PgType) => string;
25
25
  * DEFAULT, which is why it is rendered here and not in the DEFAULT slot.
26
26
  */
27
27
  export declare const renderColumnDefinition: (column: ColumnPlan) => string;
28
+ /**
29
+ * `PRIMARY KEY ("a", "b")`, the clause a composite key takes inside a
30
+ * `CREATE TABLE`. Shared by `schema.sql` and boot-ensure's `CREATE TABLE`, so
31
+ * the two cannot list the columns differently.
32
+ */
33
+ export declare const renderPrimaryKeyConstraint: (table: Pick<TablePlan, "primaryKey">) => string;
28
34
  /**
29
35
  * One policy as its `DROP` / `CREATE` pair — each a complete statement.
30
36
  *