@shivaedev/local-postgres 0.2.1 → 0.4.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/CHANGELOG.md +17 -0
- package/README.md +7 -6
- package/dist/server/{local-container.d.ts → localContainer.d.ts} +2 -1
- package/dist/server/localContainer.d.ts.map +1 -0
- package/dist/server/{local-container.js → localContainer.js} +27 -12
- package/dist/server/localContainer.js.map +1 -0
- package/dist/server/{local-docker.d.ts → localDocker.d.ts} +1 -1
- package/dist/server/localDocker.d.ts.map +1 -0
- package/dist/server/{local-docker.js → localDocker.js} +1 -1
- package/dist/server/localDocker.js.map +1 -0
- package/dist/server/localPostgres.d.ts +6 -2
- package/dist/server/localPostgres.d.ts.map +1 -1
- package/dist/server/localPostgres.js +23 -10
- package/dist/server/localPostgres.js.map +1 -1
- package/docs/roadmap.md +6 -4
- package/package.json +6 -5
- package/src/server/{local-container.ts → localContainer.ts} +25 -11
- package/src/server/localPostgres.ts +29 -10
- package/dist/server/local-container.d.ts.map +0 -1
- package/dist/server/local-container.js.map +0 -1
- package/dist/server/local-docker.d.ts.map +0 -1
- package/dist/server/local-docker.js.map +0 -1
- /package/src/server/{local-docker.ts → localDocker.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- Modules are named in camelCase: `local-container.ts` becomes `localContainer.ts`, and `local-docker.ts` becomes `localDocker.ts`.
|
|
8
|
+
|
|
9
|
+
## 0.3.0 - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `POSTGRES_IMAGE` exports the pinned PostgreSQL image, so CI can pull it before `db:setup` starts the container.
|
|
14
|
+
- `localPostgres(environment, { attempts, delayMillis })` sets how many readiness checks `startPostgres` makes after it starts the container and how long it waits between them. The defaults stay at 30 checks one second apart.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- PostgreSQL setup query errors no longer expose connection passwords from userinfo or query parameters through host or Docker client commands, while preserving diagnostics and the original error as `cause`.
|
|
19
|
+
|
|
3
20
|
## 0.2.1 - 2026-10-16
|
|
4
21
|
|
|
5
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const postgres = localPostgres(process.env);
|
|
|
43
43
|
postgres.prepareDatabases([development, tests]);
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
This is a complete preparation script for two application-owned databases. `assertLocalDatabase` checks the name against the list the caller supplies. It rejects a remote host, the wrong port, a different database name or
|
|
46
|
+
This is a complete preparation script for two application-owned databases. `assertLocalDatabase` checks the name against the list the caller supplies. It rejects a remote host, the wrong port, a different database name or any redirecting connection parameter: `host`, `hostaddr`, `port`, `dbname` or `service`. Preparation and SQL execution also reject remote database targets.
|
|
47
47
|
|
|
48
48
|
Keep the explicit name checks even though the preparation functions check their destinations. The application's allowlist says which database it intends to touch; service preparation does not choose that policy for it.
|
|
49
49
|
|
|
@@ -76,7 +76,7 @@ Import the setup API from `@shivaedev/local-postgres/localPostgres.ts`:
|
|
|
76
76
|
| --- | --- |
|
|
77
77
|
| `localServer` | The base URL `postgresql://postgres:postgres@127.0.0.1:55432`. Append the application database name. |
|
|
78
78
|
| `assertLocalDatabase(value: string, names: readonly string[]): URL` | Validate a local database URL against application-owned allowed names. |
|
|
79
|
-
| `localPostgres(environment: DockerEnvironment)` | Create the setup functions using the Docker settings to validate. |
|
|
79
|
+
| `localPostgres(environment: DockerEnvironment, readiness?: ReadinessOptions)` | Create the setup functions using the Docker settings to validate. After starting the container, `startPostgres` checks readiness `attempts` times (default 30), waiting `delayMillis` between checks (default 1000). |
|
|
80
80
|
| `prepareDatabases(values: readonly string[]): void` | Prepare the service and create absent databases. |
|
|
81
81
|
| `startPostgres(connection?: string): void` | Prepare the service; the default connection is `localServer`. |
|
|
82
82
|
| `sql(value: string \| URL, query: string): string` | Execute SQL through PostgreSQL's command-line client. |
|
|
@@ -87,8 +87,8 @@ The package's module export pattern also exposes these lower-level functions:
|
|
|
87
87
|
|
|
88
88
|
| Module | Exports |
|
|
89
89
|
| --- | --- |
|
|
90
|
-
| `@shivaedev/local-postgres/
|
|
91
|
-
| `@shivaedev/local-postgres/
|
|
90
|
+
| `@shivaedev/local-postgres/localDocker.ts` | `DockerEnvironment`, the type with optional `DOCKER_CONTEXT` and `DOCKER_HOST`; `docker(environment, args, options?)`, the local-endpoint-checked Docker command function. |
|
|
91
|
+
| `@shivaedev/local-postgres/localContainer.ts` | `startContainer(docker)`, the container-start function; `sqlInContainer(docker, url, args, options)`, the container SQL-client function. Both take a Docker command callback. |
|
|
92
92
|
|
|
93
93
|
Use `localPostgres.ts` for application preparation. The lower-level functions describe the command paths it uses and take Node child-process options where shown.
|
|
94
94
|
|
|
@@ -98,7 +98,7 @@ Use `localPostgres.ts` for application preparation. The lower-level functions de
|
|
|
98
98
|
pnpm add --save-dev @shivaedev/local-postgres
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
The package requires Node 24 or later and has no runtime dependencies. It needs host `psql` or a local Docker installation. SQL uses host `psql` when available and falls back to the container's client when the host executable is missing.
|
|
101
|
+
The package requires Node 24 or later and has no runtime dependencies. It needs host `psql` or a local Docker installation. SQL uses host `psql` when available and falls back to the container's client when the host executable is missing. Both clients receive the connection password through `PGPASSWORD`, outside the command line. Query failures retain PostgreSQL's diagnostics and the original error as `cause` without exposing the connection password.
|
|
102
102
|
|
|
103
103
|
The shared baseline is PostgreSQL 18.6 on loopback port 55432. Docker preparation uses container and volume name `development-postgres`, loopback publication `127.0.0.1:55432:5432`, and this image:
|
|
104
104
|
|
|
@@ -116,5 +116,6 @@ Pass the process's actual Docker settings to `localPostgres`, as `process.env` i
|
|
|
116
116
|
- Commands block the calling thread. Keep them in development and test setup tooling.
|
|
117
117
|
- Keep allowed names, schema changes, test isolation and cleanup in the application. Preparation is not a database reset.
|
|
118
118
|
- A prepared database name has 1–63 lowercase letters, digits or underscores and starts with a letter. An allowlisted connection URL does not bypass this SQL-name constraint.
|
|
119
|
-
-
|
|
119
|
+
- Scripted command tests cover container start/create, image-mismatch refusal, client selection, Docker context precedence and local endpoint variants. A lifecycle test simulates host-client absence and delayed container discovery, then uses real Docker and PostgreSQL to check readiness. Real service tests cover supplied container-client credentials, password-free failure diagnostics on both clients and data preservation across repeated preparation.
|
|
120
|
+
- Rejection of an exact running PostgreSQL version mismatch still needs a test; image-mismatch command tests do not prove that check.
|
|
120
121
|
- Concurrent creation of the same absent database is not covered by the package's tests. Prepare databases before parallel tests begin.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
|
|
2
|
+
export declare const POSTGRES_IMAGE = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
|
|
2
3
|
type Docker = (args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding) => string;
|
|
3
4
|
export declare function sqlInContainer(docker: Docker, url: URL, args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding): string;
|
|
4
5
|
export declare function startContainer(docker: Docker): void;
|
|
5
6
|
export {};
|
|
6
|
-
//# sourceMappingURL=
|
|
7
|
+
//# sourceMappingURL=localContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localContainer.d.ts","sourceRoot":"","sources":["../../src/server/localContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qCAAqC,EAAE,MAAM,oBAAoB,CAAC;AAGhF,eAAO,MAAM,cAAc,iGAAiG,CAAC;AAE7H,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,qCAAqC,KAAK,MAAM,CAAC;AAEnG,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qCAAqC,UAuB/H;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,QA+B5C"}
|
|
@@ -1,38 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
export const POSTGRES_IMAGE = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
|
|
3
|
+
const CONTAINER = "development-postgres";
|
|
3
4
|
export function sqlInContainer(docker, url, args, options) {
|
|
4
5
|
const id = docker(["ps", "--filter", "publish=55432", "--format", "{{.ID}}"], options).trim();
|
|
5
6
|
if (!id || id.includes("\n")) {
|
|
6
7
|
throw new Error("Install psql or start the shared PostgreSQL service with Docker.");
|
|
7
8
|
}
|
|
8
|
-
if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !==
|
|
9
|
+
if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !== POSTGRES_IMAGE) {
|
|
9
10
|
throw new Error("The shared PostgreSQL image differs from the pinned baseline. Upgrade explicitly, preserving data.");
|
|
10
11
|
}
|
|
12
|
+
// libpq treats + literally in URI passwords.
|
|
13
|
+
const password = new URLSearchParams(url.search.replaceAll("+", "%2B")).getAll("password").at(-1) ?? decodeURIComponent(url.password);
|
|
11
14
|
const inside = new URL(url);
|
|
12
15
|
inside.hostname = "127.0.0.1";
|
|
13
16
|
inside.port = "5432";
|
|
14
|
-
|
|
17
|
+
inside.password = "";
|
|
18
|
+
inside.searchParams.delete("password");
|
|
19
|
+
try {
|
|
20
|
+
return docker(["exec", "--env", "PGPASSWORD", id, "psql", ...args, "--dbname", inside.toString()], {
|
|
21
|
+
...options,
|
|
22
|
+
env: { ...env, ...options.env, ...(password && { "PGPASSWORD": password }) },
|
|
23
|
+
}).trim();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
|
|
27
|
+
}
|
|
15
28
|
}
|
|
16
29
|
export function startContainer(docker) {
|
|
17
30
|
docker(["info"]);
|
|
18
|
-
let existing
|
|
31
|
+
let existing;
|
|
19
32
|
try {
|
|
20
|
-
docker(["container", "inspect",
|
|
33
|
+
docker(["container", "inspect", CONTAINER]);
|
|
21
34
|
existing = true;
|
|
22
35
|
}
|
|
23
|
-
catch {
|
|
36
|
+
catch {
|
|
37
|
+
existing = false;
|
|
38
|
+
}
|
|
24
39
|
if (existing) {
|
|
25
|
-
if (docker(["inspect", "--format", "{{.Config.Image}}",
|
|
40
|
+
if (docker(["inspect", "--format", "{{.Config.Image}}", CONTAINER]).trim() !== POSTGRES_IMAGE) {
|
|
26
41
|
throw new Error("The shared PostgreSQL container uses a different image. Preserve its volume and upgrade it explicitly.");
|
|
27
42
|
}
|
|
28
|
-
docker(["start",
|
|
43
|
+
docker(["start", CONTAINER]);
|
|
29
44
|
}
|
|
30
45
|
else {
|
|
31
46
|
docker([
|
|
32
47
|
"run",
|
|
33
48
|
"--detach",
|
|
34
49
|
"--name",
|
|
35
|
-
|
|
50
|
+
CONTAINER,
|
|
36
51
|
"--publish",
|
|
37
52
|
"127.0.0.1:55432:5432",
|
|
38
53
|
"--env",
|
|
@@ -41,8 +56,8 @@ export function startContainer(docker) {
|
|
|
41
56
|
"POSTGRES_PASSWORD=postgres",
|
|
42
57
|
"--mount",
|
|
43
58
|
"type=volume,src=development-postgres,dst=/var/lib/postgresql",
|
|
44
|
-
|
|
59
|
+
POSTGRES_IMAGE,
|
|
45
60
|
]);
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
|
-
//# sourceMappingURL=
|
|
63
|
+
//# sourceMappingURL=localContainer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localContainer.js","sourceRoot":"","sources":["../../src/server/localContainer.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,4 +4,4 @@ export type DockerEnvironment = Readonly<{
|
|
|
4
4
|
"DOCKER_HOST"?: string | undefined;
|
|
5
5
|
}>;
|
|
6
6
|
export declare function docker(environment: DockerEnvironment, args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding): string;
|
|
7
|
-
//# sourceMappingURL=
|
|
7
|
+
//# sourceMappingURL=localDocker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localDocker.d.ts","sourceRoot":"","sources":["../../src/server/localDocker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qCAAqC,EAAgB,MAAM,oBAAoB,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAExH,wBAAgB,MAAM,CACrB,WAAW,EAAE,iBAAiB,EAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,qCAA4D,UAmBrE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localDocker.js","sourceRoot":"","sources":["../../src/server/localDocker.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI9F,MAAM,UAAU,MAAM,CACrB,WAA8B,EAC9B,IAAuB,EACvB,OAAO,GAA0C,EAAE,QAAQ,EAAE,MAAM,EAAE;IAErE,MAAM,OAAO,GAAG,WAAW,CAAC,cAAc,CAAC;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,WAAW,CAAC;IACrC,MAAM,QAAQ,GACb,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC;WACf,YAAY,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,4BAA4B,CAAC,EAAE;YAC1H,QAAQ,EAAE,MAAM;SAChB,CAAC,CAAC,IAAI,EAAE,CAAC;IACX,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC9B,IACC,CAAC,CACA,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;WACrF,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAC1F,EACA,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,qFAAqF,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { type DockerEnvironment } from "./
|
|
1
|
+
import { type DockerEnvironment } from "./localDocker.ts";
|
|
2
2
|
export declare const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
|
|
3
3
|
export declare function assertLocalDatabase(value: string, names: readonly string[]): import("url").URL;
|
|
4
|
-
export
|
|
4
|
+
export interface ReadinessOptions {
|
|
5
|
+
readonly attempts?: number;
|
|
6
|
+
readonly delayMillis?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function localPostgres(environment: DockerEnvironment, { attempts, delayMillis }?: ReadinessOptions): {
|
|
5
9
|
prepareDatabases: (values: readonly string[]) => void;
|
|
6
10
|
sql: (value: string | URL, query: string) => string;
|
|
7
11
|
startPostgres: (connection?: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localPostgres.d.ts","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"localPostgres.d.ts","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,iBAAiB,EAAU,MAAM,kBAAkB,CAAC;AAElE,eAAO,MAAM,WAAW,mDAAmD,CAAC;AAE5E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,qBAY1E;AAQD,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,iBAAiB,EAAE,EAAE,QAAa,EAAE,WAAkB,EAAE,GAAE,gBAAqB;+BAwDvF,SAAS,MAAM,EAAE;iBApD/B,MAAM,GAAG,GAAG,SAAS,MAAM;;EAwE/C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { env } from "node:process";
|
|
3
|
+
import { sqlInContainer, startContainer } from "./localContainer.js";
|
|
4
|
+
import { docker } from "./localDocker.js";
|
|
4
5
|
export const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
|
|
5
6
|
export function assertLocalDatabase(value, names) {
|
|
6
7
|
const url = new URL(value);
|
|
@@ -13,13 +14,21 @@ export function assertLocalDatabase(value, names) {
|
|
|
13
14
|
}
|
|
14
15
|
return url;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
function clientEnvironment(url) {
|
|
18
|
+
// libpq treats + literally in URI passwords.
|
|
19
|
+
const password = new URLSearchParams(url.search.replaceAll("+", "%2B")).getAll("password").at(-1) ?? decodeURIComponent(url.password);
|
|
20
|
+
return { ...env, ...(password && { "PGPASSWORD": password }) };
|
|
21
|
+
}
|
|
22
|
+
export function localPostgres(environment, { attempts = 30, delayMillis = 1000 } = {}) {
|
|
17
23
|
function localDocker(args, options = { encoding: "utf8" }) {
|
|
18
24
|
return docker(environment, args, options);
|
|
19
25
|
}
|
|
20
26
|
function sql(value, query) {
|
|
21
27
|
const url = new URL(value);
|
|
22
28
|
assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
|
|
29
|
+
const childEnvironment = clientEnvironment(url);
|
|
30
|
+
url.password = "";
|
|
31
|
+
url.searchParams.delete("password");
|
|
23
32
|
for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
|
|
24
33
|
url.searchParams.delete(key);
|
|
25
34
|
}
|
|
@@ -27,15 +36,19 @@ export function localPostgres(environment) {
|
|
|
27
36
|
if (!url.searchParams.has("connect_timeout")) {
|
|
28
37
|
url.searchParams.set("connect_timeout", "3");
|
|
29
38
|
}
|
|
30
|
-
const options = {
|
|
39
|
+
const options = {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
env: childEnvironment,
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
+
};
|
|
31
44
|
try {
|
|
32
|
-
return execFileSync("psql", [...args, "--dbname",
|
|
45
|
+
return execFileSync("psql", [...args, "--dbname", url.toString()], options).trim();
|
|
33
46
|
}
|
|
34
47
|
catch (error) {
|
|
35
|
-
if (
|
|
36
|
-
|
|
48
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
49
|
+
return sqlInContainer(localDocker, url, args, options);
|
|
37
50
|
}
|
|
38
|
-
|
|
51
|
+
throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
54
|
function startPostgres(connection = localServer) {
|
|
@@ -48,13 +61,13 @@ export function localPostgres(environment) {
|
|
|
48
61
|
}
|
|
49
62
|
catch {
|
|
50
63
|
startContainer(localDocker);
|
|
51
|
-
for (let attempt = 0; attempt <
|
|
64
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
52
65
|
try {
|
|
53
66
|
version = sql(server, "SHOW server_version");
|
|
54
67
|
break;
|
|
55
68
|
}
|
|
56
69
|
catch {
|
|
57
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
70
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMillis);
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localPostgres.js","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"localPostgres.js","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"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"}
|
package/docs/roadmap.md
CHANGED
|
@@ -5,15 +5,17 @@ This file owns local service preparation, its command paths and package API ques
|
|
|
5
5
|
## Built
|
|
6
6
|
|
|
7
7
|
- [x] The main setup module exposes an explicit local server URL, application name validation, service preparation, missing-database preparation and SQL execution.
|
|
8
|
-
- [x] Tests reject a remote database host, the wrong port, an unapproved database name and
|
|
8
|
+
- [x] Tests reject a remote database host, the wrong port, an unapproved database name and each redirecting query parameter (`host`, `hostaddr`, `port`, `dbname` and `service`). Invalid database identifiers are rejected before preparation. All three setup functions reject remote database targets before connecting.
|
|
9
9
|
- [x] A real PostgreSQL test retains an inserted row across repeated preparation and checks PostgreSQL 18.6, including queries whose URLs carry `schema` and `connection_limit` settings.
|
|
10
|
-
- [x]
|
|
10
|
+
- [x] Scripted container tests cover daemon failures, starting an existing pinned container, creating a missing container with persistent storage and loopback publication, refusing image mismatches without replacement, and refusing client selection when discovery finds zero or multiple containers.
|
|
11
|
+
- [x] Scripted Docker tests cover active and explicit context selection, context precedence over `DOCKER_HOST`, remote endpoint refusal, and local Unix socket, Windows pipe and loopback TCP endpoints.
|
|
12
|
+
- [x] The lifecycle test simulates missing host `psql` and delayed container discovery, then checks readiness through the real Docker client and PostgreSQL service. It scripts container startup rather than starting or creating a real container.
|
|
13
|
+
- [x] Real PostgreSQL tests check supplied container-client credentials and failure diagnostics on both host and Docker clients, including password-free original errors retained as causes.
|
|
11
14
|
- [x] The installed consumer fixture checks the main module's exports and types.
|
|
12
15
|
|
|
13
16
|
## Next
|
|
14
17
|
|
|
15
|
-
- [ ] Add
|
|
16
|
-
- [ ] Extend destination tests to each redirected connection parameter and invalid database name, and Docker tests to context selection and local endpoint variants.
|
|
18
|
+
- [ ] Add a test for rejection of an exact running PostgreSQL version mismatch. Scripted image-mismatch checks do not prove this service-version check.
|
|
17
19
|
- [ ] Establish whether parallel setup needs coordinated creation of an absent database; keep application test startup ordered until there is evidence for another contract.
|
|
18
20
|
|
|
19
21
|
## Open questions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/local-postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Repeatable local PostgreSQL preparation with one shared persistent service",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/local-postgres#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -24,9 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
26
|
"./*.ts": {
|
|
27
|
-
"types": "./dist/server/*.d.ts",
|
|
28
27
|
"source": "./src/server/*.ts",
|
|
29
|
-
"
|
|
28
|
+
"types": "./dist/server/*.d.ts",
|
|
30
29
|
"default": "./dist/server/*.js"
|
|
31
30
|
},
|
|
32
31
|
"./package.json": "./package.json"
|
|
@@ -42,9 +41,11 @@
|
|
|
42
41
|
"README.md"
|
|
43
42
|
],
|
|
44
43
|
"devDependencies": {
|
|
45
|
-
"@shivaedev/quality": "0.
|
|
46
|
-
"@
|
|
44
|
+
"@shivaedev/quality": "0.25.0",
|
|
45
|
+
"@shivaedev/test-story": "0.7.1",
|
|
46
|
+
"@types/node": "24.19.2",
|
|
47
47
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
48
|
+
"effect": "4.0.4",
|
|
48
49
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
49
50
|
"vitest": "5.0.3"
|
|
50
51
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
|
|
2
|
+
import { env } from "node:process";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
4
|
+
export const POSTGRES_IMAGE = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
|
|
5
|
+
const CONTAINER = "development-postgres";
|
|
5
6
|
type Docker = (args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding) => string;
|
|
6
7
|
|
|
7
8
|
export function sqlInContainer(docker: Docker, url: URL, args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding) {
|
|
@@ -9,33 +10,46 @@ export function sqlInContainer(docker: Docker, url: URL, args: readonly string[]
|
|
|
9
10
|
if (!id || id.includes("\n")) {
|
|
10
11
|
throw new Error("Install psql or start the shared PostgreSQL service with Docker.");
|
|
11
12
|
}
|
|
12
|
-
if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !==
|
|
13
|
+
if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !== POSTGRES_IMAGE) {
|
|
13
14
|
throw new Error("The shared PostgreSQL image differs from the pinned baseline. Upgrade explicitly, preserving data.");
|
|
14
15
|
}
|
|
16
|
+
// libpq treats + literally in URI passwords.
|
|
17
|
+
const password = new URLSearchParams(url.search.replaceAll("+", "%2B")).getAll("password").at(-1) ?? decodeURIComponent(url.password);
|
|
15
18
|
const inside = new URL(url);
|
|
16
19
|
inside.hostname = "127.0.0.1";
|
|
17
20
|
inside.port = "5432";
|
|
18
|
-
|
|
21
|
+
inside.password = "";
|
|
22
|
+
inside.searchParams.delete("password");
|
|
23
|
+
try {
|
|
24
|
+
return docker(["exec", "--env", "PGPASSWORD", id, "psql", ...args, "--dbname", inside.toString()], {
|
|
25
|
+
...options,
|
|
26
|
+
env: { ...env, ...options.env, ...(password && { "PGPASSWORD": password }) },
|
|
27
|
+
}).trim();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
|
|
30
|
+
}
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
export function startContainer(docker: Docker) {
|
|
22
34
|
docker(["info"]);
|
|
23
|
-
let existing
|
|
35
|
+
let existing: boolean;
|
|
24
36
|
try {
|
|
25
|
-
docker(["container", "inspect",
|
|
37
|
+
docker(["container", "inspect", CONTAINER]);
|
|
26
38
|
existing = true;
|
|
27
|
-
} catch {
|
|
39
|
+
} catch {
|
|
40
|
+
existing = false;
|
|
41
|
+
}
|
|
28
42
|
if (existing) {
|
|
29
|
-
if (docker(["inspect", "--format", "{{.Config.Image}}",
|
|
43
|
+
if (docker(["inspect", "--format", "{{.Config.Image}}", CONTAINER]).trim() !== POSTGRES_IMAGE) {
|
|
30
44
|
throw new Error("The shared PostgreSQL container uses a different image. Preserve its volume and upgrade it explicitly.");
|
|
31
45
|
}
|
|
32
|
-
docker(["start",
|
|
46
|
+
docker(["start", CONTAINER]);
|
|
33
47
|
} else {
|
|
34
48
|
docker([
|
|
35
49
|
"run",
|
|
36
50
|
"--detach",
|
|
37
51
|
"--name",
|
|
38
|
-
|
|
52
|
+
CONTAINER,
|
|
39
53
|
"--publish",
|
|
40
54
|
"127.0.0.1:55432:5432",
|
|
41
55
|
"--env",
|
|
@@ -44,7 +58,7 @@ export function startContainer(docker: Docker) {
|
|
|
44
58
|
"POSTGRES_PASSWORD=postgres",
|
|
45
59
|
"--mount",
|
|
46
60
|
"type=volume,src=development-postgres,dst=/var/lib/postgresql",
|
|
47
|
-
|
|
61
|
+
POSTGRES_IMAGE,
|
|
48
62
|
]);
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ExecFileSyncOptionsWithStringEncoding, execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { env } from "node:process";
|
|
3
|
+
import { sqlInContainer, startContainer } from "./localContainer.ts";
|
|
4
|
+
import { type DockerEnvironment, docker } from "./localDocker.ts";
|
|
4
5
|
|
|
5
6
|
export const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
|
|
6
7
|
|
|
@@ -18,13 +19,27 @@ export function assertLocalDatabase(value: string, names: readonly string[]) {
|
|
|
18
19
|
return url;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
function clientEnvironment(url: URL) {
|
|
23
|
+
// libpq treats + literally in URI passwords.
|
|
24
|
+
const password = new URLSearchParams(url.search.replaceAll("+", "%2B")).getAll("password").at(-1) ?? decodeURIComponent(url.password);
|
|
25
|
+
return { ...env, ...(password && { "PGPASSWORD": password }) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReadinessOptions {
|
|
29
|
+
readonly attempts?: number;
|
|
30
|
+
readonly delayMillis?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function localPostgres(environment: DockerEnvironment, { attempts = 30, delayMillis = 1000 }: ReadinessOptions = {}) {
|
|
22
34
|
function localDocker(args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding = { encoding: "utf8" }) {
|
|
23
35
|
return docker(environment, args, options);
|
|
24
36
|
}
|
|
25
37
|
function sql(value: string | URL, query: string) {
|
|
26
38
|
const url = new URL(value);
|
|
27
39
|
assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
|
|
40
|
+
const childEnvironment = clientEnvironment(url);
|
|
41
|
+
url.password = "";
|
|
42
|
+
url.searchParams.delete("password");
|
|
28
43
|
for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
|
|
29
44
|
url.searchParams.delete(key);
|
|
30
45
|
}
|
|
@@ -33,14 +48,18 @@ export function localPostgres(environment: DockerEnvironment) {
|
|
|
33
48
|
if (!url.searchParams.has("connect_timeout")) {
|
|
34
49
|
url.searchParams.set("connect_timeout", "3");
|
|
35
50
|
}
|
|
36
|
-
const options = {
|
|
51
|
+
const options = {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
env: childEnvironment,
|
|
54
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
55
|
+
} satisfies ExecFileSyncOptionsWithStringEncoding;
|
|
37
56
|
try {
|
|
38
|
-
return execFileSync("psql", [...args, "--dbname",
|
|
57
|
+
return execFileSync("psql", [...args, "--dbname", url.toString()], options).trim();
|
|
39
58
|
} catch (error) {
|
|
40
|
-
if (
|
|
41
|
-
|
|
59
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
60
|
+
return sqlInContainer(localDocker, url, args, options);
|
|
42
61
|
}
|
|
43
|
-
|
|
62
|
+
throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
|
|
@@ -53,12 +72,12 @@ export function localPostgres(environment: DockerEnvironment) {
|
|
|
53
72
|
version = sql(server, "SHOW server_version");
|
|
54
73
|
} catch {
|
|
55
74
|
startContainer(localDocker);
|
|
56
|
-
for (let attempt = 0; attempt <
|
|
75
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
57
76
|
try {
|
|
58
77
|
version = sql(server, "SHOW server_version");
|
|
59
78
|
break;
|
|
60
79
|
} catch {
|
|
61
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMillis);
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-container.d.ts","sourceRoot":"","sources":["../../src/server/local-container.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qCAAqC,EAAE,MAAM,oBAAoB,CAAC;AAIhF,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,qCAAqC,KAAK,MAAM,CAAC;AAEnG,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qCAAqC,UAY/H;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,QA6B5C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-container.js","sourceRoot":"","sources":["../../src/server/local-container.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAG,8FAA8F,CAAC;AAC7G,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAGzC,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,GAAQ,EAAE,IAAuB,EAAE,OAA8C;IAC/H,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9F,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACrF,CAAC;IACD,IAAI,MAAM,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;QACxF,MAAM,IAAI,KAAK,CAAC,oGAAoG,CAAC,CAAC;IACvH,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,MAAM,CAAC,QAAQ,GAAG,WAAW,CAAC;IAC9B,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC;IACrB,OAAO,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc;IAC5C,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACjB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,CAAC;QACJ,MAAM,CAAC,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5C,QAAQ,GAAG,IAAI,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,IAAI,QAAQ,EAAE,CAAC;QACd,IAAI,MAAM,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;YACtF,MAAM,IAAI,KAAK,CAAC,wGAAwG,CAAC,CAAC;QAC3H,CAAC;QACD,MAAM,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;IAC9B,CAAC;SAAM,CAAC;QACP,MAAM,CAAC;YACN,KAAK;YACL,UAAU;YACV,QAAQ;YACR,SAAS;YACT,WAAW;YACX,sBAAsB;YACtB,OAAO;YACP,wBAAwB;YACxB,OAAO;YACP,4BAA4B;YAC5B,SAAS;YACT,8DAA8D;YAC9D,KAAK;SACL,CAAC,CAAC;IACJ,CAAC;AACF,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-docker.d.ts","sourceRoot":"","sources":["../../src/server/local-docker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qCAAqC,EAAgB,MAAM,oBAAoB,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAExH,wBAAgB,MAAM,CACrB,WAAW,EAAE,iBAAiB,EAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,qCAA4D,UAmBrE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-docker.js","sourceRoot":"","sources":["../../src/server/local-docker.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI9F,MAAM,UAAU,MAAM,CACrB,WAA8B,EAC9B,IAAuB,EACvB,OAAO,GAA0C,EAAE,QAAQ,EAAE,MAAM,EAAE;IAErE,MAAM,OAAO,GAAG,WAAW,CAAC,cAAc,CAAC;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,WAAW,CAAC;IACrC,MAAM,QAAQ,GACb,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC;WACf,YAAY,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,4BAA4B,CAAC,EAAE;YAC1H,QAAQ,EAAE,MAAM;SAChB,CAAC,CAAC,IAAI,EAAE,CAAC;IACX,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC9B,IACC,CAAC,CACA,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;WACrF,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAC1F,EACA,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,qFAAqF,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC"}
|
|
File without changes
|