@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.
- package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- 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
|
-
|
|
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,
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
91
|
-
if (!collection
|
|
92
|
-
return collection.relations
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
20
|
-
pool.on("error", (err) => {
|
|
21
|
-
console.error("Database connection error:", err);
|
|
65
|
+
const pool = new Pool(pgPoolConfig);
|
|
22
66
|
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|