@kairos-es/store-postgres 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +167 -0
- package/dist/cjs/clients.js +62 -0
- package/dist/cjs/clients.js.map +1 -0
- package/dist/cjs/config.js +94 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureSchema.js +53 -0
- package/dist/cjs/ensureSchema.js.map +1 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/ddl.js +436 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/matchSql.js +189 -0
- package/dist/cjs/internal/matchSql.js.map +1 -0
- package/dist/cjs/internal/readPlan.js +67 -0
- package/dist/cjs/internal/readPlan.js.map +1 -0
- package/dist/cjs/internal/subscribe.js +120 -0
- package/dist/cjs/internal/subscribe.js.map +1 -0
- package/dist/cjs/internal/transport.js +66 -0
- package/dist/cjs/internal/transport.js.map +1 -0
- package/dist/cjs/store.js +256 -0
- package/dist/cjs/store.js.map +1 -0
- package/dist/dts/clients.d.ts +34 -0
- package/dist/dts/clients.d.ts.map +1 -0
- package/dist/dts/config.d.ts +58 -0
- package/dist/dts/config.d.ts.map +1 -0
- package/dist/dts/ensureSchema.d.ts +35 -0
- package/dist/dts/ensureSchema.d.ts.map +1 -0
- package/dist/dts/index.d.ts +41 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/ddl.d.ts +174 -0
- package/dist/dts/internal/ddl.d.ts.map +1 -0
- package/dist/dts/internal/matchSql.d.ts +140 -0
- package/dist/dts/internal/matchSql.d.ts.map +1 -0
- package/dist/dts/internal/readPlan.d.ts +72 -0
- package/dist/dts/internal/readPlan.d.ts.map +1 -0
- package/dist/dts/internal/subscribe.d.ts +87 -0
- package/dist/dts/internal/subscribe.d.ts.map +1 -0
- package/dist/dts/internal/transport.d.ts +116 -0
- package/dist/dts/internal/transport.d.ts.map +1 -0
- package/dist/dts/store.d.ts +17 -0
- package/dist/dts/store.d.ts.map +1 -0
- package/dist/esm/clients.js +51 -0
- package/dist/esm/clients.js.map +1 -0
- package/dist/esm/config.js +87 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureSchema.js +45 -0
- package/dist/esm/ensureSchema.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ddl.js +424 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/matchSql.js +176 -0
- package/dist/esm/internal/matchSql.js.map +1 -0
- package/dist/esm/internal/readPlan.js +59 -0
- package/dist/esm/internal/readPlan.js.map +1 -0
- package/dist/esm/internal/subscribe.js +113 -0
- package/dist/esm/internal/subscribe.js.map +1 -0
- package/dist/esm/internal/transport.js +56 -0
- package/dist/esm/internal/transport.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/store.js +249 -0
- package/dist/esm/store.js.map +1 -0
- package/package.json +35 -0
- package/src/clients.ts +69 -0
- package/src/config.ts +92 -0
- package/src/ensureSchema.ts +46 -0
- package/src/index.ts +45 -0
- package/src/internal/ddl.ts +599 -0
- package/src/internal/matchSql.ts +219 -0
- package/src/internal/readPlan.ts +117 -0
- package/src/internal/transport.ts +141 -0
- package/src/store.ts +413 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON/JSONB transport for the Postgres engine: the wire shapes and the
|
|
3
|
+
* (de)serialisers that carry an event batch / query items ACROSS the boundary into
|
|
4
|
+
* the append functions, and map a raw driver row BACK to a core `SequencedEvent`.
|
|
5
|
+
* Extracted from `store.ts` (the write side) and `internal/readPlan.ts` (the read
|
|
6
|
+
* side) so the transport concept lives in ONE module rather than being split
|
|
7
|
+
* across the write path and the read-plan compiler — the module name now predicts
|
|
8
|
+
* where transport code lives, and the two per-column type maps (`EventJson`,
|
|
9
|
+
* `EventRow`) sit next to each other rather than in two files.
|
|
10
|
+
*
|
|
11
|
+
* The 5-column event contract stays single-sourced on `EventColumnName`
|
|
12
|
+
* (`internal/matchSql.ts`), the one column-NAME descriptor: both type maps here
|
|
13
|
+
* are mapped types over it, so a column rename in `EVENT_COLUMNS` is a COMPILE
|
|
14
|
+
* error in this module, never a silent transport drift the lax read boundary
|
|
15
|
+
* (ADR-0006) would only surface as corrupt data under test.
|
|
16
|
+
*
|
|
17
|
+
* The read boundary (`rowToSequenced`) is the trusted, deliberately-lax
|
|
18
|
+
* deserialisation point (ADR-0006 store-laxness); the write boundary
|
|
19
|
+
* (`toEventJson`) serialises `data` as base64 text and domain time as an ISO
|
|
20
|
+
* string, both restored in SQL.
|
|
21
|
+
*/
|
|
22
|
+
import type { DcbEvent, Query, SequencedEvent } from '@kairos-es/core';
|
|
23
|
+
import type { EventColumnName } from './matchSql.js';
|
|
24
|
+
/**
|
|
25
|
+
* Serialise a value to a JSON STRING for transport as a `::jsonb`-cast bound
|
|
26
|
+
* parameter.
|
|
27
|
+
*
|
|
28
|
+
* WHY not `sql.json(value)`: `@effect/sql-pg`'s `json` helper binds the RAW
|
|
29
|
+
* value, and node-postgres serialises a JS *array* bound param as a Postgres
|
|
30
|
+
* ARRAY literal (`{…}`), not JSON — so `sql.json([...])::jsonb` fails with a
|
|
31
|
+
* `22P02` invalid-JSON error (an array literal is not valid JSON). Passing a
|
|
32
|
+
* pre-stringified JSON string and casting `::jsonb` sends plain text that
|
|
33
|
+
* Postgres parses as JSON, which round-trips reliably for both objects and
|
|
34
|
+
* arrays. Kept as a named helper so every JSON-transport call site is consistent
|
|
35
|
+
* and the reasoning lives in one place.
|
|
36
|
+
*/
|
|
37
|
+
export declare const jsonbText: (value: unknown) => string;
|
|
38
|
+
/** The JSON shape of one query item: types OR-filter, tags AND-superset. */
|
|
39
|
+
export interface QueryItemJson {
|
|
40
|
+
readonly types: ReadonlyArray<string>;
|
|
41
|
+
readonly tags: ReadonlyArray<string>;
|
|
42
|
+
}
|
|
43
|
+
/** Serialise a `Query`'s items into their transport JSON shape. */
|
|
44
|
+
export declare const toQueryItemsJson: (query: Query) => ReadonlyArray<QueryItemJson>;
|
|
45
|
+
/**
|
|
46
|
+
* The per-column TS types of the transport JSON shape — one entry per
|
|
47
|
+
* `EVENT_COLUMNS` name. `data` is base64 TEXT and `occurred_at` an ISO string
|
|
48
|
+
* (both restored in SQL: `decode(…, 'base64')` / `timestamptz`); the rest map
|
|
49
|
+
* straight across. Kept as a plain map so `EventJson` below is a mapped type over
|
|
50
|
+
* the ONE column descriptor rather than a hand-restated column list.
|
|
51
|
+
*/
|
|
52
|
+
interface EventJsonColumns {
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly data: string;
|
|
55
|
+
readonly tags: ReadonlyArray<string>;
|
|
56
|
+
readonly uuid: string;
|
|
57
|
+
readonly occurred_at: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The JSON shape of one event as sent to the append functions, as a mapped type
|
|
61
|
+
* over the shared `EventColumnName` descriptor (`internal/matchSql.ts`). `data`
|
|
62
|
+
* is base64 text (restored to `bytea` by `decode(…, 'base64')` in plpgsql);
|
|
63
|
+
* `occurred_at` is an ISO string parsed by `timestamptz`. Because the keys come
|
|
64
|
+
* from the same descriptor that builds the `jsonb_to_recordset(new_events)`
|
|
65
|
+
* column list, the two cannot drift — rename a column in `EVENT_COLUMNS` and this
|
|
66
|
+
* mapped type stops compiling (the old key is gone from `EventJsonColumns`).
|
|
67
|
+
*/
|
|
68
|
+
export type EventJson = {
|
|
69
|
+
readonly [K in EventColumnName]: EventJsonColumns[K];
|
|
70
|
+
};
|
|
71
|
+
/** Serialise one core `DcbEvent` into its transport JSON shape. */
|
|
72
|
+
export declare const toEventJson: (event: DcbEvent) => EventJson;
|
|
73
|
+
/**
|
|
74
|
+
* The per-column TS types of a raw driver row — one entry per `EVENT_COLUMNS`
|
|
75
|
+
* name. These are node-postgres's decoded JS types: `data` a `Buffer` (or null
|
|
76
|
+
* for a payload-less event), `occurred_at` a JS `Date`, `tags` a `string[]`. `id`
|
|
77
|
+
* (the `bigserial` position) is NOT here — it is prepended by the read projection
|
|
78
|
+
* and absent from the events recordset, so it sits outside the shared descriptor.
|
|
79
|
+
*/
|
|
80
|
+
interface EventRowColumns {
|
|
81
|
+
readonly type: string;
|
|
82
|
+
readonly data: Uint8Array | null;
|
|
83
|
+
readonly tags: ReadonlyArray<string>;
|
|
84
|
+
readonly uuid: string;
|
|
85
|
+
readonly occurred_at: Date;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A raw row from the main table as node-postgres returns it, as `id` plus a
|
|
89
|
+
* mapped type over the shared `EventColumnName` descriptor. `id` is `int8`, which
|
|
90
|
+
* the driver hands back as a decimal STRING (JS `number` is unsafe past 2^53).
|
|
91
|
+
* The payload columns cannot drift from the read projection: a rename in
|
|
92
|
+
* `EVENT_COLUMNS` breaks this mapped type.
|
|
93
|
+
*/
|
|
94
|
+
export type EventRow = {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
} & {
|
|
97
|
+
readonly [K in EventColumnName]: EventRowColumns[K];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Map a raw main-table row to a core `SequencedEvent`. `data` NULL → empty bytes
|
|
101
|
+
* (a payload-less event round-trips to a zero-length `Uint8Array`); `occurred_at`
|
|
102
|
+
* Date → `DateTime.Utc`; `id` string → branded `Position`.
|
|
103
|
+
*
|
|
104
|
+
* The branded scalars (`type`, `tags`, `uuid`, `position`) are minted by trusted
|
|
105
|
+
* `as`-cast, NOT by re-running `EventType.make` / `Tag.make` etc. This is
|
|
106
|
+
* deliberate: these bytes were validated at write time, and the store schema is
|
|
107
|
+
* intentionally LAX on read (ADR-0006 store-laxness — hand-assembled or pre-ADR
|
|
108
|
+
* events must remain readable), so re-validating here could REJECT a legitimately
|
|
109
|
+
* stored value and break that contract. It mirrors the in-memory oracle, which
|
|
110
|
+
* likewise mints branded positions by cast at its trusted allocation boundary
|
|
111
|
+
* (`inMemory.ts`). The read boundary is the analogous trusted deserialisation
|
|
112
|
+
* point.
|
|
113
|
+
*/
|
|
114
|
+
export declare const rowToSequenced: (row: EventRow) => SequencedEvent;
|
|
115
|
+
export {};
|
|
116
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/internal/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAY,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MAA+B,CAAA;AAE1E,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,KAAG,aAAa,CAAC,aAAa,CAItE,CAAA;AAEL;;;;;;GAMG;AACH,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC;CAAE,CAAA;AAEhF,mEAAmE;AACnE,eAAO,MAAM,WAAW,GAAI,OAAO,QAAQ,KAAG,SAQ5C,CAAA;AAEF;;;;;;GAMG;AACH,UAAU,eAAe;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAC/C,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC;CACpD,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,QAAQ,KAAG,cAS7C,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DcbEventStore } from '@kairos-es/core';
|
|
2
|
+
import { Layer } from 'effect';
|
|
3
|
+
import { PgClientDirect, PgClientPooled } from './clients.js';
|
|
4
|
+
import { type PostgresStoreConfig } from './config.js';
|
|
5
|
+
/**
|
|
6
|
+
* The Postgres `DcbEventStore` layer factory. Builds the engine over
|
|
7
|
+
* `PgClientPooled` (reads/append/notify) and `PgClientDirect` (the subscribe
|
|
8
|
+
* listener). The caller wires those two client layers — pointing `PgClientDirect`
|
|
9
|
+
* at a direct (non-pooler) endpoint on Neon, or at the same URL as `PgClientPooled`
|
|
10
|
+
* everywhere else.
|
|
11
|
+
*
|
|
12
|
+
* `ensureSchema(config)` must have been run (by the caller's migrator or at
|
|
13
|
+
* boot) before the layer is used; the layer itself performs no DDL, so it stays
|
|
14
|
+
* a pure runtime dependency requiring only the two clients.
|
|
15
|
+
*/
|
|
16
|
+
export declare const layer: (config?: PostgresStoreConfig) => Layer.Layer<DcbEventStore, never, PgClientPooled | PgClientDirect>;
|
|
17
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AAoDA,OAAO,EAGL,aAAa,EAQd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAA6B,KAAK,EAAsB,MAAM,QAAQ,CAAA;AAC7E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAoU9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,SAAQ,mBAAwB,KAC/B,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,cAAc,CASnE,CAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TWO store-owned Postgres clients — the "Neon trap" mitigation (ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* `@effect/sql-pg` exposes a single `PgClient` (and `SqlClient`) tag; a
|
|
5
|
+
* `PgClient.layer` claims BOTH. If we built two `PgClient.layer`s they would
|
|
6
|
+
* collide on that one tag — the second would shadow the first. So instead we
|
|
7
|
+
* define two DISTINCT `Context.Tag`s, each HOLDING a `PgClient` value:
|
|
8
|
+
*
|
|
9
|
+
* - `PgClientPooled` — the pooled client used for reads, the append function,
|
|
10
|
+
* and `notify`. Backed by a `pg.Pool`; connections are recycled per query.
|
|
11
|
+
* - `PgClientDirect` — a client used ONLY by `subscribe`'s `listen`. Its
|
|
12
|
+
* built-in `listen` constructs a STANDALONE `pg.Client` from the pool's
|
|
13
|
+
* `options`, inheriting the pool's host. On a Neon POOLED endpoint
|
|
14
|
+
* (`…-pooler…`) that standalone connection lands on the PgBouncer front door,
|
|
15
|
+
* which SILENTLY DROPS `LISTEN`/`NOTIFY` — the subscription would simply never
|
|
16
|
+
* wake. Pointing `PgClientDirect` at Neon's DIRECT (non-pooler) endpoint fixes
|
|
17
|
+
* it. `subscribe` is the ONLY method that touches `PgClientDirect`.
|
|
18
|
+
*
|
|
19
|
+
* For a plain (non-Neon) Postgres both tags point at the same URL; the split
|
|
20
|
+
* costs nothing there. The two-tag design is what makes the Neon fix a
|
|
21
|
+
* deployment-time wiring choice rather than a code change.
|
|
22
|
+
*/
|
|
23
|
+
import { Reactivity } from '@effect/experimental';
|
|
24
|
+
import { PgClient } from '@effect/sql-pg';
|
|
25
|
+
import { Context, Layer } from 'effect';
|
|
26
|
+
/**
|
|
27
|
+
* The pooled client tag: reads, the append function, and `notify`. Holds a
|
|
28
|
+
* `PgClient` value rather than being a `PgClient.layer`, so it never collides
|
|
29
|
+
* with the direct client on the shared `PgClient`/`SqlClient` tag.
|
|
30
|
+
*/
|
|
31
|
+
export class PgClientPooled extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientPooled')() {}
|
|
32
|
+
/**
|
|
33
|
+
* The direct (non-pooler) client tag: the `subscribe` listener ONLY. Point this
|
|
34
|
+
* at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the pooler.
|
|
35
|
+
*/
|
|
36
|
+
export class PgClientDirect extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientDirect')() {}
|
|
37
|
+
/**
|
|
38
|
+
* Build a `PgClientPooled` layer from a `PgClient` config. We construct the
|
|
39
|
+
* client via `PgClient.make` (scoped) and place it in our OWN tag, then provide
|
|
40
|
+
* `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
|
|
41
|
+
* built-in `PgClient.layer` does internally, minus the collision-prone claim on
|
|
42
|
+
* the shared `PgClient` tag.
|
|
43
|
+
*/
|
|
44
|
+
export const pooledLayer = config => Layer.scoped(PgClientPooled, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
|
|
45
|
+
/**
|
|
46
|
+
* Build a `PgClientDirect` layer from a `PgClient` config. Same construction as
|
|
47
|
+
* `pooledLayer`; the two differ only in which endpoint the caller points them
|
|
48
|
+
* at (and that only matters on Neon).
|
|
49
|
+
*/
|
|
50
|
+
export const directLayer = config => Layer.scoped(PgClientDirect, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
|
|
51
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","names":["Reactivity","PgClient","Context","Layer","PgClientPooled","Tag","PgClientDirect","pooledLayer","config","scoped","make","pipe","provide","layer","directLayer"],"sources":["../../src/clients.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAASA,UAAU,QAAQ,sBAAsB;AAEjD,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,EAAEC,KAAK,QAAQ,QAAQ;AAEvC;;;;;AAKA,OAAM,MAAOC,cAAe,sBAAQF,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;AAIA,OAAM,MAAOC,cAAe,sBAAQJ,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;;;;AAOA,OAAO,MAAME,WAAW,GACtBC,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACL,cAAc,EAAEH,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC;AAEH;;;;;AAKA,OAAO,MAAMC,WAAW,GACtBN,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACH,cAAc,EAAEL,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC","ignoreList":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public configuration for a Postgres `DcbEventStore`.
|
|
3
|
+
*
|
|
4
|
+
* A store is defined entirely by its `{ schema, tablePrefix }` naming pair plus
|
|
5
|
+
* a `lockTimeout`. Everything else — table/index/function names, the NOTIFY
|
|
6
|
+
* channel, the lock scope — is DERIVED from these (see `internal/ddl.ts`), so
|
|
7
|
+
* one config value is one completely isolated store. There is deliberately NO
|
|
8
|
+
* lockless option here: the exclusive-lock serialisation is not negotiable on the
|
|
9
|
+
* production surface — the lockless variant is a test-fixture-only seam, living in
|
|
10
|
+
* an internal module reachable only from the test graph.
|
|
11
|
+
*
|
|
12
|
+
* The config is a `Schema`, not a bare interface, so that malformed input fails
|
|
13
|
+
* LOUDLY at construction (`layer`/`ensureSchema` decode through it) rather than
|
|
14
|
+
* late and obscurely on the first DDL or append — the same fail-at-the-boundary
|
|
15
|
+
* discipline core applies to `Tag`/`ReadLimit`. It rejects: an empty or dotted
|
|
16
|
+
* `schema`/`tablePrefix` atom (a dot would break the qualified-name dot-split),
|
|
17
|
+
* a negative/non-finite `lockTimeout`, and — via `oversizedIdentifier` — any
|
|
18
|
+
* `{ schema, tablePrefix }` whose derived names would exceed Postgres's 63-byte
|
|
19
|
+
* identifier limit and be silently truncated.
|
|
20
|
+
*/
|
|
21
|
+
import { subscriptionTuningFields } from '@kairos-es/core';
|
|
22
|
+
import { Schema } from 'effect';
|
|
23
|
+
import { oversizedIdentifier } from "./internal/ddl.js";
|
|
24
|
+
/**
|
|
25
|
+
* A single Postgres identifier atom: non-empty and dot-free. Dots are barred
|
|
26
|
+
* because `schema`/`tablePrefix` are composed into qualified names that
|
|
27
|
+
* `sql(identifier)` dot-splits, so an embedded dot would silently re-partition
|
|
28
|
+
* the name into the wrong schema/object.
|
|
29
|
+
*/
|
|
30
|
+
const IdentifierAtom = /*#__PURE__*/Schema.String.pipe(/*#__PURE__*/Schema.nonEmptyString(), /*#__PURE__*/Schema.pattern(/^[^.]+$/, {
|
|
31
|
+
identifier: 'PostgresIdentifierAtom',
|
|
32
|
+
description: 'a non-empty identifier atom containing no "."'
|
|
33
|
+
}));
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for a Postgres store instance.
|
|
36
|
+
*
|
|
37
|
+
* - `schema` — the Postgres schema that owns the store's objects (default
|
|
38
|
+
* `public`). `ensureSchema` creates it defensively.
|
|
39
|
+
* - `tablePrefix` — the base name for the store's tables/indexes/functions
|
|
40
|
+
* (default `dcb_events`). Two stores with different prefixes (or schemas)
|
|
41
|
+
* cohabit one database without interference.
|
|
42
|
+
* - `lockTimeout` — seconds passed to `set_config('lock_timeout', …, true)`
|
|
43
|
+
* inside every append, bounding how long a blocked writer waits on the
|
|
44
|
+
* exclusive lock. `0` (the default) means wait indefinitely — parity with the
|
|
45
|
+
* in-memory semaphore's unbounded wait, so the shared contract suite behaves
|
|
46
|
+
* identically across engines. A non-zero timeout that fires (`55P03`) is a
|
|
47
|
+
* Postgres-only outcome surfaced OFF the shared `append` contract (a defect),
|
|
48
|
+
* never as `AppendConditionFailed` — the in-memory oracle cannot produce it.
|
|
49
|
+
* - `pollInterval` — MILLISECONDS a live `subscribe` waits for a NOTIFY wake
|
|
50
|
+
* before re-reading from its cursor anyway, defaulting to core's
|
|
51
|
+
* `DEFAULT_POLL_INTERVAL_MILLIS`. Because this engine HAS a wake source, the
|
|
52
|
+
* value bounds worst-case live-tail latency rather than being it — only a
|
|
53
|
+
* subscriber no NOTIFY reaches waits the whole interval; lower it (e.g. in tests)
|
|
54
|
+
* to poll faster. See `packages/core/src/SubscribeMachine.ts` for what the poll
|
|
55
|
+
* guarantees and what a wake source is for.
|
|
56
|
+
* - `catchUpPageSize` — the `subscribe` reconcile page size, defaulting to core's
|
|
57
|
+
* `DEFAULT_CATCH_UP_PAGE_SIZE`: how many events a single catch-up/live read
|
|
58
|
+
* fetches before looping.
|
|
59
|
+
*/
|
|
60
|
+
export const PostgresStoreConfig = /*#__PURE__*/Schema.Struct({
|
|
61
|
+
// `exact: true` keeps the decoded type at `key?: T` (never `T | undefined`),
|
|
62
|
+
// matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
|
|
63
|
+
// not an explicit `undefined`.
|
|
64
|
+
schema: Schema.optionalWith(IdentifierAtom, {
|
|
65
|
+
exact: true
|
|
66
|
+
}),
|
|
67
|
+
tablePrefix: Schema.optionalWith(IdentifierAtom, {
|
|
68
|
+
exact: true
|
|
69
|
+
}),
|
|
70
|
+
lockTimeout: Schema.optionalWith(Schema.Number.pipe(Schema.nonNegative(), Schema.finite()), {
|
|
71
|
+
exact: true
|
|
72
|
+
}),
|
|
73
|
+
// The two subscribe knobs are core's field definitions, not this engine's: see
|
|
74
|
+
// `subscriptionTuningFields`.
|
|
75
|
+
...subscriptionTuningFields
|
|
76
|
+
}).pipe(/*#__PURE__*/
|
|
77
|
+
// Cross-field guard: the derived table/index/function/channel names must all
|
|
78
|
+
// fit Postgres's 63-byte identifier limit. Returning the message string fails
|
|
79
|
+
// the decode with it; `undefined`/`true` passes.
|
|
80
|
+
Schema.filter(config => oversizedIdentifier(config) ?? true));
|
|
81
|
+
/**
|
|
82
|
+
* Decode an unknown value into a validated `PostgresStoreConfig`, throwing a
|
|
83
|
+
* `ParseError` on any violation. Used by `layer`/`ensureSchema` so a bad config
|
|
84
|
+
* fails at construction time.
|
|
85
|
+
*/
|
|
86
|
+
export const decodePostgresStoreConfig = /*#__PURE__*/Schema.decodeUnknownSync(PostgresStoreConfig);
|
|
87
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":["subscriptionTuningFields","Schema","oversizedIdentifier","IdentifierAtom","String","pipe","nonEmptyString","pattern","identifier","description","PostgresStoreConfig","Struct","schema","optionalWith","exact","tablePrefix","lockTimeout","Number","nonNegative","finite","filter","config","decodePostgresStoreConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;AAoBA,SAASA,wBAAwB,QAAQ,iBAAiB;AAC1D,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,mBAAmB,QAAQ,mBAAgB;AAEpD;;;;;;AAMA,MAAMC,cAAc,gBAAGF,MAAM,CAACG,MAAM,CAACC,IAAI,cACvCJ,MAAM,CAACK,cAAc,EAAE,eACvBL,MAAM,CAACM,OAAO,CAAC,SAAS,EAAE;EACxBC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,OAAO,MAAMC,mBAAmB,gBAAGT,MAAM,CAACU,MAAM,CAAC;EAC/C;EACA;EACA;EACAC,MAAM,EAAEX,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EAC5DC,WAAW,EAAEd,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EACjEE,WAAW,EAAEf,MAAM,CAACY,YAAY,CAC9BZ,MAAM,CAACgB,MAAM,CAACZ,IAAI,CAACJ,MAAM,CAACiB,WAAW,EAAE,EAAEjB,MAAM,CAACkB,MAAM,EAAE,CAAC,EACzD;IAAEL,KAAK,EAAE;EAAI,CAAE,CAChB;EACD;EACA;EACA,GAAGd;CACJ,CAAC,CAACK,IAAI;AACL;AACA;AACA;AACAJ,MAAM,CAACmB,MAAM,CAAEC,MAAM,IAAKnB,mBAAmB,CAACmB,MAAM,CAAC,IAAI,IAAI,CAAC,CAC/D;AAGD;;;;;AAKA,OAAO,MAAMC,yBAAyB,gBACpCrB,MAAM,CAACsB,iBAAiB,CAACb,mBAAmB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensureSchema` — the idempotent DDL migration building block (ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT a migrator. It is a single `Effect<void, SqlError,
|
|
5
|
+
* SqlClient>` that creates (or refreshes) every object a store owns: both
|
|
6
|
+
* tables, both indexes, and both plpgsql functions. It requires the GENERIC
|
|
7
|
+
* `SqlClient` tag — NOT `PgClientPooled` — precisely so a consumer can drop it
|
|
8
|
+
* straight into THEIR OWN migrator, e.g.:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* Migrator.fromRecord({
|
|
12
|
+
* '0001_kairos_store_dcb_events': ensureSchema(config),
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* or simply run it once at boot. We ship no `FileSystem`/`Path`/`ChildProcess`
|
|
17
|
+
* dependency and no `PgMigrator`; migration ORCHESTRATION belongs to the
|
|
18
|
+
* consumer, this package only provides the idempotent PAYLOAD.
|
|
19
|
+
*
|
|
20
|
+
* Idempotence: every statement is `CREATE … IF NOT EXISTS` or `CREATE OR REPLACE
|
|
21
|
+
* FUNCTION`, so re-running against an existing store is a no-op. Names come from
|
|
22
|
+
* `sql(identifier)` (via the pre-quoted builder in `internal/ddl.ts`); the DDL
|
|
23
|
+
* bodies carry no bound values (pure schema), and the append functions keep
|
|
24
|
+
* their runtime VALUES as bound parameters, never interpolated.
|
|
25
|
+
*/
|
|
26
|
+
import { SqlClient } from '@effect/sql';
|
|
27
|
+
import { Effect } from 'effect';
|
|
28
|
+
import { decodePostgresStoreConfig } from "./config.js";
|
|
29
|
+
import { resolveNames, runDdl } from "./internal/ddl.js";
|
|
30
|
+
/**
|
|
31
|
+
* Produce the migration payload for `config`. Resolves the object names once and
|
|
32
|
+
* runs the ordered idempotent DDL against whatever `SqlClient` the caller's
|
|
33
|
+
* migrator (or boot code) provides.
|
|
34
|
+
*/
|
|
35
|
+
export const ensureSchema = config => Effect.gen(function* () {
|
|
36
|
+
const sql = yield* SqlClient.SqlClient;
|
|
37
|
+
// Decode through the config schema (same guard as `layer`) so a malformed
|
|
38
|
+
// config fails at construction rather than mid-migration.
|
|
39
|
+
const names = resolveNames(decodePostgresStoreConfig(config));
|
|
40
|
+
// Sequentially, in dependency order (tables → indexes → FK → functions).
|
|
41
|
+
yield* Effect.all(runDdl(sql, names), {
|
|
42
|
+
discard: true
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=ensureSchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensureSchema.js","names":["SqlClient","Effect","decodePostgresStoreConfig","resolveNames","runDdl","ensureSchema","config","gen","sql","names","all","discard"],"sources":["../../src/ensureSchema.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAASA,SAAS,QAAuB,aAAa;AACtD,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,yBAAyB,QAAkC,aAAU;AAC9E,SAASC,YAAY,EAAEC,MAAM,QAAQ,mBAAgB;AAErD;;;;;AAKA,OAAO,MAAMC,YAAY,GACvBC,MAA2B,IAE3BL,MAAM,CAACM,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOR,SAAS,CAACA,SAAS;EACtC;EACA;EACA,MAAMS,KAAK,GAAGN,YAAY,CAACD,yBAAyB,CAACI,MAAM,CAAC,CAAC;EAC7D;EACA,OAAOL,MAAM,CAACS,GAAG,CAACN,MAAM,CAACI,GAAG,EAAEC,KAAK,CAAC,EAAE;IAAEE,OAAO,EAAE;EAAI,CAAE,CAAC;AAC1D,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/store-postgres — the Postgres `DcbEventStore` backend.
|
|
3
|
+
*
|
|
4
|
+
* A single store is one `{ schema, tablePrefix }` naming pair (`PostgresStoreConfig`);
|
|
5
|
+
* every object it owns is derived from that pair, so multiple isolated stores
|
|
6
|
+
* cohabit one database. The engine is the two-table B-tree + exclusive-lock
|
|
7
|
+
* conditional append of ADR-0002, with JSON transport and a two-client Layer to
|
|
8
|
+
* survive Neon's pooled LISTEN/NOTIFY drop.
|
|
9
|
+
*
|
|
10
|
+
* Wiring, end to end:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { PgClient } from '@effect/sql-pg'
|
|
14
|
+
* import {
|
|
15
|
+
* ensureSchema,
|
|
16
|
+
* layer,
|
|
17
|
+
* pooledLayer,
|
|
18
|
+
* directLayer,
|
|
19
|
+
* } from '@kairos-es/store-postgres'
|
|
20
|
+
*
|
|
21
|
+
* const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
22
|
+
*
|
|
23
|
+
* // 1. Register `ensureSchema(config)` in YOUR migrator (or run it at boot).
|
|
24
|
+
* // 2. Provide the two clients (same URL off Neon; a direct endpoint for
|
|
25
|
+
* // `directLayer` on Neon), then the store layer over them.
|
|
26
|
+
* const clients = Layer.merge(
|
|
27
|
+
* pooledLayer({ url: Redacted.make(poolUrl) }),
|
|
28
|
+
* directLayer({ url: Redacted.make(directUrl) }),
|
|
29
|
+
* )
|
|
30
|
+
* const store = layer(config).pipe(Layer.provide(clients))
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The store `layer` performs NO DDL: it depends only on the two client tags and
|
|
34
|
+
* assumes `ensureSchema` has run. `ensureSchema` requires the generic `SqlClient`
|
|
35
|
+
* tag so it slots into any migrator; the runtime uses the store-owned tags.
|
|
36
|
+
*/
|
|
37
|
+
export { directLayer, PgClientDirect, PgClientPooled, pooledLayer } from "./clients.js";
|
|
38
|
+
export { ensureSchema } from "./ensureSchema.js";
|
|
39
|
+
export { layer } from "./store.js";
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["directLayer","PgClientDirect","PgClientPooled","pooledLayer","ensureSchema","layer"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SACEA,WAAW,EACXC,cAAc,EACdC,cAAc,EACdC,WAAW,QACN,cAAW;AAElB,SAASC,YAAY,QAAQ,mBAAgB;AAC7C,SAASC,KAAK,QAAQ,YAAS","ignoreList":[]}
|