@shivaedev/local-postgres 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-10-17
4
+
5
+ ### Added
6
+
7
+ - `POSTGRES_IMAGE` exports the pinned PostgreSQL image, so CI can pull it before `db:setup` starts the container.
8
+ - `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.
9
+
10
+ ### Fixed
11
+
12
+ - 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`.
13
+
14
+ ## 0.2.1 - 2026-10-16
15
+
16
+ ### Changed
17
+
18
+ - Document local service preparation, application-owned database names and schemas, data-preservation priorities and test boundaries in the README, agent guidance, north star and roadmap. Publish `docs/` with the README while keeping agent guidance out of the package.
19
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
20
+
3
21
  ## 0.2.0 - 2026-10-04
4
22
 
5
23
  ### Changed
package/README.md CHANGED
@@ -1,20 +1,121 @@
1
1
  # @shivaedev/local-postgres
2
2
 
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`.
3
+ Make development and integration tests share one persistent local PostgreSQL service. Repeat setup without rebuilding the database contents, and keep each application's database names and schema preparation in its own setup script.
4
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.
5
+ ## Why you want this
6
+
7
+ A setup command should make a machine ready to work without erasing the work already on it. Reimplementing PostgreSQL startup in every repository makes that promise harder to keep. This package gives a setup script one place to prepare the service and its databases:
6
8
 
7
9
  ```ts
8
10
  import { assertLocalDatabase, localPostgres, localServer } from "@shivaedev/local-postgres/localPostgres.ts";
9
11
 
10
12
  const development = `${localServer}/example_dev`;
11
13
  assertLocalDatabase(development, ["example_dev"]);
14
+ localPostgres(process.env).prepareDatabases([development]);
15
+ ```
16
+
17
+ The application chooses `example_dev`. Preparation can run again: the package's real PostgreSQL test inserts a row, repeats preparation and reads the same row back.
18
+
19
+ ## Using it
20
+
21
+ ### How to think about it
22
+
23
+ There are three separate responsibilities:
24
+
25
+ 1. The **service** is PostgreSQL running on the local machine. This package prepares access to it.
26
+ 2. A **database** is an application-chosen name on that service. This package creates missing databases; the application explicitly permits their names.
27
+ 3. The **schema and data** belong to the application. Its setup script runs migrations, loads fixtures or seeds data after preparation.
28
+
29
+ `localPostgres(environment)` returns three synchronous functions: `startPostgres`, `prepareDatabases` and `sql`. Use them in setup tooling. Use your application's database client and `@shivaedev/effect-sql` for persistence and migrations in an Effect application.
30
+
31
+ ### 1. Once per application: choose and validate the databases
32
+
33
+ ```ts
34
+ import { assertLocalDatabase, localPostgres, localServer } from "@shivaedev/local-postgres/localPostgres.ts";
35
+
36
+ const development = process.env.DATABASE_URL ?? `${localServer}/example_dev`;
37
+ const tests = process.env.TEST_DATABASE_URL ?? `${localServer}/example_test`;
38
+
39
+ assertLocalDatabase(development, ["example_dev"]);
40
+ assertLocalDatabase(tests, ["example_test"]);
41
+
12
42
  const postgres = localPostgres(process.env);
