@rebasepro/server-postgres 0.17.3 → 0.18.1

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 (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,56 +0,0 @@
1
- /**
2
- * The PostgreSQL type → Rebase property type mapping.
3
- *
4
- * Split out of `introspect-db-logic` so that the structural analysis can use it
5
- * without importing the generator, which imports the analysis. Re-exported from
6
- * `introspect-db-logic` so existing callers keep their import path.
7
- */
8
-
9
- /**
10
- * Map a PostgreSQL data type to a Rebase property type.
11
- */
12
- export function mapPgType(dataType: string): string {
13
- const dt = dataType.toLowerCase();
14
-
15
- // Interval MUST be checked before numeric ("interval" contains "int")
16
- if (dt === "interval") return "string";
17
-
18
- // Array types MUST be checked before numeric ("_int4" contains "int")
19
- if (dt === "array" || dt.startsWith("_")) return "array";
20
-
21
- // Numeric types
22
- if (
23
- dt.includes("int") || // integer, smallint, bigint
24
- dt.includes("numeric") ||
25
- dt.includes("decimal") ||
26
- dt.includes("serial") || // serial, bigserial
27
- dt === "real" ||
28
- dt === "float4" ||
29
- dt === "float8" ||
30
- dt === "double precision" ||
31
- dt === "money"
32
- ) {
33
- return "number";
34
- }
35
-
36
- // Boolean
37
- if (dt.includes("bool")) return "boolean";
38
-
39
- // Date / Time
40
- if (dt.includes("time") || dt.includes("date")) return "date";
41
-
42
- // JSON
43
- if (dt === "json" || dt === "jsonb") return "map";
44
-
45
- // Binary
46
- if (dt === "bytea") return "binary";
47
-
48
- // Network types
49
- if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
50
-
51
- // UUID
52
- if (dt === "uuid") return "string";
53
-
54
- // Text/varchar/char — default to string
55
- return "string";
56
- }
@@ -1,257 +0,0 @@
1
- import chalk from "chalk";
2
- import fs from "fs";
3
- import path from "path";
4
- import pg from "pg";
5
- import arg from "arg";
6
- import * as dotenv from "dotenv";
7
- import readline from "readline";
8
-
9
- import {
10
- buildTablesMap,
11
- buildEnumMap,
12
- generateCollectionFile,
13
- generateIndexContent,
14
- mergeIndexContent,
15
- safeHostFromUrl
16
- } from "./introspect-db-logic";
17
- import { detectCollectionBuilder } from "./introspect-db-project";
18
- import { countRowsUpTo, readSchemaMetadata } from "./introspect-db-queries";
19
- import { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from "./introspect-db-structure";
20
- import { parseCheckConstraints } from "./introspect-db-constraints";
21
- import { out, outWarn, outError } from "../cli-output";
22
-
23
- async function main() {
24
- const args = arg(
25
- {
26
- "--output": String,
27
- "--collections": String,
28
- "--force": Boolean,
29
- "--schema": String,
30
- "--data-inference": Boolean,
31
- "--no-data-inference": Boolean,
32
- "-o": "--output",
33
- "-c": "--collections",
34
- "-f": "--force"
35
- },
36
- { permissive: true }
37
- );
38
-
39
- const cwd = process.cwd();
40
- const isBackendDir = path.basename(cwd) === "backend";
41
- const defaultOutDir = isBackendDir
42
- ? path.resolve(cwd, "..", "config", "collections")
43
- : path.resolve(cwd, "config", "collections");
44
-
45
- const outDir = args["--output"] || args["--collections"] || defaultOutDir;
46
- const force = args["--force"] || false;
47
- const pgSchema = args["--schema"] || "public";
48
-
49
- if (!fs.existsSync(outDir)) {
50
- fs.mkdirSync(outDir, { recursive: true });
51
- }
52
-
53
- // Load env
54
- const envPaths = [
55
- process.env.DOTENV_CONFIG_PATH,
56
- path.resolve(process.cwd(), ".env"),
57
- path.resolve(process.cwd(), "../.env"),
58
- path.resolve(process.cwd(), "../../.env")
59
- ].filter(Boolean) as string[];
60
-
61
- for (const p of envPaths) {
62
- if (fs.existsSync(p)) {
63
- dotenv.config({ path: p, quiet: true });
64
- break;
65
- }
66
- }
67
-
68
- const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
69
- if (!databaseUrl) {
70
- outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
71
- process.exit(1);
72
- }
73
-
74
- const client = new pg.Client({ connectionString: databaseUrl });
75
-
76
- try {
77
- await client.connect();
78
- } catch (err) {
79
- outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
80
- outError(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
81
- process.exit(1);
82
- }
83
-
84
- // Log the host portion safely — handle URLs without "@"
85
- const hostPart = safeHostFromUrl(databaseUrl);
86
- out(chalk.gray(`Connected to database: ${hostPart}`));
87
- out(chalk.gray(`Introspecting schema '${pgSchema}'...`));
88
-
89
- try {
90
- const metadata = await readSchemaMetadata(client, pgSchema);
91
- const enumMap = buildEnumMap(metadata.enumValues);
92
- const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);
93
- const fks = metadata.fks;
94
-
95
- // Only tables that could structurally be a code list are counted, and
96
- // each count stops at the threshold — see `countRowsUpTo`. Introspection
97
- // runs against a database it does not own, so "cheap on a table of any
98
- // size" is a requirement, not an optimization.
99
- for (const table of lookupCandidates(metadata, tablesMap)) {
100
- try {
101
- metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);
102
- } catch (err) {
103
- // A table this run cannot read is simply not classified as a code
104
- // list; everything else about it still generates.
105
- out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));
106
- }
107
- }
108
-
109
- const classifications = classifyTables(metadata, tablesMap);
110
- const checkFacts = parseCheckConstraints(metadata.checks);
111
-
112
- // Which builder the generated files may import — read from the manifests
113
- // above `outDir`, because a project without `@rebasepro/cms-types` cannot
114
- // resolve that import and has no `admin` block to write into either.
115
- const builder = detectCollectionBuilder(outDir);
116
- if (builder === "annotation") {
117
- outWarn(chalk.yellow(
118
- "⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above " +
119
- `${outDir}.`
120
- ));
121
- outWarn(chalk.gray(
122
- " Generating plain type annotations, which widen the property keys to `string`: " +
123
- "`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the " +
124
- "config package to turn that checking on."
125
- ));
126
- }
127
- const joinTables = new Set(
128
- Array.from(classifications.values())
129
- .filter((c) => c.role === "junction")
130
- .map((c) => c.table)
131
- );
132
-
133
- const roleCount = (role: string) =>
134
- Array.from(classifications.values()).filter((c) => c.role === role).length;
135
-
136
- out(chalk.blue(`Found ${tablesMap.size} tables.`));
137
- out(chalk.gray(
138
- ` ${roleCount("entity")} entities, ${joinTables.size} join tables (folded into relations), ` +
139
- `${roleCount("lookup")} code lists, ${roleCount("owned-child")} owned by another table (hidden from navigation).`
140
- ));
141
-
142
- let runDataInference = false;
143
- if (args["--no-data-inference"]) {
144
- runDataInference = false;
145
- } else if (args["--data-inference"] !== undefined) {
146
- runDataInference = args["--data-inference"];
147
- } else if (!process.stdin.isTTY) {
148
- // No terminal to answer the question below (scaffolding scripts, CI,
149
- // `rebase init --introspect`) — asking would hang forever.
150
- out(chalk.gray("Skipping data inference (non-interactive run; pass --data-inference to enable)."));
151
- } else {
152
- const rl = readline.createInterface({
153
- input: process.stdin,
154
- output: process.stdout
155
- });
156
- 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));
157
- runDataInference = answer.trim().toLowerCase() === "y";
158
- rl.close();
159
- }
160
-
161
- if (runDataInference) {
162
- out(chalk.gray("Sampling database rows for data inference..."));
163
- }
164
-
165
- // Generate Collections
166
- const generatedFiles: string[] = [];
167
- const skippedFiles: string[] = [];
168
-
169
- const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
170
-
171
- const BATCH_SIZE = 10;
172
- for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
173
- const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
174
-
175
- await Promise.all(batch.map(async ([tableName, meta]) => {
176
- // ── File overwrite protection ──────────────────────────────
177
- const filePath = path.join(outDir, `${tableName}.ts`);
178
- if (fs.existsSync(filePath) && !force) {
179
- skippedFiles.push(tableName);
180
- return;
181
- }
182
-
183
- let sampleData: Record<string, unknown>[] | undefined = undefined;
184
- if (runDataInference) {
185
- try {
186
- const { rows } = await client.query(`SELECT * FROM "${pgSchema}"."${tableName}" LIMIT 100`);
187
- sampleData = rows;
188
- } catch (err) {
189
- outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
190
- }
191
- }
192
-
193
- const fileContent = generateCollectionFile(
194
- tableName,
195
- meta,
196
- fks,
197
- joinTables,
198
- tablesMap,
199
- enumMap,
200
- sampleData,
201
- { metadata, classifications, checkFacts, builder }
202
- );
203
-
204
- fs.writeFileSync(filePath, fileContent, "utf-8");
205
- generatedFiles.push(tableName);
206
- out(chalk.green(` ✓ ${filePath}`));
207
- }));
208
- }
209
-
210
- // Generate index.ts (sorted alphabetically for deterministic output)
211
- if (generatedFiles.length > 0) {
212
- const indexPath = path.join(outDir, "index.ts");
213
-
214
- if (fs.existsSync(indexPath) && !force) {
215
- // Merge: read existing index, add new exports that don't already exist
216
- const existing = fs.readFileSync(indexPath, "utf-8");
217
- const merged = mergeIndexContent(existing, generatedFiles);
218
- fs.writeFileSync(indexPath, merged, "utf-8");
219
- } else {
220
- // --force replaces collections derived from the database, but the
221
- // directory can also hold hand-written ones with no table in the
222
- // introspected schema (the auth users collection lives in
223
- // "rebase"). The backend discovers the whole directory, so an
224
- // index listing only introspected tables would silently drop them
225
- // from the admin UI while the API still served them.
226
- const siblings = fs.readdirSync(outDir)
227
- .filter(f => f.endsWith(".ts") && f !== "index.ts")
228
- .map(f => f.replace(/\.ts$/, ""));
229
- const allFiles = [...new Set([...generatedFiles, ...siblings])];
230
- const indexContent = generateIndexContent(allFiles);
231
- fs.writeFileSync(indexPath, indexContent, "utf-8");
232
- }
233
- out(chalk.green(` ✓ ${indexPath}`));
234
- }
235
-
236
- out("");
237
- if (skippedFiles.length > 0) {
238
- out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
239
- out(chalk.gray(" Use --force to overwrite existing files."));
240
- out("");
241
- }
242
- out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
243
- out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
244
- out("");
245
-
246
- } catch (e) {
247
- outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
248
- process.exit(1);
249
- } finally {
250
- await client.end();
251
- }
252
- }
253
-
254
- main().catch((err) => {
255
- outError(String(err));
256
- process.exit(1);
257
- });
@@ -1,260 +0,0 @@
1
- import { describe, expect, it } from "@jest/globals";
2
- import { resolveCollectionRelations } from "@rebasepro/common";
3
- import type { CollectionConfig } from "@rebasepro/types";
4
-
5
- import { buildCollectionsFromSchema, introspectSchema, readRlsStatus, IntrospectedSchema, Queryable } from "./introspect-runtime";
6
- import { buildTablesMap, identifyJoinTables, TableColumn, ForeignKeyRow, PrimaryKeyRow } from "./introspect-db-logic";
7
-
8
- function column(overrides: Partial<TableColumn> & { table_name: string; column_name: string }): TableColumn {
9
- return {
10
- data_type: "text",
11
- udt_name: "text",
12
- is_nullable: "YES",
13
- column_default: null,
14
- atttypmod: null,
15
- ...overrides
16
- };
17
- }
18
-
19
- function schemaOf(
20
- columns: TableColumn[],
21
- pks: PrimaryKeyRow[],
22
- fks: ForeignKeyRow[] = [],
23
- enums: Map<string, string[]> = new Map()
24
- ): IntrospectedSchema {
25
- const tableNames = [...new Set(columns.map((c) => c.table_name))].map((table_name) => ({ table_name }));
26
- const tablesMap = buildTablesMap(tableNames, columns, pks, fks);
27
- return { tablesMap, enumMap: enums, joinTables: identifyJoinTables(tablesMap) };
28
- }
29
-
30
- describe("buildCollectionsFromSchema", () => {
31
- it("derives a collection per table with humanized names and the pg schema", () => {
32
- const schema = schemaOf(
33
- [
34
- column({ table_name: "blog_posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
35
- column({ table_name: "blog_posts", column_name: "title", is_nullable: "NO" })
36
- ],
37
- [{ table_name: "blog_posts", column_name: "id" }]
38
- );
39
-
40
- const [collection] = buildCollectionsFromSchema(schema, "public");
41
-
42
- expect(collection.slug).toBe("blog_posts");
43
- expect(collection.table).toBe("blog_posts");
44
- expect((collection as { schema?: string }).schema).toBe("public");
45
- expect(collection.name).toBe("Blog Posts");
46
- expect(collection.singularName).toBe("Blog Post");
47
- });
48
-
49
- it("maps a uuid primary key to isId uuid and a serial one to increment", () => {
50
- const schema = schemaOf(
51
- [
52
- column({ table_name: "users", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
53
- column({ table_name: "orders", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" })
54
- ],
55
- [
56
- { table_name: "users", column_name: "id" },
57
- { table_name: "orders", column_name: "id" }
58
- ]
59
- );
60
-
61
- const collections = buildCollectionsFromSchema(schema, "public");
62
- const users = collections.find((c) => c.slug === "users")!;
63
- const orders = collections.find((c) => c.slug === "orders")!;
64
-
65
- expect((users.properties as any).id.isId).toBe("uuid");
66
- expect((orders.properties as any).id.isId).toBe("increment");
67
- });
68
-
69
- it("marks NOT NULL columns without a default as required, and leaves others optional", () => {
70
- const schema = schemaOf(
71
- [
72
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
73
- column({ table_name: "posts", column_name: "title", is_nullable: "NO" }),
74
- column({ table_name: "posts", column_name: "subtitle", is_nullable: "YES" }),
75
- column({ table_name: "posts", column_name: "status", is_nullable: "NO", column_default: "'draft'" })
76
- ],
77
- [{ table_name: "posts", column_name: "id" }]
78
- );
79
-
80
- const props = buildCollectionsFromSchema(schema, "public")[0].properties as any;
81
-
82
- expect(props.title.validation).toEqual({ required: true });
83
- expect(props.subtitle.validation).toBeUndefined();
84
- // A default means the database fills it in, so the API must not demand it.
85
- expect(props.status.validation).toBeUndefined();
86
- });
87
-
88
- it("expands a pg enum column into enum entries", () => {
89
- const schema = schemaOf(
90
- [
91
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
92
- column({ table_name: "posts", column_name: "status", data_type: "USER-DEFINED", udt_name: "post_status" })
93
- ],
94
- [{ table_name: "posts", column_name: "id" }],
95
- [],
96
- new Map([["post_status", ["draft", "in_review"]]])
97
- );
98
-
99
- const props = buildCollectionsFromSchema(schema, "public")[0].properties as any;
100
-
101
- expect(props.status.type).toBe("string");
102
- expect(props.status.enum).toEqual([
103
- { id: "draft", label: "Draft" },
104
- { id: "in_review", label: "In Review" }
105
- ]);
106
- });
107
-
108
- it("turns a foreign key into an owning relation and drops the raw fk column", () => {
109
- const schema = schemaOf(
110
- [
111
- column({ table_name: "authors", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
112
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
113
- column({ table_name: "posts", column_name: "author_id", data_type: "uuid", udt_name: "uuid" })
114
- ],
115
- [
116
- { table_name: "authors", column_name: "id" },
117
- { table_name: "posts", column_name: "id" }
118
- ],
119
- [{ table_name: "posts", column_name: "author_id", foreign_table_name: "authors", foreign_column_name: "id" }]
120
- );
121
-
122
- const posts = buildCollectionsFromSchema(schema, "public").find((c) => c.slug === "posts")!;
123
- const props = posts.properties as any;
124
-
125
- expect(props.author_id).toBeUndefined();
126
- expect(props.author.name).toBe("Author");
127
- expect(props.author.type).toBe("relation");
128
- // The descriptor is nested under `relation`, carries `kind`, and its
129
- // `target` is a thunk — see the resolvability test below for why none of
130
- // those three are stylistic.
131
- expect(props.author.relation).toMatchObject({ kind: "belongsTo", localKey: "author_id" });
132
- expect(typeof props.author.relation.target).toBe("function");
133
- expect(props.author.relation.target().slug).toBe("authors");
134
- });
135
-
136
- /**
137
- * The shape has to be one the resolver actually reads.
138
- *
139
- * This path emitted `cardinality` / `direction` / a bare-string `target`
140
- * flat on the property — the shape `Relation` stopped accepting, and the
141
- * same drift `introspect-emits-valid-relations.test.ts` caught in the
142
- * generated-file path and this one was missed by. Nothing threw:
143
- * `resolveCollectionRelations` reads `property.relation`, found none, and
144
- * reported a collection with no relations at all.
145
- *
146
- * The visible cost was writes. `assertKnownWriteFields` learns an owning
147
- * relation's FK column from the *resolved* relation's `localKey`, and the FK
148
- * column is deliberately absent from `properties` ("surfaces as a
149
- * relation"), so with nothing resolving, `POST /api/data/orders` with
150
- * `product_id` came back 400 `has no field 'product_id'` — the column was
151
- * simultaneously the only way to set the relation and not a known field.
152
- *
153
- * Asserting the literal object is what let it drift, so this asserts
154
- * through the resolver instead: a future reshape has to keep it resolvable,
155
- * not merely keep the keys someone once wrote down.
156
- */
157
- it("emits a relation the resolver can read, so the fk column is writable", () => {
158
- const schema = schemaOf(
159
- [
160
- column({ table_name: "products", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" }),
161
- column({ table_name: "orders", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" }),
162
- column({ table_name: "orders", column_name: "product_id", data_type: "integer", udt_name: "int4" })
163
- ],
164
- [
165
- { table_name: "products", column_name: "id" },
166
- { table_name: "orders", column_name: "id" }
167
- ],
168
- [{ table_name: "orders", column_name: "product_id", foreign_table_name: "products", foreign_column_name: "id" }]
169
- );
170
-
171
- const orders = buildCollectionsFromSchema(schema, "public").find((c) => c.slug === "orders")!;
172
- const resolved = resolveCollectionRelations(orders as unknown as CollectionConfig);
173
-
174
- expect(Object.keys(resolved)).toEqual(["product"]);
175
- expect(resolved.product.kind).toBe("belongsTo");
176
- // `localKey` is the field `assertKnownWriteFields` adds to the known set,
177
- // which is what makes `product_id` writable. The write itself is covered
178
- // end-to-end by `scripts/smoke-baas.ts` against a real database.
179
- expect((resolved.product as { localKey: string }).localKey).toBe("product_id");
180
- });
181
-
182
- it("skips join tables — they are an edge between collections, not a collection", () => {
183
- const schema = schemaOf(
184
- [
185
- column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
186
- column({ table_name: "tags", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
187
- column({ table_name: "posts_tags", column_name: "post_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
188
- column({ table_name: "posts_tags", column_name: "tag_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })
189
- ],
190
- [
191
- { table_name: "posts", column_name: "id" },
192
- { table_name: "tags", column_name: "id" },
193
- { table_name: "posts_tags", column_name: "post_id" },
194
- { table_name: "posts_tags", column_name: "tag_id" }
195
- ],
196
- [
197
- { table_name: "posts_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
198
- { table_name: "posts_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" }
199
- ]
200
- );
201
-
202
- const slugs = buildCollectionsFromSchema(schema, "public").map((c) => c.slug);
203
-
204
- expect(slugs).toEqual(expect.arrayContaining(["posts", "tags"]));
205
- expect(slugs).not.toContain("posts_tags");
206
- });
207
- });
208
-
209
- describe("introspectSchema", () => {
210
- it("scopes every query to the requested schema", async () => {
211
- const calls: unknown[][] = [];
212
- const client: Queryable = {
213
- query: async (_text: string, values?: unknown[]) => {
214
- calls.push(values ?? []);
215
- return { rows: [] as any[] };
216
- }
217
- };
218
-
219
- await introspectSchema(client, "analytics");
220
-
221
- expect(calls.length).toBeGreaterThan(0);
222
- for (const values of calls) {
223
- expect(values).toEqual(["analytics"]);
224
- }
225
- });
226
- });
227
-
228
- describe("readRlsStatus", () => {
229
- it("reports RLS state and policy count per table", async () => {
230
- const client: Queryable = {
231
- query: async () => ({
232
- rows: [
233
- { table: "posts", rls_enabled: true, policy_count: "2" },
234
- { table: "secrets", rls_enabled: false, policy_count: "0" },
235
- { table: "locked", rls_enabled: true, policy_count: 0 }
236
- ] as never[]
237
- })
238
- };
239
-
240
- const status = await readRlsStatus(client, "public");
241
-
242
- // Unprotected: no authorization model, so baas must not serve it.
243
- expect(status.get("secrets")).toEqual({ table: "secrets", rlsEnabled: false, policyCount: 0 });
244
- // Protected and policied.
245
- expect(status.get("posts")).toEqual({ table: "posts", rlsEnabled: true, policyCount: 2 });
246
- // RLS on, no policies — legal, and returns nothing at all.
247
- expect(status.get("locked")).toEqual({ table: "locked", rlsEnabled: true, policyCount: 0 });
248
- });
249
-
250
- it("scopes the lookup to the requested schema", async () => {
251
- const calls: unknown[][] = [];
252
- const client: Queryable = {
253
- query: async (_t: string, v?: unknown[]) => { calls.push(v ?? []); return { rows: [] as never[] }; }
254
- };
255
-
256
- await readRlsStatus(client, "analytics");
257
-
258
- expect(calls[0]).toEqual(["analytics"]);
259
- });
260
- });