@porulle/adapter-postgres 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,52 @@
1
+ # @porulle/adapter-postgres
2
+
3
+ The PostgreSQL `DatabaseAdapter` for `@porulle/core`. Drizzle on `postgres-js`. The only database backend Porulle ships today.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { defineConfig } from "@porulle/core";
9
+ import { postgresAdapter } from "@porulle/adapter-postgres";
10
+
11
+ export default defineConfig({
12
+ databaseAdapter: postgresAdapter({
13
+ connectionString: process.env.DATABASE_URL!,
14
+ pool: {
15
+ max: 20,
16
+ idleTimeout: 30,
17
+ connectTimeout: 10,
18
+ statementTimeoutMs: 30_000,
19
+ lockTimeoutMs: 10_000,
20
+ pooled: false, // set true behind pgbouncer / pgcat / Fly MPG
21
+ },
22
+ }),
23
+ // …
24
+ });
25
+ ```
26
+
27
+ ## `pooled: true` — when to set it
28
+
29
+ If your `DATABASE_URL` points at a **transaction-mode pooler** (pgbouncer, pgcat, Fly Managed Postgres), set `pooled: true`. The pooler rejects libpq startup parameters like `statement_timeout` — when `pooled` is on, the adapter skips them. Set the timeouts on the DB role instead:
30
+
31
+ ```sql
32
+ ALTER ROLE app SET statement_timeout = '30s';
33
+ ALTER ROLE app SET lock_timeout = '10s';
34
+ ```
35
+
36
+ Direct PostgreSQL connections (no pooler) leave `pooled: false` (the default) so the adapter sets the timeouts per-session.
37
+
38
+ ## What it does
39
+
40
+ - Opens a `postgres-js` connection pool with the given options.
41
+ - Wraps it in Drizzle (`drizzle-orm/postgres-js`).
42
+ - Implements `DatabaseAdapter` from `@porulle/core` — `transaction(fn)` honours nested transactions correctly.
43
+
44
+ ## What it doesn't do
45
+
46
+ - No automatic migrations. Use `bunx drizzle-kit push` (dev) or `bunx drizzle-kit migrate` (prod) — see the CLI's `migrate` command.
47
+ - No connection retry on cold start. The kernel surfaces the error; redeploy or your platform's healthcheck handles it.
48
+
49
+ ## See also
50
+
51
+ - [Root README — Quick Start](../../../README.md#quick-start)
52
+ - [`SECURITY.md`](../../../SECURITY.md) — multi-tenant org-scoping is enforced at the repo layer; this adapter is the substrate
@@ -0,0 +1,41 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import type { DatabaseAdapter } from "@porulle/core";
3
+ export interface PostgresPoolOptions {
4
+ /** Maximum number of connections in the pool. Default: 20. */
5
+ max?: number;
6
+ /** Seconds a connection can idle before being closed. Default: 30. */
7
+ idleTimeout?: number;
8
+ /** Seconds to wait for a connection before throwing. Default: 10. */
9
+ connectTimeout?: number;
10
+ /**
11
+ * Maximum time (ms) any single SQL statement can run before PostgreSQL kills it.
12
+ * Default: 30000. Set to 0 to disable.
13
+ *
14
+ * NOTE: this is sent as a libpq startup parameter, which transaction-mode
15
+ * poolers (pgbouncer, pgcat, Fly Managed Postgres' pooler) reject as an
16
+ * "unsupported startup parameter". When `pooled: true` is set we skip the
17
+ * startup parameter — set the timeouts on the DB role instead, e.g.
18
+ * `ALTER ROLE app SET statement_timeout = '30s';`
19
+ */
20
+ statementTimeoutMs?: number;
21
+ /**
22
+ * Maximum time (ms) to wait for a row lock. Default: 10000. Same pooler
23
+ * caveat as `statementTimeoutMs` — skipped when `pooled: true`.
24
+ */
25
+ lockTimeoutMs?: number;
26
+ /**
27
+ * Set to true when connecting through a transaction-mode pooler
28
+ * (pgbouncer, pgcat, Fly Managed Postgres). Skips startup parameters that
29
+ * poolers reject. Defaults to false (assumes a direct PG connection).
30
+ */
31
+ pooled?: boolean;
32
+ }
33
+ export interface PostgresAdapterOptions {
34
+ connectionString: string;
35
+ /** Connection pool tuning. Defaults are production-reasonable. */
36
+ pool?: PostgresPoolOptions;
37
+ }
38
+ export type PostgresDrizzleClient = ReturnType<typeof drizzle>;
39
+ export type PostgresDatabaseAdapter = DatabaseAdapter<PostgresDrizzleClient, unknown>;
40
+ export declare function postgresAdapter(options: PostgresAdapterOptions): PostgresDatabaseAdapter;
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B;AAED,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC/D,MAAM,MAAM,uBAAuB,GAAG,eAAe,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AAEtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,uBAAuB,CAgCxF"}
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ import postgres from "postgres";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ export function postgresAdapter(options) {
4
+ const pool = options.pool ?? {};
5
+ const statementTimeout = pool.statementTimeoutMs ?? 30_000;
6
+ const lockTimeout = pool.lockTimeoutMs ?? 10_000;
7
+ const pooled = pool.pooled ?? false;
8
+ const client = postgres(options.connectionString, {
9
+ max: pool.max ?? 20,
10
+ idle_timeout: pool.idleTimeout ?? 30,
11
+ connect_timeout: pool.connectTimeout ?? 10,
12
+ prepare: false,
13
+ // Direct PG connections accept these as startup parameters. Pooled
14
+ // connections reject them — set timeouts via `ALTER ROLE ... SET` instead.
15
+ ...(pooled
16
+ ? {}
17
+ : {
18
+ connection: {
19
+ statement_timeout: String(statementTimeout),
20
+ lock_timeout: String(lockTimeout),
21
+ },
22
+ }),
23
+ });
24
+ const db = drizzle(client);
25
+ return {
26
+ provider: "postgresql",
27
+ db,
28
+ async transaction(fn) {
29
+ return db.transaction(async (tx) => fn(tx));
30
+ },
31
+ };
32
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@porulle/adapter-postgres",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "types": "./src/index.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
15
+ "check-types": "tsc --noEmit",
16
+ "lint": "eslint . --max-warnings 1000",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@porulle/core": "workspace:*",
21
+ "drizzle-orm": "^0.45.1",
22
+ "postgres": "^3.4.7"
23
+ },
24
+ "devDependencies": {
25
+ "@repo/eslint-config": "*",
26
+ "@repo/typescript-config": "*",
27
+ "@types/node": "^24.5.2",
28
+ "eslint": "^9.39.1",
29
+ "typescript": "5.9.2",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "description": "The PostgreSQL DatabaseAdapter for @porulle/core. Drizzle on postgres-js. The only database backend Porulle ships today.",
41
+ "homepage": "https://porulle-docs.vercel.app",
42
+ "bugs": {
43
+ "url": "https://github.com/asyncdotengineering/porulle/issues"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/asyncdotengineering/porulle.git",
48
+ "directory": "packages/adapters/adapter-postgres"
49
+ },
50
+ "author": "Porulle contributors"
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ import postgres from "postgres";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ import type { DatabaseAdapter } from "@porulle/core";
4
+
5
+ export interface PostgresPoolOptions {
6
+ /** Maximum number of connections in the pool. Default: 20. */
7
+ max?: number;
8
+ /** Seconds a connection can idle before being closed. Default: 30. */
9
+ idleTimeout?: number;
10
+ /** Seconds to wait for a connection before throwing. Default: 10. */
11
+ connectTimeout?: number;
12
+ /**
13
+ * Maximum time (ms) any single SQL statement can run before PostgreSQL kills it.
14
+ * Default: 30000. Set to 0 to disable.
15
+ *
16
+ * NOTE: this is sent as a libpq startup parameter, which transaction-mode
17
+ * poolers (pgbouncer, pgcat, Fly Managed Postgres' pooler) reject as an
18
+ * "unsupported startup parameter". When `pooled: true` is set we skip the
19
+ * startup parameter — set the timeouts on the DB role instead, e.g.
20
+ * `ALTER ROLE app SET statement_timeout = '30s';`
21
+ */
22
+ statementTimeoutMs?: number;
23
+ /**
24
+ * Maximum time (ms) to wait for a row lock. Default: 10000. Same pooler
25
+ * caveat as `statementTimeoutMs` — skipped when `pooled: true`.
26
+ */
27
+ lockTimeoutMs?: number;
28
+ /**
29
+ * Set to true when connecting through a transaction-mode pooler
30
+ * (pgbouncer, pgcat, Fly Managed Postgres). Skips startup parameters that
31
+ * poolers reject. Defaults to false (assumes a direct PG connection).
32
+ */
33
+ pooled?: boolean;
34
+ }
35
+
36
+ export interface PostgresAdapterOptions {
37
+ connectionString: string;
38
+ /** Connection pool tuning. Defaults are production-reasonable. */
39
+ pool?: PostgresPoolOptions;
40
+ }
41
+
42
+ export type PostgresDrizzleClient = ReturnType<typeof drizzle>;
43
+ export type PostgresDatabaseAdapter = DatabaseAdapter<PostgresDrizzleClient, unknown>;
44
+
45
+ export function postgresAdapter(options: PostgresAdapterOptions): PostgresDatabaseAdapter {
46
+ const pool = options.pool ?? {};
47
+
48
+ const statementTimeout = pool.statementTimeoutMs ?? 30_000;
49
+ const lockTimeout = pool.lockTimeoutMs ?? 10_000;
50
+ const pooled = pool.pooled ?? false;
51
+
52
+ const client = postgres(options.connectionString, {
53
+ max: pool.max ?? 20,
54
+ idle_timeout: pool.idleTimeout ?? 30,
55
+ connect_timeout: pool.connectTimeout ?? 10,
56
+ prepare: false,
57
+ // Direct PG connections accept these as startup parameters. Pooled
58
+ // connections reject them — set timeouts via `ALTER ROLE ... SET` instead.
59
+ ...(pooled
60
+ ? {}
61
+ : {
62
+ connection: {
63
+ statement_timeout: String(statementTimeout),
64
+ lock_timeout: String(lockTimeout),
65
+ } as Record<string, string>,
66
+ }),
67
+ });
68
+ const db = drizzle(client);
69
+
70
+ return {
71
+ provider: "postgresql",
72
+ db,
73
+ async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
74
+ return db.transaction(async (tx) => fn(tx));
75
+ },
76
+ };
77
+ }