13
- postgres.prepareDatabases([development]);
43
+ postgres.prepareDatabases([development, tests]);
14
44
  ```
15
45
 
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.
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
+
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
+
50
+ ### 2. Once per feature: prepare its schema in application tooling
51
+
52
+ After `prepareDatabases`, run the application's own schema or migration entry point. This package provides no schema model, migration ledger, fixture catalogue or seed convention. In this monorepo, `script/setup-database.ts` chooses the database names and loads test schemas; `@shivaedev/effect-sql` owns the SQL and migration APIs.
53
+
54
+ ### 3. In daily setup: prepare again and inspect with SQL
55
+
56
+ ```ts
57
+ import { assertLocalDatabase, localPostgres, localServer } from "@shivaedev/local-postgres/localPostgres.ts";
58
+
59
+ const database = `${localServer}/example_dev`;
60
+ assertLocalDatabase(database, ["example_dev"]);
61
+
62
+ const postgres = localPostgres(process.env);
63
+ postgres.prepareDatabases([database]);
64
+ console.log(postgres.sql(database, "SHOW server_version"));
65
+ ```
66
+
67
+ The version query returns the string `18.6`. `sql` also accepts a `URL` and returns the command's trimmed text. URLs carrying the application's `schema` and `connection_limit` settings work for this query; those settings are removed before calling PostgreSQL's client.
68
+
69
+ `startPostgres(connection)` prepares the service without choosing an application database to create. Its connection defaults to `localServer`. Prefer `prepareDatabases` when the setup script needs named databases as well.
70
+
71
+ ### API
72
+
73
+ Import the setup API from `@shivaedev/local-postgres/localPostgres.ts`:
74
+
75
+ | Export | Purpose |
76
+ | --- | --- |
77
+ | `localServer` | The base URL `postgresql://postgres:postgres@127.0.0.1:55432`. Append the application database name. |
78
+ | `assertLocalDatabase(value: string, names: readonly string[]): URL` | Validate a local database URL against application-owned allowed names. |
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
+ | `prepareDatabases(values: readonly string[]): void` | Prepare the service and create absent databases. |
81
+ | `startPostgres(connection?: string): void` | Prepare the service; the default connection is `localServer`. |
82
+ | `sql(value: string \| URL, query: string): string` | Execute SQL through PostgreSQL's command-line client. |
83
+
84
+ The last three functions are members of the object returned by `localPostgres`, not module-level exports. They throw on failure; they are synchronous Node functions rather than Effects.
85
+
86
+ The package's module export pattern also exposes these lower-level functions:
87
+
88
+ | Module | Exports |
89
+ | --- | --- |
90
+ | `@shivaedev/local-postgres/local-docker.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/local-container.ts` | `startContainer(docker)`, the container-start function; `sqlInContainer(docker, url, args, options)`, the container SQL-client function. Both take a Docker command callback. |
92
+
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
+
95
+ ### Install and machine setup
96
+
97
+ ```sh
98
+ pnpm add --save-dev @shivaedev/local-postgres
99
+ ```
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. 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
+
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
+
105
+ ```text
106
+ postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873
107
+ ```
108
+
109
+ A newly created container uses username and password `postgres`; connection URLs supply credentials for client commands. An existing service with a different version requires an explicit upgrade that preserves its data. Container preparation and container-client fallback also check the managed image; a successful host-`psql` version probe does not inspect that image.
110
+
111
+ Pass the process's actual Docker settings to `localPostgres`, as `process.env` in the examples does. With `DOCKER_CONTEXT`, endpoint validation uses that context and ignores `DOCKER_HOST`; otherwise it validates `DOCKER_HOST` or the active context. Remote endpoints are rejected before a Docker operation. Docker subprocesses inherit the process environment. Supplying a different object does not configure those subprocesses.
17
112
 
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.
113
+ ### Limits
19
114
 
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.
115
+ - The service version, port, container name and volume name are fixed. Choose any upgrade and data-preservation procedure outside this package.
116
+ - Commands block the calling thread. Keep them in development and test setup tooling.
117
+ - Keep allowed names, schema changes, test isolation and cleanup in the application. Preparation is not a database reset.
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
+ - 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.
121
+ - Concurrent creation of the same absent database is not covered by the package's tests. Prepare databases before parallel tests begin.
@@ -1,4 +1,5 @@
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;
@@ -0,0 +1 @@
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;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
- const image = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
2
- const container = "development-postgres";
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() !== image) {
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
- return docker(["exec", id, "psql", ...args, "--dbname", inside.toString()], options).trim();
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 = false;
31
+ let existing;
19
32
  try {
20
- docker(["container", "inspect", container]);
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}}", container]).trim() !== 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", container]);
43
+ docker(["start", CONTAINER]);
29
44
  }
