@smithy-hono/adapter-postgres 0.2.2

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,158 @@
1
+ # @smithy-hono/adapter-postgres
2
+
3
+ Postgres-backed `DataStore<T>` for [`@smithy-hono/data-core`](../data-core)
4
+ (Plan 13 D7). It maps the `DataStore<T>` persistence port onto Postgres over a
5
+ narrow *structural client port* so nothing here imports the `pg` driver at
6
+ runtime (ARCH-01).
7
+
8
+ **Postgres is the recommended durable store of record for the Node deployment.**
9
+ The Redis `DataStore` in [`@smithy-hono/adapter-node`](../adapter-node) is the
10
+ optional **cache-grade** alternative — for ephemeral/cache-like entities or shops
11
+ already running Redis. Redis is RAM-bound and weaker at rich list/filter/count
12
+ (it needs hand-maintained index SETs + a capped client-side scan for undeclared
13
+ filters). For durable CRUD, reach for Postgres. (Redis remains correct for the
14
+ *security* stores — that is a separate concern.)
15
+
16
+ | Concern | Backend | Class / fn | Consistency |
17
+ |---------|---------|------------|-------------|
18
+ | `DataStore<T>` | Postgres (`jsonb`) | `PostgresDataStore` / `createPostgresDataStore` | **strong** (SQL CAS) |
19
+
20
+ Full capabilities: optimistic concurrency (versioned CAS), equality `filter` +
21
+ `count` on **any** field (server-side, no client scan), opaque-cursor
22
+ pagination, and (opt-in) soft-delete.
23
+
24
+ ## The PORT pattern
25
+
26
+ The store never imports `pg`. All logic runs against a narrow **SEMANTIC** port
27
+ (`PgDataPort` — `getRow` / `insertIfAbsent` / `putRow` / `updateCas` /
28
+ `deleteCas` / `listRows` / `count`), never raw SQL. Two ports satisfy it:
29
+
30
+ - **`createPgDataPort(client, table?)`** — the REAL port, the only place that
31
+ speaks SQL. It codes against a structural **`PgClientLike`**:
32
+
33
+ ```ts
34
+ interface PgClientLike {
35
+ query(text: string, params?: unknown[]): Promise<{
36
+ rows: Array<Record<string, unknown>>
37
+ rowCount: number | null
38
+ }>
39
+ }
40
+ ```
41
+
42
+ A `pg` `Pool`/`Client` is a structural superset; a `postgres.js` client
43
+ (adapted to this `query` shape) or a Neon serverless client satisfies it too.
44
+ All queries are parameterized (`$1`, `$2`, ...).
45
+ - **`createFakePgDataPort()`** — an in-process port over a `Map` that honors the
46
+ SAME atomicity contract synchronously (single JS tick == atomic). It backs the
47
+ always-on conformance suite, so all store logic is exercised with no Postgres.
48
+
49
+ ## Table schema (run as a migration)
50
+
51
+ The adapter never issues DDL at runtime. The consumer runs `pgCreateTableSql()`
52
+ (+ a `pgCreateIndexSql()` per declared `@persisted` index) once per collection:
53
+
54
+ ```sql
55
+ CREATE TABLE IF NOT EXISTS "data_store" (
56
+ scope text NOT NULL,
57
+ id text NOT NULL,
58
+ value jsonb NOT NULL,
59
+ version bigint NOT NULL,
60
+ deleted_at timestamptz,
61
+ PRIMARY KEY (scope, id)
62
+ );
63
+ -- one per declared @persisted index field (a short field-name hash is appended
64
+ -- to the index name to keep distinct fields from colliding):
65
+ CREATE INDEX IF NOT EXISTS "data_store_idx_ownerId_e51b6a79"
66
+ ON "data_store" (scope, (value->>'ownerId'));
67
+ ```
68
+
69
+ - `(scope, id)` is the composite PRIMARY KEY → tenant A literally cannot address
70
+ tenant B's rows (cross-scope `get` returns `null`), and the same key in two
71
+ scopes never collides. `scope` is a length-prefixed `DataScope` segment.
72
+ - `value` is `jsonb`, so an equality filter on **any** field is a server-side
73
+ `WHERE value->>'field' = $n` — **there is no client-side scan fallback**.
74
+ Declared indexes are purely a performance optimization (btree expression
75
+ indexes that keep the filter sargable at scale); an undeclared filter is still
76
+ answered server-side, just without a dedicated index.
77
+ - `version` (`bigint`) is the optimistic-concurrency token; a versioned write is
78
+ a single conditional `UPDATE ... WHERE version = $` whose `rowCount` we inspect.
79
+ - `deleted_at` (`timestamptz`) is NULL for live rows; a tombstone when
80
+ soft-deleted (hidden from `get`/`list`/`count`).
81
+
82
+ ## Consumer wiring
83
+
84
+ ### node-postgres (`pg`)
85
+
86
+ ```ts
87
+ import { Pool } from 'pg'
88
+ import {
89
+ createPostgresDataStore,
90
+ createPgDataPort,
91
+ pgCreateTableSql,
92
+ pgCreateIndexSql,
93
+ } from '@smithy-hono/adapter-postgres'
94
+
95
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL })
96
+
97
+ // Migration (once):
98
+ await pool.query(pgCreateTableSql('todos'))
99
+ await pool.query(pgCreateIndexSql('todos', 'ownerId'))
100
+
101
+ const store = createPostgresDataStore(
102
+ createPgDataPort(pool, 'todos'),
103
+ { table: 'todos', indexes: ['ownerId'], softDelete: false },
104
+ )
105
+ // const ops = createDefaultTodoOperations(store) // zero-handler CRUD
106
+ ```
107
+
108
+ ### postgres.js
109
+
110
+ `postgres.js`'s tagged-template client is not `PgClientLike`-shaped, so wrap it:
111
+
112
+ ```ts
113
+ import postgres from 'postgres'
114
+ const sql = postgres(process.env.DATABASE_URL!)
115
+ const client = {
116
+ async query(text: string, params: unknown[] = []) {
117
+ const rows = await sql.unsafe(text, params as never[])
118
+ return { rows: rows as unknown as Array<Record<string, unknown>>, rowCount: rows.count }
119
+ },
120
+ }
121
+ const store = createPostgresDataStore(createPgDataPort(client, 'todos'), { table: 'todos' })
122
+ ```
123
+
124
+ ### Neon serverless
125
+
126
+ The Neon serverless driver exports a `pg`-compatible `Pool`, so it satisfies
127
+ `PgClientLike` directly:
128
+
129
+ ```ts
130
+ import { Pool } from '@neondatabase/serverless'
131
+ const store = createPostgresDataStore(
132
+ createPgDataPort(new Pool({ connectionString: process.env.DATABASE_URL })),
133
+ )
134
+ ```
135
+
136
+ ## Test / verify
137
+
138
+ ```
139
+ npx tsc --noEmit -p tsconfig.build.json # types (source-only, ARCH-01 guard)
140
+ npx vitest run # conformance (fake-backed)
141
+ ```
142
+
143
+ The always-on conformance (`src/dataStore.conformance.test.ts`) runs the
144
+ `@smithy-hono/data-core` `describeDataStore` suite against `createFakePgDataPort`
145
+ — full capabilities, hard-delete + soft-delete variants — so it proves adapter
146
+ logic with root-hoisted tooling only (no Postgres, no `pg` install).
147
+
148
+ ## Live verification (real Postgres)
149
+
150
+ `src/live.postgres.dataStore.test.ts` (gated on `DATABASE_URL` / `POSTGRES_URL`,
151
+ self-skips when unset) runs the SAME conformance suite against a **real
152
+ Postgres** via the real `createPgDataPort` over a `pg` `Pool` — validating the
153
+ genuine versioned CAS, `INSERT ... ON CONFLICT`, `WHERE value->>'field'` filter +
154
+ `COUNT(*)`, and opaque-cursor pagination server-side. `pg` is imported via a
155
+ non-literal specifier so the file typechecks without it installed. Run it locally
156
+ with `./scripts/verify-live.sh` (Postgres in Docker) or in CI via
157
+ `.github/workflows/live-conformance.yml` (a `postgres:16-alpine` service
158
+ container).
@@ -0,0 +1,255 @@
1
+ /**
2
+ * `DataStore<T>` (Plan 13 D7 — Postgres is the durable Node default) over
3
+ * Postgres, behind a narrow structural client port (ARCH-01).
4
+ *
5
+ * Postgres is a *database* backend, not a runtime, so this is its own package —
6
+ * usable from any runtime with a structural client (`pg`, `postgres.js`, Neon
7
+ * serverless). It is the **recommended durable store of record for the Node
8
+ * deployment**; the Redis `DataStore` in `@smithy-hono/adapter-node` is the
9
+ * optional cache-grade alternative (RAM-bound, weaker at rich list/filter/count).
10
+ *
11
+ * The store mirrors the adapter-cf D1 store almost line-for-line — both are SQL,
12
+ * so the SEMANTIC port + real-impl + Map-fake + factory + schema-helper structure
13
+ * is the same. The one Postgres advantage over D1/Redis: a `jsonb` value column
14
+ * means an equality filter on **any** field is a server-side
15
+ * `WHERE value->>'field' = $n` (and `COUNT(*)`) — so there is **NO client-side
16
+ * scan fallback** (unlike Redis). Declared `@persisted(indexes)` are purely a
17
+ * performance optimization: btree expression indexes on `(value->>'field')` that
18
+ * keep the filter sargable at scale; an undeclared filter is still answered
19
+ * server-side, just without a dedicated index.
20
+ *
21
+ * Like the security stores it never imports the `pg` driver: the consumer's real
22
+ * `pg` `Pool`/`Client`, a `postgres.js` client (adapted), or a Neon serverless
23
+ * client structurally satisfies {@link PgClientLike}, and all store logic runs
24
+ * against a narrow SEMANTIC port ({@link PgDataPort}) — never raw SQL — so the
25
+ * conformance fake reimplements the semantics in JS with no SQL parser.
26
+ *
27
+ * Storage model (one entity = one row):
28
+ * - table {@link PG_TABLE_DEFAULT} (override via `opts.table`), columns:
29
+ * `scope` (length-prefixed {@link DataScope}), `id` (the entity key),
30
+ * `value` (`jsonb`, the JSON entity), `version` (`bigint`, the
31
+ * optimistic-concurrency token), `deleted_at` (`timestamptz` tombstone, only
32
+ * written when `softDelete`). PRIMARY KEY is `(scope, id)`, so tenant A
33
+ * literally cannot address tenant B's rows (cross-scope `get` → null), and
34
+ * the same key in two scopes never collides.
35
+ * - Declared `@persisted(indexes)` map to btree expression indexes over the
36
+ * JSONB so a filtered `list`/`count` stays sargable. See
37
+ * {@link pgCreateTableSql} for the migration the consumer runs.
38
+ */
39
+ import { type DataScope, type DataStore, type ListQuery, type Page, type Stored } from '@smithy-hono/data-core';
40
+ /** A persisted row as the port exchanges it (the store owns the JSON shape). */
41
+ export interface PgRow {
42
+ /** The entity key (the resource identifier). */
43
+ id: string;
44
+ /** The JSON-serialized entity (without the store-managed `version`). */
45
+ value: string;
46
+ /** The optimistic-concurrency version. */
47
+ version: number;
48
+ /** ISO tombstone timestamp, or `null` for a live row. */
49
+ deletedAt: string | null;
50
+ }
51
+ /** A query the Postgres port resolves server-side (the store never builds SQL). */
52
+ export interface PgListArgs {
53
+ /** Max rows to return (the store asks for `limit + 1` to detect more pages). */
54
+ limit: number;
55
+ /** Exclusive lower bound on `id` (the decoded cursor), or undefined for the start. */
56
+ after?: string;
57
+ /** Equality filter over JSONB fields (resolved as `value->>'field' = $n` predicates). */
58
+ filter?: Record<string, string | number | boolean>;
59
+ /** When true, tombstoned rows are excluded (soft-delete stores). */
60
+ excludeDeleted: boolean;
61
+ }
62
+ /**
63
+ * The minimal SEMANTIC port {@link PostgresDataStore} uses. Implementations MUST
64
+ * preserve the atomicity of {@link updateCas} / {@link deleteCas} (the
65
+ * versioned read-compare-write) and the create-if-absent of
66
+ * {@link insertIfAbsent}.
67
+ */
68
+ export interface PgDataPort {
69
+ /** Fetch one row by `(scope, id)`, or `null`. Tombstones are returned (the store hides them). */
70
+ getRow(scope: string, id: string): Promise<PgRow | null>;
71
+ /**
72
+ * Create-if-absent. Returns the ASSIGNED version on a write (`1` for a
73
+ * brand-new key or a create after a HARD delete; `tombstone.version + 1` when
74
+ * resurrecting a soft-delete tombstone — the version CONTINUES, never resets),
75
+ * or `null` if a (live) row already existed at `(scope, id)` (atomic
76
+ * `INSERT ... ON CONFLICT DO NOTHING`). `allowOverTombstone` resurrects a
77
+ * tombstoned row (soft-delete create).
78
+ */
79
+ insertIfAbsent(scope: string, row: PgRow, allowOverTombstone: boolean): Promise<number | null>;
80
+ /** Unconditional upsert (idempotent `put`). Returns the resulting version. */
81
+ putRow(scope: string, row: PgRow, prevVersion: number | null): Promise<number>;
82
+ /**
83
+ * Versioned compare-and-set. Writes `row` only if the current version equals
84
+ * `expectedVersion` (when given) and the row exists+is live. Returns the new
85
+ * version on success, `-1` on miss, `-2` on a version conflict.
86
+ */
87
+ updateCas(scope: string, row: PgRow, expectedVersion: number | undefined): Promise<number>;
88
+ /**
89
+ * Versioned delete. Hard-removes (or, when `softDeletePayload` is given,
90
+ * tombstones) the row at `(scope, id)`. Returns the new version on a
91
+ * soft-delete, `0` on a successful hard delete, `-1` on miss, `-2` on conflict.
92
+ */
93
+ deleteCas(scope: string, id: string, expectedVersion: number | undefined, softDeletePayload: PgRow | null): Promise<number>;
94
+ /** List rows in a scope, ordered by `id`, honoring filter + cursor. */
95
+ listRows(scope: string, args: PgListArgs): Promise<PgRow[]>;
96
+ /** Count rows in a scope honoring the filter (server-side `COUNT(*)`). */
97
+ count(scope: string, filter: Record<string, string | number | boolean> | undefined, excludeDeleted: boolean): Promise<number>;
98
+ }
99
+ /**
100
+ * The slice of a node-postgres client {@link createPgDataPort} maps onto. A
101
+ * consumer's real `pg` `Pool`/`Client` is a structural superset — so nothing
102
+ * here imports `pg` (ARCH-01). A `postgres.js` client (adapted to this `query`
103
+ * shape) or a Neon serverless client satisfies it too. Only `query` is used;
104
+ * queries are always parameterized (`$1`, `$2`, ...).
105
+ */
106
+ export interface PgClientLike {
107
+ query(text: string, params?: unknown[]): Promise<{
108
+ rows: Array<Record<string, unknown>>;
109
+ rowCount: number | null;
110
+ }>;
111
+ /**
112
+ * OPTIONAL: check out a single dedicated connection (the node-postgres
113
+ * `Pool.connect()` shape). When present, the `statement_timeout`-scoped read
114
+ * path runs its whole `BEGIN`/`SET LOCAL`/read/`COMMIT` on this ONE connection
115
+ * so the timeout actually applies (a bare `Pool.query()` per statement can land
116
+ * each on a different pooled connection, defeating `SET LOCAL`). Adding this is
117
+ * a backward-compatible widening: a bare `query()`-only client still satisfies
118
+ * the interface and falls back to an untimed read.
119
+ */
120
+ connect?(): Promise<{
121
+ query(text: string, params?: unknown[]): Promise<{
122
+ rows: Array<Record<string, unknown>>;
123
+ rowCount: number | null;
124
+ }>;
125
+ release(err?: unknown): void;
126
+ }>;
127
+ }
128
+ /** Default Postgres table name; override via {@link PostgresDataStoreOptions.table}. */
129
+ export declare const PG_TABLE_DEFAULT = "data_store";
130
+ /** Options for {@link createPostgresDataStore} / {@link PostgresDataStore}. */
131
+ export interface PostgresDataStoreOptions {
132
+ /**
133
+ * The Postgres table backing this collection. Default {@link PG_TABLE_DEFAULT}.
134
+ * Used by {@link createPgDataPort} when building SQL; the consumer must have
135
+ * created the table (see {@link pgCreateTableSql}).
136
+ */
137
+ table?: string;
138
+ /**
139
+ * Tombstone on delete instead of hard-removing, and hide tombstoned rows from
140
+ * `get`/`list`/`count` (default `false`). Mirrors `@persisted(softDelete:)`.
141
+ */
142
+ softDelete?: boolean;
143
+ /**
144
+ * Declared `@persisted` index field names. With Postgres an equality filter on
145
+ * ANY field is a server-side `WHERE value->>'field' = $n` (no client scan, even
146
+ * undeclared) — declaring an index just adds a matching btree expression index
147
+ * (see {@link pgCreateIndexSql}) so the filter stays sargable at scale. Kept for
148
+ * parity with the other adapters.
149
+ */
150
+ indexes?: readonly string[];
151
+ /**
152
+ * OPTIONAL upper bound on a `list`'s effective page size (defense-in-depth). When
153
+ * set, `list` silently CLAMPS `query.limit` down to `maxLimit`
154
+ * (`Math.min(query.limit, maxLimit)`) so a large caller `limit` cannot drive an
155
+ * unbounded scan — it does NOT reject the request. Unset (default) means NO clamp:
156
+ * behavior is identical to before this knob existed. The clamp only lowers rows
157
+ * returned per page; the opaque resume-cursor contract is unchanged.
158
+ */
159
+ maxLimit?: number;
160
+ }
161
+ declare class PostgresDataStore<T extends Record<string, unknown>> implements DataStore<T> {
162
+ #private;
163
+ constructor(port: PgDataPort, opts?: PostgresDataStoreOptions);
164
+ get(key: string, scope: DataScope): Promise<Stored<T> | null>;
165
+ create(key: string, value: T, scope: DataScope): Promise<Stored<T>>;
166
+ put(key: string, value: T, scope: DataScope): Promise<Stored<T>>;
167
+ update(key: string, value: T, expectedVersion: number | undefined, scope: DataScope): Promise<Stored<T>>;
168
+ patch(key: string, partial: Partial<T>, expectedVersion: number | undefined, scope: DataScope): Promise<Stored<T>>;
169
+ delete(key: string, expectedVersion: number | undefined, scope: DataScope): Promise<boolean>;
170
+ list(query: ListQuery, scope: DataScope): Promise<Page<T>>;
171
+ count(query: Omit<ListQuery, 'cursor' | 'limit'>, scope: DataScope): Promise<number>;
172
+ }
173
+ /**
174
+ * The `CREATE TABLE` for a Postgres-backed {@link DataStore}. The consumer runs
175
+ * this once (a migration) per collection — the adapter never issues DDL at
176
+ * runtime.
177
+ *
178
+ * - `(scope, id)` is the composite PRIMARY KEY → cross-scope get returns
179
+ * nothing, same key in two scopes never collides.
180
+ * - `value` is `jsonb`, so an equality filter on ANY field is a server-side
181
+ * `value->>'field' = $n`; declared indexes are btree expressions over it
182
+ * (see {@link pgCreateIndexSql}).
183
+ * - `version` is `bigint` (the optimistic-concurrency token).
184
+ * - `deleted_at` is NULL for live rows; a tombstone `timestamptz` when soft-deleted.
185
+ *
186
+ * @param table the table name (default {@link PG_TABLE_DEFAULT}); must match the
187
+ * `opts.table` passed to {@link createPostgresDataStore} / {@link createPgDataPort}.
188
+ */
189
+ export declare function pgCreateTableSql(table?: string): string;
190
+ /**
191
+ * A btree expression index for one declared `@persisted` index field, so an
192
+ * equality `filter` on it stays sargable in SQL. Mirrors the adapter-cf D1
193
+ * computed-column index. The consumer runs this alongside {@link pgCreateTableSql}.
194
+ *
195
+ * @example pgCreateIndexSql('data_store', 'ownerId')
196
+ */
197
+ export declare function pgCreateIndexSql(table: string, field: string): string;
198
+ export interface PgDataPortOptions {
199
+ /**
200
+ * Per-query `statement_timeout` (ms) applied to the `list`/`count` read
201
+ * statements via a transaction-scoped `SET LOCAL`, so a single pathological
202
+ * unindexed scan cannot run unbounded. Omit (or `0`) to leave the
203
+ * server/pool default in force.
204
+ */
205
+ statementTimeoutMs?: number;
206
+ }
207
+ /**
208
+ * Build the REAL {@link PgDataPort} over a structural {@link PgClientLike}. This
209
+ * is the only SQL in the adapter; the store logic stays SQL-free.
210
+ *
211
+ * Versioned writes are a single conditional `UPDATE ... WHERE scope=$ AND id=$
212
+ * AND version=$ AND deleted_at IS NULL` — Postgres runs each statement
213
+ * atomically, so `rowCount === 0` means "no row matched the version"; a follow-up
214
+ * `SELECT` disambiguates miss vs {@link CONFLICT}. Create is `INSERT ... ON
215
+ * CONFLICT (scope, id) DO NOTHING` (0 rows ⇒ already exists; the soft-delete
216
+ * variant `DO UPDATE ... WHERE deleted_at IS NOT NULL` resurrects a tombstone).
217
+ *
218
+ * Production wiring: `createPgDataPort(new Pool({ connectionString }))`.
219
+ *
220
+ * @param client the consumer's `pg` `Pool`/`Client` (structural).
221
+ * @param table the table name; must match {@link createPostgresDataStore}'s `opts.table`.
222
+ * @param opts.statementTimeoutMs optional per-query `statement_timeout` for reads.
223
+ */
224
+ export declare function createPgDataPort(client: PgClientLike, table?: string, opts?: PgDataPortOptions): PgDataPort;
225
+ /**
226
+ * An in-process {@link PgDataPort} backed by a `Map`. Reimplements the CAS /
227
+ * create-if-absent / filtered-list semantics in JS — exactly as adapter-cf's
228
+ * `createFakeD1DataPort` reimplements the SQL CAS — so the always-on conformance
229
+ * suite exercises all store logic with no Postgres. The real SQL is validated by
230
+ * `live.postgres.dataStore.test.ts` in CI.
231
+ *
232
+ * Each call runs its read-compare-write in one synchronous section before the
233
+ * returned promise settles, so there is no interleaving (JS single-thread), the
234
+ * same atomicity Postgres gives per statement.
235
+ */
236
+ export declare function createFakePgDataPort(): PgDataPort;
237
+ /**
238
+ * Construct a Postgres-backed {@link DataStore} over a {@link PgDataPort}. Like
239
+ * the security stores, the logic lives only against the port, so the same class
240
+ * passes the `@smithy-hono/data-core` conformance suite against the in-process
241
+ * fake AND runs unchanged against real Postgres in production.
242
+ *
243
+ * Full capabilities: optimistic concurrency (SQL CAS), equality filter + count on
244
+ * ANY field (server-side `WHERE value->>'field' = $n` / `COUNT(*)` — no client
245
+ * scan), opaque-cursor pagination, and (opt-in) soft-delete.
246
+ *
247
+ * @example
248
+ * const store = createPostgresDataStore(
249
+ * createPgDataPort(new Pool({ connectionString }), 'todos'),
250
+ * { table: 'todos', indexes: ['ownerId'], softDelete: false },
251
+ * )
252
+ */
253
+ export declare function createPostgresDataStore<T extends Record<string, unknown> = Record<string, unknown>>(port: PgDataPort, opts?: PostgresDataStoreOptions): DataStore<T>;
254
+ export { PostgresDataStore };
255
+ //# sourceMappingURL=dataStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataStore.d.ts","sourceRoot":"","sources":["../src/dataStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,MAAM,EACZ,MAAM,wBAAwB,CAAA;AA8B/B,gFAAgF;AAChF,MAAM,WAAW,KAAK;IACpB,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAA;IACV,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAA;IACb,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IAClD,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAA;IACxD;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC9F,8EAA8E;IAC9E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9E;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1F;;;;OAIG;IACH,SAAS,CACP,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,iBAAiB,EAAE,KAAK,GAAG,IAAI,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAA;IAClB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAA;IAC3D,0EAA0E;IAC1E,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC9H;AAUD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAC7E;;;;;;;;OAQG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC;QAClB,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC;YAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC,CAAA;QAC7E,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAC7B,CAAC,CAAA;CACH;AAMD,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,eAAe,CAAA;AAE5C,+EAA+E;AAC/E,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,cAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,YAAW,SAAS,CAAC,CAAC,CAAC;;gBAOpE,IAAI,EAAE,UAAU,EAAE,IAAI,GAAE,wBAA6B;IAc3D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAO7D,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAcnE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAUhE,MAAM,CACV,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,EACR,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAUf,KAAK,CACT,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAsBf,MAAM,CACV,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,OAAO,CAAC;IA2Bb,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAmC1D,KAAK,CACT,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC,EAC1C,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,MAAM,CAAC;CAyCnB;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAyB,GAAG,MAAM,CAYzE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAWrE;AAwED,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,EACpB,KAAK,GAAE,MAAyB,EAChC,IAAI,GAAE,iBAAsB,GAC3B,UAAU,CAqLZ;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CA2FjD;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,IAAI,EAAE,UAAU,EAAE,IAAI,GAAE,wBAA6B,GAAG,SAAS,CAAC,CAAC,CAAC,CAErE;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA"}
@@ -0,0 +1,620 @@
1
+ /**
2
+ * `DataStore<T>` (Plan 13 D7 — Postgres is the durable Node default) over
3
+ * Postgres, behind a narrow structural client port (ARCH-01).
4
+ *
5
+ * Postgres is a *database* backend, not a runtime, so this is its own package —
6
+ * usable from any runtime with a structural client (`pg`, `postgres.js`, Neon
7
+ * serverless). It is the **recommended durable store of record for the Node
8
+ * deployment**; the Redis `DataStore` in `@smithy-hono/adapter-node` is the
9
+ * optional cache-grade alternative (RAM-bound, weaker at rich list/filter/count).
10
+ *
11
+ * The store mirrors the adapter-cf D1 store almost line-for-line — both are SQL,
12
+ * so the SEMANTIC port + real-impl + Map-fake + factory + schema-helper structure
13
+ * is the same. The one Postgres advantage over D1/Redis: a `jsonb` value column
14
+ * means an equality filter on **any** field is a server-side
15
+ * `WHERE value->>'field' = $n` (and `COUNT(*)`) — so there is **NO client-side
16
+ * scan fallback** (unlike Redis). Declared `@persisted(indexes)` are purely a
17
+ * performance optimization: btree expression indexes on `(value->>'field')` that
18
+ * keep the filter sargable at scale; an undeclared filter is still answered
19
+ * server-side, just without a dedicated index.
20
+ *
21
+ * Like the security stores it never imports the `pg` driver: the consumer's real
22
+ * `pg` `Pool`/`Client`, a `postgres.js` client (adapted), or a Neon serverless
23
+ * client structurally satisfies {@link PgClientLike}, and all store logic runs
24
+ * against a narrow SEMANTIC port ({@link PgDataPort}) — never raw SQL — so the
25
+ * conformance fake reimplements the semantics in JS with no SQL parser.
26
+ *
27
+ * Storage model (one entity = one row):
28
+ * - table {@link PG_TABLE_DEFAULT} (override via `opts.table`), columns:
29
+ * `scope` (length-prefixed {@link DataScope}), `id` (the entity key),
30
+ * `value` (`jsonb`, the JSON entity), `version` (`bigint`, the
31
+ * optimistic-concurrency token), `deleted_at` (`timestamptz` tombstone, only
32
+ * written when `softDelete`). PRIMARY KEY is `(scope, id)`, so tenant A
33
+ * literally cannot address tenant B's rows (cross-scope `get` → null), and
34
+ * the same key in two scopes never collides.
35
+ * - Declared `@persisted(indexes)` map to btree expression indexes over the
36
+ * JSONB so a filtered `list`/`count` stays sargable. See
37
+ * {@link pgCreateTableSql} for the migration the consumer runs.
38
+ */
39
+ import { OptimisticConflictError, } from '@smithy-hono/data-core';
40
+ // ---------------------------------------------------------------------------
41
+ // Shared scope + cursor helpers (mirror the adapter-cf D1 DataStore exactly).
42
+ // ---------------------------------------------------------------------------
43
+ /** Length-prefixed scope segment (collision-proof, mirrors the other adapters). */
44
+ function scopeSeg(scope) {
45
+ const t = scope.tenantId ?? '';
46
+ const o = scope.ownerId ?? '';
47
+ return `${t.length}:${t}|${o.length}:${o}`;
48
+ }
49
+ /** Opaque base64 cursor of the last entity key emitted — NEVER an offset. */
50
+ function encodeCursor(lastKey) {
51
+ return btoa(unescape(encodeURIComponent(lastKey)));
52
+ }
53
+ function decodeCursor(cursor) {
54
+ return decodeURIComponent(escape(atob(cursor)));
55
+ }
56
+ /** Reply sentinels from {@link PgDataPort.updateCas} / {@link PgDataPort.deleteCas}. */
57
+ const MISS = -1;
58
+ const CONFLICT = -2;
59
+ // ---------------------------------------------------------------------------
60
+ // PostgresDataStore.
61
+ // ---------------------------------------------------------------------------
62
+ /** Default Postgres table name; override via {@link PostgresDataStoreOptions.table}. */
63
+ export const PG_TABLE_DEFAULT = 'data_store';
64
+ class PostgresDataStore {
65
+ #port;
66
+ #softDelete;
67
+ /** Declared `@persisted` index fields (for the undeclared-filter warn/refuse guard). */
68
+ #indexes;
69
+ #maxLimit;
70
+ constructor(port, opts = {}) {
71
+ this.#port = port;
72
+ this.#softDelete = opts.softDelete ?? false;
73
+ this.#indexes = new Set(opts.indexes ?? []);
74
+ this.#maxLimit = opts.maxLimit;
75
+ }
76
+ /** Filter field names that are NOT declared indexes (force a sequential scope scan). */
77
+ #undeclaredFilterFields(filter) {
78
+ return filter ? Object.keys(filter).filter((f) => !this.#indexes.has(f)) : [];
79
+ }
80
+ async get(key, scope) {
81
+ const row = await this.#port.getRow(scopeSeg(scope), key);
82
+ if (row === null)
83
+ return null;
84
+ if (row.deletedAt !== null)
85
+ return null; // tombstone invisible
86
+ return this.#decode(row);
87
+ }
88
+ async create(key, value, scope) {
89
+ // The port assigns the version: 1 for a brand-new key (or create after a HARD
90
+ // delete), tombstone.version + 1 when resurrecting a soft-delete tombstone.
91
+ const version = await this.#port.insertIfAbsent(scopeSeg(scope), { id: key, value: JSON.stringify(this.#strip(value)), version: 1, deletedAt: null }, this.#softDelete);
92
+ if (version === null) {
93
+ throw new Error(`Entity already exists at key '${key}'`);
94
+ }
95
+ return { ...value, version };
96
+ }
97
+ async put(key, value, scope) {
98
+ const existing = await this.#port.getRow(scopeSeg(scope), key);
99
+ const newVersion = await this.#port.putRow(scopeSeg(scope), { id: key, value: JSON.stringify(this.#strip(value)), version: 0, deletedAt: null }, existing?.version ?? null);
100
+ return { ...value, version: newVersion };
101
+ }
102
+ async update(key, value, expectedVersion, scope) {
103
+ const reply = await this.#port.updateCas(scopeSeg(scope), { id: key, value: JSON.stringify(this.#strip(value)), version: 0, deletedAt: null }, expectedVersion);
104
+ this.#assertWritten(reply, key);
105
+ return { ...value, version: reply };
106
+ }
107
+ async patch(key, partial, expectedVersion, scope) {
108
+ const existing = await this.#port.getRow(scopeSeg(scope), key);
109
+ if (!existing || existing.deletedAt !== null) {
110
+ throw new Error(`Entity not found at key '${key}'`);
111
+ }
112
+ if (expectedVersion !== undefined && existing.version !== expectedVersion) {
113
+ throw new OptimisticConflictError(`Version mismatch: expected ${expectedVersion}, found ${existing.version}`);
114
+ }
115
+ const current = JSON.parse(existing.value);
116
+ const merged = { ...current, ...partial };
117
+ const reply = await this.#port.updateCas(scopeSeg(scope), { id: key, value: JSON.stringify(this.#strip(merged)), version: 0, deletedAt: null },
118
+ // Guard on the version we just read so a racing writer still trips CONFLICT.
119
+ existing.version);
120
+ this.#assertWritten(reply, key);
121
+ return { ...merged, version: reply };
122
+ }
123
+ async delete(key, expectedVersion, scope) {
124
+ let payload = null;
125
+ if (this.#softDelete) {
126
+ const existing = await this.#port.getRow(scopeSeg(scope), key);
127
+ if (!existing || existing.deletedAt !== null)
128
+ return false;
129
+ if (expectedVersion !== undefined && existing.version !== expectedVersion) {
130
+ throw new OptimisticConflictError(`Version mismatch: expected ${expectedVersion}, stale delete on key '${key}'`);
131
+ }
132
+ payload = {
133
+ id: key,
134
+ value: existing.value,
135
+ version: existing.version + 1,
136
+ deletedAt: new Date().toISOString(),
137
+ };
138
+ }
139
+ const reply = await this.#port.deleteCas(scopeSeg(scope), key, expectedVersion, payload);
140
+ if (reply === MISS)
141
+ return false;
142
+ if (reply === CONFLICT) {
143
+ throw new OptimisticConflictError(`Version mismatch: expected ${expectedVersion}, stale delete on key '${key}'`);
144
+ }
145
+ return true;
146
+ }
147
+ async list(query, scope) {
148
+ // A non-positive limit would silently make all data unreachable — fail fast.
149
+ if (query.limit < 1)
150
+ throw new RangeError('list limit must be >= 1');
151
+ // Optional defense-in-depth clamp: lower the effective page size to maxLimit
152
+ // when configured. Unset = no clamp = identical to the prior behavior.
153
+ const effectiveLimit = this.#maxLimit !== undefined ? Math.min(query.limit, this.#maxLimit) : query.limit;
154
+ // An undeclared filter field has no btree expression index → a sequential scan
155
+ // of the scope partition before the LIMIT applies. Warn (matching adapter-node)
156
+ // so operators see the unindexed scan; the LIMIT + optional statement_timeout
157
+ // bound it.
158
+ const undeclared = this.#undeclaredFilterFields(query.filter);
159
+ if (undeclared.length > 0) {
160
+ console.warn(`[adapter-postgres] DataStore.list: filtering on non-declared index ` +
161
+ `${undeclared.join(',')} via a sequential scope scan; declare it in ` +
162
+ `@persisted(indexes) for a sargable btree expression index.`);
163
+ }
164
+ const after = query.cursor ? decodeCursor(query.cursor) : undefined;
165
+ // Ask for one extra row to know whether a further page exists.
166
+ const rows = await this.#port.listRows(scopeSeg(scope), {
167
+ limit: effectiveLimit + 1,
168
+ after,
169
+ filter: query.filter,
170
+ excludeDeleted: this.#softDelete,
171
+ });
172
+ const page = rows.slice(0, effectiveLimit);
173
+ const items = page.map((r) => this.#decode(r));
174
+ const hasMore = rows.length > effectiveLimit;
175
+ const cursor = hasMore && page.length > 0 ? encodeCursor(page[page.length - 1].id) : undefined;
176
+ return cursor ? { items, cursor } : { items };
177
+ }
178
+ async count(query, scope) {
179
+ // `count` must be EXACT, so an undeclared-filter scan can't be silently
180
+ // row-capped (a capped count would lie). Refuse it loudly — mirroring
181
+ // adapter-aws — so the field is declared in @persisted(indexes) for a
182
+ // server-side, index-backed count.
183
+ const undeclared = this.#undeclaredFilterFields(query.filter);
184
+ if (undeclared.length > 0) {
185
+ throw new Error(`[adapter-postgres] DataStore.count: an exact count on a non-declared index ` +
186
+ `(${undeclared.join(',')}) would require an unbounded sequential scope scan; ` +
187
+ `declare it in @persisted(indexes) for a server-side index count.`);
188
+ }
189
+ return this.#port.count(scopeSeg(scope), query.filter, this.#softDelete);
190
+ }
191
+ // --- internals ----------------------------------------------------------
192
+ /** Reconstruct a {@link Stored} envelope from a row's JSON + version. */
193
+ #decode(row) {
194
+ const obj = JSON.parse(row.value);
195
+ return { ...obj, version: row.version };
196
+ }
197
+ /**
198
+ * Drop the store-managed `version` and `deletedAt` from the persisted JSON —
199
+ * `version` lives in its own column and `deletedAt` in `deleted_at`, so neither
200
+ * belongs in the value blob (and dropping `deletedAt` stops a caller injecting
201
+ * one to forge a tombstone, matching the memory store).
202
+ */
203
+ #strip(stored) {
204
+ const { version: _v, deletedAt: _d, ...rest } = stored;
205
+ return rest;
206
+ }
207
+ #assertWritten(reply, key) {
208
+ if (reply === MISS)
209
+ throw new Error(`Entity not found at key '${key}'`);
210
+ if (reply === CONFLICT) {
211
+ throw new OptimisticConflictError(`Version mismatch on key '${key}'`);
212
+ }
213
+ }
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Postgres schema helpers (the consumer runs these as a migration).
217
+ // ---------------------------------------------------------------------------
218
+ /**
219
+ * The `CREATE TABLE` for a Postgres-backed {@link DataStore}. The consumer runs
220
+ * this once (a migration) per collection — the adapter never issues DDL at
221
+ * runtime.
222
+ *
223
+ * - `(scope, id)` is the composite PRIMARY KEY → cross-scope get returns
224
+ * nothing, same key in two scopes never collides.
225
+ * - `value` is `jsonb`, so an equality filter on ANY field is a server-side
226
+ * `value->>'field' = $n`; declared indexes are btree expressions over it
227
+ * (see {@link pgCreateIndexSql}).
228
+ * - `version` is `bigint` (the optimistic-concurrency token).
229
+ * - `deleted_at` is NULL for live rows; a tombstone `timestamptz` when soft-deleted.
230
+ *
231
+ * @param table the table name (default {@link PG_TABLE_DEFAULT}); must match the
232
+ * `opts.table` passed to {@link createPostgresDataStore} / {@link createPgDataPort}.
233
+ */
234
+ export function pgCreateTableSql(table = PG_TABLE_DEFAULT) {
235
+ const t = quoteIdent(table);
236
+ return (`CREATE TABLE IF NOT EXISTS ${t} (\n` +
237
+ ` scope text NOT NULL,\n` +
238
+ ` id text NOT NULL,\n` +
239
+ ` value jsonb NOT NULL,\n` +
240
+ ` version bigint NOT NULL,\n` +
241
+ ` deleted_at timestamptz,\n` +
242
+ ` PRIMARY KEY (scope, id)\n` +
243
+ `);`);
244
+ }
245
+ /**
246
+ * A btree expression index for one declared `@persisted` index field, so an
247
+ * equality `filter` on it stays sargable in SQL. Mirrors the adapter-cf D1
248
+ * computed-column index. The consumer runs this alongside {@link pgCreateTableSql}.
249
+ *
250
+ * @example pgCreateIndexSql('data_store', 'ownerId')
251
+ */
252
+ export function pgCreateIndexSql(table, field) {
253
+ const safeTable = table.replace(/[^A-Za-z0-9_]/g, '_');
254
+ const safeField = field.replace(/[^A-Za-z0-9_]/g, '_');
255
+ // Sanitizing punctuation to `_` collapses distinct fields (`a.b` vs `a-b`) onto
256
+ // the same name; with IF NOT EXISTS the second would silently get no index.
257
+ // A short hash of the RAW field keeps each declared index distinct.
258
+ const suffix = fieldNameHash(field);
259
+ return (`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${safeTable}_idx_${safeField}_${suffix}`)} ` +
260
+ `ON ${quoteIdent(table)} (scope, (value->>${quoteLiteral(field)}));`);
261
+ }
262
+ /** Short, stable hex hash of a raw field name (disambiguates index names). */
263
+ function fieldNameHash(field) {
264
+ // FNV-1a (32-bit) — tiny, dependency-free, stable across runs.
265
+ let h = 0x811c9dc5;
266
+ for (let i = 0; i < field.length; i++) {
267
+ h ^= field.charCodeAt(i);
268
+ h = Math.imul(h, 0x01000193);
269
+ }
270
+ return (h >>> 0).toString(16).padStart(8, '0');
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // Real Postgres port — the ONLY place that speaks SQL (ARCH-01: structural client).
274
+ // ---------------------------------------------------------------------------
275
+ /** Quote a SQL identifier (table / index name) — doubles embedded quotes. */
276
+ function quoteIdent(name) {
277
+ return `"${name.replace(/"/g, '""')}"`;
278
+ }
279
+ /** Quote a SQL string literal (used only for the static field name in DDL). */
280
+ function quoteLiteral(s) {
281
+ return `'${s.replace(/'/g, "''")}'`;
282
+ }
283
+ /**
284
+ * Build an equality predicate list from a filter, starting bind placeholders at
285
+ * `startIndex`.
286
+ *
287
+ * The comparison is TYPED to the JS filter value so it matches the stored JSON
288
+ * value the way `@persisted` callers expect — NOT via `->>`'s text projection,
289
+ * which silently diverges from `String(value)` for exotic numbers (`1e21` →
290
+ * `'1000000000000000000000'` vs `'1e+21'`, `0.0000001` → `'0.0000001'` vs
291
+ * `'1e-7'`, `1.10` → `'1.10'` vs `'1.1'`) and would return NO rows:
292
+ * - number → `jsonb_typeof(value->'field') = 'number' AND (value->'field')::numeric = $n::numeric`
293
+ * - boolean → `jsonb_typeof(value->'field') = 'boolean' AND (value->'field')::boolean = $n::boolean`
294
+ * - string → `value->>'field' = $n` (text; `->>` IS the canonical text here)
295
+ *
296
+ * The `jsonb_typeof(...) = '...'` guard is what makes the numeric/boolean cast
297
+ * safe for mixed-type rows: a row whose field is absent, null, or a different
298
+ * type fails the guard and is filtered out by short-circuit BEFORE the `::numeric`
299
+ * / `::boolean` cast is reached, so an unrelated row never raises a cast error.
300
+ */
301
+ function filterPredicates(filter, startIndex) {
302
+ if (!filter)
303
+ return { sql: '', binds: [], next: startIndex };
304
+ const clauses = [];
305
+ const binds = [];
306
+ let n = startIndex;
307
+ for (const [field, value] of Object.entries(filter)) {
308
+ const path = `value->${quoteLiteral(field)}`;
309
+ if (typeof value === 'number') {
310
+ // typeof guard short-circuits before the cast → cast-safe for mixed rows.
311
+ clauses.push(`(jsonb_typeof(${path}) = 'number' AND (${path})::numeric = $${n}::numeric)`);
312
+ binds.push(value);
313
+ }
314
+ else if (typeof value === 'boolean') {
315
+ clauses.push(`(jsonb_typeof(${path}) = 'boolean' AND (${path})::boolean = $${n}::boolean)`);
316
+ binds.push(value);
317
+ }
318
+ else {
319
+ // String value: `->>` IS the canonical text, so a text compare is exact.
320
+ clauses.push(`value->>${quoteLiteral(field)} = $${n}`);
321
+ binds.push(value);
322
+ }
323
+ n++;
324
+ }
325
+ return { sql: clauses.length ? ` AND ${clauses.join(' AND ')}` : '', binds, next: n };
326
+ }
327
+ /**
328
+ * Build the REAL {@link PgDataPort} over a structural {@link PgClientLike}. This
329
+ * is the only SQL in the adapter; the store logic stays SQL-free.
330
+ *
331
+ * Versioned writes are a single conditional `UPDATE ... WHERE scope=$ AND id=$
332
+ * AND version=$ AND deleted_at IS NULL` — Postgres runs each statement
333
+ * atomically, so `rowCount === 0` means "no row matched the version"; a follow-up
334
+ * `SELECT` disambiguates miss vs {@link CONFLICT}. Create is `INSERT ... ON
335
+ * CONFLICT (scope, id) DO NOTHING` (0 rows ⇒ already exists; the soft-delete
336
+ * variant `DO UPDATE ... WHERE deleted_at IS NOT NULL` resurrects a tombstone).
337
+ *
338
+ * Production wiring: `createPgDataPort(new Pool({ connectionString }))`.
339
+ *
340
+ * @param client the consumer's `pg` `Pool`/`Client` (structural).
341
+ * @param table the table name; must match {@link createPostgresDataStore}'s `opts.table`.
342
+ * @param opts.statementTimeoutMs optional per-query `statement_timeout` for reads.
343
+ */
344
+ export function createPgDataPort(client, table = PG_TABLE_DEFAULT, opts = {}) {
345
+ const t = quoteIdent(table);
346
+ const statementTimeoutMs = opts.statementTimeoutMs ?? 0;
347
+ /**
348
+ * Run a read query (`list`/`count`) under an optional `statement_timeout`. When
349
+ * a timeout is configured we wrap the single statement in a transaction so
350
+ * `SET LOCAL` scopes the timeout to JUST this query (and resets on COMMIT/
351
+ * ROLLBACK).
352
+ *
353
+ * ⚠️ The transaction MUST run on a single dedicated connection: with a
354
+ * node-postgres `Pool`, each `Pool.query()` checks out a possibly-different
355
+ * pooled connection, so `BEGIN`, `SET LOCAL`, the SELECT and `COMMIT` could
356
+ * land on different backends — the timeout would not apply and the BEGIN's
357
+ * connection would be returned idle-in-transaction. So when the client exposes
358
+ * `connect()` we check out ONE connection and run the whole transaction on it
359
+ * (releasing with the error on the failure path so the pool discards the
360
+ * in-transaction connection). A bare `query()`-only client (no `connect`) can't
361
+ * safely scope `SET LOCAL`, so it falls back to a plain untimed read.
362
+ */
363
+ const runRead = async (text, binds) => {
364
+ if (statementTimeoutMs <= 0 || client.connect === undefined) {
365
+ return client.query(text, binds);
366
+ }
367
+ const conn = await client.connect();
368
+ try {
369
+ await conn.query('BEGIN');
370
+ await conn.query(`SET LOCAL statement_timeout = ${Math.floor(statementTimeoutMs)}`);
371
+ const res = await conn.query(text, binds);
372
+ await conn.query('COMMIT');
373
+ conn.release();
374
+ return res;
375
+ }
376
+ catch (err) {
377
+ // Release WITH the error so the pool discards this connection rather than
378
+ // returning it idle-in-transaction (no ROLLBACK round-trip needed).
379
+ conn.release(err);
380
+ throw err;
381
+ }
382
+ };
383
+ const toRow = (r) => {
384
+ if (!r)
385
+ return null;
386
+ // `value` is jsonb: node-postgres parses it to an object, so re-serialize to
387
+ // the JSON string the store's #decode expects. (A `postgres.js`/Neon client
388
+ // may already hand back a string — handle both.)
389
+ const value = typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
390
+ const deleted = r.deleted_at;
391
+ return {
392
+ id: String(r.id),
393
+ value,
394
+ version: Number(r.version),
395
+ deletedAt: deleted === null || deleted === undefined
396
+ ? null
397
+ : deleted instanceof Date
398
+ ? deleted.toISOString()
399
+ : String(deleted),
400
+ };
401
+ };
402
+ const selectOne = async (scope, id) => {
403
+ const res = await client.query(`SELECT id, value, version, deleted_at FROM ${t} WHERE scope = $1 AND id = $2`, [scope, id]);
404
+ return toRow(res.rows[0]);
405
+ };
406
+ return {
407
+ async getRow(scope, id) {
408
+ return selectOne(scope, id);
409
+ },
410
+ async insertIfAbsent(scope, row, allowOverTombstone) {
411
+ // INSERT-if-absent. When soft-delete create-over-tombstone is allowed, the
412
+ // ON CONFLICT path resurrects a tombstoned row (deleted_at IS NOT NULL) but
413
+ // still refuses to overwrite a LIVE row (the WHERE keeps create exclusive).
414
+ const onConflict = allowOverTombstone
415
+ ? ` ON CONFLICT (scope, id) DO UPDATE SET value = excluded.value, ` +
416
+ // CONTINUE the version off the tombstone (never reset to 1) so a
417
+ // resurrected row never reuses a version a stale holder still has.
418
+ `version = ${t}.version + 1, deleted_at = NULL WHERE ${t}.deleted_at IS NOT NULL`
419
+ : ` ON CONFLICT (scope, id) DO NOTHING`;
420
+ const res = await client.query(`INSERT INTO ${t} (scope, id, value, version, deleted_at) ` +
421
+ `VALUES ($1, $2, $3::jsonb, $4, NULL)` +
422
+ onConflict +
423
+ // RETURNING the assigned version surfaces the CONTINUED tombstone version
424
+ // (or 1 for a fresh insert); 0 rows ⇒ a live row blocked the create.
425
+ ` RETURNING version`, [scope, row.id, row.value, row.version]);
426
+ const r = res.rows[0];
427
+ return r ? Number(r.version) : null;
428
+ },
429
+ async putRow(scope, row, prevVersion) {
430
+ const newVersion = prevVersion === null ? 1 : prevVersion + 1;
431
+ await client.query(`INSERT INTO ${t} (scope, id, value, version, deleted_at) ` +
432
+ `VALUES ($1, $2, $3::jsonb, $4, NULL) ` +
433
+ `ON CONFLICT (scope, id) DO UPDATE SET value = excluded.value, ` +
434
+ `version = excluded.version, deleted_at = NULL`, [scope, row.id, row.value, newVersion]);
435
+ return newVersion;
436
+ },
437
+ async updateCas(scope, row, expectedVersion) {
438
+ // Read the live row to disambiguate miss vs conflict (rowCount alone can't
439
+ // tell them apart). The conditional UPDATE then enforces the CAS atomically
440
+ // — a racing writer still flips rowCount to 0.
441
+ const cur = await selectOne(scope, row.id);
442
+ if (!cur || cur.deletedAt !== null)
443
+ return MISS;
444
+ const guard = expectedVersion === undefined ? cur.version : expectedVersion;
445
+ const res = await client.query(`UPDATE ${t} SET value = $1::jsonb, version = version + 1 ` +
446
+ `WHERE scope = $2 AND id = $3 AND version = $4 AND deleted_at IS NULL`, [row.value, scope, row.id, guard]);
447
+ if ((res.rowCount ?? 0) === 0)
448
+ return CONFLICT;
449
+ return guard + 1;
450
+ },
451
+ async deleteCas(scope, id, expectedVersion, softDeletePayload) {
452
+ const cur = await selectOne(scope, id);
453
+ if (!cur || cur.deletedAt !== null)
454
+ return MISS;
455
+ if (expectedVersion !== undefined && cur.version !== expectedVersion)
456
+ return CONFLICT;
457
+ const guard = cur.version;
458
+ if (softDeletePayload) {
459
+ const res = await client.query(`UPDATE ${t} SET deleted_at = $1, version = version + 1 ` +
460
+ `WHERE scope = $2 AND id = $3 AND version = $4 AND deleted_at IS NULL`, [softDeletePayload.deletedAt, scope, id, guard]);
461
+ if ((res.rowCount ?? 0) === 0)
462
+ return CONFLICT;
463
+ return guard + 1;
464
+ }
465
+ const res = await client.query(`DELETE FROM ${t} WHERE scope = $1 AND id = $2 AND version = $3`, [scope, id, guard]);
466
+ if ((res.rowCount ?? 0) === 0)
467
+ return CONFLICT;
468
+ return 0;
469
+ },
470
+ async listRows(scope, args) {
471
+ const { sql: filterSql, binds: filterBinds, next } = filterPredicates(args.filter, 2);
472
+ const binds = [scope, ...filterBinds];
473
+ let n = next;
474
+ const deletedSql = args.excludeDeleted ? ` AND deleted_at IS NULL` : '';
475
+ let afterSql = '';
476
+ if (args.after !== undefined) {
477
+ afterSql = ` AND id > $${n}`;
478
+ binds.push(args.after);
479
+ n++;
480
+ }
481
+ const limitPlaceholder = `$${n}`;
482
+ binds.push(args.limit);
483
+ const res = await runRead(`SELECT id, value, version, deleted_at FROM ${t} ` +
484
+ `WHERE scope = $1${filterSql}${deletedSql}${afterSql} ORDER BY id ASC LIMIT ${limitPlaceholder}`, binds);
485
+ return res.rows.map((r) => toRow(r)).filter((r) => r !== null);
486
+ },
487
+ async count(scope, filter, excludeDeleted) {
488
+ const { sql: filterSql, binds: filterBinds } = filterPredicates(filter, 2);
489
+ const deletedSql = excludeDeleted ? ` AND deleted_at IS NULL` : '';
490
+ const res = await runRead(`SELECT COUNT(*) AS n FROM ${t} WHERE scope = $1${filterSql}${deletedSql}`, [scope, ...filterBinds]);
491
+ const r = res.rows[0];
492
+ return r ? Number(r.n) : 0;
493
+ },
494
+ };
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // Fake Postgres port — in-process Map, reimplements the semantics (no SQL parser).
498
+ // ---------------------------------------------------------------------------
499
+ /**
500
+ * An in-process {@link PgDataPort} backed by a `Map`. Reimplements the CAS /
501
+ * create-if-absent / filtered-list semantics in JS — exactly as adapter-cf's
502
+ * `createFakeD1DataPort` reimplements the SQL CAS — so the always-on conformance
503
+ * suite exercises all store logic with no Postgres. The real SQL is validated by
504
+ * `live.postgres.dataStore.test.ts` in CI.
505
+ *
506
+ * Each call runs its read-compare-write in one synchronous section before the
507
+ * returned promise settles, so there is no interleaving (JS single-thread), the
508
+ * same atomicity Postgres gives per statement.
509
+ */
510
+ export function createFakePgDataPort() {
511
+ /** `${scope} ${id}` → row. */
512
+ const rows = new Map();
513
+ const k = (scope, id) => `${scope} ${id}`;
514
+ const liveMatches = (row, scope, filter, excludeDeleted, afterId, scopeKey) => {
515
+ if (!scopeKey.startsWith(`${scope} `))
516
+ return false;
517
+ if (excludeDeleted && row.deletedAt !== null)
518
+ return false;
519
+ if (afterId !== undefined && !(row.id > afterId))
520
+ return false;
521
+ if (filter) {
522
+ const obj = JSON.parse(row.value);
523
+ for (const [field, want] of Object.entries(filter)) {
524
+ // Both sides are real parsed JS values here (stored value + filter value),
525
+ // so compare them with strict equality — matching the REAL port's TYPED
526
+ // comparison (numbers/booleans compared as values, not via `String()`,
527
+ // which diverges from Postgres `->>` text for exotic numbers).
528
+ if (obj[field] !== want)
529
+ return false;
530
+ }
531
+ }
532
+ return true;
533
+ };
534
+ return {
535
+ async getRow(scope, id) {
536
+ return rows.get(k(scope, id)) ?? null;
537
+ },
538
+ async insertIfAbsent(scope, row, allowOverTombstone) {
539
+ const cur = rows.get(k(scope, row.id));
540
+ if (cur && !(allowOverTombstone && cur.deletedAt !== null))
541
+ return null;
542
+ // Resurrecting a tombstone CONTINUES the version (mirrors the real port's
543
+ // `version = <table>.version + 1`); a fresh insert starts at 1.
544
+ const version = cur ? cur.version + 1 : 1;
545
+ rows.set(k(scope, row.id), { ...row, version, deletedAt: null });
546
+ return version;
547
+ },
548
+ async putRow(scope, row, prevVersion) {
549
+ const newVersion = prevVersion === null ? 1 : prevVersion + 1;
550
+ rows.set(k(scope, row.id), { ...row, version: newVersion, deletedAt: null });
551
+ return newVersion;
552
+ },
553
+ async updateCas(scope, row, expectedVersion) {
554
+ const cur = rows.get(k(scope, row.id));
555
+ if (!cur || cur.deletedAt !== null)
556
+ return MISS;
557
+ if (expectedVersion !== undefined && cur.version !== expectedVersion)
558
+ return CONFLICT;
559
+ const newVersion = cur.version + 1;
560
+ rows.set(k(scope, row.id), { ...row, version: newVersion, deletedAt: null });
561
+ return newVersion;
562
+ },
563
+ async deleteCas(scope, id, expectedVersion, softDeletePayload) {
564
+ const cur = rows.get(k(scope, id));
565
+ if (!cur || cur.deletedAt !== null)
566
+ return MISS;
567
+ if (expectedVersion !== undefined && cur.version !== expectedVersion)
568
+ return CONFLICT;
569
+ if (softDeletePayload) {
570
+ const newVersion = cur.version + 1;
571
+ rows.set(k(scope, id), { ...softDeletePayload, version: newVersion });
572
+ return newVersion;
573
+ }
574
+ rows.delete(k(scope, id));
575
+ return 0;
576
+ },
577
+ async listRows(scope, args) {
578
+ const out = [];
579
+ for (const [key, row] of rows) {
580
+ if (liveMatches(row, scope, args.filter, args.excludeDeleted, args.after, key)) {
581
+ out.push(row);
582
+ }
583
+ }
584
+ out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
585
+ return out.slice(0, args.limit);
586
+ },
587
+ async count(scope, filter, excludeDeleted) {
588
+ let n = 0;
589
+ for (const [key, row] of rows) {
590
+ if (liveMatches(row, scope, filter, excludeDeleted, undefined, key))
591
+ n++;
592
+ }
593
+ return n;
594
+ },
595
+ };
596
+ }
597
+ // ---------------------------------------------------------------------------
598
+ // Postgres factory.
599
+ // ---------------------------------------------------------------------------
600
+ /**
601
+ * Construct a Postgres-backed {@link DataStore} over a {@link PgDataPort}. Like
602
+ * the security stores, the logic lives only against the port, so the same class
603
+ * passes the `@smithy-hono/data-core` conformance suite against the in-process
604
+ * fake AND runs unchanged against real Postgres in production.
605
+ *
606
+ * Full capabilities: optimistic concurrency (SQL CAS), equality filter + count on
607
+ * ANY field (server-side `WHERE value->>'field' = $n` / `COUNT(*)` — no client
608
+ * scan), opaque-cursor pagination, and (opt-in) soft-delete.
609
+ *
610
+ * @example
611
+ * const store = createPostgresDataStore(
612
+ * createPgDataPort(new Pool({ connectionString }), 'todos'),
613
+ * { table: 'todos', indexes: ['ownerId'], softDelete: false },
614
+ * )
615
+ */
616
+ export function createPostgresDataStore(port, opts = {}) {
617
+ return new PostgresDataStore(port, opts);
618
+ }
619
+ export { PostgresDataStore };
620
+ //# sourceMappingURL=dataStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataStore.js","sourceRoot":"","sources":["../src/dataStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@smithy-hono/adapter-postgres` — the Postgres-backed `DataStore<T>` for
3
+ * `@smithy-hono/data-core` (Plan 13 D7).
4
+ *
5
+ * Postgres is the **recommended durable store of record for the Node
6
+ * deployment**: a `jsonb` value column with versioned CAS, server-side
7
+ * `WHERE value->>'field' = $n` filtering on ANY field (no client scan), opaque-
8
+ * cursor pagination, and soft-delete. The Redis `DataStore` in
9
+ * `@smithy-hono/adapter-node` is the optional cache-grade alternative.
10
+ *
11
+ * All store logic runs against a narrow SEMANTIC port ({@link PgDataPort}); the
12
+ * real port ({@link createPgDataPort}) is the only place that speaks SQL, over a
13
+ * structural client ({@link PgClientLike}) so nothing here imports the `pg`
14
+ * driver at runtime (ARCH-01). The in-process fake ({@link createFakePgDataPort})
15
+ * backs the always-on conformance suite; the live test validates the real SQL.
16
+ */
17
+ export { createPostgresDataStore, PostgresDataStore, type PostgresDataStoreOptions, } from './dataStore.js';
18
+ export { createPgDataPort, createFakePgDataPort, type PgDataPort, type PgDataPortOptions, type PgClientLike, type PgRow, type PgListArgs, } from './dataStore.js';
19
+ export { pgCreateTableSql, pgCreateIndexSql, PG_TABLE_DEFAULT, } from './dataStore.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `@smithy-hono/adapter-postgres` — the Postgres-backed `DataStore<T>` for
3
+ * `@smithy-hono/data-core` (Plan 13 D7).
4
+ *
5
+ * Postgres is the **recommended durable store of record for the Node
6
+ * deployment**: a `jsonb` value column with versioned CAS, server-side
7
+ * `WHERE value->>'field' = $n` filtering on ANY field (no client scan), opaque-
8
+ * cursor pagination, and soft-delete. The Redis `DataStore` in
9
+ * `@smithy-hono/adapter-node` is the optional cache-grade alternative.
10
+ *
11
+ * All store logic runs against a narrow SEMANTIC port ({@link PgDataPort}); the
12
+ * real port ({@link createPgDataPort}) is the only place that speaks SQL, over a
13
+ * structural client ({@link PgClientLike}) so nothing here imports the `pg`
14
+ * driver at runtime (ARCH-01). The in-process fake ({@link createFakePgDataPort})
15
+ * backs the always-on conformance suite; the live test validates the real SQL.
16
+ */
17
+ // --- DataStore<T> factory + class (Plan 13 D7) ------------------------------
18
+ export { createPostgresDataStore, PostgresDataStore, } from './dataStore.js';
19
+ // --- Ports: the SEMANTIC port + the real/fake builders + structural client ---
20
+ export { createPgDataPort, createFakePgDataPort, } from './dataStore.js';
21
+ // --- Schema helpers (the consumer runs these as a migration) ----------------
22
+ export { pgCreateTableSql, pgCreateIndexSql, PG_TABLE_DEFAULT, } from './dataStore.js';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,+EAA+E;AAC/E,OAAO,EACL,uBAAuB,EACvB,iBAAiB,GAElB,MAAM,gBAAgB,CAAA;AAEvB,gFAAgF;AAChF,OAAO,EACL,gBAAgB,EAChB,oBAAoB,GAMrB,MAAM,gBAAgB,CAAA;AAEvB,+EAA+E;AAC/E,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@smithy-hono/adapter-postgres",
3
+ "version": "0.2.2",
4
+ "type": "module",
5
+ "description": "Postgres-backed DataStore<T> for @smithy-hono/data-core (Plan 13 D7) — the recommended durable store of record for the Node deployment. A JSONB value column with versioned CAS, server-side WHERE/COUNT filtering on any field, opaque-cursor pagination, and soft-delete, all behind a narrow structural client port — no pg SDK at runtime (ARCH-01).",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rm -rf dist",
21
+ "build": "npm run clean && tsc -p tsconfig.build.json",
22
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
23
+ "prepare": "npm run build",
24
+ "test": "vitest run",
25
+ "test:ci": "vitest run --exclude '**/live.*.test.ts'",
26
+ "test:watch": "vitest",
27
+ "lint": "eslint src"
28
+ },
29
+ "peerDependencies": {
30
+ "@smithy-hono/data-core": "^0.2.2",
31
+ "pg": "^8"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "pg": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@smithy-hono/data-core": "^0.2.2",
40
+ "@types/pg": "^8",
41
+ "@typescript-eslint/parser": "^8.0.0",
42
+ "eslint": "^9.0.0",
43
+ "pg": "^8",
44
+ "typescript": "^5.4.0",
45
+ "vitest": "^2.0.0"
46
+ },
47
+ "optionalDependencies": {},
48
+ "//optionalDevDependencies": {
49
+ "pg": "The node-postgres driver. Used ONLY by the gated live test (src/live.postgres.dataStore.test.ts) via dynamic import — the runtime source codes against the structural PgClientLike, never `import 'pg'` (ARCH-01). @types/pg is likewise live-test-only and must NOT leak into the src build (tsconfig types:[])."
50
+ }
51
+ }