@rebasepro/server-postgresql 0.6.0 → 0.7.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 (82) hide show
  1. package/package.json +24 -18
  2. package/src/PostgresBackendDriver.ts +65 -44
  3. package/src/PostgresBootstrapper.ts +49 -20
  4. package/src/auth/ensure-tables.ts +56 -1
  5. package/src/auth/services.ts +94 -1
  6. package/src/cli-errors.ts +162 -0
  7. package/src/cli-helpers.ts +183 -0
  8. package/src/cli.ts +198 -251
  9. package/src/data-transformer.ts +9 -1
  10. package/src/schema/auth-default-policies.ts +90 -0
  11. package/src/schema/auth-schema.ts +25 -2
  12. package/src/schema/doctor.ts +2 -4
  13. package/src/schema/generate-drizzle-schema-logic.ts +4 -3
  14. package/src/schema/generate-drizzle-schema.ts +3 -5
  15. package/src/schema/generate-postgres-ddl-logic.ts +524 -0
  16. package/src/schema/generate-postgres-ddl.ts +116 -0
  17. package/src/services/EntityPersistService.ts +18 -16
  18. package/src/services/entityService.ts +28 -3
  19. package/src/utils/pg-array-null-patch.ts +42 -0
  20. package/src/utils/pg-error-utils.ts +16 -0
  21. package/src/utils/table-classification.ts +16 -0
  22. package/src/websocket.ts +9 -0
  23. package/test/array-null-safety.test.ts +335 -0
  24. package/test/auth-default-policies.test.ts +89 -0
  25. package/test/cli-helpers-extended.test.ts +324 -0
  26. package/test/cli-helpers.test.ts +59 -0
  27. package/test/connection.test.ts +292 -0
  28. package/test/data-transformer.test.ts +53 -2
  29. package/test/databasePoolManager.test.ts +289 -0
  30. package/test/doctor-extended.test.ts +443 -0
  31. package/test/e2e/db-e2e.test.ts +293 -0
  32. package/test/e2e/pg-setup.ts +79 -0
  33. package/test/entity-persist-composite-keys.test.ts +451 -0
  34. package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
  35. package/test/generate-postgres-ddl.test.ts +300 -0
  36. package/test/mfa-service.test.ts +544 -0
  37. package/test/pg-array-null-patch.test.ts +65 -0
  38. package/test/pg-error-utils.test.ts +50 -1
  39. package/test/realtimeService-channels.test.ts +696 -0
  40. package/test/unmapped-tables-safety.test.ts +55 -342
  41. package/vite.config.ts +8 -6
  42. package/vitest.e2e.config.ts +10 -0
  43. package/build-errors.txt +0 -37
  44. package/dist/PostgresAdapter.d.ts +0 -6
  45. package/dist/PostgresBackendDriver.d.ts +0 -110
  46. package/dist/PostgresBootstrapper.d.ts +0 -46
  47. package/dist/auth/ensure-tables.d.ts +0 -10
  48. package/dist/auth/services.d.ts +0 -231
  49. package/dist/cli.d.ts +0 -1
  50. package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
  51. package/dist/connection.d.ts +0 -65
  52. package/dist/data-transformer.d.ts +0 -55
  53. package/dist/databasePoolManager.d.ts +0 -20
  54. package/dist/history/HistoryService.d.ts +0 -71
  55. package/dist/history/ensure-history-table.d.ts +0 -7
  56. package/dist/index.d.ts +0 -14
  57. package/dist/index.es.js +0 -10764
  58. package/dist/index.es.js.map +0 -1
  59. package/dist/index.umd.js +0 -11055
  60. package/dist/index.umd.js.map +0 -1
  61. package/dist/interfaces.d.ts +0 -18
  62. package/dist/schema/auth-schema.d.ts +0 -2149
  63. package/dist/schema/doctor-cli.d.ts +0 -2
  64. package/dist/schema/doctor.d.ts +0 -52
  65. package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
  66. package/dist/schema/generate-drizzle-schema.d.ts +0 -1
  67. package/dist/schema/introspect-db-inference.d.ts +0 -5
  68. package/dist/schema/introspect-db-logic.d.ts +0 -118
  69. package/dist/schema/introspect-db.d.ts +0 -1
  70. package/dist/schema/test-schema.d.ts +0 -24
  71. package/dist/services/BranchService.d.ts +0 -47
  72. package/dist/services/EntityFetchService.d.ts +0 -214
  73. package/dist/services/EntityPersistService.d.ts +0 -40
  74. package/dist/services/RelationService.d.ts +0 -98
  75. package/dist/services/entity-helpers.d.ts +0 -38
  76. package/dist/services/entityService.d.ts +0 -110
  77. package/dist/services/index.d.ts +0 -4
  78. package/dist/services/realtimeService.d.ts +0 -220
  79. package/dist/types.d.ts +0 -3
  80. package/dist/utils/drizzle-conditions.d.ts +0 -138
  81. package/dist/utils/pg-error-utils.d.ts +0 -55
  82. package/dist/websocket.d.ts +0 -11
