@effectstream/db 0.104.1 → 0.200.2
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/README.md +49 -1
- package/package.json +6 -6
- package/scripts/pgtyped-update.ts +137 -40
- package/scripts/start-pglite.ts +219 -21
- package/scripts/wait-for-db.ts +28 -22
- package/test/fixtures/pglite-close/01-sentinel.test.ts +10 -0
- package/test/fixtures/pglite-close/02-live-client.test.ts +27 -0
- package/test/fixtures/pglite-close/process-exit.ts +10 -0
- package/test/fixtures/pgtyped/failure.mjs +17 -0
- package/test/fixtures/pgtyped/hang.mjs +12 -0
- package/test/fixtures/pgtyped/success.mjs +30 -0
- package/test/pgtyped-update.test.ts +132 -0
- package/test/start-pglite-multifile.test.ts +33 -0
- package/test/start-pglite.test.ts +572 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { afterAll, expect, test } from "bun:test";
|
|
2
|
+
import { Client } from "pg";
|
|
3
|
+
import { startPglite } from "../../../scripts/start-pglite.ts";
|
|
4
|
+
|
|
5
|
+
const enabled = process.env.EFFECTSTREAM_PGLITE_MULTIFILE_FIXTURE === "1";
|
|
6
|
+
|
|
7
|
+
if (!enabled) {
|
|
8
|
+
test.skip("multi-file retained-client fixture runs only through its parent", () => {});
|
|
9
|
+
} else {
|
|
10
|
+
const handle = await startPglite(0);
|
|
11
|
+
const client = new Client({
|
|
12
|
+
host: "127.0.0.1",
|
|
13
|
+
port: handle.port,
|
|
14
|
+
user: "postgres",
|
|
15
|
+
database: "postgres",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("leaves a raw client connected during default close", async () => {
|
|
19
|
+
await client.connect();
|
|
20
|
+
expect((await client.query("SELECT 1 AS value")).rows).toEqual([{ value: 1 }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await handle.close();
|
|
25
|
+
console.info("PGLITE_LIVE_CLIENT_CLOSED_DEFAULT");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { startPglite } from "../../../scripts/start-pglite.ts";
|
|
2
|
+
|
|
3
|
+
const handle = await startPglite(0);
|
|
4
|
+
console.info(`PGLITE_PROCESS_EXIT_PORT:${handle.port}`);
|
|
5
|
+
|
|
6
|
+
await new Promise<void>((resolve) => {
|
|
7
|
+
handle.server.once("connection", () => resolve());
|
|
8
|
+
});
|
|
9
|
+
await handle.close();
|
|
10
|
+
console.info("PGLITE_PROCESS_EXIT_DEFAULT_CLOSED");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const configIndex = process.argv.indexOf("-c");
|
|
4
|
+
const configPath = configIndex === -1 ? undefined : process.argv[configIndex + 1];
|
|
5
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
6
|
+
if (config.db.host !== "127.0.0.1") {
|
|
7
|
+
console.error(`unexpected pgtyped host: ${String(config.db.host)}`);
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
const port = Number(config.db.port);
|
|
11
|
+
if (!Number.isInteger(port) || port <= 0 || port === 5432) {
|
|
12
|
+
console.error(`pgtyped did not receive the reported ephemeral port: ${port}`);
|
|
13
|
+
process.exit(3);
|
|
14
|
+
}
|
|
15
|
+
console.log(`PGTYPED_CONFIG_PATH:${configPath}`);
|
|
16
|
+
console.error("PGTYPED_INJECTED_FAILURE");
|
|
17
|
+
process.exit(23);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const configIndex = process.argv.indexOf("-c");
|
|
4
|
+
const configPath = configIndex === -1 ? undefined : process.argv[configIndex + 1];
|
|
5
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
6
|
+
if (config.db.host !== "127.0.0.1" || !Number.isInteger(config.db.port)) {
|
|
7
|
+
console.error("unexpected pgtyped connection config");
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
console.log(`PGTYPED_CONFIG_PATH:${configPath}`);
|
|
11
|
+
console.log("PGTYPED_INJECTED_HANG");
|
|
12
|
+
setInterval(() => {}, 1_000);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const configIndex = process.argv.indexOf("-c");
|
|
4
|
+
const configPath = configIndex === -1 ? undefined : process.argv[configIndex + 1];
|
|
5
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
6
|
+
if (config.db.host !== "127.0.0.1") {
|
|
7
|
+
console.error(`unexpected pgtyped host: ${String(config.db.host)}`);
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
const port = Number(config.db.port);
|
|
11
|
+
if (!Number.isInteger(port) || port <= 0 || port === 5432) {
|
|
12
|
+
console.error(`pgtyped did not receive the reported ephemeral port: ${port}`);
|
|
13
|
+
process.exit(3);
|
|
14
|
+
}
|
|
15
|
+
if (config.db.user !== "postgres" || config.db.dbName !== "postgres") {
|
|
16
|
+
console.error("unexpected pgtyped user or database");
|
|
17
|
+
process.exit(4);
|
|
18
|
+
}
|
|
19
|
+
if (
|
|
20
|
+
config.customMarker !== "quoted \"value\" with newline\nand unicode ✓" ||
|
|
21
|
+
config.db.password !== "special ✓ password" ||
|
|
22
|
+
config.db.ssl !== false ||
|
|
23
|
+
"dbUrl" in config
|
|
24
|
+
) {
|
|
25
|
+
console.error("derived pgtyped config did not preserve its shape safely");
|
|
26
|
+
process.exit(5);
|
|
27
|
+
}
|
|
28
|
+
console.log(`PGTYPED_CONNECTION_OK:${port}`);
|
|
29
|
+
console.log("PGTYPED_CONFIG_SHAPE_OK");
|
|
30
|
+
console.log(`PGTYPED_CONFIG_PATH:${configPath}`);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { main } from "../scripts/pgtyped-update.ts";
|
|
9
|
+
|
|
10
|
+
const configMarker = "quoted \"value\" with newline\nand unicode ✓";
|
|
11
|
+
|
|
12
|
+
async function expectPortClosed(port: number): Promise<void> {
|
|
13
|
+
await expect(new Promise<void>((resolve, reject) => {
|
|
14
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
15
|
+
socket.once("connect", () => {
|
|
16
|
+
socket.destroy();
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
socket.once("error", reject);
|
|
20
|
+
})).rejects.toMatchObject({ code: "ECONNREFUSED" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runAndCapture(
|
|
24
|
+
pgtypedBin: URL,
|
|
25
|
+
pgtypedTimeoutMs = 30_000,
|
|
26
|
+
): Promise<{
|
|
27
|
+
closeCount: number;
|
|
28
|
+
derivedConfigPath: string;
|
|
29
|
+
output: string;
|
|
30
|
+
port: number;
|
|
31
|
+
error?: unknown;
|
|
32
|
+
}> {
|
|
33
|
+
const configDirectory = await mkdtemp(join(tmpdir(), "effectstream-pgtyped-test-"));
|
|
34
|
+
const configPath = join(configDirectory, "pgtypedconfig.json");
|
|
35
|
+
await writeFile(configPath, JSON.stringify({
|
|
36
|
+
customMarker: configMarker,
|
|
37
|
+
db: {
|
|
38
|
+
dbName: "wrong-db",
|
|
39
|
+
host: "wrong-host",
|
|
40
|
+
password: "special ✓ password",
|
|
41
|
+
port: 1,
|
|
42
|
+
ssl: false,
|
|
43
|
+
user: "wrong-user",
|
|
44
|
+
},
|
|
45
|
+
dbUrl: "postgresql://must-not-be-used",
|
|
46
|
+
failOnError: false,
|
|
47
|
+
maxWorkerThreads: 1,
|
|
48
|
+
srcDir: "./sql",
|
|
49
|
+
transforms: [],
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const originalClose = PGlite.prototype.close;
|
|
53
|
+
const originalInfo = console.info;
|
|
54
|
+
const originalLog = console.log;
|
|
55
|
+
const originalError = console.error;
|
|
56
|
+
let closeCount = 0;
|
|
57
|
+
const output: string[] = [];
|
|
58
|
+
|
|
59
|
+
PGlite.prototype.close = async function () {
|
|
60
|
+
closeCount += 1;
|
|
61
|
+
return await originalClose.call(this);
|
|
62
|
+
};
|
|
63
|
+
console.info = (...args: unknown[]) => output.push(args.join(" "));
|
|
64
|
+
console.log = (...args: unknown[]) => output.push(args.join(" "));
|
|
65
|
+
console.error = (...args: unknown[]) => output.push(args.join(" "));
|
|
66
|
+
|
|
67
|
+
let error: unknown;
|
|
68
|
+
try {
|
|
69
|
+
await main({
|
|
70
|
+
userMigrations: [],
|
|
71
|
+
pgtypedConfigPath: configPath,
|
|
72
|
+
pgtypedBin: pgtypedBin.pathname,
|
|
73
|
+
pgtypedTimeoutMs,
|
|
74
|
+
});
|
|
75
|
+
} catch (caught) {
|
|
76
|
+
error = caught;
|
|
77
|
+
} finally {
|
|
78
|
+
PGlite.prototype.close = originalClose;
|
|
79
|
+
console.info = originalInfo;
|
|
80
|
+
console.log = originalLog;
|
|
81
|
+
console.error = originalError;
|
|
82
|
+
await rm(configDirectory, { force: true, recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const text = output.join("\n");
|
|
86
|
+
const match = text.match(/database: server listening on port (\d+)/);
|
|
87
|
+
if (!match) throw new Error(`No reported PGlite port in output:\n${text}`);
|
|
88
|
+
const configMatch = text.match(/PGTYPED_CONFIG_PATH:(.+)/);
|
|
89
|
+
if (!configMatch) throw new Error(`No derived pgtyped config path in output:\n${text}`);
|
|
90
|
+
return {
|
|
91
|
+
closeCount,
|
|
92
|
+
derivedConfigPath: configMatch[1],
|
|
93
|
+
output: text,
|
|
94
|
+
port: Number(match[1]),
|
|
95
|
+
error,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test("pgtyped update uses the reported IPv4 connection and closes every resource on success", async () => {
|
|
100
|
+
const result = await runAndCapture(new URL("./fixtures/pgtyped/success.mjs", import.meta.url));
|
|
101
|
+
|
|
102
|
+
expect(result.error).toBeUndefined();
|
|
103
|
+
expect(result.output).toContain(`PGTYPED_CONNECTION_OK:${result.port}`);
|
|
104
|
+
expect(result.output).toContain("PGTYPED_CONFIG_SHAPE_OK");
|
|
105
|
+
expect(result.port).not.toBe(5432);
|
|
106
|
+
expect(result.closeCount).toBe(1);
|
|
107
|
+
expect(existsSync(result.derivedConfigPath)).toBe(false);
|
|
108
|
+
await expectPortClosed(result.port);
|
|
109
|
+
}, 30_000);
|
|
110
|
+
|
|
111
|
+
test("pgtyped update closes every resource when its child fails", async () => {
|
|
112
|
+
const result = await runAndCapture(new URL("./fixtures/pgtyped/failure.mjs", import.meta.url));
|
|
113
|
+
|
|
114
|
+
expect(result.error).toEqual(new Error("pgtyped failed with exit code 23"));
|
|
115
|
+
expect(result.output).toContain("PGTYPED_INJECTED_FAILURE");
|
|
116
|
+
expect(result.closeCount).toBe(1);
|
|
117
|
+
expect(existsSync(result.derivedConfigPath)).toBe(false);
|
|
118
|
+
await expectPortClosed(result.port);
|
|
119
|
+
}, 30_000);
|
|
120
|
+
|
|
121
|
+
test("pgtyped update times out its child and removes the derived config", async () => {
|
|
122
|
+
const result = await runAndCapture(
|
|
123
|
+
new URL("./fixtures/pgtyped/hang.mjs", import.meta.url),
|
|
124
|
+
100,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(result.error).toEqual(new Error("pgtyped timed out after 100ms"));
|
|
128
|
+
expect(result.output).toContain("PGTYPED_INJECTED_HANG");
|
|
129
|
+
expect(result.closeCount).toBe(1);
|
|
130
|
+
expect(existsSync(result.derivedConfigPath)).toBe(false);
|
|
131
|
+
await expectPortClosed(result.port);
|
|
132
|
+
}, 30_000);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
test("default close with a retained raw client does not poison the next test file", async () => {
|
|
4
|
+
const fixtures = new URL("./fixtures/pglite-close/", import.meta.url);
|
|
5
|
+
const child = Bun.spawn([
|
|
6
|
+
process.execPath,
|
|
7
|
+
"test",
|
|
8
|
+
"--max-concurrency=1",
|
|
9
|
+
new URL("01-sentinel.test.ts", fixtures).pathname,
|
|
10
|
+
new URL("02-live-client.test.ts", fixtures).pathname,
|
|
11
|
+
], {
|
|
12
|
+
cwd: new URL("../../../..", import.meta.url).pathname,
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
EFFECTSTREAM_PGLITE_MULTIFILE_FIXTURE: "1",
|
|
16
|
+
},
|
|
17
|
+
stdout: "pipe",
|
|
18
|
+
stderr: "pipe",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
22
|
+
new Response(child.stdout).text(),
|
|
23
|
+
new Response(child.stderr).text(),
|
|
24
|
+
child.exited,
|
|
25
|
+
]);
|
|
26
|
+
const output = `${stdout}\n${stderr}`;
|
|
27
|
+
|
|
28
|
+
expect(exitCode, output).toBe(0);
|
|
29
|
+
expect(output).toContain("PGLITE_LIVE_CLIENT_CLOSED_DEFAULT");
|
|
30
|
+
expect(output).toContain("PGLITE_SENTINEL_REGISTERED");
|
|
31
|
+
expect(output).not.toContain("Unhandled error between tests");
|
|
32
|
+
expect(output).not.toContain("Connection terminated unexpectedly");
|
|
33
|
+
}, 30_000);
|