@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (136) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index.es.js +1254 -591
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +1254 -591
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
  10. package/dist/server-postgresql/src/auth/services.d.ts +7 -3
  11. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
  12. package/dist/server-postgresql/src/connection.d.ts +34 -1
  13. package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
  14. package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
  15. package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
  16. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  17. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  18. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
  19. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  20. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
  21. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
  22. package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
  23. package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
  24. package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
  25. package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
  26. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
  27. package/dist/types/src/controllers/auth.d.ts +2 -0
  28. package/dist/types/src/controllers/client.d.ts +119 -7
  29. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  30. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  31. package/dist/types/src/controllers/data.d.ts +34 -7
  32. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  33. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  34. package/dist/types/src/controllers/email.d.ts +34 -0
  35. package/dist/types/src/controllers/index.d.ts +1 -0
  36. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  37. package/dist/types/src/controllers/navigation.d.ts +5 -5
  38. package/dist/types/src/controllers/registry.d.ts +6 -3
  39. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  40. package/dist/types/src/controllers/storage.d.ts +24 -26
  41. package/dist/types/src/rebase_context.d.ts +8 -4
  42. package/dist/types/src/types/backend.d.ts +4 -1
  43. package/dist/types/src/types/builders.d.ts +5 -4
  44. package/dist/types/src/types/chips.d.ts +1 -1
  45. package/dist/types/src/types/collections.d.ts +169 -125
  46. package/dist/types/src/types/cron.d.ts +102 -0
  47. package/dist/types/src/types/data_source.d.ts +1 -1
  48. package/dist/types/src/types/entity_actions.d.ts +8 -8
  49. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  50. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  51. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  52. package/dist/types/src/types/entity_views.d.ts +8 -8
  53. package/dist/types/src/types/export_import.d.ts +3 -3
  54. package/dist/types/src/types/index.d.ts +1 -0
  55. package/dist/types/src/types/plugins.d.ts +72 -18
  56. package/dist/types/src/types/properties.d.ts +118 -33
  57. package/dist/types/src/types/relations.d.ts +1 -1
  58. package/dist/types/src/types/slots.d.ts +30 -6
  59. package/dist/types/src/types/translations.d.ts +44 -0
  60. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  61. package/drizzle-test/0000_woozy_junta.sql +6 -0
  62. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  63. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  64. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  65. package/drizzle-test/meta/0000_snapshot.json +47 -0
  66. package/drizzle-test/meta/0001_snapshot.json +48 -0
  67. package/drizzle-test/meta/0002_snapshot.json +38 -0
  68. package/drizzle-test/meta/0003_snapshot.json +48 -0
  69. package/drizzle-test/meta/_journal.json +34 -0
  70. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  71. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  72. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  73. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  74. package/drizzle-test-out/meta/_journal.json +20 -0
  75. package/drizzle.test.config.ts +10 -0
  76. package/package.json +88 -89
  77. package/scratch.ts +41 -0
  78. package/src/PostgresBackendDriver.ts +63 -79
  79. package/src/PostgresBootstrapper.ts +7 -8
  80. package/src/auth/ensure-tables.ts +158 -86
  81. package/src/auth/services.ts +109 -50
  82. package/src/cli.ts +259 -16
  83. package/src/collections/PostgresCollectionRegistry.ts +6 -6
  84. package/src/connection.ts +70 -48
  85. package/src/data-transformer.ts +155 -116
  86. package/src/databasePoolManager.ts +6 -5
  87. package/src/history/HistoryService.ts +3 -12
  88. package/src/interfaces.ts +3 -3
  89. package/src/schema/auth-schema.ts +26 -3
  90. package/src/schema/doctor-cli.ts +47 -0
  91. package/src/schema/doctor.ts +595 -0
  92. package/src/schema/generate-drizzle-schema-logic.ts +204 -57
  93. package/src/schema/generate-drizzle-schema.ts +6 -6
  94. package/src/schema/test-schema.ts +11 -0
  95. package/src/services/BranchService.ts +5 -5
  96. package/src/services/EntityFetchService.ts +317 -188
  97. package/src/services/EntityPersistService.ts +15 -17
  98. package/src/services/RelationService.ts +299 -37
  99. package/src/services/entity-helpers.ts +39 -13
  100. package/src/services/entityService.ts +11 -9
  101. package/src/services/realtimeService.ts +58 -29
  102. package/src/utils/drizzle-conditions.ts +25 -24
  103. package/src/websocket.ts +52 -21
  104. package/test/auth-services.test.ts +131 -39
  105. package/test/batch-many-to-many-regression.test.ts +573 -0
  106. package/test/branchService.test.ts +22 -12
  107. package/test/data-transformer-hardening.test.ts +417 -0
  108. package/test/data-transformer.test.ts +175 -0
  109. package/test/doctor.test.ts +182 -0
  110. package/test/entityService.errors.test.ts +31 -16
  111. package/test/entityService.relations.test.ts +155 -59
  112. package/test/entityService.subcollection-search.test.ts +107 -57
  113. package/test/entityService.test.ts +105 -47
  114. package/test/generate-drizzle-schema.test.ts +262 -69
  115. package/test/historyService.test.ts +31 -16
  116. package/test/n-plus-one-regression.test.ts +314 -0
  117. package/test/postgresDataDriver.test.ts +260 -168
  118. package/test/realtimeService.test.ts +70 -39
  119. package/test/relation-pipeline-gaps.test.ts +637 -0
  120. package/test/relations.test.ts +492 -39
  121. package/test-drizzle-bug.ts +18 -0
  122. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  123. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  124. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  125. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  126. package/test-drizzle-out/meta/_journal.json +20 -0
  127. package/test-drizzle-prompt.sh +2 -0
  128. package/test-policy-prompt.sh +3 -0
  129. package/test-programmatic.ts +30 -0
  130. package/test-programmatic2.ts +59 -0
  131. package/test-schema-no-policies.ts +12 -0
  132. package/test_drizzle_mock.js +2 -2
  133. package/test_find_changed.mjs +3 -1
  134. package/test_hash.js +14 -0
  135. package/tsconfig.json +1 -1
  136. package/vite.config.ts +5 -5
