@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.
- package/package.json +24 -18
- package/src/PostgresBackendDriver.ts +65 -44
- package/src/PostgresBootstrapper.ts +49 -20
- package/src/auth/ensure-tables.ts +56 -1
- package/src/auth/services.ts +94 -1
- package/src/cli-errors.ts +162 -0
- package/src/cli-helpers.ts +183 -0
- package/src/cli.ts +198 -251
- package/src/data-transformer.ts +9 -1
- package/src/schema/auth-default-policies.ts +90 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +4 -3
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +524 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +18 -16
- package/src/services/entityService.ts +28 -3
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +16 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +9 -0
- package/test/array-null-safety.test.ts +335 -0
- package/test/auth-default-policies.test.ts +89 -0
- package/test/cli-helpers-extended.test.ts +324 -0
- package/test/cli-helpers.test.ts +59 -0
- package/test/connection.test.ts +292 -0
- package/test/data-transformer.test.ts +53 -2
- package/test/databasePoolManager.test.ts +289 -0
- package/test/doctor-extended.test.ts +443 -0
- package/test/e2e/db-e2e.test.ts +293 -0
- package/test/e2e/pg-setup.ts +79 -0
- package/test/entity-persist-composite-keys.test.ts +451 -0
- package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
- package/test/generate-postgres-ddl.test.ts +300 -0
- package/test/mfa-service.test.ts +544 -0
- package/test/pg-array-null-patch.test.ts +65 -0
- package/test/pg-error-utils.test.ts +50 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vite.config.ts +8 -6
- package/vitest.e2e.config.ts +10 -0
- package/build-errors.txt +0 -37
- package/dist/PostgresAdapter.d.ts +0 -6
- package/dist/PostgresBackendDriver.d.ts +0 -110
- package/dist/PostgresBootstrapper.d.ts +0 -46
- package/dist/auth/ensure-tables.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -231
- package/dist/cli.d.ts +0 -1
- package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
- package/dist/connection.d.ts +0 -65
- package/dist/data-transformer.d.ts +0 -55
- package/dist/databasePoolManager.d.ts +0 -20
- package/dist/history/HistoryService.d.ts +0 -71
- package/dist/history/ensure-history-table.d.ts +0 -7
- package/dist/index.d.ts +0 -14
- package/dist/index.es.js +0 -10764
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -11055
- package/dist/index.umd.js.map +0 -1
- package/dist/interfaces.d.ts +0 -18
- package/dist/schema/auth-schema.d.ts +0 -2149
- package/dist/schema/doctor-cli.d.ts +0 -2
- package/dist/schema/doctor.d.ts +0 -52
- package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
- package/dist/schema/generate-drizzle-schema.d.ts +0 -1
- package/dist/schema/introspect-db-inference.d.ts +0 -5
- package/dist/schema/introspect-db-logic.d.ts +0 -118
- package/dist/schema/introspect-db.d.ts +0 -1
- package/dist/schema/test-schema.d.ts +0 -24
- package/dist/services/BranchService.d.ts +0 -47
- package/dist/services/EntityFetchService.d.ts +0 -214
- package/dist/services/EntityPersistService.d.ts +0 -40
- package/dist/services/RelationService.d.ts +0 -98
- package/dist/services/entity-helpers.d.ts +0 -38
- package/dist/services/entityService.d.ts +0 -110
- package/dist/services/index.d.ts +0 -4
- package/dist/services/realtimeService.d.ts +0 -220
- package/dist/types.d.ts +0 -3
- package/dist/utils/drizzle-conditions.d.ts +0 -138
- package/dist/utils/pg-error-utils.d.ts +0 -55
- package/dist/websocket.d.ts +0 -11
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test for the database migration pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Scenario 1 — Fresh database:
|
|
5
|
+
* Clean Postgres → db:generate → db:migrate → verify tables + auth bootstrap
|
|
6
|
+
*
|
|
7
|
+
* Scenario 2 — Incremental migration:
|
|
8
|
+
* Add a second collection → db:generate → db:migrate → verify new + old tables
|
|
9
|
+
*
|
|
10
|
+
* Requires Docker to be running. The test spins up a temporary Postgres
|
|
11
|
+
* container and tears it down afterwards.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import pg from "pg";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { execa } from "execa";
|
|
21
|
+
import { startPgContainer, stopPgContainer, type PgContainer } from "./pg-setup.js";
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
const pkgRoot = path.resolve(__dirname, "../.."); // packages/server-postgresql
|
|
26
|
+
const monorepoRoot = path.resolve(pkgRoot, "../.."); // repo root
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Minimal collection definition used by the tests. */
|
|
31
|
+
function tasksCollectionJs(): string {
|
|
32
|
+
return `
|
|
33
|
+
const tasksCollection = {
|
|
34
|
+
name: "Tasks",
|
|
35
|
+
singularName: "Task",
|
|
36
|
+
slug: "tasks",
|
|
37
|
+
table: "tasks",
|
|
38
|
+
icon: "CheckCircle",
|
|
39
|
+
group: "Core",
|
|
40
|
+
properties: {
|
|
41
|
+
id: { name: "ID", type: "string", isId: "uuid", validation: { required: true } },
|
|
42
|
+
title: { name: "Title", type: "string", validation: { required: true } },
|
|
43
|
+
done: { name: "Done", type: "boolean" }
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export default tasksCollection;
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function notesCollectionJs(): string {
|
|
51
|
+
return `
|
|
52
|
+
const notesCollection = {
|
|
53
|
+
name: "Notes",
|
|
54
|
+
singularName: "Note",
|
|
55
|
+
slug: "notes",
|
|
56
|
+
table: "notes",
|
|
57
|
+
icon: "Notebook",
|
|
58
|
+
group: "Core",
|
|
59
|
+
properties: {
|
|
60
|
+
id: { name: "ID", type: "string", isId: "uuid", validation: { required: true } },
|
|
61
|
+
content: { name: "Content", type: "string" }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export default notesCollection;
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectionsIndex(names: string[]): string {
|
|
69
|
+
const imports = names.map(n => `import ${n}Collection from "./${n}.js";`).join("\n");
|
|
70
|
+
const exports = names.map(n => `${n}Collection`).join(", ");
|
|
71
|
+
return `${imports}\nexport default [${exports}];\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cleanEnv(connectionString: string): Record<string, string> {
|
|
75
|
+
const env = { ...process.env } as Record<string, string>;
|
|
76
|
+
// Strip pnpm/npm runner variables to avoid leaking the parent workspace context
|
|
77
|
+
for (const key of Object.keys(env)) {
|
|
78
|
+
if (/^(npm_|PNPM_|pnpm_|NPM_)/i.test(key)) delete env[key];
|
|
79
|
+
}
|
|
80
|
+
env.DATABASE_URL = connectionString;
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve the CLI script path that the `rebase` command dispatches to
|
|
85
|
+
function resolveCliScript(): string {
|
|
86
|
+
const cliScript = path.join(pkgRoot, "src", "cli.ts");
|
|
87
|
+
if (!fs.existsSync(cliScript)) {
|
|
88
|
+
throw new Error(`CLI script not found at ${cliScript}`);
|
|
89
|
+
}
|
|
90
|
+
return cliScript;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Test suite ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("Database migration E2E", () => {
|
|
96
|
+
let container: PgContainer;
|
|
97
|
+
let dbClient: pg.Client;
|
|
98
|
+
let workDir: string; // temp directory acting as the "backend" cwd
|
|
99
|
+
let collectionsDir: string; // temp directory for collection definitions
|
|
100
|
+
let env: Record<string, string>;
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
// 1. Spin up Postgres container
|
|
104
|
+
container = await startPgContainer();
|
|
105
|
+
|
|
106
|
+
// 2. Connect with pg.Client for assertions (retry a few times)
|
|
107
|
+
let connectAttempts = 0;
|
|
108
|
+
const maxConnectAttempts = 10;
|
|
109
|
+
while (connectAttempts < maxConnectAttempts) {
|
|
110
|
+
try {
|
|
111
|
+
dbClient = new pg.Client({ connectionString: container.connectionString });
|
|
112
|
+
await dbClient.connect();
|
|
113
|
+
break;
|
|
114
|
+
} catch {
|
|
115
|
+
connectAttempts++;
|
|
116
|
+
if (connectAttempts === maxConnectAttempts) {
|
|
117
|
+
throw new Error("Could not connect to Postgres after retries");
|
|
118
|
+
}
|
|
119
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Create temp working directory structure:
|
|
124
|
+
// workDir/ ← acts like app/backend
|
|
125
|
+
// workDir/../config/collections/ ← collection definitions
|
|
126
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-db-e2e-"));
|
|
127
|
+
workDir = path.join(base, "backend");
|
|
128
|
+
collectionsDir = path.join(base, "config", "collections");
|
|
129
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
130
|
+
fs.mkdirSync(collectionsDir, { recursive: true });
|
|
131
|
+
|
|
132
|
+
// Create a minimal package.json so tsx can resolve modules
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(workDir, "package.json"),
|
|
135
|
+
JSON.stringify({ name: "e2e-test-backend", type: "module" }),
|
|
136
|
+
"utf-8"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Collections dir also needs type:module for .js ESM imports
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
path.join(collectionsDir, "package.json"),
|
|
142
|
+
JSON.stringify({ type: "module" }),
|
|
143
|
+
"utf-8"
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
env = cleanEnv(container.connectionString);
|
|
147
|
+
}, 60_000);
|
|
148
|
+
|
|
149
|
+
afterAll(async () => {
|
|
150
|
+
if (dbClient) try { await dbClient.end(); } catch { /* ignore */ }
|
|
151
|
+
if (container) await stopPgContainer(container.containerName);
|
|
152
|
+
}, 30_000);
|
|
153
|
+
|
|
154
|
+
// Resolve tsx binary from the monorepo
|
|
155
|
+
function tsx(...args: string[]) {
|
|
156
|
+
const tsxBin = path.join(monorepoRoot, "node_modules", ".bin", "tsx");
|
|
157
|
+
return execa(tsxBin, args, { cwd: workDir, env, stdio: "inherit" });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Scenario 1: Fresh database ──────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
it("should generate and apply the initial migration on an empty database", async () => {
|
|
163
|
+
const cliScript = resolveCliScript();
|
|
164
|
+
|
|
165
|
+
// Write a single collection
|
|
166
|
+
fs.writeFileSync(path.join(collectionsDir, "tasks.js"), tasksCollectionJs());
|
|
167
|
+
fs.writeFileSync(path.join(collectionsDir, "index.js"), collectionsIndex(["tasks"]));
|
|
168
|
+
|
|
169
|
+
// ── db:generate ──
|
|
170
|
+
console.log("[e2e] Running db generate…");
|
|
171
|
+
await tsx(cliScript, "db", "generate", "--collections", collectionsDir, "init");
|
|
172
|
+
|
|
173
|
+
// Verify migration file was created
|
|
174
|
+
const migrationsDir = path.join(workDir, "drizzle", "migrations");
|
|
175
|
+
expect(fs.existsSync(migrationsDir)).toBe(true);
|
|
176
|
+
const sqlFiles = fs.readdirSync(migrationsDir).filter(f => f.endsWith(".sql"));
|
|
177
|
+
expect(sqlFiles.length).toBe(1);
|
|
178
|
+
expect(sqlFiles[0]).toContain("init");
|
|
179
|
+
|
|
180
|
+
// Verify the migration contains our table
|
|
181
|
+
const migrationSql = fs.readFileSync(path.join(migrationsDir, sqlFiles[0]), "utf-8");
|
|
182
|
+
expect(migrationSql).toContain("CREATE TABLE");
|
|
183
|
+
expect(migrationSql).toContain("tasks");
|
|
184
|
+
|
|
185
|
+
// ── db:migrate ──
|
|
186
|
+
console.log("[e2e] Running db migrate…");
|
|
187
|
+
await tsx(cliScript, "db", "migrate");
|
|
188
|
+
|
|
189
|
+
// Verify collection table was created
|
|
190
|
+
const tablesAfterMigrate = await dbClient.query(`
|
|
191
|
+
SELECT table_schema, table_name
|
|
192
|
+
FROM information_schema.tables
|
|
193
|
+
WHERE table_schema IN ('public', 'rebase')
|
|
194
|
+
ORDER BY table_schema, table_name
|
|
195
|
+
`);
|
|
196
|
+
const tableList = tablesAfterMigrate.rows.map(
|
|
197
|
+
(r: { table_schema: string; table_name: string }) =>
|
|
198
|
+
`${r.table_schema}.${r.table_name}`
|
|
199
|
+
);
|
|
200
|
+
console.log("[e2e] Tables after migrate:", tableList);
|
|
201
|
+
expect(tableList).toContain("public.tasks");
|
|
202
|
+
|
|
203
|
+
// Verify atlas_schema_revisions is in the rebase schema, NOT public
|
|
204
|
+
expect(tableList).toContain("rebase.atlas_schema_revisions");
|
|
205
|
+
expect(tableList.filter(t => t === "public.atlas_schema_revisions")).toHaveLength(0);
|
|
206
|
+
|
|
207
|
+
// ── Auth bootstrap (ensureAuthTablesExist) ──
|
|
208
|
+
console.log("[e2e] Testing auth table bootstrap…");
|
|
209
|
+
const { ensureAuthTablesExist } = await import("../../src/auth/ensure-tables.js");
|
|
210
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
211
|
+
const pool = new pg.Pool({ connectionString: container.connectionString });
|
|
212
|
+
const db = drizzle(pool);
|
|
213
|
+
try {
|
|
214
|
+
await ensureAuthTablesExist(db);
|
|
215
|
+
} finally {
|
|
216
|
+
await pool.end();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Verify auth tables were created
|
|
220
|
+
const authTablesRes = await dbClient.query(`
|
|
221
|
+
SELECT table_name
|
|
222
|
+
FROM information_schema.tables
|
|
223
|
+
WHERE table_schema = 'rebase'
|
|
224
|
+
ORDER BY table_name
|
|
225
|
+
`);
|
|
226
|
+
const authTables = authTablesRes.rows.map((r: { table_name: string }) => r.table_name);
|
|
227
|
+
console.log("[e2e] Auth tables:", authTables);
|
|
228
|
+
|
|
229
|
+
expect(authTables).toContain("users");
|
|
230
|
+
expect(authTables).toContain("user_identities");
|
|
231
|
+
expect(authTables).toContain("refresh_tokens");
|
|
232
|
+
expect(authTables).toContain("password_reset_tokens");
|
|
233
|
+
|
|
234
|
+
// Verify user registration works (INSERT into users)
|
|
235
|
+
const insertRes = await dbClient.query(`
|
|
236
|
+
INSERT INTO rebase.users (email, password_hash, email_verified, metadata)
|
|
237
|
+
VALUES ('e2e@test.com', 'hash123', true, '{}'::jsonb)
|
|
238
|
+
RETURNING id, email
|
|
239
|
+
`);
|
|
240
|
+
expect(insertRes.rows.length).toBe(1);
|
|
241
|
+
expect(insertRes.rows[0].email).toBe("e2e@test.com");
|
|
242
|
+
}, 120_000);
|
|
243
|
+
|
|
244
|
+
// ── Scenario 2: Incremental migration ───────────────────────────────
|
|
245
|
+
|
|
246
|
+
it("should generate and apply an incremental migration preserving existing data", async () => {
|
|
247
|
+
const cliScript = resolveCliScript();
|
|
248
|
+
|
|
249
|
+
// Add a second collection
|
|
250
|
+
fs.writeFileSync(path.join(collectionsDir, "notes.js"), notesCollectionJs());
|
|
251
|
+
fs.writeFileSync(
|
|
252
|
+
path.join(collectionsDir, "index.js"),
|
|
253
|
+
collectionsIndex(["tasks", "notes"])
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// ── db:generate (incremental) ──
|
|
257
|
+
console.log("[e2e] Running incremental db generate…");
|
|
258
|
+
await tsx(cliScript, "db", "generate", "--collections", collectionsDir, "add_notes");
|
|
259
|
+
|
|
260
|
+
// Verify a SECOND migration file was created
|
|
261
|
+
const migrationsDir = path.join(workDir, "drizzle", "migrations");
|
|
262
|
+
const sqlFiles = fs.readdirSync(migrationsDir)
|
|
263
|
+
.filter(f => f.endsWith(".sql"))
|
|
264
|
+
.sort();
|
|
265
|
+
expect(sqlFiles.length).toBe(2);
|
|
266
|
+
expect(sqlFiles[1]).toContain("add_notes");
|
|
267
|
+
|
|
268
|
+
// ── db:migrate (incremental) ──
|
|
269
|
+
console.log("[e2e] Running incremental db migrate…");
|
|
270
|
+
await tsx(cliScript, "db", "migrate");
|
|
271
|
+
|
|
272
|
+
// Verify both tables exist
|
|
273
|
+
const tablesRes = await dbClient.query(`
|
|
274
|
+
SELECT table_schema, table_name
|
|
275
|
+
FROM information_schema.tables
|
|
276
|
+
WHERE table_schema = 'public'
|
|
277
|
+
ORDER BY table_name
|
|
278
|
+
`);
|
|
279
|
+
const publicTables = tablesRes.rows.map(
|
|
280
|
+
(r: { table_name: string }) => r.table_name
|
|
281
|
+
);
|
|
282
|
+
console.log("[e2e] Public tables after incremental migrate:", publicTables);
|
|
283
|
+
|
|
284
|
+
expect(publicTables).toContain("tasks");
|
|
285
|
+
expect(publicTables).toContain("notes");
|
|
286
|
+
|
|
287
|
+
// Verify existing data survived (the user inserted in scenario 1)
|
|
288
|
+
const userRes = await dbClient.query(
|
|
289
|
+
"SELECT email FROM rebase.users WHERE email = 'e2e@test.com'"
|
|
290
|
+
);
|
|
291
|
+
expect(userRes.rows.length).toBe(1);
|
|
292
|
+
}, 120_000);
|
|
293
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
export interface PgContainer {
|
|
5
|
+
containerName: string;
|
|
6
|
+
connectionString: string;
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Spins up a temporary PostgreSQL instance using a Docker container.
|
|
12
|
+
* Publishes port 5432 to a random host port to prevent collisions.
|
|
13
|
+
*/
|
|
14
|
+
export async function startPgContainer(): Promise<PgContainer> {
|
|
15
|
+
const containerName = `rebase-db-e2e-${crypto.randomUUID().slice(0, 8)}`;
|
|
16
|
+
|
|
17
|
+
console.log(`[pg-setup] Starting PostgreSQL container: ${containerName}`);
|
|
18
|
+
|
|
19
|
+
await execa("docker", [
|
|
20
|
+
"run",
|
|
21
|
+
"--name", containerName,
|
|
22
|
+
"-e", "POSTGRES_DB=rebase",
|
|
23
|
+
"-e", "POSTGRES_USER=rebase",
|
|
24
|
+
"-e", "POSTGRES_PASSWORD=rebase",
|
|
25
|
+
"-p", "5432", // random host port
|
|
26
|
+
"-d",
|
|
27
|
+
"postgres:18-alpine"
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Discover the assigned host port
|
|
31
|
+
const { stdout: portOutput } = await execa("docker", ["port", containerName, "5432"]);
|
|
32
|
+
const portMatch = portOutput.match(/:(\d+)$/m);
|
|
33
|
+
if (!portMatch) {
|
|
34
|
+
await stopPgContainer(containerName);
|
|
35
|
+
throw new Error(`Failed to parse host port from: ${portOutput}`);
|
|
36
|
+
}
|
|
37
|
+
const port = parseInt(portMatch[1], 10);
|
|
38
|
+
const connectionString =
|
|
39
|
+
`postgresql://rebase:rebase@localhost:${port}/rebase?sslmode=disable`;
|
|
40
|
+
|
|
41
|
+
console.log(`[pg-setup] Container started on port ${port}. Waiting for readiness…`);
|
|
42
|
+
|
|
43
|
+
// Poll pg_isready
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
const maxAttempts = 30;
|
|
46
|
+
while (attempts < maxAttempts) {
|
|
47
|
+
try {
|
|
48
|
+
await execa("docker", [
|
|
49
|
+
"exec", containerName,
|
|
50
|
+
"pg_isready", "-U", "rebase", "-d", "rebase"
|
|
51
|
+
]);
|
|
52
|
+
console.log("[pg-setup] PostgreSQL is ready.");
|
|
53
|
+
break;
|
|
54
|
+
} catch {
|
|
55
|
+
attempts++;
|
|
56
|
+
await new Promise(r => setTimeout(r, 500));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (attempts === maxAttempts) {
|
|
61
|
+
await stopPgContainer(containerName);
|
|
62
|
+
throw new Error("Postgres container failed to become ready in time");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { containerName, connectionString, port };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stops and removes a PostgreSQL container.
|
|
70
|
+
*/
|
|
71
|
+
export async function stopPgContainer(containerName: string): Promise<void> {
|
|
72
|
+
console.log(`[pg-setup] Removing container: ${containerName}`);
|
|
73
|
+
try {
|
|
74
|
+
await execa("docker", ["rm", "-f", containerName]);
|
|
75
|
+
} catch (e: unknown) {
|
|
76
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
77
|
+
console.error(`[pg-setup] Cleanup failed for ${containerName}: ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
}
|