30
45
  else {
31
46
  docker([
32
47
  "run",
33
48
  "--detach",
34
49
  "--name",
35
- container,
50
+ CONTAINER,
36
51
  "--publish",
37
52
  "127.0.0.1:55432:5432",
38
53
  "--env",
@@ -41,7 +56,7 @@ export function startContainer(docker) {
41
56
  "POSTGRES_PASSWORD=postgres",
42
57
  "--mount",
43
58
  "type=volume,src=development-postgres,dst=/var/lib/postgresql",
44
- image,
59
+ POSTGRES_IMAGE,
45
60
  ]);
46
61
  }
47
62
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-container.js","sourceRoot":"","sources":["../../src/server/local-container.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { type ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
2
2
  export type DockerEnvironment = Readonly<{
3
- DOCKER_CONTEXT?: string | undefined;
4
- DOCKER_HOST?: string | undefined;
3
+ "DOCKER_CONTEXT"?: string | undefined;
4
+ "DOCKER_HOST"?: string | undefined;
5
5
  }>;
6
6
  export declare function docker(environment: DockerEnvironment, args: readonly string[], options?: ExecFileSyncOptionsWithStringEncoding): string;
7
7
  //# sourceMappingURL=local-docker.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1 @@
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"}
@@ -1,7 +1,11 @@
1
1
  import { type DockerEnvironment } from "./local-docker.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 declare function localPostgres(environment: DockerEnvironment): {
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;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localPostgres.d.ts","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"AAGA,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;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,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { env } from "node:process";
2
3
  import { sqlInContainer, startContainer } from "./local-container.js";
3
4
  import { docker } from "./local-docker.js";
4
5
  export const localServer = "postgresql://postgres:postgres@127.0.0.1:55432";
@@ -13,11 +14,21 @@ export function assertLocalDatabase(value, names) {
13
14
  }
14
15
  return url;
15
16
  }
16
- export function localPostgres(environment) {
17
- const localDocker = (args, options = { encoding: "utf8" }) => docker(environment, args, options);
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 } = {}) {
23
+ function localDocker(args, options = { encoding: "utf8" }) {
24
+ return docker(environment, args, options);
25
+ }
18
26
  function sql(value, query) {
19
27
  const url = new URL(value);
20
28
  assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
29
+ const childEnvironment = clientEnvironment(url);
30
+ url.password = "";
31
+ url.searchParams.delete("password");
21
32
  for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
22
33
  url.searchParams.delete(key);
23
34
  }
@@ -25,15 +36,19 @@ export function localPostgres(environment) {
25
36
  if (!url.searchParams.has("connect_timeout")) {
26
37
  url.searchParams.set("connect_timeout", "3");
27
38
  }
28
- const options = { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] };
39
+ const options = {
40
+ encoding: "utf8",
41
+ env: childEnvironment,
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ };
29
44
  try {
30
- return execFileSync("psql", [...args, "--dbname", String(url)], options).trim();
45
+ return execFileSync("psql", [...args, "--dbname", url.toString()], options).trim();
31
46
  }
32
47
  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.");
48
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
49
+ return sqlInContainer(localDocker, url, args, options);
35
50
  }
36
- return sqlInContainer(localDocker, url, args, options);
51
+ throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
37
52
  }
38
53
  }
