@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 +158 -0
- package/dist/dataStore.d.ts +255 -0
- package/dist/dataStore.d.ts.map +1 -0
- package/dist/dataStore.js +620 -0
- package/dist/dataStore.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
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":"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"}
|
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|