package/src/cli.ts CHANGED
@@ -34,6 +34,8 @@ export async function runPluginCommand(args: string[]) {
34
34
  await dbCommand(subcommand, args);
35
35
  } else if (domain === "schema") {
36
36
  await schemaCommand(subcommand, args);
37
+ } else if (domain === "doctor") {
38
+ await doctorPluginCommand(args);
37
39
  } else {
38
40
  console.error(chalk.red(`Unknown domain command: ${domain}`));
39
41
  process.exit(1);
@@ -62,11 +64,32 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
62
64
  console.log(chalk.gray(" Step 2/2: Generating SQL migration files..."));
63
65
  console.log("");
64
66
  await runDrizzleKit("generate", rawArgs);
67
+ await fixMigrationStatementOrder();
65
68
  console.log("");
66
69
  console.log(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
67
70
  console.log("");
68
71
  } else {
69
- await runDrizzleKit(subcommand, rawArgs);
72
+ console.log("");
73
+ console.log(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
74
+ console.log("");
75
+
76
+ if (subcommand === "push") {
77
+ console.log(chalk.gray(" Step 1/2: Generating Drizzle schema from collections..."));
78
+ console.log("");
79
+ await schemaCommand("generate", rawArgs);
80
+ console.log("");
81
+ console.log(chalk.gray(" Step 2/2: Pushing schema to database..."));
82
+ console.log("");
83
+ await runDrizzleKit("push", rawArgs);
84
+ } else if (subcommand === "migrate") {
85
+ await runDrizzleKit("migrate", rawArgs);
86
+ } else {
87
+ await runDrizzleKit(subcommand, rawArgs);
88
+ }
89
+
90
+ console.log("");
91
+ console.log(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
92
+ console.log("");
70
93
  }
71
94
  }
72
95
 
@@ -103,7 +126,8 @@ async function branchCommand(rawArgs: string[]): Promise<void> {
103
126
  const { drizzle } = await import("drizzle-orm/node-postgres");
104
127
  const { Pool } = await import("pg");
105
128
 
106
- const pool = new Pool({ connectionString: databaseUrl, max: 3 });
129
+ const pool = new Pool({ connectionString: databaseUrl,
130
+ max: 3 });
107
131
  const db = drizzle(pool);
108
132
  const poolManager = new DatabasePoolManager(databaseUrl);
109
133
  const branchService = new BranchService(db, poolManager);
@@ -255,6 +279,102 @@ function timeAgo(date: Date): string {
255
279
  return `${days}d ago`;
256
280
  }
257
281
 
282
+ /**
283
+ * Post-process generated migration files to fix statement ordering issues.
284
+ *
285
+ * Drizzle-kit can emit DROP POLICY statements *after* ALTER TABLE ... ALTER COLUMN
286
+ * for the same table. Postgres rejects this with:
287
+ * "cannot alter type of a column used in a policy definition"
288
+ *
289
+ * This scans the drizzle output directory for the most recently modified .sql file
290
+ * and reorders statements so that DROP POLICY on a table always precedes any
291
+ * ALTER TABLE on that same table.
292
+ */
293
+ async function fixMigrationStatementOrder(): Promise<void> {
294
+ const drizzleDir = path.join(process.cwd(), "drizzle");
295
+ if (!fs.existsSync(drizzleDir)) return;
296
+
297
+ // Find the most recently modified .sql file
298
+ const sqlFiles = fs.readdirSync(drizzleDir)
299
+ .filter(f => f.endsWith(".sql"))
300
+ .map(f => ({
301
+ name: f,
302
+ mtime: fs.statSync(path.join(drizzleDir, f)).mtimeMs
303
+ }))
304
+ .sort((a, b) => b.mtime - a.mtime);
305
+
306
+ if (sqlFiles.length === 0) return;
307
+
308
+ const latestFile = path.join(drizzleDir, sqlFiles[0].name);
309
+ const content = fs.readFileSync(latestFile, "utf-8");
310
+ const DELIMITER = "--> statement-breakpoint";
311
+ const parts = content.split(DELIMITER);
312
+
313
+ // Parse each statement to detect DROP POLICY and ALTER TABLE targets
314
+ const dropPolicyRe = /DROP\s+POLICY\s+.+?\s+ON\s+"([^"]+)"/i;
315
+ const alterTableRe = /ALTER\s+TABLE\s+"([^"]+)"\s+ALTER\s+COLUMN/i;
316
+
317
+ // Collect indices of DROP POLICY statements and what tables they target
318
+ const dropPolicyIndices = new Map<string, number[]>(); // table -> indices
319
+ const alterColumnIndices = new Map<string, number>(); // table -> first ALTER index
320
+
321
+ for (let i = 0; i < parts.length; i++) {
322
+ const stmt = parts[i].trim();
323
+ const dropMatch = stmt.match(dropPolicyRe);
324
+ if (dropMatch) {
325
+ const table = dropMatch[1];
326
+ if (!dropPolicyIndices.has(table)) dropPolicyIndices.set(table, []);
327
+ dropPolicyIndices.get(table)!.push(i);
328
+ }
329
+ const alterMatch = stmt.match(alterTableRe);
330
+ if (alterMatch) {
331
+ const table = alterMatch[1];
332
+ if (!alterColumnIndices.has(table)) alterColumnIndices.set(table, i);
333
+ }
334
+ }
335
+
336
+ // Check if any DROP POLICY comes after an ALTER COLUMN on the same table
337
+ let needsReorder = false;
338
+ for (const [table, dropIndices] of dropPolicyIndices) {
339
+ const firstAlter = alterColumnIndices.get(table);
340
+ if (firstAlter !== undefined) {
341
+ for (const dropIdx of dropIndices) {
342
+ if (dropIdx > firstAlter) {
343
+ needsReorder = true;
344
+ break;
345
+ }
346
+ }
347
+ }
348
+ if (needsReorder) break;
349
+ }
350
+
351
+ if (!needsReorder) return;
352
+
353
+ // Reorder: move DROP POLICY statements for affected tables before their ALTER TABLE
354
+ // Strategy: stable sort — DROP POLICY on table X gets priority over ALTER on table X
355
+ const stmtEntries = parts.map((stmt, idx) => ({ stmt,
356
+ idx }));
357
+
358
+ stmtEntries.sort((a, b) => {
359
+ const aDropMatch = a.stmt.trim().match(dropPolicyRe);
360
+ const bAlterMatch = b.stmt.trim().match(alterTableRe);
361
+ const bDropMatch = b.stmt.trim().match(dropPolicyRe);
362
+ const aAlterMatch = a.stmt.trim().match(alterTableRe);
363
+
364
+ // If a is DROP POLICY on table X and b is ALTER on table X, a goes first
365
+ if (aDropMatch && bAlterMatch && aDropMatch[1] === bAlterMatch[1]) return -1;
366
+ // If b is DROP POLICY on table X and a is ALTER on table X, b goes first
367
+ if (bDropMatch && aAlterMatch && bDropMatch[1] === aAlterMatch[1]) return 1;
368
+ // Otherwise preserve original order
369
+ return a.idx - b.idx;
370
+ });
371
+
372
+ const reordered = stmtEntries.map(e => e.stmt).join(DELIMITER);
373
+ fs.writeFileSync(latestFile, reordered, "utf-8");
374
+
375
+ console.log(chalk.yellow(` ⚠ Reordered migration statements in ${sqlFiles[0].name} (DROP POLICY before ALTER COLUMN)`));
376
+ }
377
+
258
378
  async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void> {
259
379
  const drizzleKitBin = resolveLocalBin("drizzle-kit");
260
380
  if (!drizzleKitBin) {
@@ -263,14 +383,87 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
263
383
  process.exit(1);
264
384
  }
265
385
 
386
+ const env = { ...process.env as Record<string, string> };
266
387
  try {
267
- await execa(drizzleKitBin, [action], {
268
- cwd: process.cwd(),
269
- stdio: "inherit",
270
- env: { ...process.env as Record<string, string> },
271
- });
388
+ const dotenv = await import("dotenv");
389
+ const envPaths = [
390
+ process.env.DOTENV_CONFIG_PATH,
391
+ path.resolve(process.cwd(), ".env"),
392
+ path.resolve(process.cwd(), "../.env"),
393
+ path.resolve(process.cwd(), "../../.env")
394
+ ].filter(Boolean) as string[];
395
+
396
+ for (const p of envPaths) {
397
+ if (fs.existsSync(p)) {
398
+ const parsed = dotenv.config({ path: p });
399
+ if (parsed.parsed) {
400
+ Object.assign(env, parsed.parsed);
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ } catch {
406
+ // dotenv may not be available — fall through
407
+ }
408
+
409
+ const interactive = ["generate", "push"].includes(action);
410
+
411
+ try {
412
+ if (interactive) {
413
+ await execa(drizzleKitBin, [action], {
414
+ cwd: process.cwd(),
415
+ stdio: "inherit",
416
+ env
417
+ });
418
+ } else {
419
+ const child = execa(drizzleKitBin, [action], {
420
+ cwd: process.cwd(),
421
+ env,
422
+ reject: false
423
+ });
424
+
425
+ // Natively stream output while still capturing it for error parsing
426
+ child.stdout?.pipe(process.stdout);
427
+ child.stderr?.pipe(process.stderr);
428
+
429
+ const result = await child;
430
+
431
+ // eslint-disable-next-line no-control-regex
432
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
433
+ const stdout = stripAnsi(result.stdout || "").trim();
434
+ const stderr = stripAnsi(result.stderr || "").trim();
435
+
436
+ if (result.exitCode !== 0) {
437
+ console.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
438
+ const errorOutput = stderr || stdout;
439
+ if (errorOutput) {
440
+ const lines = errorOutput.split("\n").filter((l: string) => l.trim());
441
+ for (const line of lines) {
442
+ if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
443
+ console.error(chalk.red(` ${line.trim()}`));
444
+ }
445
+ }
446
+ }
447
+ console.error("");
448
+ process.exit(1);
449
+ }
450
+ }
272
451
  } catch (err: unknown) {
273
- console.error(chalk.red(`✗ Failed to run drizzle-kit ${action}: ${err instanceof Error ? err.message : String(err)}`));
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ // eslint-disable-next-line no-control-regex
454
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
455
+ const cleaned = stripAnsi(msg).trim();
456
+ console.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
457
+ const lines = cleaned.split("\n").filter((l: string) => l.trim());
458
+ for (const line of lines) {
459
+ if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates")) {
460
+ console.error(chalk.red(` ${line.trim()}`));
461
+ }
462
+ }
463
+ if (lines.length === 0) {
464
+ console.error(chalk.gray(` ${cleaned}`));
465
+ }
466
+ console.error("");
274
467
  process.exit(1);
275
468
  }
276
469
  }
@@ -284,11 +477,11 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
284
477
  "--watch": Boolean,
285
478
  "-c": "--collections",
286
479
  "-o": "--output",
287
- "-w": "--watch",
480
+ "-w": "--watch"
288
481
  },