39
54
  function startPostgres(connection = localServer) {
@@ -46,13 +61,13 @@ export function localPostgres(environment) {
46
61
  }
47
62
  catch {
48
63
  startContainer(localDocker);
49
- for (let attempt = 0; attempt < 30; attempt++) {
64
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
50
65
  try {
51
66
  version = sql(server, "SHOW server_version");
52
67
  break;
53
68
  }
54
69
  catch {
55
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
70
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMillis);
56
71
  }
57
72
  }
58
73
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localPostgres.js","sourceRoot":"","sources":["../../src/server/localPostgres.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ # North star
2
+
3
+ ## The problem
4
+
5
+ Development and integration tests need PostgreSQL, but each repository has its own database names, schemas and test fixtures. Copying the whole setup script between repositories mixes those application choices with service management. A later shortcut in one copy can replace a container, lose a persistent volume or point a local setup command at a remote machine.
6
+
7
+ Setup also runs more than once. The developer's useful state must survive another checkout, another dependency install and another test run. A mismatch is a reason to stop and inspect the machine, not a reason to rebuild its database service automatically.
8
+
9
+ ## The ideal
10
+
11
+ There is one small utility for preparing the shared local service, and each application writes only its own choices. A caller chooses explicit database names, checks incoming URLs against those names, prepares absent databases, and then runs its own schema and seed work. The common utility handles reaching the service and PostgreSQL's command-line client.
12
+
13
+ Repeated preparation preserves existing data. Locality checks come before a command can act on a database or Docker endpoint. Upgrades are explicit decisions with a data-preservation plan rather than recovery work hidden inside a readiness check.
14
+
15
+ ## What good looks like
16
+
17
+ - A setup script names the databases it owns and validates configured URLs against those names.
18
+ - A service mismatch fails clearly without destructive repair.
19
+ - The application owns schemas, migrations, fixture loading, production guards and test cleanup. The package knows none of its domain tables or database naming conventions.
20
+ - The common setup path stays visible: ordinary Node tools, PostgreSQL client commands and a fixed local baseline. Application runtime persistence goes through its database client and the framework's SQL path.
21
+ - Tests distinguish real database preservation from inspection of generated commands. A test against an already-running PostgreSQL service cannot stand in for a test that starts Docker or exercises a missing client.
22
+
23
+ ## Trade-offs
24
+
25
+ When these goals conflict, prefer existing data, then an explicit local destination, then application ownership, then a small setup utility.
26
+
27
+ A fixed service baseline is less flexible than a configurable process manager. It makes one shared machine service easier to reason about and avoids changing the version as a side effect of setup. A machine that needs an upgrade requires a deliberate action outside preparation.
28
+
29
+ Explicit application name checks add a line to a setup script. That line records which database the script is authorized to prepare; an environment URL alone cannot answer that question.
30
+
31
+ Synchronous commands are a fit for setup tooling. They should not become an application request path or a second database abstraction. A need for runtime SQL belongs with the native Effect SQL client and repository packages.
32
+
33
+ ## What it deliberately leaves out
34
+
35
+ - Remote or production database provisioning.
36
+ - Automatic upgrades, container replacement, volume removal, database resets and test cleanup policy.
37
+ - An application database catalogue, schema authoring, migrations and seeds.
38
+ - Query composition, connection pooling, repository APIs or an Effect runtime owner.
39
+ - A configurable multi-service process manager.
40
+
41
+ The [package roadmap](./roadmap.md) owns implementation work and open decisions. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns composed database fixtures and host-level validation.
@@ -0,0 +1,24 @@
1
+ # Roadmap
2
+
3
+ This file owns local service preparation, its command paths and package API questions. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns composed repository and migration fixtures, application adoption and host-level validation. Their completion status is recorded there rather than repeated here.
4
+
5
+ ## Built
6
+
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 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
+ - [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] 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.
14
+ - [x] The installed consumer fixture checks the main module's exports and types.
15
+
16
+ ## Next
17
+
18
+ - [ ] Add a test for rejection of an exact running PostgreSQL version mismatch. Scripted image-mismatch checks do not prove this service-version check.
19
+ - [ ] Establish whether parallel setup needs coordinated creation of an absent database; keep application test startup ordered until there is evidence for another contract.
20
+
21
+ ## Open questions
22
+
23
+ - Should `localPostgres(environment)` configure the environment of its Docker subprocesses, or only validate it? The implementation validates the supplied settings while Docker commands inherit the process environment. The README therefore asks callers to pass the actual process settings. Selecting a different context through the argument would need an explicit API decision and tests.
24
+ - Should `docker`, `startContainer` and `sqlInContainer` be supported standalone public APIs? The package's wildcard module exports expose them, while the usage examples and installed consumer fixture use `localPostgres.ts`. Narrowing the exports would change the public API; supporting the helpers would need direct consumer and command-path tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/local-postgres",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -15,34 +15,38 @@
15
15
  "sideEffects": false,
16
16
  "type": "module",
17
17
  "imports": {
18
- "#test/*": "./test/*",
18
+ "#test/*": "./src/test-support/*",
19
19
  "#*.ts": {
20
- "source": "./src/*.ts",
21
- "types": "./dist/*.d.ts",
22
- "default": "./dist/*.js"
20
+ "source": "./src/server/*.ts",
21
+ "types": "./dist/server/*.d.ts",
22
+ "default": "./dist/server/*.js"
23
23
  }
24
24
  },
25
25
  "exports": {
26
26
  "./*.ts": {
27
- "types": "./dist/*.d.ts",
28
- "source": "./src/*.ts",
29
- "import": "./dist/*.js",
30
- "default": "./dist/*.js"
27
+ "types": "./dist/server/*.d.ts",
28
+ "source": "./src/server/*.ts",
29
+ "import": "./dist/server/*.js",
30
+ "default": "./dist/server/*.js"
31
31
  },
32
32
  "./package.json": "./package.json"
33
33
  },
34
34
  "files": [
35
35
  "dist",
36
36
  "src",
37
+ "!src/**/*.test.ts",
38
+ "!src/**/*.spec.ts",
39
+ "!src/**/test-support",
40
+ "docs",
37
41
  "CHANGELOG.md",
38
42
  "README.md"
39
43
  ],
40
44
  "devDependencies": {
41
- "@shivaedev/quality": "0.9.0",
42
- "@types/node": "24.10.1",
45
+ "@shivaedev/quality": "0.23.0",
46
+ "@types/node": "24.19.2",
43
47
  "@typescript/native": "npm:typescript@7.0.2",
44
48
  "typescript": "npm:@typescript/typescript6@6.0.2",
45
- "vitest": "4.1.9"
49
+ "vitest": "5.0.3"
46
50
  },
47
51
  "engines": {
48
52
  "node": ">=24"
@@ -52,7 +56,7 @@
52
56
  "provenance": true
53
57
  },
54
58
  "scripts": {
55
- "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
59
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
56
60
  "ready": "pnpm typecheck && pnpm test && pnpm build",
57
61
  "test": "vitest run",
58
62
  "typecheck": "tsc --noEmit"
@@ -1,7 +1,8 @@
1
1
  import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process";
2
+ import { env } from "node:process";
2
3
 
3
- const image = "postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873";
4
- const container = "development-postgres";
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() !== image) {
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
- return docker(["exec", id, "psql", ...args, "--dbname", inside.toString()], options).trim();
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 = false;
35
+ let existing: boolean;
24
36
  try {
25
- docker(["container", "inspect", container]);
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}}", container]).trim() !== 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", container]);
46
+ docker(["start", CONTAINER]);
33
47
  } else {
34
48
  docker([
35
49
  "run",
36
50
  "--detach",
37
51
  "--name",
38
- container,
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
- image,
61
+ POSTGRES_IMAGE,
48
62
  ]);
49
63
  }