package/src/cli.ts CHANGED
@@ -3,32 +3,20 @@ import chalk from "chalk";
3
3
  import { execa } from "execa";
4
4
  import path from "path";
5
5
  import fs from "fs";
6
- import { execSync } from "child_process";
7
6
  import { fileURLToPath } from "url";
8
7
  import { logger } from "@rebasepro/server-core";
8
+ import {
9
+ resolveLocalBin,
10
+ getTableIncludes,
11
+ getDevDatabaseUrl,
12
+ ensureDevDatabaseExists,
13
+ getTableExcludes
14
+ } from "./cli-helpers";
15
+ import { checkDatabaseConnectivity, diagnoseDbError } from "./cli-errors";
16
+
17
+ const __cliDirname = path.dirname(fileURLToPath(import.meta.url));
18
+
9
19
 
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
-
12
- function resolveLocalBin(binName: string): string | null {
13
- let cwd = process.cwd();
14
- // Try to find node_modules/.bin upwards
15
- while (true) {
16
- const candidate = path.join(cwd, "node_modules", ".bin", binName);
17
- if (fs.existsSync(candidate)) return candidate;
18
- const parent = path.dirname(cwd);
19
- if (parent === cwd) break;
20
- cwd = parent;
21
- }
22
- // Fall back to globally installed binary via which/where
23
- try {
24
- const cmd = process.platform === "win32" ? `where ${binName}` : `which ${binName}`;
25
- const globalPath = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0].trim();
26
- if (globalPath && fs.existsSync(globalPath)) return globalPath;
27
- } catch {
28
- // not found globally
29
- }
30
- return null;
31
- }
32
20
 
33
21
  export async function runPluginCommand(args: string[]) {
34
22
  const domain = args[0]; // "db" or "schema"
@@ -47,7 +35,7 @@ export async function runPluginCommand(args: string[]) {
47
35
  }
48
36
 
49
37
  async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
50
- const VALID_ACTIONS = ["push", "generate", "migrate", "studio", "branch"];
38
+ const VALID_ACTIONS = ["push", "generate", "migrate", "branch"];
51
39
  if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
52
40
  logger.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
53
41
  process.exit(1);
@@ -58,50 +46,98 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
58
46
  return;
59
47
  }
60
48
 