289
482
  {
290
483
  argv: rawArgs.slice(2), // db generate ... or schema generate ...
291
- permissive: true,
484
+ permissive: true
292
485
  }
293
486
  );
294
487
 
@@ -299,7 +492,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
299
492
  console.error(chalk.red(`✗ Could not find generate-drizzle-schema.ts at ${generatorScript}`));
300
493
  process.exit(1);
301
494
  }
302
-
495
+
303
496
  const tsxBin = resolveLocalBin("tsx");
304
497
  if (!tsxBin) {
305
498
  console.error(chalk.red("✗ Could not find tsx binary."));
@@ -313,12 +506,12 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
313
506
  console.log("");
314
507
  console.log(chalk.bold(" 🔧 Rebase Schema Generator"));
315
508
  console.log("");
316
-
509
+
317
510
  const cmdParts = [
318
511
  tsxBin,
319
512
  generatorScript,
320
513
  `--collections=${collectionsPath}`,
321
- `--output=${outputPath}`,
514
+ `--output=${outputPath}`
322
515
  ];
323
516
  if (watch) {
324
517
  cmdParts.push("--watch");
@@ -328,20 +521,70 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
328
521
  await execa(cmdParts[0], cmdParts.slice(1), {
329
522
  cwd: process.cwd(),
330
523
  stdio: "inherit",
331
- env: { ...process.env as Record<string, string> },
524
+ env: { ...process.env as Record<string, string> }
332
525
  });
333
526
  } catch (err: unknown) {
334
527
  console.error(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
335
528
  process.exit(1);
336
529
  }
337
530
  } else {
338
- console.error(chalk.red(`Unknown schema command.`));
531
+ console.error(chalk.red("Unknown schema command."));
532
+ process.exit(1);
533
+ }
534
+ }
535
+
536
+ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
537
+ const parsedArgs = arg(
538
+ {
539
+ "--collections": String,
540
+ "--schema": String,
541
+ "-c": "--collections",
542
+ "-s": "--schema"
543
+ },
544
+ {
545
+ argv: rawArgs.slice(1), // skip "doctor"
546
+ permissive: true
547
+ }
548
+ );
549
+
550
+ const doctorScript = path.join(__dirname, "schema", "doctor-cli.ts");
551
+ if (!fs.existsSync(doctorScript)) {
552
+ console.error(chalk.red(`✗ Could not find doctor.ts at ${doctorScript}`));
553
+ process.exit(1);
554
+ }
555
+
556
+ const tsxBin = resolveLocalBin("tsx");
557
+ if (!tsxBin) {
558
+ console.error(chalk.red("✗ Could not find tsx binary."));
559
+ process.exit(1);
560
+ }
561
+
562
+ const collectionsPath = parsedArgs["--collections"] || path.join("..", "shared", "collections");
563
+ const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
564
+
565
+ const cmdParts = [
566
+ tsxBin,
567
+ doctorScript,
568
+ `--collections=${collectionsPath}`,
569
+ `--schema=${schemaPath}`
570
+ ];
571
+
572
+ try {
573
+ await execa(cmdParts[0], cmdParts.slice(1), {
574
+ cwd: process.cwd(),
575
+ stdio: "inherit",
576
+ env: { ...process.env as Record<string, string> }
577
+ });
578
+ } catch {
339
579
  process.exit(1);
340
580
  }
