@shivaedev/local-postgres 0.0.0 → 0.1.1

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 ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-10-04
4
+
5
+ ### Changed
6
+
7
+ - Imports that leave their folder go through `#` aliases declared in the `imports` field of `package.json`: `#*.ts` resolves to `src` under the `source` condition and to `dist` otherwise, and `#test/*` names a test file. The published modules and declarations name the package's own files through these aliases.
8
+
9
+ - Build with the `@shivaedev/quality` tsconfig presets, which target ESNext and allow only erasable TypeScript syntax.
10
+
11
+ - Type-check with TypeScript 7 only. TypeScript 6 still builds the package.
12
+
13
+ - Lint and format with the `@shivaedev/quality` Biome preset through `quality lint` at the repository root, which replaces the package's `check` script.
14
+
15
+ - Run tests through the `@shivaedev/quality` Vitest projects, which take a test's environment from its file name. Type tests are named `*.typecheck.test.ts`.
16
+
17
+ - Keep `package.json` in sort-package-json key order, checked by `quality lint`.
18
+
19
+ ## 0.1.0
20
+
21
+ - Prepare one persistent local PostgreSQL 18.6 service and create missing databases without resetting existing data.
22
+ - Validate loopback database targets and local Docker endpoints; use host or container PostgreSQL client tools.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
1
  # @shivaedev/local-postgres
2
2
 