49
+ const argsList = arg(
50
+ {
51
+ "--collections": String,
52
+ "-c": "--collections"
53
+ },
54
+ {
55
+ argv: rawArgs.slice(2),
56
+ permissive: true
57
+ }
58
+ );
59
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
60
+
61
61
  if (subcommand === "generate") {
62
62
  logger.info("");
63
63
  logger.info(chalk.bold(" 📦 Rebase DB Generate"));
64
- logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema from collections..."));
64
+ logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
65
65
  logger.info("");
66
66
  await schemaCommand("generate", rawArgs);
67
+ await generatePostgresDdlCommand(rawArgs);
67
68
  logger.info("");
68
- logger.info(chalk.gray(" Step 2/2: Generating SQL migration files..."));
69
+ logger.info(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
69
70
  logger.info("");
70
- await runDrizzleKit("generate", rawArgs);
71
- await fixMigrationStatementOrder();
71
+ const migrationName = argsList._[0] || "migration";
72
+ await runAtlas("migrate", ["diff", migrationName, "--dir", "file://drizzle/migrations", "--to", "file://drizzle/schema.sql"], collectionsPath);
73
+
74
+ // Post-process the newest migration file
75
+ try {
76
+ const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
77
+ if (fs.existsSync(migrationsDir)) {
78
+ const files = fs.readdirSync(migrationsDir);
79
+ const sqlFiles = files
80
+ .filter(f => f.endsWith(".sql"))
81
+ .sort();
82
+ if (sqlFiles.length > 0) {
83
+ const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
84
+
85
+ // Make CREATE SCHEMA idempotent so it doesn't conflict with
86
+ // --revisions-schema (Atlas pre-creates the rebase schema
87
+ // for its revision table before running migrations).
88
+ let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
89
+ migrationContent = migrationContent.replace(
90
+ /CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g,
91
+ "CREATE SCHEMA IF NOT EXISTS $1;"
92
+ );
93
+ fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
94
+
95
+ // Append RLS policies
96
+ const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
97
+ if (fs.existsSync(policiesFile)) {
98
+ const policiesContent = fs.readFileSync(policiesFile, "utf-8");
99
+ fs.appendFileSync(newestMigrationFile, "\n\n" + policiesContent);
100
+ logger.info(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
101
+
102
+ // Re-hash the migration directory
103
+ logger.info(chalk.gray(" Re-hashing migration files..."));
104
+ await runAtlas("migrate", ["hash", "--dir", "file://drizzle/migrations"], collectionsPath);
105
+ logger.info(chalk.gray(" ✓ Migration directory checksum updated successfully."));
106
+ }
107
+ }
108
+ }
109
+ } catch (err) {
110
+ logger.warn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
111
+ }
112
+
72
113
  logger.info("");
73
114
  logger.info(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
74
115
  logger.info("");
75
- } else if (subcommand === "pull") {
76
- logger.info("");
77
- logger.info(chalk.yellow(" ⚠ \"rebase db pull\" has been removed."));
78
- logger.info(chalk.gray(" Use \"rebase schema introspect\" instead."));
79
- logger.info("");
80
- process.exit(1);
81
116
  } else {
82
117
  logger.info("");
83
118
  logger.info(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
84
119
  logger.info("");
85
120
 
86
121
  if (subcommand === "push") {
87
- logger.info(chalk.gray(" Step 1/2: Generating Drizzle schema from collections..."));
122
+ logger.info(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
88
123
  logger.info("");
89
124
  await schemaCommand("generate", rawArgs);
125
+ await generatePostgresDdlCommand(rawArgs);
90
126
  logger.info("");
91
- logger.info(chalk.gray(" Step 2/2: Pushing schema to database..."));
127
+ logger.info(chalk.gray(" Step 2/3: Pushing schema to database with Atlas..."));
92
128
  logger.info("");
93
- await runDrizzleKit("push", rawArgs);
94
- } else if (subcommand === "migrate") {
95
- await runDrizzleKit("migrate", rawArgs);
96
- } else if (subcommand === "studio") {
97
- const schemaPath = path.join(process.cwd(), "src", "schema.generated.ts");
98
- if (!fs.existsSync(schemaPath)) {
99
- logger.info(chalk.yellow(" ⚠ schema.generated.ts not found. Generating schema first..."));
100
- await schemaCommand("generate", rawArgs);
129
+ await runAtlas("schema", ["apply", "--to", "file://drizzle/schema.sql", "--auto-approve"], collectionsPath);
130
+ logger.info("");
131
+
132
+ const databaseUrl = process.env.DATABASE_URL;
133
+ if (databaseUrl) {
134
+ await applyPolicies(databaseUrl);
135
+ } else {
136
+ logger.warn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
101
137
  }
102
- await runDrizzleKit("studio", rawArgs);
103
- } else {
104
- await runDrizzleKit(subcommand, rawArgs);
138
+ } else if (subcommand === "migrate") {
139
+ const extraArgs = argsList._.filter(arg => arg !== "migrate");
140
+ await runAtlas("migrate", ["apply", "--dir", "file://drizzle/migrations", ...extraArgs], collectionsPath);
105
141
  }
106
142
 
107
143
  logger.info("");
@@ -110,6 +146,35 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
110
146
  }
111
147
  }
112
148
 
149
+ async function applyPolicies(databaseUrl: string): Promise<void> {
150
+ try {
151
+ const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
152
+ if (!fs.existsSync(policiesPath)) return;
153
+
154
+ logger.info(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
155
+ logger.info("");
156
+
157
+ const policiesContent = fs.readFileSync(policiesPath, "utf-8");
158
+ const { Client } = await import("pg");
159
+ const client = new Client({ connectionString: databaseUrl });
160
+ await client.connect();
161
+ try {
162
+ await client.query(policiesContent);
163
+ logger.info(chalk.green(" ✓ RLS policies applied successfully."));
164
+ } finally {
165
+ await client.end();
166
+ }
167
+ } catch (err) {
168
+ const hint = diagnoseDbError(err, databaseUrl);
169
+ if (hint) {
170
+ logger.error(hint);
171
+ } else {
172
+ logger.error(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
173
+ }
174
+ process.exit(1);
175
+ }
176
+ }
177
+
113
178
  async function branchCommand(rawArgs: string[]): Promise<void> {
114
179
  const branchAction = rawArgs[2]; // create, list, delete, info
115
180
 
@@ -296,118 +361,13 @@ function timeAgo(date: Date): string {
296
361
  return `${days}d ago`;
297
362
  }
298
363
 
299
- /**
300
- * Post-process generated migration files to fix statement ordering issues.
301
- *
302
- * Drizzle-kit can emit DROP POLICY statements *after* ALTER TABLE ... ALTER COLUMN
303
- * for the same table. Postgres rejects this with:
304
- * "cannot alter type of a column used in a policy definition"
305
- *
306
- * This scans the drizzle output directory for the most recently modified .sql file
307
- * and reorders statements so that DROP POLICY on a table always precedes any
308
- * ALTER TABLE on that same table.
309
- */
310
- async function fixMigrationStatementOrder(): Promise<void> {
311
- const drizzleDir = path.join(process.cwd(), "drizzle");
312
- if (!fs.existsSync(drizzleDir)) return;
313
-
314
- // Find the most recently modified .sql file
315
- const sqlFiles = fs.readdirSync(drizzleDir)
316
- .filter(f => f.endsWith(".sql"))
317
- .map(f => ({
318
- name: f,
319
- mtime: fs.statSync(path.join(drizzleDir, f)).mtimeMs
320
- }))
321
- .sort((a, b) => b.mtime - a.mtime);
322
-
323
- if (sqlFiles.length === 0) return;
324
-
325
- const latestFile = path.join(drizzleDir, sqlFiles[0].name);
326
- let content = fs.readFileSync(latestFile, "utf-8");
327
- const originalContent = content;
328
-
329
- // Replace CREATE SCHEMA with CREATE SCHEMA IF NOT EXISTS to prevent failures
330
- content = content.replace(/CREATE SCHEMA "([^"]+)";/g, 'CREATE SCHEMA IF NOT EXISTS "$1";');
331
-
332
- const DELIMITER = "--> statement-breakpoint";
333
- const parts = content.split(DELIMITER);
334
-
335
- // Parse each statement to detect DROP POLICY and ALTER TABLE targets
336
- const dropPolicyRe = /DROP\s+POLICY\s+.+?\s+ON\s+"([^"]+)"/i;
337
- const alterTableRe = /ALTER\s+TABLE\s+"([^"]+)"\s+ALTER\s+COLUMN/i;
338
-
339
- // Collect indices of DROP POLICY statements and what tables they target
340
- const dropPolicyIndices = new Map<string, number[]>(); // table -> indices
341
- const alterColumnIndices = new Map<string, number>(); // table -> first ALTER index
342
-
343
- for (let i = 0; i < parts.length; i++) {
344
- const stmt = parts[i].trim();
345
- const dropMatch = stmt.match(dropPolicyRe);
346
- if (dropMatch) {
347
- const table = dropMatch[1];
348
- if (!dropPolicyIndices.has(table)) dropPolicyIndices.set(table, []);
349
- dropPolicyIndices.get(table)!.push(i);
350
- }
351
- const alterMatch = stmt.match(alterTableRe);
352
- if (alterMatch) {
353
- const table = alterMatch[1];
354
- if (!alterColumnIndices.has(table)) alterColumnIndices.set(table, i);
355
- }
356
- }
357
-
358
- // Check if any DROP POLICY comes after an ALTER COLUMN on the same table
359
- let needsReorder = false;
360
- for (const [table, dropIndices] of dropPolicyIndices) {
361
- const firstAlter = alterColumnIndices.get(table);
362
- if (firstAlter !== undefined) {
363
- for (const dropIdx of dropIndices) {
364
- if (dropIdx > firstAlter) {
365
- needsReorder = true;
366
- break;
367
- }
368
- }
369
- }
370
- if (needsReorder) break;
371
- }
372
-
373
- if (!needsReorder) {
374
- if (content !== originalContent) {
375
- fs.writeFileSync(latestFile, content, "utf-8");
376
- }
377
- return;
378
- }
379
364
 
380
- // Reorder: move DROP POLICY statements for affected tables before their ALTER TABLE
381
- // Strategy: stable sort — DROP POLICY on table X gets priority over ALTER on table X
382
- const stmtEntries = parts.map((stmt, idx) => ({ stmt,
383
- idx }));
384
-
385
- stmtEntries.sort((a, b) => {
386
- const aDropMatch = a.stmt.trim().match(dropPolicyRe);
387
- const bAlterMatch = b.stmt.trim().match(alterTableRe);
388
- const bDropMatch = b.stmt.trim().match(dropPolicyRe);
389
- const aAlterMatch = a.stmt.trim().match(alterTableRe);
390
-
391
- // If a is DROP POLICY on table X and b is ALTER on table X, a goes first
392
- if (aDropMatch && bAlterMatch && aDropMatch[1] === bAlterMatch[1]) return -1;
393
- // If b is DROP POLICY on table X and a is ALTER on table X, b goes first
394
- if (bDropMatch && aAlterMatch && bDropMatch[1] === aAlterMatch[1]) return 1;
395
- // Otherwise preserve original order
396
- return a.idx - b.idx;
397
- });
398
-
399
- const reordered = stmtEntries.map(e => e.stmt).join(DELIMITER);
400
- fs.writeFileSync(latestFile, reordered, "utf-8");
401
-
402
- logger.info(chalk.yellow(` \u26A0 Reordered migration statements in ${sqlFiles[0].name} (DROP POLICY before ALTER COLUMN)`));
403
- }
404
365
 
405
- async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void> {
406
- const drizzleKitBin = resolveLocalBin("drizzle-kit");
407
- if (!drizzleKitBin) {
408
- logger.error(chalk.red("✗ Could not find drizzle-kit binary."));
409
- const isNpm = (process.env.npm_config_user_agent ?? "").startsWith("npm/") || fs.existsSync(path.join(process.cwd(), "package-lock.json"));
410
- const installCmd = isNpm ? "npm install -D drizzle-kit" : "pnpm add -D drizzle-kit";
366
+ async function runAtlas(domain: "schema" | "migrate", args: string[], collectionsPath?: string): Promise<void> {
367
+ const atlasBin = resolveLocalBin("atlas");
368
+ if (!atlasBin) {
369
+ logger.error(chalk.red("✗ Could not find atlas binary."));
370
+ const installCmd = "pnpm add -D @ariga/atlas";
411
371
  logger.error(chalk.gray(` Install it with: ${installCmd}`));
412
372
  process.exit(1);
413
373
  }
@@ -436,109 +396,97 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
436
396
  }
437
397
  }
438
398
  } catch {
439
- // dotenv may not be available — fall through
399
+ // ignore
440
400
  }
441
401
 
442
- const interactive = ["generate", "push"].includes(action) && Boolean(process.stdout.isTTY);
443
-
444
- // For push: always use --strict (prompts before destructive ops) and --verbose
445
- // (shows all SQL). This ensures unmapped tables are never silently dropped.
446
- const drizzleKitArgs = [action];
447
- if (action === "push") {
448
- drizzleKitArgs.push("--strict", "--verbose");
402
+ const databaseUrl = env.DATABASE_URL;
403
+ if (!databaseUrl) {
404
+ logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
405
+ process.exit(1);
449
406
  }
450
407
 
451
- // Forward any additional arguments, excluding schema-generator-specific options
452
- const excludedFlags = ["--collections", "-c", "--output", "-o", "--watch", "-w"];
453
- for (let i = 2; i < _rawArgs.length; i++) {
454
- const arg = _rawArgs[i];
455
- if (excludedFlags.includes(arg)) {
456
- // Skip this flag and its value if it takes a parameter
457
- if (["--collections", "-c", "--output", "-o"].includes(arg)) {
458
- i++; // Skip the next arg (the value)
459
- }
460
- continue;
408
+ // Pre-flight: verify the database is reachable before running Atlas.
409
+ // This catches ECONNREFUSED / auth failures with a friendly banner
410
+ // instead of letting Atlas surface a raw error.
411
+ await checkDatabaseConnectivity(databaseUrl);
412
+
413
+ const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
414
+ await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl);
415
+
416
+ const atlasArgs = [domain, ...args];
417
+
418
+ if (domain === "schema") {
419
+ if (args.includes("apply")) {
420
+ atlasArgs.push("--url", databaseUrl, "--dev-url", devDatabaseUrl);
421
+ } else if (args.includes("clean") || args.includes("inspect")) {
422
+ atlasArgs.push("--url", databaseUrl);
461
423
  }
462
- if (!drizzleKitArgs.includes(arg)) {
463
- drizzleKitArgs.push(arg);
424
+ } else if (domain === "migrate") {
425
+ if (args.includes("diff")) {
426
+ atlasArgs.push("--dev-url", devDatabaseUrl);
427
+ } else if (args.includes("apply") || args.includes("status")) {
428
+ atlasArgs.push("--url", databaseUrl, "--revisions-schema", "rebase");
429
+ }
430
+ }
431
+
432
+ if (domain === "schema" && args.includes("apply") && collectionsPath) {
433
+ const excludes = await getTableExcludes(databaseUrl, collectionsPath);
434
+ for (const exc of excludes) {
435
+ atlasArgs.push("--exclude", exc);
464
436
  }
465
437
  }
466
438
 
467
439
  try {
468
- if (interactive) {
469
- await execa(drizzleKitBin, drizzleKitArgs, {
470
- cwd: process.cwd(),
471
- stdio: "inherit",
472
- env
473
- });
474
- } else {
475
- const child = execa(drizzleKitBin, drizzleKitArgs, {
476
- cwd: process.cwd(),
477
- env,
478
- reject: false
479
- });
440
+ await execa(atlasBin, atlasArgs, {
441
+ cwd: process.cwd(),
442
+ stdio: "inherit",
443
+ env
444
+ });
445
+ } catch (err: unknown) {
446
+ logger.error(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
447
+ process.exit(1);
448
+ }
449
+ }
480
450
 
481
- // Natively stream output while still capturing it for error parsing
482
- child.stdout?.pipe(process.stdout);
483
- child.stderr?.pipe(process.stderr);
451
+ async function generatePostgresDdlCommand(rawArgs: string[]): Promise<void> {
452
+ const argsList = arg(
453
+ {
454
+ "--collections": String,
455
+ "--output": String,
456
+ "-c": "--collections",
457
+ "-o": "--output"
458
+ },
459
+ {
460
+ argv: rawArgs.slice(2),
461
+ permissive: true
462
+ }
463
+ );
484
464
 
485
- const result = await child;
465
+ const ddlScript = path.join(__cliDirname, "schema", "generate-postgres-ddl.ts");
466
+ const tsxBin = resolveLocalBin("tsx");
467
+ if (!tsxBin) {
468
+ logger.error(chalk.red("✗ Could not find tsx binary."));
469
+ process.exit(1);
470
+ }
486
471
 
487
- // eslint-disable-next-line no-control-regex
488
- const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
489
- const stdout = stripAnsi(result.stdout || "").trim();
490
- const stderr = stripAnsi(result.stderr || "").trim();
472
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
473
+ const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
491
474
 
492
- const hasTtyError = stdout.includes("Interactive prompts require a TTY terminal") ||
493
- stderr.includes("Interactive prompts require a TTY terminal");
475
+ const cmdParts = [
476
+ tsxBin,
477
+ ddlScript,
478
+ `--collections=${collectionsPath}`,
479
+ `--output=${outputPath}`
480
+ ];
494
481
 
495
- if (result.exitCode !== 0 || hasTtyError) {
496
- logger.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
497
- if (hasTtyError) {
498
- logger.error(chalk.red(" Error: Interactive prompts require a TTY terminal."));
499
- logger.error(chalk.gray(" Please run with --force to skip interactive prompts or run in an interactive terminal."));
500
- } else {
501
- const errorOutput = stderr || stdout;
502
- if (errorOutput) {
503
- const lines = errorOutput.split("\n").filter((l: string) => l.trim());
504
- let printedCount = 0;
505
- for (const line of lines) {
506
- if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
507
- logger.error(chalk.red(` ${line.trim()}`));
508
- printedCount++;
509
- }
510
- }
511
- if (printedCount === 0) {
512
- lines.slice(0, 10).forEach(line => logger.error(chalk.red(` ${line.trim()}`)));
513
- }
514
- }
515
- }
516
- logger.error("");
517
- process.exit(1);
518
- }
519
- }
482
+ try {
483
+ await execa(cmdParts[0], cmdParts.slice(1), {
484
+ cwd: process.cwd(),
485
+ stdio: "inherit",
486
+ env: { ...process.env as Record<string, string> }
487
+ });
520
488
  } catch (err: unknown) {
521
- const msg = err instanceof Error ? err.message : String(err);
522
- // eslint-disable-next-line no-control-regex
523
- const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
524
- const cleaned = stripAnsi(msg).trim();
525
- const hasTtyError = cleaned.includes("Interactive prompts require a TTY terminal");
526
- logger.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
527
- if (hasTtyError) {
528
- logger.error(chalk.red(" Error: Interactive prompts require a TTY terminal."));
529
- logger.error(chalk.gray(" Please run with --force to skip interactive prompts or run in an interactive terminal."));
530
- } else {
531
- const lines = cleaned.split("\n").filter((l: string) => l.trim());
532
- for (const line of lines) {
533
- if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates")) {
534
- logger.error(chalk.red(` ${line.trim()}`));
535
- }
536
- }
537
- if (lines.length === 0) {
538
- logger.error(chalk.gray(` ${cleaned}`));
539
- }
540
- }
541
- logger.error("");
489
+ logger.error(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
542
490
  process.exit(1);
543
491
  }
544
492
  }
@@ -561,8 +509,8 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
561
509
  );
562
510
 
563
511
  // Here we just invoke the local generate-drizzle-schema.ts since we are inside the postgresql-backend
564
- // If installed in node_modules, __dirname is node_modules/@rebasepro/server-postgresql/dist or src.
565
- const generatorScript = path.join(__dirname, "schema", "generate-drizzle-schema.ts");
512
+ // If installed in node_modules, __cliDirname is node_modules/@rebasepro/server-postgresql/dist or src.
513
+ const generatorScript = path.join(__cliDirname, "schema", "generate-drizzle-schema.ts");
566
514
  if (!fs.existsSync(generatorScript)) {
567
515
  logger.error(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
568
516
  process.exit(1);
@@ -619,7 +567,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
619
567
  }
620
568
  );
621
569
 
622
- const introspectScript = path.join(__dirname, "schema", "introspect-db.ts");
570
+ const introspectScript = path.join(__cliDirname, "schema", "introspect-db.ts");
623
571
  if (!fs.existsSync(introspectScript)) {
624
572
  logger.error(chalk.red(`✗ Could not find introspect-db.ts at ${introspectScript}`));
625
573
  process.exit(1);
@@ -677,7 +625,7 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
677
625
  }
678
626
  );
679
627
 
680
- const doctorScript = path.join(__dirname, "schema", "doctor-cli.ts");
628
+ const doctorScript = path.join(__cliDirname, "schema", "doctor-cli.ts");
681
629
  if (!fs.existsSync(doctorScript)) {
682
630
  logger.error(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
683
631
  process.exit(1);
@@ -714,8 +662,7 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
714
662
 
715
663
 
716
664
  // Entry point when called directly
717
- import fsSync from "fs";
718
- const argv1Real = process.argv[1] ? fsSync.realpathSync(process.argv[1]) : "";
665
+ const argv1Real = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
719
666
  if (import.meta.url === `file://${argv1Real}`) {
720
667
  // Drop node and script path
721
668
  runPluginCommand(process.argv.slice(2)).catch(() => process.exit(1));
@@ -244,8 +244,12 @@ export function serializePropertyToServer(value: unknown, property: Property): u
244
244
  };
245
245
  });
246
246
  }
247
+ return value;
247
248
  }
248
- return value;
249
+ // Non-array value for an array property — coerce to avoid .map() crashes downstream
250
+ logger.warn(`Expected array value for array property, got ${typeof value}. Coercing to empty array.`);
251
+ return [];
252
+
249
253
 
250
254
  case "map":
251
255
  if (typeof value === "object" && property.properties) {
@@ -585,6 +589,10 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
585
589
  };
586
590
  });
587
591
  }
592
+ } else {
593
+ // Non-array value from DB for an array property — coerce to avoid .map() crashes
594
+ logger.warn(`Expected array value from DB for array property, got ${typeof value}. Coercing to array.`);
595
+ return typeof value === "string" ? [value] : [];
588
596
  }
589
597
  return value;
590
598