341
581
  }
342
582
 
583
+
343
584
  // Entry point when called directly
344
- if (import.meta.url === `file://${process.argv[1]}`) {
585
+ import fsSync from "fs";
586
+ const argv1Real = process.argv[1] ? fsSync.realpathSync(process.argv[1]) : "";
587
+ if (import.meta.url === `file://${argv1Real}`) {
345
588
  // Drop node and script path
346
589
  runPluginCommand(process.argv.slice(2)).catch(() => process.exit(1));
347
590
  }
@@ -1,5 +1,5 @@
1
1
  import { CollectionRegistry } from "@rebasepro/common";
2
- import type { EntityCollection } from "@rebasepro/types";
2
+ import { type CollectionWithRelations, type EntityCollection, type Relation, getDataSourceCapabilities } from "@rebasepro/types";
3
3
  import { PgEnum, PgTable } from "drizzle-orm/pg-core";
4
4
  import { Relations } from "drizzle-orm";
5
5
  import { CollectionRegistryInterface } from "../interfaces";
@@ -8,7 +8,7 @@ import { getTableName } from "@rebasepro/common";
8
8
  /**
9
9
  * PostgreSQL-specific collection registry.
10
10
  * Extends the base CollectionRegistry with support for Drizzle ORM tables, enums, and relations.
11
- *
11
+ *
12
12
  * Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.
13
13
  */
14
14
  export class PostgresCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {
@@ -35,7 +35,7 @@ export class PostgresCollectionRegistry extends CollectionRegistry implements Co
35
35
  /**
36
36
  * Finds collections assigned to a specific driver that do not have a registered table.
37
37
  */
38
- getCollectionsWithoutTables(driverId: string = "(default)"): EntityCollection[] {
38
+ getCollectionsWithoutTables(driverId = "(default)"): EntityCollection[] {
39
39
  const collections = this.getCollections().filter(
40
40
  c => c.driver === driverId || (!c.driver && driverId === "(default)")
41
41
  );
@@ -87,9 +87,9 @@ export class PostgresCollectionRegistry extends CollectionRegistry implements Co
87
87
  * defined in the schema.
88
88
  */
89
89
  getRelationKeysForCollection(collectionPath: string): string[] {
90
- const collection = this.getCollectionByPath(collectionPath) as import("@rebasepro/types").PostgresCollection<Record<string, unknown>, import("@rebasepro/types").User>;
91
- if (!collection?.relations) return [];
92
- return collection.relations.map(r => r.relationName || r.localKey || "").filter(Boolean);
90
+ const collection = this.getCollectionByPath(collectionPath);
91
+ if (!collection || !getDataSourceCapabilities(collection.driver).supportsRelations || !(collection as CollectionWithRelations).relations) return [];
92
+ return (collection as CollectionWithRelations).relations!.map((r: Relation) => r.relationName || r.localKey || "").filter(Boolean);
93
93
  }
94
94
 
95
95
  }
package/src/connection.ts CHANGED
@@ -1,62 +1,84 @@
1
- import { Pool } from "pg";
1
+ import { Pool, PoolConfig } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
 
4
- export function createPostgresDatabaseConnection(connectionString: string, schema?: Record<string, unknown>) {
5
- const pool = new Pool({
4
+ /**
5
+ * Configuration for the Postgres connection pool.
6
+ *
7
+ * Sensible defaults are provided for production Cloud Run / single-instance
8
+ * deployments. Override via environment variables or explicit config.
9
+ */
10
+ export interface PostgresPoolConfig {
11
+ /** Maximum number of connections in the pool (default: 20) */
12
+ max?: number;
13
+ /** Close idle connections after this many ms (default: 30 000) */
14
+ idleTimeoutMillis?: number;
15
+ /** Abort connection attempts after this many ms (default: 10 000) */
16
+ connectionTimeoutMillis?: number;
17
+ /** Per-query timeout in ms (default: 30 000) */
18
+ queryTimeout?: number;
19
+ /** Per-statement timeout in ms (default: 30 000) */
20
+ statementTimeout?: number;
21
+ /** Enable TCP keep-alive (default: true) */
22
+ keepAlive?: boolean;
23
+ }
24
+
25
+ const DEFAULT_POOL: Required<PostgresPoolConfig> = {
26
+ max: 20,
27
+ idleTimeoutMillis: 30_000,
28
+ connectionTimeoutMillis: 10_000,
29
+ queryTimeout: 30_000,
30
+ statementTimeout: 30_000,
31
+ keepAlive: true
32
+ };
33
+
34
+ /**
35
+ * Create a Drizzle-backed Postgres connection with a production-grade
36
+ * connection pool.
37
+ *
38
+ * @param connectionString Postgres connection URL
39
+ * @param schema Optional Drizzle schema for the relational API
40
+ * @param poolConfig Optional pool tuning (merged over defaults)
41
+ *
42
+ * @returns `{ db, pool, connectionString }` — the `pool` is exposed so
43
+ * callers can register shutdown hooks (`pool.end()`) or monitor
44
+ * pool metrics.
45
+ */
46
+ export function createPostgresDatabaseConnection(
47
+ connectionString: string,
48
+ schema?: Record<string, unknown>,
49
+ poolConfig?: PostgresPoolConfig
50
+ ) {
51
+ const opts = { ...DEFAULT_POOL,
52
+ ...poolConfig };
53
+
54
+ const pgPoolConfig: PoolConfig = {
6
55
  connectionString,
7
- // Connection pool settings for resilience
8
- max: 20, // Maximum number of connections in the pool
9
- idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
10
- connectionTimeoutMillis: 10000, // Timeout for new connections
11
- // Retry configuration
12
- query_timeout: 30000, // Query timeout
13
- statement_timeout: 30000, // Statement timeout
14
- // Keep connections alive
15
- keepAlive: true,
56
+ max: opts.max,
57
+ idleTimeoutMillis: opts.idleTimeoutMillis,
58
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
59
+ query_timeout: opts.queryTimeout,
60
+ statement_timeout: opts.statementTimeout,
61
+ keepAlive: opts.keepAlive,
16
62
  keepAliveInitialDelayMillis: 0
17
- });
63
+ };
18
64
 
19
- // Handle connection errors and implement reconnection logic
20
- pool.on("error", (err) => {
21
- console.error("Database connection error:", err);
65
+ const pool = new Pool(pgPoolConfig);
22
66
 
23
- // Handle specific timeout errors
67
+ // ── Pool event logging ────────────────────────────────────────────────
68
+ // Uses console.* because the structured logger lives in server-core
69
+ // (a separate package). The caller can replace these with the structured
70
+ // logger if desired via pool.on() after creation.
71
+ pool.on("error", (err) => {
72
+ console.error("[pg-pool] Unexpected pool error:", err.message);
24
73
  if (err.message.includes("ETIMEDOUT")) {
25
- console.warn("Connection timeout detected, pool will automatically retry...");
74
+ console.warn("[pg-pool] Connection timeout detected pool will auto-retry");
26
75
  }
27
76
  });
28
77
 
29
- // Handle successful connections
30
- pool.on("connect", (client) => {
31
- console.debug("Database client connected");
32
-
33
- // Set up client-level error handling
34
- client.on("error", (err) => {
35
- console.error("Database client error:", err);
36
- });
37
- });
38
-
39
- // Handle client removal from pool
40
- pool.on("remove", (client) => {
41
- console.debug("Database client removed from pool");
42
- });
43
-
44
78
  // Create drizzle instance — pass schema when available to enable db.query relational API
45
79
  const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
46
80
 
47
- // Graceful shutdown handler
48
- process.on("SIGINT", async () => {
49
- console.log("SIGINT: Closing database pool...");
50
- await pool.end();
51
- process.exit(0);
52
- });
53
-
54
- process.on("SIGTERM", async () => {
55
- console.log("SIGTERM: Closing database pool...");
56
- await pool.end();
57
- process.exit(0);
58
- });
59
-
60
- return { db, connectionString };
81
+ return { db,
82
+ pool,
83
+ connectionString };
61
84
  }
62
-