50
64
  }
@@ -1,6 +1,6 @@
1
1
  import { type ExecFileSyncOptionsWithStringEncoding, execFileSync } from "node:child_process";
2
2
 
3
- export type DockerEnvironment = Readonly<{ DOCKER_CONTEXT?: string | undefined; DOCKER_HOST?: string | undefined }>;
3
+ export type DockerEnvironment = Readonly<{ "DOCKER_CONTEXT"?: string | undefined; "DOCKER_HOST"?: string | undefined }>;
4
4
 
5
5
  export function docker(
6
6
  environment: DockerEnvironment,
@@ -1,4 +1,5 @@
1
1
  import { type ExecFileSyncOptionsWithStringEncoding, execFileSync } from "node:child_process";
2
+ import { env } from "node:process";
2
3
  import { sqlInContainer, startContainer } from "./local-container.ts";
3
4
  import { type DockerEnvironment, docker } from "./local-docker.ts";
4
5
 
@@ -18,12 +19,27 @@ export function assertLocalDatabase(value: string, names: readonly string[]) {
18
19
  return url;
19
20
  }
20
21
 
21
- export function localPostgres(environment: DockerEnvironment) {
22
- const localDocker = (args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding = { encoding: "utf8" }) =>
23
- docker(environment, args, options);
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 = {}) {
34
+ function localDocker(args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding = { encoding: "utf8" }) {
35
+ return docker(environment, args, options);
36
+ }
24
37
  function sql(value: string | URL, query: string) {
25
38
  const url = new URL(value);
26
39
  assertLocalDatabase(url.toString(), [url.pathname.slice(1)]);
40
+ const childEnvironment = clientEnvironment(url);
41
+ url.password = "";
42
+ url.searchParams.delete("password");
27
43
  for (const key of ["schema", "connection_limit", "pool_timeout", "pgbouncer"]) {
28
44
  url.searchParams.delete(key);
29
45
  }
@@ -32,14 +48,18 @@ export function localPostgres(environment: DockerEnvironment) {
32
48
  if (!url.searchParams.has("connect_timeout")) {
33
49
  url.searchParams.set("connect_timeout", "3");
34
50
  }
35
- const options = { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] } satisfies ExecFileSyncOptionsWithStringEncoding;
51
+ const options = {
52
+ encoding: "utf8",
53
+ env: childEnvironment,
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ } satisfies ExecFileSyncOptionsWithStringEncoding;
36
56
  try {
37
- return execFileSync("psql", [...args, "--dbname", String(url)], options).trim();
57
+ return execFileSync("psql", [...args, "--dbname", url.toString()], options).trim();
38
58
  } 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.");
59
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
60
+ return sqlInContainer(localDocker, url, args, options);
41
61
  }
42
- return sqlInContainer(localDocker, url, args, options);
62
+ throw new Error(error instanceof Error && "stderr" in error ? String(error.stderr) : "PostgreSQL query failed.", { cause: error });
43
63
  }
44
64
  }