3
- Placeholder that reserves the package name. The first real release follows.
3
+ Prepare one shared local PostgreSQL service without replacing containers, deleting volumes or resetting data. The service uses PostgreSQL 18.6 on loopback port 55432, pinned to `postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873`.
4
+
5
+ This Node 24+ package has no runtime dependencies. It uses a local Docker daemon when no service is available; an existing native service requires host `psql`. With Docker, the container provides `psql` when it is absent from the host.
6
+
7
+ ```ts
8
+ import { assertLocalDatabase, localPostgres, localServer } from "@shivaedev/local-postgres";
9
+
10
+ const development = `${localServer}/example_dev`;
11
+ assertLocalDatabase(development, ["example_dev"]);
12
+ const postgres = localPostgres(process.env);
13
+ postgres.prepareDatabases([development]);
14
+ ```
15
+
16
+ `localPostgres(environment)` reads only `DOCKER_CONTEXT` and `DOCKER_HOST` from the supplied settings. Pass the actual process environment or both settings from the application's configuration service so remote Docker endpoints are rejected. Importing this package does not start a service.
17
+
18
+ `prepareDatabases(urls)` starts or reuses the service, checks its version and creates missing databases. `startPostgres(connection)` only prepares the service. `sql(connection, query)` uses host `psql`, or the pinned container's client. Database connection settings come from the supplied URL, including credentials. A new container uses the documented local defaults and a persistent `development-postgres` volume; it is never automatically upgraded.
19
+
20
+ `assertLocalDatabase(url, names)` permits only the supplied names on localhost or 127.0.0.1 port 55432. It rejects connection parameters that override the destination. Callers own their application-specific names, test isolation, schema initialization, migrations and seeds. This package does not select application databases or run migrations or seeds.
@@ -0,0 +1,9 @@
1
+ import { type DockerEnvironment } from "./local-docker.ts";
2
+ export declare const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
3
+ export declare function assertLocalDatabase(value: string, names: readonly string[]): import("url").URL;
4
+ export declare function localPostgres(environment: DockerEnvironment): {
5
+ prepareDatabases: (values: readonly string[]) => void;
6
+ sql: (value: string | URL, query: string) => string;
7
+ startPostgres: (connection?: string) => void;
8
+ };
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAU,MAAM,mBAAmB,CAAC;AAEnE,eAAO,MAAM,WAAW,mDAAmD,CAAC;AAE5E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,qBAY1E;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,iBAAiB;+BAgDzB,SAAS,MAAM,EAAE;iBA7C/B,MAAM,GAAG,GAAG,SAAS,MAAM;;EAiE/C"}
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { sqlInContainer, startContainer } from "./local-container.js";
3
+ import { docker } from "./local-docker.js";
4
+ export const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
5
+ export function assertLocalDatabase(value, names) {
6
+ const url = new URL(value);
7
+ const name = url.pathname.slice(1);
8
+ if (!(["postgres:", "postgresql:"].includes(url.protocol) && ["localhost", "127.0.0.1"].includes(url.hostname))
9
+ || url.port !== "55432"
10
+ || ["host", "hostaddr", "port", "dbname", "service"].some((key) => url.searchParams.has(key))
11
+ || !names.includes(name)) {
12
+ throw new Error(`Use a local database on 127.0.0.1:55432 named ${names.join(" or ")}.`);
13
+ }
14
+ return url;
15
+ }
16
+ export function localPostgres(environment) {
17
+ const localDocker = (args, options = { encoding: "utf8" }) => docker(environment, args, options);
18
+ function sql(value, query) {
19
+ const url = new URL(value);
20
+ assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
21
+ for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
22
+ url.searchParams.delete(key);
23
+ }
24
+ const args = ["-X", "--set", "ON_ERROR_STOP=1", "-At", "-c", query];
25
+ if (!url.searchParams.has("connect_timeout")) {
26
+ url.searchParams.set("connect_timeout", "3");
27
+ }
28
+ const options = { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] };
29
+ try {
30
+ return execFileSync("psql", [...args, "--dbname", String(url)], options).trim();
31
+ }
32
+ catch (error) {
33
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
34
+ throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.");
35
+ }
36
+ return sqlInContainer(localDocker, url, args, options);
37
+ }
38
+ }
39
+ function startPostgres(connection = localServer) {
40
+ const server = new URL(connection);
41
+ server.pathname = "/postgres";
42
+ assertLocalDatabase(server.toString(), ["postgres"]);
43
+ let version;
44
+ try {
45
+ version = sql(server, "SHOW server_version");
46
+ }
47
+ catch {
48
+ startContainer(localDocker);
49
+ for (let attempt = 0; attempt < 30; attempt++) {
50
+ try {
51
+ version = sql(server, "SHOW server_version");
52
+ break;
53
+ }
54
+ catch {
55
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
56
+ }
57
+ }
58
+ }
59
+ if (version !== "18.6") {
60
+ throw new Error("Expected local PostgreSQL 18.6. Check the existing service; setup never removes containers or data.");
61
+ }
62
+ }
63
+ function prepareDatabases(values) {
64
+ for (const value of values) {
65
+ const name = new URL(value).pathname.slice(1);
66
+ assertLocalDatabase(value, [name]);
67
+ if (!/^[a-z][a-z0-9_]{0,62}$/u.test(name)) {
68
+ throw new Error("Invalid local database name.");
69
+ }
70
+ }
71
+ startPostgres(values[0]);
72
+ for (const value of values) {
73
+ const url = new URL(value);
74
+ const name = url.pathname.slice(1);
75
+ const server = new URL(url);
76
+ server.pathname = "/postgres";
77
+ if (!sql(server, `SELECT 1 FROM pg_database WHERE datname = '${name}'`)) {
78
+ sql(server, `CREATE DATABASE "${name}"`);
79
+ }
80
+ }
81
+ }
82
+ return { prepareDatabases, sql, startPostgres };
83
+ }
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
2
+ type Docker = (args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding) => string;
3
+ export declare function sqlInContainer(docker: Docker, url: URL, args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding): string;
4
+ export declare function startContainer(docker: Docker): void;
5
+ export {};
6
+ //# sourceMappingURL=local-container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-container.d.ts","sourceRoot":"","sources":["../src/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"}
@@ -0,0 +1,48 @@
1
+ const image = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
2
+ const container = "development-postgres";
3
+ export function sqlInContainer(docker, url, args, options) {
4
+ const id = docker(["ps", "--filter", "publish=55432", "--format", "{{.ID}}"], options).trim();
5
+ if (!id || id.includes("\n")) {
6
+ throw new Error("Install psql or start the shared PostgreSQL service with Docker.");
7
+ }
8
+ if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !== image) {
9
+ throw new Error("The shared PostgreSQL image differs from the pinned baseline. Upgrade explicitly, preserving data.");
10
+ }
11
+ const inside = new URL(url);
12
+ inside.hostname = "127.0.0.1";
13
+ inside.port = "5432";
14
+ return docker(["exec", id, "psql", ...args, "--dbname", inside.toString()], options).trim();
15
+ }
16
+ export function startContainer(docker) {
17
+ docker(["info"]);
18
+ let existing = false;
19
+ try {
20
+ docker(["container", "inspect", container]);
21
+ existing = true;
22
+ }
23
+ catch { }
24
+ if (existing) {
25
+ if (docker(["inspect", "--format", "{{.Config.Image}}", container]).trim() !== image) {
26
+ throw new Error("The shared PostgreSQL container uses a different image. Preserve its volume and upgrade it explicitly.");
27
+ }
28
+ docker(["start", container]);
29
+ }
30
+ else {
31
+ docker([
32
+ "run",
33
+ "--detach",
34
+ "--name",
35
+ container,
36
+ "--publish",
37
+ "127.0.0.1:55432:5432",
38
+ "--env",
39
+ "POSTGRES_USER=postgres",
40
+ "--env",
41
+ "POSTGRES_PASSWORD=postgres",
42
+ "--mount",
43
+ "type=volume,src=development-postgres,dst=/var/lib/postgresql",
44
+ image,
45
+ ]);
46
+ }
47
+ }
48
+ //# sourceMappingURL=local-container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-container.js","sourceRoot":"","sources":["../src/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"}
@@ -0,0 +1,7 @@
1
+ import { type ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
2
+ export type DockerEnvironment = Readonly<{
3
+ DOCKER_CONTEXT?: string | undefined;
4
+ DOCKER_HOST?: string | undefined;
5
+ }>;
6
+ export declare function docker(environment: DockerEnvironment, args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding): string;
7
+ //# sourceMappingURL=local-docker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-docker.d.ts","sourceRoot":"","sources":["../src/local-docker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qCAAqC,EAAgB,MAAM,oBAAoB,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAEpH,wBAAgB,MAAM,CACrB,WAAW,EAAE,iBAAiB,EAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,qCAA4D,UAmBrE"}
@@ -0,0 +1,16 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export function docker(environment, args, options = { encoding: "utf8" }) {
3
+ const context = environment.DOCKER_CONTEXT;
4
+ const host = environment.DOCKER_HOST;
5
+ const endpoint = (!context && host)
6
+ || execFileSync("docker", ["context", "inspect", ...(context ? [context] : []), "--format", "{{.Endpoints.docker.Host}}"], {
7
+ encoding: "utf8",
8
+ }).trim();
9
+ const url = new URL(endpoint);
10
+ if (!((["unix:", "npipe:"].includes(url.protocol) && ["", "localhost"].includes(url.hostname))
11
+ || (url.protocol === "tcp:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))) {
12
+ throw new Error("Local database setup requires a local Docker endpoint; remote contexts are refused.");
13
+ }
14
+ return execFileSync("docker", args, options);
15
+ }
16
+ //# sourceMappingURL=local-docker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-docker.js","sourceRoot":"","sources":["../src/local-docker.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI9F,MAAM,UAAU,MAAM,CACrB,WAA8B,EAC9B,IAAuB,EACvB,UAAiD,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"}
package/package.json CHANGED
@@ -1,10 +1,60 @@
1
1
  {
2
2
  "name": "@shivaedev/local-postgres",
3
- "version": "0.0.0",
4
- "description": "Placeholder that reserves the package name; the first real release follows.",
5
- "license": "MIT",
3
+ "version": "0.1.1",
4
+ "description": "Repeatable local PostgreSQL preparation with one shared persistent service",
5
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/local-postgres#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ShivaeDev/platform/issues"
8
+ },
6
9
  "repository": {
7
10
  "type": "git",
8
- "url": "git+https://github.com/ShivaeDev/platform.git"
11
+ "url": "git+https://github.com/ShivaeDev/platform.git",
12
+ "directory": "packages/local-postgres"
13
+ },
14
+ "license": "MIT",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "imports": {
18
+ "#test/*": "./test/*",
19
+ "#*.ts": {
20
+ "source": "./src/*.ts",
21
+ "types": "./dist/*.d.ts",
22
+ "default": "./dist/*.js"
23
+ }
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "source": "./src/index.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "CHANGELOG.md",
38
+ "README.md"
39
+ ],
40
+ "devDependencies": {
41
+ "@shivaedev/quality": "0.7.3",
42
+ "@types/node": "24.10.1",
43
+ "@typescript/native": "npm:typescript@7.0.2",
44
+ "typescript": "npm:@typescript/typescript6@6.0.2",
45
+ "vitest": "4.1.9"
46
+ },
47
+ "engines": {
48
+ "node": ">=24"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "scripts": {
55
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
56
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit"
9
59
  }
10
60
  }
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { type ExecFileSyncOptionsWithStringEncoding, execFileSync } from "node:child_process";
2
+ import { sqlInContainer, startContainer } from "./local-container.ts";
3
+ import { type DockerEnvironment, docker } from "./local-docker.ts";
4
+
5
+ export const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
6
+
7
+ export function assertLocalDatabase(value: string, names: readonly string[]) {
8
+ const url = new URL(value);
9
+ const name = url.pathname.slice(1);
10
+ if (
11
+ !(["postgres:", "postgresql:"].includes(url.protocol) && ["localhost", "127.0.0.1"].includes(url.hostname))
12
+ || url.port !== "55432"
13
+ || ["host", "hostaddr", "port", "dbname", "service"].some((key) => url.searchParams.has(key))
14
+ || !names.includes(name)
15
+ ) {
16
+ throw new Error(`Use a local database on 127.0.0.1:55432 named ${names.join(" or ")}.`);
17
+ }
18
+ return url;
19
+ }
20
+
21
+ export function localPostgres(environment: DockerEnvironment) {
22
+ const localDocker = (args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding = { encoding: "utf8" }) =>
23
+ docker(environment, args, options);
24
+ function sql(value: string | URL, query: string) {
25
+ const url = new URL(value);
26
+ assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
27
+ for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
28
+ url.searchParams.delete(key);
29
+ }
30
+
31
+ const args = ["-X", "--set", "ON_ERROR_STOP=1", "-At", "-c", query];
32
+ if (!url.searchParams.has("connect_timeout")) {
33
+ url.searchParams.set("connect_timeout", "3");
34
+ }
35
+ const options = { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] } satisfies ExecFileSyncOptionsWithStringEncoding;
36
+ try {
37
+ return execFileSync("psql", [...args, "--dbname", String(url)], options).trim();
38
+ } catch (error) {
39
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
40
+ throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.");
41
+ }
42
+ return sqlInContainer(localDocker, url, args, options);
43
+ }
44
+ }
45
+
46
+ function startPostgres(connection = localServer) {
47
+ const server = new URL(connection);
48
+ server.pathname = "/postgres";
49
+ assertLocalDatabase(server.toString(), ["postgres"]);
50
+ let version: string | undefined;
51
+ try {
52
+ version = sql(server, "SHOW server_version");
53
+ } catch {
54
+ startContainer(localDocker);
55
+ for (let attempt = 0; attempt < 30; attempt++) {
56
+ try {
57
+ version = sql(server, "SHOW server_version");
58
+ break;
59
+ } catch {
60
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
61
+ }
62
+ }
63
+ }
64
+ if (version !== "18.6") {
65
+ throw new Error("Expected local PostgreSQL 18.6. Check the existing service; setup never removes containers or data.");
66
+ }
67
+ }
68
+
69
+ function prepareDatabases(values: readonly string[]) {
70
+ for (const value of values) {
71
+ const name = new URL(value).pathname.slice(1);
72
+ assertLocalDatabase(value, [name]);
73
+ if (!/^[a-z][a-z0-9_]{0,62}$/u.test(name)) {
74
+ throw new Error("Invalid local database name.");
75
+ }
76
+ }
77
+ startPostgres(values[0]);
78
+ for (const value of values) {
79
+ const url = new URL(value);
80
+ const name = url.pathname.slice(1);
81
+ const server = new URL(url);
82
+ server.pathname = "/postgres";
83
+ if (!sql(server, `SELECT 1 FROM pg_database WHERE datname = '${name}'`)) {
84
+ sql(server, `CREATE DATABASE "${name}"`);
85
+ }
86
+ }
87
+ }
88
+ return { prepareDatabases, sql, startPostgres };
89
+ }
@@ -0,0 +1,50 @@
1
+ import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
2
+
3
+ const image = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
4
+ const container = "development-postgres";
5
+ type Docker = (args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding) => string;
6
+
7
+ export function sqlInContainer(docker: Docker, url: URL, args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding) {
8
+ const id = docker(["ps", "--filter", "publish=55432", "--format", "{{.ID}}"], options).trim();
9
+ if (!id || id.includes("\n")) {
10
+ throw new Error("Install psql or start the shared PostgreSQL service with Docker.");
11
+ }
12
+ if (docker(["inspect", "--format", "{{.Config.Image}}", id], options).trim() !== image) {
13
+ throw new Error("The shared PostgreSQL image differs from the pinned baseline. Upgrade explicitly, preserving data.");
14
+ }
15
+ const inside = new URL(url);
16
+ inside.hostname = "127.0.0.1";
17
+ inside.port = "5432";
18
+ return docker(["exec", id, "psql", ...args, "--dbname", inside.toString()], options).trim();
19
+ }
20
+
21
+ export function startContainer(docker: Docker) {
22
+ docker(["info"]);
23
+ let existing = false;
24
+ try {
25
+ docker(["container", "inspect", container]);
26
+ existing = true;
27
+ } catch {}
28
+ if (existing) {
29
+ if (docker(["inspect", "--format", "{{.Config.Image}}", container]).trim() !== image) {
30
+ throw new Error("The shared PostgreSQL container uses a different image. Preserve its volume and upgrade it explicitly.");
31
+ }
32
+ docker(["start", container]);
33
+ } else {
34
+ docker([
35
+ "run",
36
+ "--detach",
37
+ "--name",
38
+ container,
39
+ "--publish",
40
+ "127.0.0.1:55432:5432",
41
+ "--env",
42
+ "POSTGRES_USER=postgres",
43
+ "--env",
44
+ "POSTGRES_PASSWORD=postgres",
45
+ "--mount",
46
+ "type=volume,src=development-postgres,dst=/var/lib/postgresql",
47
+ image,
48
+ ]);
49
+ }
50
+ }
@@ -0,0 +1,27 @@
1
+ import { type ExecFileSyncOptionsWithStringEncoding, execFileSync } from "node:child_process";
2
+
3
+ export type DockerEnvironment = Readonly<{ DOCKER_CONTEXT?: string | undefined; DOCKER_HOST?: string | undefined }>;
4
+
5
+ export function docker(
6
+ environment: DockerEnvironment,
7
+ args: readonly string[],
8
+ options: ExecFileSyncOptionsWithStringEncoding = { encoding: "utf8" },
9
+ ) {
10
+ const context = environment.DOCKER_CONTEXT;
11
+ const host = environment.DOCKER_HOST;
12
+ const endpoint =
13
+ (!context && host)
14
+ || execFileSync("docker", ["context", "inspect", ...(context ? [context] : []), "--format", "{{.Endpoints.docker.Host}}"], {
15
+ encoding: "utf8",
16
+ }).trim();
17
+ const url = new URL(endpoint);
18
+ if (
19
+ !(
20
+ (["unix:", "npipe:"].includes(url.protocol) && ["", "localhost"].includes(url.hostname))
21
+ || (url.protocol === "tcp:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
22
+ )
23
+ ) {
24
+ throw new Error("Local database setup requires a local Docker endpoint; remote contexts are refused.");
25
+ }
26
+ return execFileSync("docker", args, options);
27
+ }
package/login.log DELETED
@@ -1,26 +0,0 @@
1
- ^D
2
- https://www.npmjs.com/login?next=/login/cli/534beaea-3e9d-4402-a0fd-8783af95c2f8
3
-
4
- ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
5
- █ ▄▄▄▄▄ █ █▄███▀▄▀▀▄█▀▄█▀█▄██ ▄▄▄▄▄ █
6
- █ █ █ █ ▀▄ █▄▀ ▄█▀▄▄█ █▀▄ ██ █ █ █
7
- █ █▄▄▄█ █▀██▀▀ ▀▄▀█▀ ██▄██▄▄▄█ █▄▄▄█ █
8
- █▄▄▄▄▄▄▄█▄▀▄█ █ █▄▀ █ ▀▄▀ █ ▀ █▄▄▄▄▄▄▄█
9
- █ ▀█▄▄▄███▀▄▀▄▄▄▄ ▄▄▄ █▀▄█▀ ██▄▀▄ ▄█
10
- █▄▄█▀█ ▄ ▄▄▄ ▄ ▄ ▀ █▀▀ █▄██▄ █▄▀▀ ▀▄▀█
11
- ██ ██▄█▄▄ ▀█▄ ▀█▀▄▄▄▄▄▀▀▄█▀▀▀▄▄ ▀▀ ▀▄█
12
- █ ▄███ ▄ ▄ █▀█▀█▄▀▀██▀▄ ▀▄ ▄▄█ ▄▄▀▀▀██
13
- █▀████▄▄▀▀█ ▄▀▄▄▄▄▄▀▄▀▄▄▀▀▀▀ ▀▄▄█▀▀ ▀▄█
14
- █▀█▄█▄▀▄▀███ ▄ ▄ ▀ ▀█▄ █▄▄▄ ▄██▄█ █▀▀██
15
- ██▄ ▄▄▄█▄▄▄█▄ ▀█▀▄▀▄▄ ▀▀▀█▀ ▄█▄▄▀ ▄ ▄█
16
- █▀█▀▄▄ ▄█ ▄▄█▀█▀█▄▀▄█ ▄ █▀▄▄█ ▀█▀▀██
17
- █ ▄▄▀ ▄█▀██▄▀▄▄▄█ ▄█ ▄▄▀▀█▀ ▀▄▄▄▀ ▄ ▄█
18
- █████▄▀▄█ ▀█ ▄ ▄ ▀▀█ ▀▄ ▄▄ ▀▀▄███▀▀██
19
- █▄▄▄▄▄█▄▄ ▀ █▄ ▀ ▄▄▄▄▀ █ █ ▄ ▄▄▄ ▄███
20
- █ ▄▄▄▄▄ █▀▄▀█▀█▀▄▄ ██ █ ▄▄ █ █▄█ ▀▀▀██
21
- █ █ █ █▄ ▄▀▄▄▀▀▄▀▄▀▄▀ ▄▀▀▀ ▄▄▀ ▀ █
22
- █ █▄▄▄█ █▀██ ▄ ▄▄ ▀▀█▀ ▀ ▀▄▀▀▄▄ ▄▀▄▀▀██
23
- █▄▄▄▄▄▄▄█▄▄▄█▄▄██▄▄▄▄█▄█▄▄██▄█▄████▄▄▄█
24
- 
25
- Press ENTER to open the URL in your browser.
26
- Logged in on https://registry.npmjs.org/
package/publish.log DELETED
File without changes