@ingram-tech/nk-db 0.1.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/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @ingram-tech/nk-db
2
+
3
+ The Ingram **Postgres data layer**: one TLS-aware `pg` pool, raw-SQL helpers,
4
+ Drizzle wiring, and a **PGlite** (no-Docker) dev/test harness. It consolidates
5
+ the `src/lib/db/` layer that integrain, orbitr.ee, peppost, and thornhill each
6
+ hand-rolled when they moved off Supabase. Design + rationale:
7
+ [`docs/db-package.md`](https://github.com/ingram-technologies/nextkit/blob/main/docs/db-package.md).
8
+
9
+ `pg` and `drizzle-orm` are **peer dependencies** (one copy in the app).
10
+ `@electric-sql/pglite` + `@electric-sql/pglite-socket` are **optional** peers —
11
+ add them as `devDependencies` only if you use `nk dev` / the test harness.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bun add @ingram-tech/nk-db pg drizzle-orm
17
+ bun add -d @electric-sql/pglite @electric-sql/pglite-socket # for PGlite dev/test
18
+ ```
19
+
20
+ Env contract (validated by `keys.ts`; resolves in precedence order):
21
+
22
+ ```dotenv
23
+ DATABASE_URL=… # direct Postgres (session pooler / :5432), NOT PostgREST
24
+ # fallbacks, for running on Supabase Postgres before the data moves:
25
+ # POSTGRES_URL_NON_POOLING / POSTGRES_URL
26
+ DATABASE_CA_CERT=… # optional PEM CA → verify-full
27
+ DATABASE_SSL=true # optional
28
+ DATABASE_POOL_MAX=5 # optional; keep small on serverless
29
+ ```
30
+
31
+ ## The one barrel (`src/lib/db.ts`)
32
+
33
+ Create the pool once and share it across Drizzle, the raw helpers, and Better
34
+ Auth — exactly one pool per process.
35
+
36
+ ```ts
37
+ import { createDb, createPool, createQueries } from "@ingram-tech/nk-db";
38
+ import * as schema from "./schema";
39
+
40
+ export const pool = createPool(); // TLS-aware; local socket → max:1
41
+ export const db = createDb(pool, schema); // Drizzle — the default query path
42
+ export const { query, one, maybeOne, execute, withTx } = createQueries(pool);
43
+ export { schema };
44
+ ```
45
+
46
+ Then `import { db, query } from "@/lib/db"` everywhere. Better Auth reuses the
47
+ same pool: `betterAuth({ database: pool, … })`.
48
+
49
+ - **Drizzle** is the default: schema-first, `drizzle-kit` generates migrations
50
+ into `drizzle/`.
51
+ - **Raw helpers** (`createQueries(pool)`) are the escape hatch — Postgres
52
+ functions (`select fn($1,…)`), `pgmq` draining, `pg_trgm`. Signatures match the
53
+ hand-rolled originals, so adopting is a find-and-replace of the import.
54
+ - **`configureTimestampsAsStrings()`** — opt-in, for legacy row types that expect
55
+ `timestamptz` as ISO strings (on the golden path, prefer Drizzle's
56
+ `timestamp(..., { mode: "string" })` per column).
57
+
58
+ ## PGlite dev & test (`@ingram-tech/nk-db/pglite`)
59
+
60
+ `nk dev` runs the `nk-pglite-dev` bin automatically when this package is
61
+ installed: it boots Postgres-in-WASM persisted to `.pglite/`, applies the
62
+ `drizzle/` migrations, sets `DATABASE_URL`, then runs `next dev`. `--fresh` wipes
63
+ and rebuilds. No Docker, no daemon.
64
+
65
+ Tests use an in-memory instance:
66
+
67
+ ```ts
68
+ import { createTestDb } from "@ingram-tech/nk-db/pglite";
69
+
70
+ // Vitest: fileParallelism:false (the socket is single-connection).
71
+ const { pool, db, reset, close } = await createTestDb({ migrationsFolder: "drizzle" });
72
+ // beforeEach(reset); afterAll(close);
73
+ ```
74
+
75
+ ## Gotchas it bakes in
76
+
77
+ - **Local pool is capped at `max:1`** — the PGlite socket is single-connection;
78
+ a larger pool breaks dev with "Connection terminated unexpectedly".
79
+ - **`pg.Pool` destroys a connection on a query *error*.** Don't catch unique
80
+ violations as control flow — use `INSERT … ON CONFLICT DO NOTHING RETURNING …`.
81
+ - **`jsonb` params:** `JSON.stringify()` the value and cast `$n::jsonb` (Drizzle's
82
+ `jsonb()` columns handle this).
@@ -0,0 +1,14 @@
1
+ import { type NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { Pool } from "pg";
3
+ /**
4
+ * Build the Drizzle instance on the shared pool. This is the default query path
5
+ * for app tables — schema-first, with `drizzle-kit` generating the migrations
6
+ * (kills the hand-written-SQL drift). It rides the same `pg.Pool` that backs the
7
+ * raw helpers and Better Auth, so there's still exactly one pool per process.
8
+ *
9
+ * import * as schema from "./schema";
10
+ * export const pool = createPool();
11
+ * export const db = createDb(pool, schema);
12
+ */
13
+ export declare const createDb: <TSchema extends Record<string, unknown>>(pool: Pool, schema: TSchema) => NodePgDatabase<TSchema>;
14
+ //# sourceMappingURL=drizzle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,IAAI,EACV,QAAQ,OAAO,KACb,cAAc,CAAC,OAAO,CAA8B,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ /**
3
+ * Build the Drizzle instance on the shared pool. This is the default query path
4
+ * for app tables — schema-first, with `drizzle-kit` generating the migrations
5
+ * (kills the hand-written-SQL drift). It rides the same `pg.Pool` that backs the
6
+ * raw helpers and Better Auth, so there's still exactly one pool per process.
7
+ *
8
+ * import * as schema from "./schema";
9
+ * export const pool = createPool();
10
+ * export const db = createDb(pool, schema);
11
+ */
12
+ export const createDb = (pool, schema) => drizzle(pool, { schema });
13
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AAGzE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACvB,IAAU,EACV,MAAe,EACW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { createDb } from "./drizzle";
2
+ export { type DbEnv, dbEnv, getDatabaseUrl, isConfigured } from "./keys";
3
+ export { type CreatePoolConfig, createPool } from "./pool";
4
+ export { createQueries, type PoolQueries, type Queries } from "./queries";
5
+ export { configureTimestampsAsStrings } from "./types";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The Ingram Postgres data layer. The PGlite dev/test harness lives behind the
2
+ // "@ingram-tech/nk-db/pglite" subpath so its WASM/dev-only deps never reach a
3
+ // production bundle.
4
+ export { createDb } from "./drizzle";
5
+ export { dbEnv, getDatabaseUrl, isConfigured } from "./keys";
6
+ export { createPool } from "./pool";
7
+ export { createQueries } from "./queries";
8
+ export { configureTimestampsAsStrings } from "./types";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qBAAqB;AAErB,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAc,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAyB,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAkC,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC"}
package/dist/keys.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Environment contract for @ingram-tech/nk-db.
3
+ *
4
+ * Following the "each package owns its own env validation" pattern (see
5
+ * @ingram-tech/nk-auth). Env vars are external input, so we parse them with Zod
6
+ * (per docs/code-style.md) rather than reading `process.env` ad hoc.
7
+ *
8
+ * The connection string resolves in a fixed precedence so an app can deploy the
9
+ * direct-`pg` code **while still pointed at Supabase Postgres**, before any data
10
+ * has moved (see docs/db-package.md and the Supabase->Postgres playbook):
11
+ *
12
+ * 1. DATABASE_URL — our DO cluster (or the local PGlite socket)
13
+ * 2. POSTGRES_URL_NON_POOLING — Supabase integration's direct URL
14
+ * 3. POSTGRES_URL — Supabase integration's pooled URL
15
+ *
16
+ * Optional TLS / pool controls:
17
+ * DATABASE_SSL — "true" to require TLS (managed hosts)
18
+ * DATABASE_CA_CERT — PEM CA; when set, verify the server cert + hostname
19
+ * DATABASE_POOL_MAX — pool size cap; keep small on serverless (e.g. 5)
20
+ */
21
+ export interface DbEnv {
22
+ /** The resolved connection string (precedence applied). */
23
+ connectionString: string;
24
+ /** PEM CA cert; when set, the pool verifies the server cert + hostname. */
25
+ caCert?: string;
26
+ /** Whether TLS was explicitly requested via DATABASE_SSL. */
27
+ ssl: boolean;
28
+ /** Pool size cap, if configured. */
29
+ poolMax?: number;
30
+ }
31
+ /**
32
+ * Resolve the connection string from the environment by precedence, without
33
+ * validating the rest of the contract. Returns `undefined` if none is set —
34
+ * useful for "is a database configured?" checks (e.g. degrade in local dev).
35
+ */
36
+ export declare const getDatabaseUrl: (env?: NodeJS.ProcessEnv) => string | undefined;
37
+ /**
38
+ * Read and validate the nk-db env vars at once. Throws a single error listing
39
+ * everything missing/invalid so a misconfigured site fails fast at startup
40
+ * rather than at first query.
41
+ */
42
+ export declare const dbEnv: () => DbEnv;
43
+ /** Whether a database connection string is present (degrade gracefully if not). */
44
+ export declare const isConfigured: () => boolean;
45
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAaH,MAAM,WAAW,KAAK;IACrB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,GAAG,EAAE,OAAO,CAAC;IACb,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAC1B,MAAK,MAAM,CAAC,UAAwB,KAClC,MAAM,GAAG,SACyD,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,KAAK,QAAO,KAqBxB,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,YAAY,QAAO,OAAyC,CAAC"}
package/dist/keys.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Environment contract for @ingram-tech/nk-db.
3
+ *
4
+ * Following the "each package owns its own env validation" pattern (see
5
+ * @ingram-tech/nk-auth). Env vars are external input, so we parse them with Zod
6
+ * (per docs/code-style.md) rather than reading `process.env` ad hoc.
7
+ *
8
+ * The connection string resolves in a fixed precedence so an app can deploy the
9
+ * direct-`pg` code **while still pointed at Supabase Postgres**, before any data
10
+ * has moved (see docs/db-package.md and the Supabase->Postgres playbook):
11
+ *
12
+ * 1. DATABASE_URL — our DO cluster (or the local PGlite socket)
13
+ * 2. POSTGRES_URL_NON_POOLING — Supabase integration's direct URL
14
+ * 3. POSTGRES_URL — Supabase integration's pooled URL
15
+ *
16
+ * Optional TLS / pool controls:
17
+ * DATABASE_SSL — "true" to require TLS (managed hosts)
18
+ * DATABASE_CA_CERT — PEM CA; when set, verify the server cert + hostname
19
+ * DATABASE_POOL_MAX — pool size cap; keep small on serverless (e.g. 5)
20
+ */
21
+ import { z } from "zod";
22
+ const schema = z.object({
23
+ DATABASE_URL: z.string().min(1).optional(),
24
+ POSTGRES_URL_NON_POOLING: z.string().min(1).optional(),
25
+ POSTGRES_URL: z.string().min(1).optional(),
26
+ DATABASE_SSL: z.enum(["true", "false"]).optional(),
27
+ DATABASE_CA_CERT: z.string().min(1).optional(),
28
+ DATABASE_POOL_MAX: z.coerce.number().int().positive().optional(),
29
+ });
30
+ /**
31
+ * Resolve the connection string from the environment by precedence, without
32
+ * validating the rest of the contract. Returns `undefined` if none is set —
33
+ * useful for "is a database configured?" checks (e.g. degrade in local dev).
34
+ */
35
+ export const getDatabaseUrl = (env = process.env) => env.DATABASE_URL ?? env.POSTGRES_URL_NON_POOLING ?? env.POSTGRES_URL;
36
+ /**
37
+ * Read and validate the nk-db env vars at once. Throws a single error listing
38
+ * everything missing/invalid so a misconfigured site fails fast at startup
39
+ * rather than at first query.
40
+ */
41
+ export const dbEnv = () => {
42
+ const result = schema.safeParse(process.env);
43
+ if (!result.success) {
44
+ const issues = result.error.issues
45
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
46
+ .join(", ");
47
+ throw new Error(`@ingram-tech/nk-db: invalid environment — ${issues}`);
48
+ }
49
+ const connectionString = getDatabaseUrl();
50
+ if (!connectionString) {
51
+ throw new Error("@ingram-tech/nk-db: no database connection string — set DATABASE_URL " +
52
+ "(or POSTGRES_URL_NON_POOLING / POSTGRES_URL).");
53
+ }
54
+ return {
55
+ connectionString,
56
+ caCert: result.data.DATABASE_CA_CERT,
57
+ ssl: result.data.DATABASE_SSL === "true",
58
+ poolMax: result.data.DATABASE_POOL_MAX,
59
+ };
60
+ };
61
+ /** Whether a database connection string is present (degrade gracefully if not). */
62
+ export const isConfigured = () => getDatabaseUrl() !== undefined;
63
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC1C,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACtD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC1C,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC9C,iBAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAChE,CAAC,CAAC;AAaH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC7B,MAAyB,OAAO,CAAC,GAAG,EACf,EAAE,CACvB,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,wBAAwB,IAAI,GAAG,CAAC,YAAY,CAAC;AAEtE;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,GAAU,EAAE;IAChC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,6CAA6C,MAAM,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,gBAAgB,GAAG,cAAc,EAAE,CAAC;IAC1C,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACd,uEAAuE;YACtE,+CAA+C,CAChD,CAAC;IACH,CAAC;IACD,OAAO;QACN,gBAAgB;QAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,gBAAgB;QACpC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,MAAM;QACxC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB;KACtC,CAAC;AACH,CAAC,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,cAAc,EAAE,KAAK,SAAS,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/pglite/dev.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // The `nk-pglite-dev` bin: what `nk dev` shells out to when this package is
3
+ // installed. Boots PGlite (persisted to .pglite/), applies the `drizzle/`
4
+ // migrations, then runs `next dev` against it. `--fresh` wipes and rebuilds.
5
+ //
6
+ // Run directly by Node, so the relative import carries a `.js` extension.
7
+ import { startPgliteDev } from "./index.js";
8
+ const fresh = process.argv.includes("--fresh");
9
+ const nextArgs = process.argv.slice(2).filter((arg) => arg !== "--fresh");
10
+ startPgliteDev({ fresh, nextArgs }).catch((error) => {
11
+ console.error("nk(pglite): failed to start —", error);
12
+ process.exit(1);
13
+ });
14
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/pglite/dev.ts"],"names":[],"mappings":";AACA,4EAA4E;AAC5E,0EAA0E;AAC1E,6EAA6E;AAC7E,EAAE;AACF,0EAA0E;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;AAE1E,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC5D,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;IACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import { PGLiteSocketServer } from "@electric-sql/pglite-socket";
3
+ import type { Pool } from "pg";
4
+ export interface PgliteServerOptions {
5
+ /** Persisted data dir; omit for an in-memory database (tests). */
6
+ dataDir?: string;
7
+ /** Host to bind the TCP socket on. Default `127.0.0.1`. */
8
+ host?: string;
9
+ /** Port for the TCP socket. Default `PGLITE_PORT` env or `5432`. */
10
+ port?: number;
11
+ /** Wipe `dataDir` before booting (rebuild after adding a migration). */
12
+ fresh?: boolean;
13
+ /** Drizzle migrations folder for the default applier. Default `drizzle`. */
14
+ migrationsFolder?: string;
15
+ /** Override how migrations are applied (e.g. raw-SQL apps). */
16
+ migrate?: (db: PGlite) => Promise<void>;
17
+ }
18
+ export interface PgliteServer {
19
+ db: PGlite;
20
+ server: PGLiteSocketServer;
21
+ /** Connection string the app's `pg.Pool` should use. */
22
+ databaseUrl: string;
23
+ stop: () => Promise<void>;
24
+ }
25
+ /**
26
+ * Boot PGlite, apply migrations (on a fresh/in-memory db), and expose it over a
27
+ * TCP socket so a normal `pg.Pool` connects via `DATABASE_URL` with no app code
28
+ * changes. Reused by both `startPgliteDev` (persisted) and `createTestDb`
29
+ * (in-memory). The socket server is single-connection — keep the consuming pool
30
+ * at `max: 1` (createPool does this for local hosts).
31
+ */
32
+ export declare const createPgliteServer: (options?: PgliteServerOptions) => Promise<PgliteServer>;
33
+ /**
34
+ * `nk dev`'s PGlite mode: boot a persisted local Postgres, then `next dev` with
35
+ * `DATABASE_URL` pointed at it. Invoked by the `nk-pglite-dev` bin.
36
+ */
37
+ export declare const startPgliteDev: (options?: PgliteServerOptions & {
38
+ nextArgs?: string[];
39
+ }) => Promise<void>;
40
+ export interface TestDb {
41
+ /** A `pg.Pool` (max:1) bound to the in-memory database over the socket. */
42
+ pool: Pool;
43
+ databaseUrl: string;
44
+ /** TRUNCATE every public table — call in Vitest `beforeEach`. */
45
+ reset: () => Promise<void>;
46
+ /** Close the pool and the database — call in `afterAll`. */
47
+ close: () => Promise<void>;
48
+ }
49
+ /**
50
+ * An in-memory PGlite for integration tests. Real Postgres semantics, instant,
51
+ * no cleanup. Run with Vitest `fileParallelism: false` (the socket is
52
+ * single-connection) and close in `afterAll` (modules are isolated per file).
53
+ */
54
+ export declare const createTestDb: (options?: PgliteServerOptions) => Promise<TestDb>;
55
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IACnC,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,kBAAkB,CAAC;IAC3B,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAUD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC9B,UAAS,mBAAwB,KAC/B,OAAO,CAAC,YAAY,CAuBtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAC1B,UAAS,mBAAmB,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,KACzD,OAAO,CAAC,IAAI,CA0Bd,CAAC;AAEF,MAAM,WAAW,MAAM;IACtB,2EAA2E;IAC3E,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACxB,UAAS,mBAAwB,KAC/B,OAAO,CAAC,MAAM,CAiBhB,CAAC"}
@@ -0,0 +1,94 @@
1
+ // PGlite dev/test harness — Postgres-in-WASM, no Docker. Ported from the proven
2
+ // peppost `scripts/pglite-dev.ts` (which its own comment said should "move into
3
+ // @ingram-tech/nk-cli so the whole fleet inherits it"). PGlite 0.5.x is
4
+ // PostgreSQL 18.3, so plpgsql / gen_random_uuid() / RLS all work and dev matches
5
+ // a pg18 prod target.
6
+ //
7
+ // This module is the only place the PGlite deps are imported; it loads solely
8
+ // via the "@ingram-tech/nk-db/pglite" subpath (dev/test), never from the main
9
+ // entry, so production bundles stay clean.
10
+ import { existsSync } from "node:fs";
11
+ import { rm } from "node:fs/promises";
12
+ import { PGlite } from "@electric-sql/pglite";
13
+ import { PGLiteSocketServer } from "@electric-sql/pglite-socket";
14
+ const defaultMigrate = (migrationsFolder) => async (db) => {
15
+ const { drizzle } = await import("drizzle-orm/pglite");
16
+ const { migrate } = await import("drizzle-orm/pglite/migrator");
17
+ await migrate(drizzle(db), { migrationsFolder });
18
+ };
19
+ /**
20
+ * Boot PGlite, apply migrations (on a fresh/in-memory db), and expose it over a
21
+ * TCP socket so a normal `pg.Pool` connects via `DATABASE_URL` with no app code
22
+ * changes. Reused by both `startPgliteDev` (persisted) and `createTestDb`
23
+ * (in-memory). The socket server is single-connection — keep the consuming pool
24
+ * at `max: 1` (createPool does this for local hosts).
25
+ */
26
+ export const createPgliteServer = async (options = {}) => {
27
+ const host = options.host ?? "127.0.0.1";
28
+ const port = options.port ?? Number(process.env.PGLITE_PORT ?? 5432);
29
+ const migrationsFolder = options.migrationsFolder ?? "drizzle";
30
+ const { dataDir } = options;
31
+ if (options.fresh && dataDir && existsSync(dataDir)) {
32
+ await rm(dataDir, { recursive: true, force: true });
33
+ }
34
+ const needsMigrations = !dataDir || !existsSync(dataDir);
35
+ const db = dataDir ? await PGlite.create({ dataDir }) : await PGlite.create();
36
+ if (needsMigrations) {
37
+ await (options.migrate ?? defaultMigrate(migrationsFolder))(db);
38
+ }
39
+ const server = new PGLiteSocketServer({ db, host, port });
40
+ await server.start();
41
+ const databaseUrl = `postgresql://postgres:postgres@${host}:${port}/postgres`;
42
+ const stop = async () => {
43
+ await server.stop().catch(() => { });
44
+ await db.close().catch(() => { });
45
+ };
46
+ return { db, server, databaseUrl, stop };
47
+ };
48
+ /**
49
+ * `nk dev`'s PGlite mode: boot a persisted local Postgres, then `next dev` with
50
+ * `DATABASE_URL` pointed at it. Invoked by the `nk-pglite-dev` bin.
51
+ */
52
+ export const startPgliteDev = async (options = {}) => {
53
+ const { spawn } = await import("node:child_process");
54
+ const { databaseUrl, stop } = await createPgliteServer({
55
+ dataDir: options.dataDir ?? ".pglite",
56
+ ...options,
57
+ });
58
+ console.log(`nk(pglite): Postgres 18 (WASM) ready on ${databaseUrl} — no Docker, no Supabase CLI`);
59
+ const child = spawn("bunx", ["next", "dev", "--turbopack", ...(options.nextArgs ?? [])], { stdio: "inherit", env: { ...process.env, DATABASE_URL: databaseUrl } });
60
+ let closing = false;
61
+ const close = async (code) => {
62
+ if (closing)
63
+ return;
64
+ closing = true;
65
+ await stop();
66
+ process.exit(code);
67
+ };
68
+ process.on("SIGINT", () => child.kill("SIGINT"));
69
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
70
+ child.on("exit", (code) => void close(code ?? 0));
71
+ };
72
+ /**
73
+ * An in-memory PGlite for integration tests. Real Postgres semantics, instant,
74
+ * no cleanup. Run with Vitest `fileParallelism: false` (the socket is
75
+ * single-connection) and close in `afterAll` (modules are isolated per file).
76
+ */
77
+ export const createTestDb = async (options = {}) => {
78
+ const { Pool } = await import("pg");
79
+ const { databaseUrl, stop } = await createPgliteServer(options);
80
+ const pool = new Pool({ connectionString: databaseUrl, max: 1 });
81
+ const reset = async () => {
82
+ const result = await pool.query("select tablename from pg_tables where schemaname = 'public'");
83
+ if (result.rows.length === 0)
84
+ return;
85
+ const list = result.rows.map((row) => `"${row.tablename}"`).join(", ");
86
+ await pool.query(`truncate table ${list} restart identity cascade`);
87
+ };
88
+ const close = async () => {
89
+ await pool.end();
90
+ await stop();
91
+ };
92
+ return { pool, databaseUrl, reset, close };
93
+ };
94
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"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"}
package/dist/pool.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Pool } from "pg";
2
+ export interface CreatePoolConfig {
3
+ /** Override the resolved connection string (defaults to the env contract). */
4
+ connectionString?: string;
5
+ /** PEM CA cert; when set, the server cert + hostname are verified. */
6
+ caCert?: string;
7
+ /** Pool size cap. Defaults to env DATABASE_POOL_MAX, or 1 for a local socket. */
8
+ max?: number;
9
+ }
10
+ /**
11
+ * The one shared `pg.Pool`. Reuse this for everything — app queries (via
12
+ * `createQueries` / Drizzle) AND Better Auth's adapter — so there's exactly one
13
+ * pool per process (the playbook's rule).
14
+ *
15
+ * TLS handling matches the fleet standard (lifted from nk-auth's
16
+ * `createAuthPool`):
17
+ *
18
+ * - `caCert` set -> verify the server cert + hostname (`sslmode=verify-full`).
19
+ * - local host -> no TLS, and cap at `max: 1` (the PGlite socket server is
20
+ * single-connection/multiplexed; a larger pool breaks dev).
21
+ * - otherwise -> TLS **without** chain verification. Managed certs (DO,
22
+ * Supabase) aren't in Node's trust store, so full
23
+ * verification fails with "self-signed certificate in
24
+ * certificate chain"; the link is still encrypted. `sslmode`
25
+ * is stripped from the URL because `pg` ignores the `ssl`
26
+ * object when the URL carries SSL settings.
27
+ *
28
+ * On Vercel Fluid / serverless, hold the pool on `globalThis` so warm
29
+ * invocations reuse it instead of opening a connection per request (see the
30
+ * app-side `src/lib/db.ts` pattern in docs/db-package.md).
31
+ */
32
+ export declare const createPool: (config?: CreatePoolConfig) => Pool;
33
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,IAAI,CAAC;AAG3C,MAAM,WAAW,gBAAgB;IAChC,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAKD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAQ,gBAAqB,KAAG,IA6B1D,CAAC"}
package/dist/pool.js ADDED
@@ -0,0 +1,54 @@
1
+ import { Pool } from "pg";
2
+ import { dbEnv } from "./keys";
3
+ const isLocal = (connectionString) => connectionString.includes("127.0.0.1") || connectionString.includes("localhost");
4
+ /**
5
+ * The one shared `pg.Pool`. Reuse this for everything — app queries (via
6
+ * `createQueries` / Drizzle) AND Better Auth's adapter — so there's exactly one
7
+ * pool per process (the playbook's rule).
8
+ *
9
+ * TLS handling matches the fleet standard (lifted from nk-auth's
10
+ * `createAuthPool`):
11
+ *
12
+ * - `caCert` set -> verify the server cert + hostname (`sslmode=verify-full`).
13
+ * - local host -> no TLS, and cap at `max: 1` (the PGlite socket server is
14
+ * single-connection/multiplexed; a larger pool breaks dev).
15
+ * - otherwise -> TLS **without** chain verification. Managed certs (DO,
16
+ * Supabase) aren't in Node's trust store, so full
17
+ * verification fails with "self-signed certificate in
18
+ * certificate chain"; the link is still encrypted. `sslmode`
19
+ * is stripped from the URL because `pg` ignores the `ssl`
20
+ * object when the URL carries SSL settings.
21
+ *
22
+ * On Vercel Fluid / serverless, hold the pool on `globalThis` so warm
23
+ * invocations reuse it instead of opening a connection per request (see the
24
+ * app-side `src/lib/db.ts` pattern in docs/db-package.md).
25
+ */
26
+ export const createPool = (config = {}) => {
27
+ const env = config.connectionString ? undefined : dbEnv();
28
+ const connectionString = config.connectionString ?? env?.connectionString;
29
+ if (!connectionString) {
30
+ throw new Error("@ingram-tech/nk-db: createPool needs a connection string.");
31
+ }
32
+ const caCert = config.caCert ?? env?.caCert;
33
+ const local = isLocal(connectionString);
34
+ const max = config.max ?? env?.poolMax ?? (local ? 1 : undefined);
35
+ const base = max === undefined ? {} : { max };
36
+ if (caCert) {
37
+ return new Pool({
38
+ ...base,
39
+ connectionString,
40
+ ssl: { ca: caCert, rejectUnauthorized: true },
41
+ });
42
+ }
43
+ if (local) {
44
+ return new Pool({ ...base, connectionString });
45
+ }
46
+ const url = new URL(connectionString);
47
+ url.searchParams.delete("sslmode");
48
+ return new Pool({
49
+ ...base,
50
+ connectionString: url.toString(),
51
+ ssl: { rejectUnauthorized: false },
52
+ });
53
+ };
54
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAW/B,MAAM,OAAO,GAAG,CAAC,gBAAwB,EAAW,EAAE,CACrD,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,SAA2B,EAAE,EAAQ,EAAE;IACjE,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAC1D,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,GAAG,EAAE,gBAAgB,CAAC;IAC1E,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,MAAM,CAAC;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAElE,MAAM,IAAI,GAAe,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;IAE1D,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,IAAI,CAAC;YACf,GAAG,IAAI;YACP,gBAAgB;YAChB,GAAG,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE;SAC7C,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACtC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;QACf,GAAG,IAAI;QACP,gBAAgB,EAAE,GAAG,CAAC,QAAQ,EAAE;QAChC,GAAG,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE;KAClC,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { Pool, QueryResultRow } from "pg";
2
+ export interface Queries {
3
+ /** Run a query and return all rows. */
4
+ query: <T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) => Promise<T[]>;
5
+ /** Run a query expected to return at most one row; `null` when none. */
6
+ maybeOne: <T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) => Promise<T | null>;
7
+ /** Run a query that must return exactly one row; throws otherwise. */
8
+ one: <T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) => Promise<T>;
9
+ /** Run a write and return the affected row count. */
10
+ execute: (text: string, params?: unknown[]) => Promise<number>;
11
+ }
12
+ export interface PoolQueries extends Queries {
13
+ /**
14
+ * Run `fn` inside a single transaction on a dedicated client (`begin` /
15
+ * `commit`, `rollback` on throw). The `tx` passed to `fn` is a `Queries`
16
+ * bound to that client — use it, not the pool-level helpers, for the
17
+ * statements that must be atomic.
18
+ */
19
+ withTx: <T>(fn: (tx: Queries) => Promise<T>) => Promise<T>;
20
+ }
21
+ /**
22
+ * Bind the raw-SQL helpers (`query` / `one` / `maybeOne` / `execute` / `withTx`)
23
+ * to a pool. Signatures match what peppost/orbitr.ee hand-rolled, so adopting
24
+ * this is a find-and-replace of the import. Drizzle (via `createDb`) is the
25
+ * default query path; these are the escape hatch for SQL the ORM is awkward at
26
+ * (Postgres-function `select fn($1,…)` calls, `pgmq` draining, `pg_trgm`).
27
+ */
28
+ export declare const createQueries: (pool: Pool) => PoolQueries;
29
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,cAAc,EAAE,MAAM,IAAI,CAAC;AAK3D,MAAM,WAAW,OAAO;IACvB,uCAAuC;IACvC,KAAK,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAChD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClB,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACnD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAC9C,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAoCD,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC3C;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,KAAG,WAiB1C,CAAC"}
@@ -0,0 +1,53 @@
1
+ const bind = (executor) => {
2
+ const query = async (text, params = []) => {
3
+ const result = await executor.query(text, params);
4
+ return result.rows;
5
+ };
6
+ const maybeOne = async (text, params = []) => {
7
+ const rows = await query(text, params);
8
+ return rows[0] ?? null;
9
+ };
10
+ const one = async (text, params = []) => {
11
+ const rows = await query(text, params);
12
+ const row = rows[0];
13
+ if (!row)
14
+ throw new Error("Expected exactly one row, got none");
15
+ if (rows.length > 1) {
16
+ throw new Error(`Expected exactly one row, got ${rows.length}`);
17
+ }
18
+ return row;
19
+ };
20
+ const execute = async (text, params = []) => {
21
+ const result = await executor.query(text, params);
22
+ return result.rowCount ?? 0;
23
+ };
24
+ return { query, maybeOne, one, execute };
25
+ };
26
+ /**
27
+ * Bind the raw-SQL helpers (`query` / `one` / `maybeOne` / `execute` / `withTx`)
28
+ * to a pool. Signatures match what peppost/orbitr.ee hand-rolled, so adopting
29
+ * this is a find-and-replace of the import. Drizzle (via `createDb`) is the
30
+ * default query path; these are the escape hatch for SQL the ORM is awkward at
31
+ * (Postgres-function `select fn($1,…)` calls, `pgmq` draining, `pg_trgm`).
32
+ */
33
+ export const createQueries = (pool) => {
34
+ const base = bind(pool);
35
+ const withTx = async (fn) => {
36
+ const client = await pool.connect();
37
+ try {
38
+ await client.query("begin");
39
+ const result = await fn(bind(client));
40
+ await client.query("commit");
41
+ return result;
42
+ }
43
+ catch (error) {
44
+ await client.query("rollback");
45
+ throw error;
46
+ }
47
+ finally {
48
+ client.release();
49
+ }
50
+ };
51
+ return { ...base, withTx };
52
+ };
53
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAyBA,MAAM,IAAI,GAAG,CAAC,QAAkB,EAAW,EAAE;IAC5C,MAAM,KAAK,GAAG,KAAK,EAClB,IAAY,EACZ,SAAoB,EAAE,EACP,EAAE;QACjB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,IAAI,CAAC;IACpB,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,EACrB,IAAY,EACZ,SAAoB,EAAE,EACF,EAAE;QACtB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACxB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,EAChB,IAAY,EACZ,SAAoB,EAAE,EACT,EAAE;QACf,MAAM,IAAI,GAAG,MAAM,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,EAAE,IAAY,EAAE,SAAoB,EAAE,EAAmB,EAAE;QAC/E,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAC1C,CAAC,CAAC;AAYF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAU,EAAe,EAAE;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,KAAK,EAAK,EAA+B,EAAc,EAAE;QACvE,MAAM,MAAM,GAAe,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAChD,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YACtC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO,MAAM,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,KAAK,CAAC;QACb,CAAC;gBAAS,CAAC;YACV,MAAM,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;IACF,CAAC,CAAC;IACF,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Opt-in: keep `timestamptz` / `timestamp` columns as the raw ISO **strings**
3
+ * Postgres sends, instead of letting `pg` parse them into JS `Date`s.
4
+ *
5
+ * Why this exists: supabase-js / PostgREST returned timestamps as strings, and
6
+ * Supabase-generated row types declare them `string`. After moving to direct
7
+ * `pg`, the same columns come back as `Date`, which silently breaks any code
8
+ * doing string ops on them (and mismatches the generated types). Call this once
9
+ * at startup if your row types expect strings.
10
+ *
11
+ * Note: with Drizzle you usually express this per-column instead
12
+ * (`timestamp(..., { mode: "string" })`), so reach for that first on the golden
13
+ * path; this global switch is for the raw-helper / legacy-types case.
14
+ */
15
+ export declare const configureTimestampsAsStrings: () => void;
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,QAAO,IAI/C,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ import pg from "pg";
2
+ /** Postgres type OIDs we care about. */
3
+ const TIMESTAMPTZ_OID = 1184;
4
+ const TIMESTAMP_OID = 1114;
5
+ /**
6
+ * Opt-in: keep `timestamptz` / `timestamp` columns as the raw ISO **strings**
7
+ * Postgres sends, instead of letting `pg` parse them into JS `Date`s.
8
+ *
9
+ * Why this exists: supabase-js / PostgREST returned timestamps as strings, and
10
+ * Supabase-generated row types declare them `string`. After moving to direct
11
+ * `pg`, the same columns come back as `Date`, which silently breaks any code
12
+ * doing string ops on them (and mismatches the generated types). Call this once
13
+ * at startup if your row types expect strings.
14
+ *
15
+ * Note: with Drizzle you usually express this per-column instead
16
+ * (`timestamp(..., { mode: "string" })`), so reach for that first on the golden
17
+ * path; this global switch is for the raw-helper / legacy-types case.
18
+ */
19
+ export const configureTimestampsAsStrings = () => {
20
+ const keep = (value) => value;
21
+ pg.types.setTypeParser(TIMESTAMPTZ_OID, keep);
22
+ pg.types.setTypeParser(TIMESTAMP_OID, keep);
23
+ };
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,wCAAwC;AACxC,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAS,EAAE;IACtD,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC;IAC9C,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;IAC9C,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@ingram-tech/nk-db",
3
+ "version": "0.1.0",
4
+ "description": "The Ingram Postgres data layer: one TLS-aware pg pool, raw-SQL helpers, Drizzle wiring, and a PGlite (no-Docker) dev/test harness for Next.js sites.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ingram-technologies/nextkit.git",
10
+ "directory": "packages/nk-db"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "bin": {
19
+ "nk-pglite-dev": "./dist/pglite/dev.js"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./pglite": {
27
+ "types": "./dist/pglite/index.d.ts",
28
+ "import": "./dist/pglite/index.js"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "type-check": "tsc -p tsconfig.json --noEmit",
34
+ "test": "vitest run"
35
+ },
36
+ "dependencies": {
37
+ "zod": "^4.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@electric-sql/pglite": ">=0.5.0",
41
+ "@electric-sql/pglite-socket": ">=0.0.10",
42
+ "drizzle-orm": ">=0.44.0",
43
+ "pg": "^8.13.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@electric-sql/pglite": {
47
+ "optional": true
48
+ },
49
+ "@electric-sql/pglite-socket": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@electric-sql/pglite": "^0.5.0",
55
+ "@electric-sql/pglite-socket": "^0.0.10",
56
+ "@ingram-tech/typescript-config": "workspace:*",
57
+ "@types/node": "^20.0.0",
58
+ "@types/pg": "^8.11.0",
59
+ "drizzle-orm": "^0.45.0",
60
+ "pg": "^8.13.0",
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^4.1.6"
63
+ }
64
+ }