@rebasepro/server-postgres 0.17.3 → 0.18.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 (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. package/src/websocket.ts +0 -855
@@ -1,569 +0,0 @@
1
- import { isManyToMany } from "@rebasepro/types";
2
- import path from "path";
3
- import fs from "fs";
4
- import { execSync } from "child_process";
5
- import { createRequire } from "module";
6
- import readline from "readline";
7
- import { pathToFileURL } from "url";
8
- import chalk from "chalk";
9
- import { isRebaseIndexName } from "./schema/collection-index";
10
- import { out, outWarn } from "./cli-output";
11
- import type { CollectionConfig, ResolvedRelation } from "@rebasepro/types";
12
- import { moduleDir as __helpersDirname } from "./module-dir";
13
-
14
-
15
-
16
- /**
17
- * Why is a dependency's binary missing — never installed, or installed with its
18
- * build script blocked?
19
- *
20
- * These need opposite advice, and getting it wrong is not a cosmetic miss. pnpm
21
- * 10+ refuses to run a dependency's lifecycle scripts unless it is allowlisted
22
- * (`pnpm.onlyBuiltDependencies`, or `allowBuilds` in `pnpm-workspace.yaml`).
23
- * `@ariga/atlas` downloads its platform binary in `preinstall`, so a blocked
24
- * script leaves a state that looks like a successful install: the package is on
25
- * disk with its `install.js` and `package.json`, `node_modules/.bin` is empty,
26
- * the install exits 0, and the only signal is `ERR_PNPM_IGNORED_BUILDS` several
27
- * screens up.
28
- *
29
- * Telling somebody in that state to install the package again sends them round
30
- * the same loop forever — the add succeeds, the script is blocked again,
31
- * nothing changes. Verified by doing it: `pnpm add @ariga/atlas` into a bare
32
- * project yields exactly this, three "Failed to create bin … ENOENT" warnings
33
- * and no binary.
34
- *
35
- * Resolution is attempted from the user's project first and this package
36
- * second, matching the order {@link resolveLocalBin} searches — the driver may
37
- * be installed a level up from where the command runs.
38
- */
39
- export function diagnoseMissingBin(packageName: string): "not-installed" | "build-script-blocked" {
40
- const bases = [
41
- pathToFileURL(path.join(process.cwd(), "package.json")),
42
- pathToFileURL(path.join(__helpersDirname, "package.json"))
43
- ];
44
-
45
- for (const base of bases) {
46
- try {
47
- // `package.json` rather than the package root: a package with an
48
- // `exports` map that omits `.` is unresolvable by name even when it
49
- // is perfectly installed, which would misreport it as absent.
50
- createRequire(base).resolve(`${packageName}/package.json`);
51
- return "build-script-blocked";
52
- } catch {
53
- // Try the next base.
54
- }
55
- }
56
- return "not-installed";
57
- }
58
-
59
- export function resolveLocalBin(binName: string): string | null {
60
- // Try to find node_modules/.bin upwards from __helpersDirname first (package-relative)
61
- let dir = __helpersDirname;
62
- while (true) {
63
- const candidate = path.join(dir, "node_modules", ".bin", binName);
64
- if (fs.existsSync(candidate)) return candidate;
65
- const parent = path.dirname(dir);
66
- if (parent === dir) break;
67
- dir = parent;
68
- }
69
-
70
- let cwd = process.cwd();
71
- // Try to find node_modules/.bin upwards from process.cwd()
72
- while (true) {
73
- const candidate = path.join(cwd, "node_modules", ".bin", binName);
74
- if (fs.existsSync(candidate)) return candidate;
75
- const parent = path.dirname(cwd);
76
- if (parent === cwd) break;
77
- cwd = parent;
78
- }
79
- // Fall back to globally installed binary via which/where
80
- try {
81
- const cmd = process.platform === "win32" ? `where ${binName}` : `which ${binName}`;
82
- const globalPath = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0].trim();
83
- if (globalPath && fs.existsSync(globalPath)) return globalPath;
84
- } catch {
85
- // not found globally
86
- }
87
- return null;
88
- }
89
-
90
- export async function getTableIncludesFromCollections(allCollections: CollectionConfig[]): Promise<string[]> {
91
- const { getTableName, resolveCollectionRelations, relationalCollections } = await import("@rebasepro/common");
92
- const { isPostgresCollectionConfig } = await import("@rebasepro/types");
93
-
94
- // The include list is the inverse of {@link getTableExcludes}: a name on it
95
- // is a name Atlas is allowed to drop. A Firestore collection called
96
- // `exercises` must not put `public.exercises` here — a real, unrelated table
97
- // of that name would lose its protection and be dropped by the next
98
- // auto-approved `db push`.
99
- const collections = relationalCollections(allCollections);
100
- const includes: string[] = [];
101
- for (const col of collections) {
102
- const tableName = getTableName(col);
103
- const schema = isPostgresCollectionConfig(col) && col.schema ? col.schema : "public";
104
- if (tableName) {
105
- includes.push(`${schema}.${tableName}`);
106
- }
107
-
108
- const resolvedRelations = resolveCollectionRelations(col);
109
- for (const relation of Object.values(resolvedRelations) as ResolvedRelation[]) {
110
- if (isManyToMany(relation)) {
111
- const junctionTableName = relation.through.table;
112
- const targetCollection = relation.target();
113
- const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
114
- includes.push(`${targetSchema}.${junctionTableName}`);
115
- }
116
- }
117
- }
118
-
119
- return Array.from(new Set(includes));
120
- }
121
-
122
- export async function getTableIncludes(collectionsPath: string): Promise<string[]> {
123
- return getTableIncludesFromCollections(await loadCollectionsForCli(collectionsPath));
124
- }
125
-
126
- /**
127
- * Load a project's collections the way the Atlas-facing commands need them.
128
- *
129
- * Deliberately forgiving — a file that fails to import is skipped rather than
130
- * fatal — because the callers use this to *narrow* what Atlas may touch, and a
131
- * hard failure here would block a push over an unrelated broken file. Callers
132
- * that cannot tolerate a partial answer (the table excludes, which fail closed)
133
- * check the result themselves.
134
- */
135
- export async function loadCollectionsForCli(collectionsPath: string): Promise<CollectionConfig[]> {
136
- // Note: a relative --collections path resolves against the *current working
137
- // directory*, not the backend package. When invoked via the generated npm
138
- // script (cwd = backend/), the script uses "../config/collections" to
139
- // compensate — so running the CLI directly from the repo root with a
140
- // different relative path can silently point at the wrong place.
141
- const resolvedPath = path.resolve(collectionsPath);
142
- const collections: CollectionConfig[] = [];
143
- if (!fs.existsSync(resolvedPath)) {
144
- outWarn(chalk.yellow(
145
- ` ⚠ Collections path not found: "${collectionsPath}"\n` +
146
- ` Resolved to: ${resolvedPath}\n` +
147
- ` (relative to cwd: ${process.cwd()})\n` +
148
- ` Pass an absolute path, or a path relative to where you run the command.`
149
- ));
150
- }
151
- if (fs.existsSync(resolvedPath)) {
152
- const stats = fs.statSync(resolvedPath);
153
- if (stats.isDirectory()) {
154
- const files = fs.readdirSync(resolvedPath);
155
- for (const file of files) {
156
- if ((file.endsWith(".ts") || file.endsWith(".js")) &&
157
- !file.includes(".test.") &&
158
- !file.endsWith(".d.ts") &&
159
- file !== "index.ts" && file !== "index.js") {
160
- try {
161
- const filePath = path.join(resolvedPath, file);
162
- const fileUrl = pathToFileURL(filePath).href;
163
- const module = await import(fileUrl);
164
- if (module && module.default) {
165
- collections.push(module.default);
166
- }
167
- } catch {
168
- // ignore
169
- }
170
- }
171
- }
172
- }
173
- }
174
-
175
- return collections;
176
- }
177
-
178
- export function getDevDatabaseUrl(databaseUrl: string): string {
179
- try {
180
- const parsed = new URL(databaseUrl);
181
- const dbName = parsed.pathname.slice(1);
182
- parsed.pathname = `/${dbName}_dev_diff`;
183
- return parsed.toString();
184
- } catch {
185
- return databaseUrl + "_dev_diff";
186
- }
187
- }
188
-
189
- export async function ensureDevDatabaseExists(databaseUrl: string, devDatabaseUrl: string) {
190
- try {
191
- const { Client } = await import("pg");
192
- const parsed = new URL(databaseUrl);
193
- const devDbName = new URL(devDatabaseUrl).pathname.slice(1);
194
-
195
- parsed.pathname = "/postgres";
196
- const client = new Client({ connectionString: parsed.toString() });
197
- await client.connect();
198
- try {
199
- const res = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [devDbName]);
200
- if (res.rowCount === 0) {
201
- await client.query(`CREATE DATABASE "${devDbName}"`);
202
- out(chalk.gray(` ✓ Created validation database "${devDbName}"`));
203
- }
204
- } finally {
205
- await client.end();
206
- }
207
- } catch {
208
- // Ignore, let Atlas handle connection failures
209
- }
210
- }
211
-
212
- /**
213
- * The generated SQL for the project's `search` blocks, if it has any.
214
- *
215
- * @param drizzleDir directory holding the generated SQL. Defaults to `drizzle`
216
- * under the working directory.
217
- */
218
- export function readSearchDdl(drizzleDir: string = path.resolve(process.cwd(), "drizzle")): string {
219
- const searchFile = path.join(drizzleDir, "search.sql");
220
- if (!fs.existsSync(searchFile)) return "";
221
- return fs.readFileSync(searchFile, "utf-8").trim();
222
- }
223
-
224
- /**
225
- * Bring the search column, its index and their helpers up to date.
226
- *
227
- * Runs *after* Atlas, not before: the statements are `ALTER TABLE ... ADD
228
- * COLUMN`, so the table has to exist. Atlas is told to ignore these objects
229
- * entirely (`getSearchExcludes`) — it cannot manage them, and left to itself it
230
- * would plan a `DROP COLUMN` for every one, since they are absent from the
231
- * desired state it was given.
232
- *
233
- * A no-op when no collection declared `search`. A failure is *not* swallowed:
234
- * silently pushing a schema whose search column never appeared is how a
235
- * collection ends up with search configured, no error anywhere, and no results.
236
- */
237
- export async function applySearchDdl(
238
- databaseUrl: string,
239
- drizzleDir: string = path.resolve(process.cwd(), "drizzle")
240
- ): Promise<void> {
241
- const sql = readSearchDdl(drizzleDir);
242
- if (!sql) return;
243
-
244
- const { Client } = await import("pg");
245
- const client = new Client({ connectionString: databaseUrl });
246
- await client.connect();
247
- try {
248
- await client.query(sql);
249
- } finally {
250
- await client.end();
251
- }
252
- out(chalk.gray(" ✓ Applied full-text search columns and indexes"));
253
- }
254
-
255
- /**
256
- * Glob patterns keeping Atlas away from the search objects.
257
- *
258
- * Returns an empty list — and so changes nothing — for a project with no
259
- * `search` block, which is every project that has not opted in.
260
- */
261
- export async function getSearchExcludes(collectionsPath: string): Promise<string[]> {
262
- const { searchExcludePatterns } = await import("./schema/generate-postgres-ddl-logic");
263
- return searchExcludePatterns(await loadCollectionsForCli(collectionsPath));
264
- }
265
-
266
- /**
267
- * The generated SQL for the project's `vector` properties, if it has any.
268
- *
269
- * @param drizzleDir directory holding the generated SQL. Defaults to `drizzle`
270
- * under the working directory.
271
- */
272
- export function readVectorDdl(drizzleDir: string = path.resolve(process.cwd(), "drizzle")): string {
273
- const vectorFile = path.join(drizzleDir, "vector.sql");
274
- if (!fs.existsSync(vectorFile)) return "";
275
- return fs.readFileSync(vectorFile, "utf-8").trim();
276
- }
277
-
278
- /**
279
- * Install pgvector and bring the vector columns and their ANN indexes up to
280
- * date.
281
- *
282
- * Runs *after* Atlas, like {@link applySearchDdl} and for the same reason: the
283
- * statements are `ALTER TABLE … ADD COLUMN`, so the table has to exist. Atlas
284
- * is told to ignore these objects entirely ({@link getVectorExcludes}) — it
285
- * cannot manage a type its dev database is structurally incapable of having,
286
- * and left to itself it would plan a `DROP COLUMN` for every one.
287
- *
288
- * A no-op when no collection declares a `vector` property. A failure is *not*
289
- * swallowed: a push that reported success while the embedding column never
290
- * appeared is how a project ends up with vector search configured, no error
291
- * anywhere, and no results.
292
- */
293
- export async function applyVectorDdl(
294
- databaseUrl: string,
295
- drizzleDir: string = path.resolve(process.cwd(), "drizzle")
296
- ): Promise<void> {
297
- const sql = readVectorDdl(drizzleDir);
298
- if (!sql) return;
299
-
300
- const { vectorExtensionHint } = await import("./schema/vector-index");
301
- const { Client } = await import("pg");
302
- const client = new Client({ connectionString: databaseUrl });
303
- await client.connect();
304
- try {
305
- await client.query(sql);
306
- } catch (err) {
307
- // The boot ensure explains this failure and the push has to as well —
308
- // it is the same missing extension, and a push is the *more* likely
309
- // place to meet it. Without this the developer gets a bare
310
- // `type "vector" does not exist`, which names nothing they can act on
311
- // and reads like a broken database rather than a line of config.
312
- const message = err instanceof Error ? err.message : String(err);
313
- const hint = vectorExtensionHint(message);
314
- if (!hint) throw err;
315
- throw new Error(`${message}${hint}`, { cause: err });
316
- } finally {
317
- await client.end();
318
- }
319
- out(chalk.gray(" ✓ Applied vector columns and ANN indexes"));
320
- }
321
-
322
- /**
323
- * Glob patterns keeping Atlas away from the vector objects.
324
- *
325
- * Returns an empty list — and so changes nothing — for a project with no
326
- * `vector` property, which is every project that has not opted in.
327
- */
328
- export async function getVectorExcludes(collectionsPath: string): Promise<string[]> {
329
- const { vectorExcludePatterns } = await import("./schema/generate-postgres-ddl-logic");
330
- return vectorExcludePatterns(await loadCollectionsForCli(collectionsPath));
331
- }
332
-
333
- /**
334
- * Query the live database for every user table/view outside the system
335
- * catalogs. Separated from {@link getTableExcludes} so its failure mode can
336
- * be handled explicitly (fail closed) and so tests can inject a stub.
337
- */
338
- /**
339
- * Every index Postgres holds on a table Rebase manages, as
340
- * `schema.table.index`.
341
- *
342
- * Constraint-backed indexes are left out: a `PRIMARY KEY` or a `UNIQUE` is a
343
- * *constraint* to Atlas, diffed from the constraint declaration, and naming its
344
- * index in an exclude would shield the constraint itself.
345
- */
346
- export async function queryExistingIndexes(databaseUrl: string): Promise<string[]> {
347
- const { Client } = await import("pg");
348
- const client = new Client({ connectionString: databaseUrl });
349
- await client.connect();
350
- try {
351
- const res = await client.query(`
352
- SELECT n.nspname || '.' || t.relname || '.' || i.relname AS full_name
353
- FROM pg_index x
354
- JOIN pg_class i ON i.oid = x.indexrelid
355
- JOIN pg_class t ON t.oid = x.indrelid
356
- JOIN pg_namespace n ON n.oid = t.relnamespace
357
- WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
358
- AND t.relkind IN ('r', 'p')
359
- AND NOT EXISTS (
360
- SELECT 1 FROM pg_constraint c WHERE c.conindid = i.oid
361
- );
362
- `);
363
- return res.rows.map((row: { full_name: string }) => row.full_name);
364
- } finally {
365
- await client.end();
366
- }
367
- }
368
-
369
- /**
370
- * Glob patterns keeping Atlas away from indexes that are not Rebase's.
371
- *
372
- * The problem this solves is live and predates the `indexes:` block. `db push`
373
- * is declarative, so an index on a managed table that is absent from
374
- * `schema.sql` is drift — and Atlas plans `DROP INDEX` for it. Verified
375
- * against atlas v1.2.3: create one by hand, re-run an unchanged push, and the
376
- * plan is a bare drop. `DROP INDEX` is not in `DESTRUCTIVE_PATTERNS`, so the
377
- * auto-approved apply took it without asking. Until now the only way to have
378
- * an index at all was to write it by hand, which made that the *only* outcome.
379
- *
380
- * Ownership rather than a prompt, because once indexes are declarable a drop
381
- * is usually correct: removing one from the config should remove it from the
382
- * database, quietly. What must never be dropped is an index Rebase never
383
- * created — a hand-written one, or one an introspected database arrived with.
384
- * {@link isRebaseIndexName} is the test, the same arrangement
385
- * `isGeneratedPolicyName` uses to let policy reconciliation drop only what it
386
- * generated.
387
- *
388
- * The named-in-the-desired-state check comes first and is what makes removal
389
- * work: a declared index that the author has just deleted still matches the
390
- * name pattern, is no longer in the plan, and so is *not* excluded — Atlas
391
- * drops it, as intended.
392
- *
393
- * Three-part `schema.table.index`, never two: a two-part pattern reads as a
394
- * *table* named `<index>` in a *schema* named `<table>`, matches nothing, and
395
- * reports no error — the trap already recorded for the search excludes.
396
- */
397
- export async function getForeignIndexExcludes(
398
- databaseUrl: string,
399
- collectionsPath: string,
400
- deps: {
401
- queryExistingIndexes?: (databaseUrl: string) => Promise<string[]>;
402
- getManagedIndexNames?: (collectionsPath: string) => Promise<Set<string>>;
403
- } = {}
404
- ): Promise<string[]> {
405
- const queryIndexes = deps.queryExistingIndexes ?? queryExistingIndexes;
406
- const getManaged = deps.getManagedIndexNames ?? managedIndexNames;
407
-
408
- const managed = await getManaged(collectionsPath);
409
-
410
- let existing: string[];
411
- try {
412
- existing = await queryIndexes(databaseUrl);
413
- } catch (err) {
414
- // Fails CLOSED, like getTableExcludes: without the catalogue we cannot
415
- // tell a foreign index from one of ours, and guessing wrong destroys
416
- // an index somebody is relying on.
417
- throw new ExcludeIntrospectionError(
418
- `Failed to introspect the database for unmanaged indexes: ${err instanceof Error ? err.message : String(err)}`,
419
- err
420
- );
421
- }
422
-
423
- return existing.filter(full => {
424
- const indexName = full.slice(full.lastIndexOf(".") + 1);
425
- if (managed.has(indexName)) return false; // ours, and still declared
426
- return !isRebaseIndexName(indexName); // ours, but no longer declared -> let it drop
427
- });
428
- }
429
-
430
- /**
431
- * The index names the current collections would create — search, vector and
432
- * declared alike. Anything Atlas would emit is by definition not foreign.
433
- */
434
- async function managedIndexNames(collectionsPath: string): Promise<Set<string>> {
435
- const collections = await loadCollectionsForCli(collectionsPath);
436
- const { resolveColumnName, searchExcludePatterns, vectorExcludePatterns } =
437
- await import("./schema/generate-postgres-ddl-logic");
438
- const { buildCollectionIndexPlan } = await import("./schema/collection-index");
439
-
440
- const names = new Set<string>(
441
- buildCollectionIndexPlan(collections, resolveColumnName).map(spec => spec.indexName)
442
- );
443
- // Search and vector objects are excluded from Atlas wholesale by their own
444
- // patterns; listing them here too is harmless and keeps this the single
445
- // answer to "is this index one of ours". The doc comment above claimed
446
- // vector was here long before it was — so an ANN index answered that
447
- // question with "somebody else's".
448
- for (const pattern of [...searchExcludePatterns(collections), ...vectorExcludePatterns(collections)]) {
449
- names.add(pattern.slice(pattern.lastIndexOf(".") + 1));
450
- }
451
- return names;
452
- }
453
-
454
- export async function queryExistingTables(databaseUrl: string): Promise<string[]> {
455
- const { Client } = await import("pg");
456
- const client = new Client({ connectionString: databaseUrl });
457
- await client.connect();
458
- try {
459
- const res = await client.query(`
460
- SELECT table_schema || '.' || table_name AS full_name
461
- FROM information_schema.tables
462
- WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
463
- AND table_type IN ('BASE TABLE', 'VIEW');
464
- `);
465
- return res.rows.map((row: { full_name: string }) => row.full_name);
466
- } finally {
467
- await client.end();
468
- }
469
- }
470
-
471
- /**
472
- * Raised when the exclude list could not be built. `db push` MUST abort on
473
- * this rather than continue: the exclude list is the only thing shielding
474
- * non-collection (user/system) tables from the auto-approved declarative
475
- * apply. A partial list — the old fail-open behaviour — meant a transient
476
- * introspection hiccup dropped every table not present in `schema.sql`.
477
- */
478
- export class ExcludeIntrospectionError extends Error {
479
- constructor(message: string, readonly cause?: unknown) {
480
- super(message);
481
- this.name = "ExcludeIntrospectionError";
482
- }
483
- }
484
-
485
- /**
486
- * Build the `--exclude` list that protects tables Rebase doesn't manage from
487
- * the declarative apply. Anything not backing a collection (or its M2M
488
- * junctions) is excluded so Atlas never drops it.
489
- *
490
- * Fails **closed**: if the database can't be introspected we cannot know
491
- * which tables to protect, so we throw {@link ExcludeIntrospectionError}
492
- * instead of returning a near-empty list and letting the caller drop
493
- * everything.
494
- *
495
- * `deps` is injectable for tests; production uses the real pg-backed queries.
496
- */
497
- export async function getTableExcludes(
498
- databaseUrl: string,
499
- collectionsPath: string,
500
- deps: {
501
- queryExistingTables?: (databaseUrl: string) => Promise<string[]>;
502
- getIncludes?: (collectionsPath: string) => Promise<string[]>;
503
- } = {}
504
- ): Promise<string[]> {
505
- const getIncludes = deps.getIncludes ?? getTableIncludes;
506
- const queryTables = deps.queryExistingTables ?? queryExistingTables;
507
-
508
- const includes = await getIncludes(collectionsPath);
509
- // Schemas the declarative apply must never touch. `rebase` holds the auth
510
- // tables (users, refresh_tokens, …), the RLS helper functions and Atlas's
511
- // own revision table. Excluding both the schema object AND its contents
512
- // keeps Atlas from planning a `DROP SCHEMA … CASCADE` — which on a live
513
- // database would take the user/auth tables with it.
514
- // `atlas_schema_revisions.*` covers a revision table living outside the
515
- // `rebase` schema — an externally managed Atlas setup — which the apply
516
- // must not plan changes against either.
517
- //
518
- // `auth` stays on this list even though Rebase no longer creates it, and
519
- // that is the point: on a database still holding the pre-1.0 schema it must
520
- // survive until the guarded cleanup retires it, and on a Supabase database
521
- // it is somebody else's and must never be in scope at all. Removing it here
522
- // would let a `db push` plan `DROP SCHEMA auth CASCADE` against a live
523
- // Supabase installation.
524
- const excludes: string[] = ["atlas_schema_revisions.*", "auth", "auth.*", "rebase", "rebase.*"];
525
-
526
- let existingTables: string[];
527
- try {
528
- existingTables = await queryTables(databaseUrl);
529
- } catch (err) {
530
- throw new ExcludeIntrospectionError(
531
- `Failed to introspect the database for unmapped tables: ${err instanceof Error ? err.message : String(err)}`,
532
- err
533
- );
534
- }
535
-
536
- for (const table of existingTables) {
537
- if (!includes.includes(table)) {
538
- excludes.push(table);
539
- }
540
- }
541
-
542
- return excludes;
543
- }
544
-
545
-
546
- /**
547
- * Ask a yes/no question on an interactive terminal.
548
- *
549
- * The `isTTY` guard is the contract, not an optimisation: non-interactive
550
- * shells (CI, pipes, agents) can never answer, and `readline` on a
551
- * non-TTY stdin resolves with whatever the pipe happens to contain — or never
552
- * resolves at all. Returning false there is what makes an unattended
553
- * `db push` abort instead of auto-confirming a destructive change. Callers
554
- * should already have gone through {@link decidePushSafety}, which decides
555
- * whether interactive confirmation is even possible; this is the backstop.
556
- *
557
- * Lives here rather than in cli.ts so it is reachable from a test — cli.ts uses
558
- * `import.meta` and cannot be imported by the jest runner.
559
- */
560
- export async function promptConfirm(question: string): Promise<boolean> {
561
- if (!process.stdin.isTTY) return false;
562
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
563
- try {
564
- const answer: string = await new Promise((resolve) => rl.question(question, resolve));
565
- return /^y(es)?$/i.test(answer.trim());
566
- } finally {
567
- rl.close();
568
- }
569
- }
package/src/cli-output.ts DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * Terminal output for the `rebase db|schema|doctor` commands.
3
- *
4
- * These commands used to write every line through `logger`, and that is a
5
- * category error with three separate consequences:
6
- *
7
- * - `logger` prefixes each line with its own level, so a box-drawn report
8
- * arrived as `ℹ️ [INFO] ┌─ ✗ Missing Column ───` and the frame no longer
9
- * lined up with anything;
10
- * - `logger` is gated by `LOG_LEVEL`, which ships in the scaffold's own
11
- * `.env.example` — a developer who quietened their dev server with
12
- * `LOG_LEVEL=warn` got a `rebase db push` that printed almost nothing and
13
- * still exited non-zero, indistinguishable from a crash;
14
- * - under `NODE_ENV=production` `logger` emits JSON, so the whole report
15
- * became log records with the chalk escape codes embedded in them.
16
- *
17
- * A CLI's report *is* its return value. It goes to the terminal unconditionally
18
- * and unadorned. `packages/cli` has always written its output this way; this is
19
- * the same three functions for the plugin CLI that `rebase` delegates to.
20
- *
21
- * `logger` still belongs in this package's *runtime* — a request handler has no
22
- * terminal and its lines want levels, timestamps and redaction. The rule is the
23
- * caller, not the severity: anything a developer reads because they typed a
24
- * command goes here, anything a server emits while running goes to `logger`.
25
- *
26
- * Errors and warnings go to stderr so `rebase db push > plan.txt` keeps the
27
- * diagnosis on the terminal where it is readable.
28
- */
29
-
30
- /** One line of human-facing output on stdout. */
31
- export const out = (line = ""): void => {
32
- console.log(line);
33
- };
34
-
35
- /** One line of human-facing warning output on stderr. */
36
- export const outWarn = (line = ""): void => {
37
- console.warn(line);
38
- };
39
-
40
- /** One line of human-facing error output on stderr. */
41
- export const outError = (line = ""): void => {
42
- console.error(line);
43
- };