45
65
 
@@ -52,12 +72,12 @@ export function localPostgres(environment: DockerEnvironment) {
52
72
  version = sql(server, "SHOW server_version");
53
73
  } catch {
54
74
  startContainer(localDocker);
55
- for (let attempt = 0; attempt < 30; attempt++) {
75
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
56
76
  try {
57
77
  version = sql(server, "SHOW server_version");
58
78
  break;
59
79
  } catch {
60
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
80
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMillis);
61
81
  }
62
82
  }
63
83
  }
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"localPostgres.d.ts","sourceRoot":"","sources":["../src/localPostgres.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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"localPostgres.js","sourceRoot":"","sources":["../src/localPostgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAA0B,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEnE,MAAM,CAAC,MAAM,WAAW,GAAG,gDAAgD,CAAC;AAE5E,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,KAAwB;IAC1E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IACC,CAAC,CAAC,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;WACxG,GAAG,CAAC,IAAI,KAAK,OAAO;WACpB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;WAC1F,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EACvB,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,iDAAiD,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,WAA8B;IAC3D,MAAM,WAAW,GAAG,CAAC,IAAuB,EAAE,UAAiD,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CACtH,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,SAAS,GAAG,CAAC,KAAmB,EAAE,KAAa;QAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,CAAC,EAAE,CAAC;YAC/E,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;YAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAkD,CAAC;QACxH,IAAI,CAAC;YACJ,OAAO,YAAY,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACjF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;gBAC7E,MAAM,IAAI,KAAK,CAAC,KAAK,YAAY,KAAK,IAAI,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC;YAClH,CAAC;YACD,OAAO,cAAc,CAAC,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACxD,CAAC;IACF,CAAC;IAED,SAAS,aAAa,CAAC,UAAU,GAAG,WAAW;QAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,CAAC,QAAQ,GAAG,WAAW,CAAC;QAC9B,mBAAmB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;QACrD,IAAI,OAA2B,CAAC;QAChC,IAAI,CAAC;YACJ,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACR,cAAc,CAAC,WAAW,CAAC,CAAC;YAC5B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;gBAC/C,IAAI,CAAC;oBACJ,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;oBAC7C,MAAM;gBACP,CAAC;gBAAC,MAAM,CAAC;oBACR,OAAO,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;gBACpE,CAAC;YACF,CAAC;QACF,CAAC;QACD,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,qGAAqG,CAAC,CAAC;QACxH,CAAC;IACF,CAAC;IAED,SAAS,gBAAgB,CAAC,MAAyB;QAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9C,mBAAmB,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YACnC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACjD,CAAC;QACF,CAAC;QACD,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;YAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACnC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,CAAC,QAAQ,GAAG,WAAW,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,8CAA8C,IAAI,GAAG,CAAC,EAAE,CAAC;gBACzE,GAAG,CAAC,MAAM,EAAE,oBAAoB,IAAI,GAAG,CAAC,CAAC;YAC1C,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC;AACjD,CAAC"}