@cnstra/persist-postgres 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @cnstra/persist-postgres
2
+
3
+ > **⚠️ Experimental**: API may change.
4
+
5
+ A **Postgres relational store** for CNStra's durable run/attempt/task model —
6
+ implements [`ICNSStimulationRepository`](../persist) (the run roster → attempt timeline →
7
+ task waterfall the retry admin renders). Makes the durable-execution history **survive a
8
+ process restart**, and a retried run resumes from the persisted frontier.
9
+
10
+ This is domain **#2** (run/attempt/task history), distinct from the resume **checkpoint**
11
+ progress store (domain #1 — `@cnstra/pg-boss/postgres-progress`, `@cnstra/progress-redis`).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @cnstra/persist-postgres pg
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import { CNSPostgresStimulationRepository } from '@cnstra/persist-postgres';
23
+
24
+ const repo = new CNSPostgresStimulationRepository({
25
+ connectionString: process.env.DATABASE_URL, // or { pool } / { queryable }
26
+ tablePrefix: 'cns_', // default
27
+ autoMigrate: true, // lazily create tables on first use
28
+ });
29
+
30
+ // Drives the persistor (write) and the retry UI (read):
31
+ // saveRun / saveStimulation / appendTask ; getRun / listRuns / getStimulations / getTasks / delete
32
+ ```
33
+
34
+ Reuses the `IPgQueryable` seam, so `pg` is an optional peer — inject any pg client/pool,
35
+ or pass a `connectionString` for the repo to own a Pool (`close()` ends it).
36
+
37
+ ## Schema
38
+
39
+ Five tables under the prefix (default `cns_`). The run's `progress` is **normalised**: the
40
+ outstanding frontier into `cns_run_frontier` (one row per task) and per-neuron context into
41
+ `cns_run_context`, so you can query the frontier — rebuilt into `TCNSProgress` on read.
42
+
43
+ ```
44
+ cns_run(stimulation_run_id PK, entry jsonb, status, created_at, updated_at)
45
+ cns_run_frontier(run_id → cns_run, ord, neuron_name, dendrite_collateral_name, input jsonb)
46
+ cns_run_context(run_id → cns_run, neuron_name, value jsonb)
47
+ cns_stimulation(stimulation_id PK, run_id → cns_run, attempt_number, status, started_at,
48
+ completed_at, hop_count, has_error, replay_of, entry jsonb,
49
+ UNIQUE(run_id, attempt_number))
50
+ cns_task(stimulation_id, index, neuron_name, dendrite_collateral_name, input_index,
51
+ output jsonb, status, error, started_at, duration, PK(stimulation_id, index))
52
+ ```
53
+
54
+ ## Migrations
55
+
56
+ The package owns its schema. Auto-created on first use, or manage it yourself:
57
+
58
+ ```bash
59
+ DATABASE_URL=postgres://... npx cnstra-persist-postgres migrate
60
+ # CNS_TABLE_PREFIX=my_ to override the prefix
61
+ ```
62
+
63
+ or call `ensureSchema(pool, prefix)` from `@cnstra/persist-postgres`.
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,97 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+
6
+ // src/migrate.ts
7
+ var DEFAULT_PREFIX = "cns_";
8
+ function assertSafePrefix(prefix) {
9
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
10
+ throw new Error(`[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`);
11
+ }
12
+ return prefix;
13
+ }
14
+ __name(assertSafePrefix, "assertSafePrefix");
15
+ function persistPostgresSchemaSql(prefix = DEFAULT_PREFIX) {
16
+ const p = assertSafePrefix(prefix);
17
+ return `
18
+ CREATE TABLE IF NOT EXISTS ${p}stimulation (
19
+ stimulation_id text PRIMARY KEY,
20
+ entry jsonb NOT NULL,
21
+ status text NOT NULL,
22
+ scope_name text,
23
+ created_at timestamptz NOT NULL DEFAULT now(),
24
+ updated_at timestamptz NOT NULL DEFAULT now()
25
+ );
26
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);
27
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);
28
+
29
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (
30
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
31
+ ord int NOT NULL,
32
+ neuron_name text NOT NULL,
33
+ dendrite_collateral_name text NOT NULL,
34
+ input jsonb,
35
+ PRIMARY KEY (stimulation_id, ord)
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_context (
39
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
40
+ neuron_name text NOT NULL,
41
+ value jsonb NOT NULL,
42
+ PRIMARY KEY (stimulation_id, neuron_name)
43
+ );
44
+
45
+ -- NOTE: no FK on stimulation_id \u2014 over the devtools wire the persistor's
46
+ -- saveStimulation and saveAttempt ride SEPARATE async batches processed
47
+ -- concurrently by the server, so an attempt insert can reach Postgres before its
48
+ -- stimulation row's transaction commits; a hard FK would reject it (23503). Same
49
+ -- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.
50
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (
51
+ stimulation_attempt_id text PRIMARY KEY,
52
+ stimulation_id text NOT NULL,
53
+ attempt_number int NOT NULL,
54
+ status text NOT NULL,
55
+ started_at bigint NOT NULL,
56
+ completed_at bigint,
57
+ hop_count int NOT NULL,
58
+ has_error boolean NOT NULL,
59
+ replay_of text,
60
+ entry jsonb NOT NULL,
61
+ UNIQUE (stimulation_id, attempt_number)
62
+ );
63
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);
64
+
65
+ -- NOTE: no FK on stimulation_attempt_id \u2014 the persistor streams settled tasks
66
+ -- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),
67
+ -- so a hard FK would reject the insert. delete() removes tasks explicitly instead.
68
+ CREATE TABLE IF NOT EXISTS ${p}task (
69
+ stimulation_attempt_id text NOT NULL,
70
+ "index" int NOT NULL,
71
+ neuron_name text NOT NULL,
72
+ dendrite_collateral_name text NOT NULL,
73
+ input_index int NOT NULL,
74
+ output jsonb,
75
+ status text NOT NULL,
76
+ error text,
77
+ started_at bigint NOT NULL,
78
+ duration int,
79
+ PRIMARY KEY (stimulation_attempt_id, "index")
80
+ );
81
+ `;
82
+ }
83
+ __name(persistPostgresSchemaSql, "persistPostgresSchemaSql");
84
+ async function ensureSchema(db, prefix = DEFAULT_PREFIX) {
85
+ await db.query(persistPostgresSchemaSql(prefix));
86
+ }
87
+ __name(ensureSchema, "ensureSchema");
88
+
89
+ export {
90
+ __name,
91
+ __publicField,
92
+ DEFAULT_PREFIX,
93
+ assertSafePrefix,
94
+ persistPostgresSchemaSql,
95
+ ensureSchema
96
+ };
97
+ //# sourceMappingURL=chunk-U4IKAHS4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\n * Schema / migrations for the CNStra stimulation/attempt/task relational store.\n *\n * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`\n * or `ensureSchema(db)`, or let the repository auto-create it on first use. Five tables\n * under a configurable prefix (default `cns_`); the stimulation's `progress` is\n * NORMALISED into `stimulation_frontier` (one row per outstanding task) +\n * `stimulation_context` (one row per neuron).\n */\n\n/** Minimal pg client surface (a `pg` Pool or Client satisfies it). */\nexport interface IPgQueryable {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\nexport const DEFAULT_PREFIX = 'cns_';\n\n/** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */\nexport function assertSafePrefix(prefix: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {\n throw new Error(\n `[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`\n );\n }\n return prefix;\n}\n\n/** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */\nexport function persistPostgresSchemaSql(prefix: string = DEFAULT_PREFIX): string {\n const p = assertSafePrefix(prefix);\n return `\nCREATE TABLE IF NOT EXISTS ${p}stimulation (\n stimulation_id text PRIMARY KEY,\n entry jsonb NOT NULL,\n status text NOT NULL,\n scope_name text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n ord int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input jsonb,\n PRIMARY KEY (stimulation_id, ord)\n);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_context (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n neuron_name text NOT NULL,\n value jsonb NOT NULL,\n PRIMARY KEY (stimulation_id, neuron_name)\n);\n\n-- NOTE: no FK on stimulation_id — over the devtools wire the persistor's\n-- saveStimulation and saveAttempt ride SEPARATE async batches processed\n-- concurrently by the server, so an attempt insert can reach Postgres before its\n-- stimulation row's transaction commits; a hard FK would reject it (23503). Same\n-- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.\nCREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (\n stimulation_attempt_id text PRIMARY KEY,\n stimulation_id text NOT NULL,\n attempt_number int NOT NULL,\n status text NOT NULL,\n started_at bigint NOT NULL,\n completed_at bigint,\n hop_count int NOT NULL,\n has_error boolean NOT NULL,\n replay_of text,\n entry jsonb NOT NULL,\n UNIQUE (stimulation_id, attempt_number)\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);\n\n-- NOTE: no FK on stimulation_attempt_id — the persistor streams settled tasks\n-- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),\n-- so a hard FK would reject the insert. delete() removes tasks explicitly instead.\nCREATE TABLE IF NOT EXISTS ${p}task (\n stimulation_attempt_id text NOT NULL,\n \"index\" int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input_index int NOT NULL,\n output jsonb,\n status text NOT NULL,\n error text,\n started_at bigint NOT NULL,\n duration int,\n PRIMARY KEY (stimulation_attempt_id, \"index\")\n);\n`;\n}\n\n/** Idempotently create all tables + indexes. Safe to run repeatedly. */\nexport async function ensureSchema(\n db: IPgQueryable,\n prefix: string = DEFAULT_PREFIX\n): Promise<void> {\n await db.query(persistPostgresSchemaSql(prefix));\n}\n"],"mappings":";;;;;;AAeO,IAAMA,iBAAiB;AAGvB,SAASC,iBAAiBC,QAAc;AAC3C,MAAI,CAAC,2BAA2BC,KAAKD,MAAAA,GAAS;AAC1C,UAAM,IAAIE,MACN,mDAAmDC,KAAKC,UAAUJ,MAAAA,CAAAA,EAAS;EAEnF;AACA,SAAOA;AACX;AAPgBD;AAUT,SAASM,yBAAyBL,SAAiBF,gBAAc;AACpE,QAAMQ,IAAIP,iBAAiBC,MAAAA;AAC3B,SAAO;6BACkBM,CAAAA;;;;;;;;6BAQAA,CAAAA,8BAA+BA,CAAAA;6BAC/BA,CAAAA,4BAA6BA,CAAAA;;6BAE7BA,CAAAA;8CACiBA,CAAAA;;;;;;;;6BAQjBA,CAAAA;8CACiBA,CAAAA;;;;;;;;;;iBAU7BA,CAAAA;6BACYA,CAAAA;;;;;;;;;;;;;6BAaAA,CAAAA,8BAA+BA,CAAAA;;;;;6BAK/BA,CAAAA;;;;;;;;;;;;;;AAc7B;AAnEgBD;AAsEhB,eAAsBE,aAClBC,IACAR,SAAiBF,gBAAc;AAE/B,QAAMU,GAAGC,MAAMJ,yBAAyBL,MAAAA,CAAAA;AAC5C;AALsBO;","names":["DEFAULT_PREFIX","assertSafePrefix","prefix","test","Error","JSON","stringify","persistPostgresSchemaSql","p","ensureSchema","db","query"]}
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/cli.ts
6
+ var import_pg = require("pg");
7
+
8
+ // src/migrate.ts
9
+ var DEFAULT_PREFIX = "cns_";
10
+ function assertSafePrefix(prefix) {
11
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
12
+ throw new Error(`[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`);
13
+ }
14
+ return prefix;
15
+ }
16
+ __name(assertSafePrefix, "assertSafePrefix");
17
+ function persistPostgresSchemaSql(prefix = DEFAULT_PREFIX) {
18
+ const p = assertSafePrefix(prefix);
19
+ return `
20
+ CREATE TABLE IF NOT EXISTS ${p}stimulation (
21
+ stimulation_id text PRIMARY KEY,
22
+ entry jsonb NOT NULL,
23
+ status text NOT NULL,
24
+ scope_name text,
25
+ created_at timestamptz NOT NULL DEFAULT now(),
26
+ updated_at timestamptz NOT NULL DEFAULT now()
27
+ );
28
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);
29
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);
30
+
31
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (
32
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
33
+ ord int NOT NULL,
34
+ neuron_name text NOT NULL,
35
+ dendrite_collateral_name text NOT NULL,
36
+ input jsonb,
37
+ PRIMARY KEY (stimulation_id, ord)
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_context (
41
+ stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,
42
+ neuron_name text NOT NULL,
43
+ value jsonb NOT NULL,
44
+ PRIMARY KEY (stimulation_id, neuron_name)
45
+ );
46
+
47
+ -- NOTE: no FK on stimulation_id \u2014 over the devtools wire the persistor's
48
+ -- saveStimulation and saveAttempt ride SEPARATE async batches processed
49
+ -- concurrently by the server, so an attempt insert can reach Postgres before its
50
+ -- stimulation row's transaction commits; a hard FK would reject it (23503). Same
51
+ -- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.
52
+ CREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (
53
+ stimulation_attempt_id text PRIMARY KEY,
54
+ stimulation_id text NOT NULL,
55
+ attempt_number int NOT NULL,
56
+ status text NOT NULL,
57
+ started_at bigint NOT NULL,
58
+ completed_at bigint,
59
+ hop_count int NOT NULL,
60
+ has_error boolean NOT NULL,
61
+ replay_of text,
62
+ entry jsonb NOT NULL,
63
+ UNIQUE (stimulation_id, attempt_number)
64
+ );
65
+ CREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);
66
+
67
+ -- NOTE: no FK on stimulation_attempt_id \u2014 the persistor streams settled tasks
68
+ -- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),
69
+ -- so a hard FK would reject the insert. delete() removes tasks explicitly instead.
70
+ CREATE TABLE IF NOT EXISTS ${p}task (
71
+ stimulation_attempt_id text NOT NULL,
72
+ "index" int NOT NULL,
73
+ neuron_name text NOT NULL,
74
+ dendrite_collateral_name text NOT NULL,
75
+ input_index int NOT NULL,
76
+ output jsonb,
77
+ status text NOT NULL,
78
+ error text,
79
+ started_at bigint NOT NULL,
80
+ duration int,
81
+ PRIMARY KEY (stimulation_attempt_id, "index")
82
+ );
83
+ `;
84
+ }
85
+ __name(persistPostgresSchemaSql, "persistPostgresSchemaSql");
86
+ async function ensureSchema(db, prefix = DEFAULT_PREFIX) {
87
+ await db.query(persistPostgresSchemaSql(prefix));
88
+ }
89
+ __name(ensureSchema, "ensureSchema");
90
+
91
+ // src/cli.ts
92
+ async function main() {
93
+ var _a;
94
+ const cmd = process.argv[2];
95
+ if (cmd !== "migrate") {
96
+ console.error("usage: cnstra-persist-postgres migrate");
97
+ process.exit(1);
98
+ }
99
+ const url = process.env.DATABASE_URL;
100
+ if (!url) {
101
+ console.error("DATABASE_URL is required");
102
+ process.exit(1);
103
+ }
104
+ const prefix = (_a = process.env.CNS_TABLE_PREFIX) != null ? _a : DEFAULT_PREFIX;
105
+ const pool = new import_pg.Pool({
106
+ connectionString: url
107
+ });
108
+ try {
109
+ await ensureSchema(pool, prefix);
110
+ console.log(`\u2713 ensured stimulation/attempt/task tables (prefix "${prefix}")`);
111
+ } finally {
112
+ await pool.end();
113
+ }
114
+ }
115
+ __name(main, "main");
116
+ void main().catch((e) => {
117
+ console.error(e);
118
+ process.exit(1);
119
+ });
120
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/migrate.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-persist-postgres migrate` — create the stimulation/attempt/task tables.\n *\n * DATABASE_URL=postgres://... cnstra-persist-postgres migrate\n * CNS_TABLE_PREFIX=my_ (optional; default cns_)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_PREFIX } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-persist-postgres migrate'); // eslint-disable-line no-console\n process.exit(1);\n }\n const url = process.env.DATABASE_URL;\n if (!url) {\n console.error('DATABASE_URL is required'); // eslint-disable-line no-console\n process.exit(1);\n }\n const prefix = process.env.CNS_TABLE_PREFIX ?? DEFAULT_PREFIX;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, prefix);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured stimulation/attempt/task tables (prefix \"${prefix}\")`);\n } finally {\n await pool.end();\n }\n}\n\nvoid main().catch(e => {\n console.error(e); // eslint-disable-line no-console\n process.exit(1);\n});\n","/**\n * Schema / migrations for the CNStra stimulation/attempt/task relational store.\n *\n * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`\n * or `ensureSchema(db)`, or let the repository auto-create it on first use. Five tables\n * under a configurable prefix (default `cns_`); the stimulation's `progress` is\n * NORMALISED into `stimulation_frontier` (one row per outstanding task) +\n * `stimulation_context` (one row per neuron).\n */\n\n/** Minimal pg client surface (a `pg` Pool or Client satisfies it). */\nexport interface IPgQueryable {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\nexport const DEFAULT_PREFIX = 'cns_';\n\n/** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */\nexport function assertSafePrefix(prefix: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {\n throw new Error(\n `[@cnstra/persist-postgres] unsafe table prefix: ${JSON.stringify(prefix)}`\n );\n }\n return prefix;\n}\n\n/** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */\nexport function persistPostgresSchemaSql(prefix: string = DEFAULT_PREFIX): string {\n const p = assertSafePrefix(prefix);\n return `\nCREATE TABLE IF NOT EXISTS ${p}stimulation (\n stimulation_id text PRIMARY KEY,\n entry jsonb NOT NULL,\n status text NOT NULL,\n scope_name text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_updated_idx ON ${p}stimulation (updated_at DESC);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_scope_idx ON ${p}stimulation (scope_name);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_frontier (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n ord int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input jsonb,\n PRIMARY KEY (stimulation_id, ord)\n);\n\nCREATE TABLE IF NOT EXISTS ${p}stimulation_context (\n stimulation_id text NOT NULL REFERENCES ${p}stimulation(stimulation_id) ON DELETE CASCADE,\n neuron_name text NOT NULL,\n value jsonb NOT NULL,\n PRIMARY KEY (stimulation_id, neuron_name)\n);\n\n-- NOTE: no FK on stimulation_id — over the devtools wire the persistor's\n-- saveStimulation and saveAttempt ride SEPARATE async batches processed\n-- concurrently by the server, so an attempt insert can reach Postgres before its\n-- stimulation row's transaction commits; a hard FK would reject it (23503). Same\n-- exposure as ${p}task; delete() removes attempts explicitly instead of cascading.\nCREATE TABLE IF NOT EXISTS ${p}stimulation_attempt (\n stimulation_attempt_id text PRIMARY KEY,\n stimulation_id text NOT NULL,\n attempt_number int NOT NULL,\n status text NOT NULL,\n started_at bigint NOT NULL,\n completed_at bigint,\n hop_count int NOT NULL,\n has_error boolean NOT NULL,\n replay_of text,\n entry jsonb NOT NULL,\n UNIQUE (stimulation_id, attempt_number)\n);\nCREATE INDEX IF NOT EXISTS ${p}stimulation_attempt_idx ON ${p}stimulation_attempt (stimulation_id, attempt_number);\n\n-- NOTE: no FK on stimulation_attempt_id — the persistor streams settled tasks\n-- (appendTask) as hops complete, BEFORE the attempt marker is flushed (saveAttempt),\n-- so a hard FK would reject the insert. delete() removes tasks explicitly instead.\nCREATE TABLE IF NOT EXISTS ${p}task (\n stimulation_attempt_id text NOT NULL,\n \"index\" int NOT NULL,\n neuron_name text NOT NULL,\n dendrite_collateral_name text NOT NULL,\n input_index int NOT NULL,\n output jsonb,\n status text NOT NULL,\n error text,\n started_at bigint NOT NULL,\n duration int,\n PRIMARY KEY (stimulation_attempt_id, \"index\")\n);\n`;\n}\n\n/** Idempotently create all tables + indexes. Safe to run repeatedly. */\nexport async function ensureSchema(\n db: IPgQueryable,\n prefix: string = DEFAULT_PREFIX\n): Promise<void> {\n await db.query(persistPostgresSchemaSql(prefix));\n}\n"],"mappings":";;;;;AAOA,gBAAqB;;;ACQd,IAAMA,iBAAiB;AAGvB,SAASC,iBAAiBC,QAAc;AAC3C,MAAI,CAAC,2BAA2BC,KAAKD,MAAAA,GAAS;AAC1C,UAAM,IAAIE,MACN,mDAAmDC,KAAKC,UAAUJ,MAAAA,CAAAA,EAAS;EAEnF;AACA,SAAOA;AACX;AAPgBD;AAUT,SAASM,yBAAyBL,SAAiBF,gBAAc;AACpE,QAAMQ,IAAIP,iBAAiBC,MAAAA;AAC3B,SAAO;6BACkBM,CAAAA;;;;;;;;6BAQAA,CAAAA,8BAA+BA,CAAAA;6BAC/BA,CAAAA,4BAA6BA,CAAAA;;6BAE7BA,CAAAA;8CACiBA,CAAAA;;;;;;;;6BAQjBA,CAAAA;8CACiBA,CAAAA;;;;;;;;;;iBAU7BA,CAAAA;6BACYA,CAAAA;;;;;;;;;;;;;6BAaAA,CAAAA,8BAA+BA,CAAAA;;;;;6BAK/BA,CAAAA;;;;;;;;;;;;;;AAc7B;AAnEgBD;AAsEhB,eAAsBE,aAClBC,IACAR,SAAiBF,gBAAc;AAE/B,QAAMU,GAAGC,MAAMJ,yBAAyBL,MAAAA,CAAAA;AAC5C;AALsBO;;;ADxFtB,eAAeG,OAAAA;;AACX,QAAMC,MAAMC,QAAQC,KAAK,CAAA;AACzB,MAAIF,QAAQ,WAAW;AACnBG,YAAQC,MAAM,wCAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMC,MAAML,QAAQM,IAAIC;AACxB,MAAI,CAACF,KAAK;AACNH,YAAQC,MAAM,0BAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMI,UAASR,aAAQM,IAAIG,qBAAZT,YAAgCU;AAC/C,QAAMC,OAAO,IAAIC,eAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,MAAAA;AAEzBN,YAAQa,IAAI,2DAAsDP,MAAAA,IAAU;EAChF,UAAA;AACI,UAAMG,KAAKK,IAAG;EAClB;AACJ;AApBelB;AAsBf,KAAKA,KAAAA,EAAOmB,MAAMC,CAAAA,MAAAA;AACdhB,UAAQC,MAAMe,CAAAA;AACdlB,UAAQI,KAAK,CAAA;AACjB,CAAA;","names":["DEFAULT_PREFIX","assertSafePrefix","prefix","test","Error","JSON","stringify","persistPostgresSchemaSql","p","ensureSchema","db","query","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","prefix","CNS_TABLE_PREFIX","DEFAULT_PREFIX","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
package/dist/cli.mjs ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_PREFIX,
4
+ __name,
5
+ ensureSchema
6
+ } from "./chunk-U4IKAHS4.mjs";
7
+
8
+ // src/cli.ts
9
+ import { Pool } from "pg";
10
+ async function main() {
11
+ var _a;
12
+ const cmd = process.argv[2];
13
+ if (cmd !== "migrate") {
14
+ console.error("usage: cnstra-persist-postgres migrate");
15
+ process.exit(1);
16
+ }
17
+ const url = process.env.DATABASE_URL;
18
+ if (!url) {
19
+ console.error("DATABASE_URL is required");
20
+ process.exit(1);
21
+ }
22
+ const prefix = (_a = process.env.CNS_TABLE_PREFIX) != null ? _a : DEFAULT_PREFIX;
23
+ const pool = new Pool({
24
+ connectionString: url
25
+ });
26
+ try {
27
+ await ensureSchema(pool, prefix);
28
+ console.log(`\u2713 ensured stimulation/attempt/task tables (prefix "${prefix}")`);
29
+ } finally {
30
+ await pool.end();
31
+ }
32
+ }
33
+ __name(main, "main");
34
+ void main().catch((e) => {
35
+ console.error(e);
36
+ process.exit(1);
37
+ });
38
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-persist-postgres migrate` — create the stimulation/attempt/task tables.\n *\n * DATABASE_URL=postgres://... cnstra-persist-postgres migrate\n * CNS_TABLE_PREFIX=my_ (optional; default cns_)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_PREFIX } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-persist-postgres migrate'); // eslint-disable-line no-console\n process.exit(1);\n }\n const url = process.env.DATABASE_URL;\n if (!url) {\n console.error('DATABASE_URL is required'); // eslint-disable-line no-console\n process.exit(1);\n }\n const prefix = process.env.CNS_TABLE_PREFIX ?? DEFAULT_PREFIX;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, prefix);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured stimulation/attempt/task tables (prefix \"${prefix}\")`);\n } finally {\n await pool.end();\n }\n}\n\nvoid main().catch(e => {\n console.error(e); // eslint-disable-line no-console\n process.exit(1);\n});\n"],"mappings":";;;;;;;;AAOA,SAASA,YAAY;AAGrB,eAAeC,OAAAA;;AACX,QAAMC,MAAMC,QAAQC,KAAK,CAAA;AACzB,MAAIF,QAAQ,WAAW;AACnBG,YAAQC,MAAM,wCAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMC,MAAML,QAAQM,IAAIC;AACxB,MAAI,CAACF,KAAK;AACNH,YAAQC,MAAM,0BAAA;AACdH,YAAQI,KAAK,CAAA;EACjB;AACA,QAAMI,UAASR,aAAQM,IAAIG,qBAAZT,YAAgCU;AAC/C,QAAMC,OAAO,IAAIC,KAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,MAAAA;AAEzBN,YAAQa,IAAI,2DAAsDP,MAAAA,IAAU;EAChF,UAAA;AACI,UAAMG,KAAKK,IAAG;EAClB;AACJ;AApBelB;AAsBf,KAAKA,KAAAA,EAAOmB,MAAMC,CAAAA,MAAAA;AACdhB,UAAQC,MAAMe,CAAAA;AACdlB,UAAQI,KAAK,CAAA;AACjB,CAAA;","names":["Pool","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","prefix","CNS_TABLE_PREFIX","DEFAULT_PREFIX","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
@@ -0,0 +1,77 @@
1
+ import { Pool } from 'pg';
2
+ import { ICNSStimulationRepository } from '@cnstra/persist';
3
+ import { TCNSStimulationPersisted, TCNSStimulationAttemptPersisted, TCNSStimulationTaskPersisted } from '@cnstra/persist-dto';
4
+
5
+ /**
6
+ * Schema / migrations for the CNStra stimulation/attempt/task relational store.
7
+ *
8
+ * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`
9
+ * or `ensureSchema(db)`, or let the repository auto-create it on first use. Five tables
10
+ * under a configurable prefix (default `cns_`); the stimulation's `progress` is
11
+ * NORMALISED into `stimulation_frontier` (one row per outstanding task) +
12
+ * `stimulation_context` (one row per neuron).
13
+ */
14
+ /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
15
+ interface IPgQueryable {
16
+ query(text: string, params?: unknown[]): Promise<{
17
+ rows: unknown[];
18
+ }>;
19
+ }
20
+ declare const DEFAULT_PREFIX = "cns_";
21
+ /** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */
22
+ declare function assertSafePrefix(prefix: string): string;
23
+ /** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */
24
+ declare function persistPostgresSchemaSql(prefix?: string): string;
25
+ /** Idempotently create all tables + indexes. Safe to run repeatedly. */
26
+ declare function ensureSchema(db: IPgQueryable, prefix?: string): Promise<void>;
27
+
28
+ /**
29
+ * Postgres relational store for the CNStra stimulation/attempt/task durable model
30
+ * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the
31
+ * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) and
32
+ * per-neuron context into `<prefix>stimulation_context` — rebuilt into `TCNSProgress`
33
+ * on read.
34
+ *
35
+ * Reuses the `IPgQueryable` seam so `pg` is an optional peer: inject any pg
36
+ * client/pool, or pass a `connectionString` for the repo to own a Pool. Behaviourally
37
+ * matches `CNSInMemoryStimulationRepository` (listStimulations newest-first, getAttempts
38
+ * by attemptNumber, getTasks by index, delete cascades).
39
+ */
40
+
41
+ interface TCNSPostgresStimulationRepositoryOptions {
42
+ connectionString?: string;
43
+ /** Inject an existing pg Pool (transactions supported; not closed by `close()`). */
44
+ pool?: Pool;
45
+ /** Inject any queryable (reads + best-effort non-transactional writes). */
46
+ queryable?: IPgQueryable;
47
+ /** Table prefix (default `cns_`). */
48
+ tablePrefix?: string;
49
+ /** Lazily ensure the schema on first use (default true). */
50
+ autoMigrate?: boolean;
51
+ }
52
+ declare class CNSPostgresStimulationRepository implements ICNSStimulationRepository {
53
+ private readonly db;
54
+ private readonly pool?;
55
+ private readonly ownsPool;
56
+ private readonly p;
57
+ private readonly autoMigrate;
58
+ private ready;
59
+ constructor(opts: TCNSPostgresStimulationRepositoryOptions);
60
+ private ensure;
61
+ /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
62
+ private tx;
63
+ saveStimulation(s: TCNSStimulationPersisted): Promise<void>;
64
+ saveAttempt(a: TCNSStimulationAttemptPersisted): Promise<void>;
65
+ appendTask(t: TCNSStimulationTaskPersisted): Promise<void>;
66
+ getStimulation(stimulationId: string): Promise<TCNSStimulationPersisted | undefined>;
67
+ listStimulations(filter?: {
68
+ scopeName?: string;
69
+ }): Promise<TCNSStimulationPersisted[]>;
70
+ getAttempts(stimulationId: string): Promise<TCNSStimulationAttemptPersisted[]>;
71
+ getTasks(stimulationAttemptId: string): Promise<TCNSStimulationTaskPersisted[]>;
72
+ delete(stimulationId: string): Promise<void>;
73
+ close(): Promise<void>;
74
+ private buildProgress;
75
+ }
76
+
77
+ export { CNSPostgresStimulationRepository, DEFAULT_PREFIX, type IPgQueryable, type TCNSPostgresStimulationRepositoryOptions, assertSafePrefix, ensureSchema, persistPostgresSchemaSql };
@@ -0,0 +1,77 @@
1
+ import { Pool } from 'pg';
2
+ import { ICNSStimulationRepository } from '@cnstra/persist';
3
+ import { TCNSStimulationPersisted, TCNSStimulationAttemptPersisted, TCNSStimulationTaskPersisted } from '@cnstra/persist-dto';
4
+
5
+ /**
6
+ * Schema / migrations for the CNStra stimulation/attempt/task relational store.
7
+ *
8
+ * The package OWNS this schema — run it explicitly via `cnstra-persist-postgres migrate`
9
+ * or `ensureSchema(db)`, or let the repository auto-create it on first use. Five tables
10
+ * under a configurable prefix (default `cns_`); the stimulation's `progress` is
11
+ * NORMALISED into `stimulation_frontier` (one row per outstanding task) +
12
+ * `stimulation_context` (one row per neuron).
13
+ */
14
+ /** Minimal pg client surface (a `pg` Pool or Client satisfies it). */
15
+ interface IPgQueryable {
16
+ query(text: string, params?: unknown[]): Promise<{
17
+ rows: unknown[];
18
+ }>;
19
+ }
20
+ declare const DEFAULT_PREFIX = "cns_";
21
+ /** Guard the table prefix (it is interpolated into DDL/DML, not parameterised). */
22
+ declare function assertSafePrefix(prefix: string): string;
23
+ /** The full CREATE TABLE + CREATE INDEX DDL (idempotent). */
24
+ declare function persistPostgresSchemaSql(prefix?: string): string;
25
+ /** Idempotently create all tables + indexes. Safe to run repeatedly. */
26
+ declare function ensureSchema(db: IPgQueryable, prefix?: string): Promise<void>;
27
+
28
+ /**
29
+ * Postgres relational store for the CNStra stimulation/attempt/task durable model
30
+ * (`ICNSStimulationRepository`). The stimulation's `progress` is NORMALISED: the
31
+ * outstanding frontier into `<prefix>stimulation_frontier` (one row per task) and
32
+ * per-neuron context into `<prefix>stimulation_context` — rebuilt into `TCNSProgress`
33
+ * on read.
34
+ *
35
+ * Reuses the `IPgQueryable` seam so `pg` is an optional peer: inject any pg
36
+ * client/pool, or pass a `connectionString` for the repo to own a Pool. Behaviourally
37
+ * matches `CNSInMemoryStimulationRepository` (listStimulations newest-first, getAttempts
38
+ * by attemptNumber, getTasks by index, delete cascades).
39
+ */
40
+
41
+ interface TCNSPostgresStimulationRepositoryOptions {
42
+ connectionString?: string;
43
+ /** Inject an existing pg Pool (transactions supported; not closed by `close()`). */
44
+ pool?: Pool;
45
+ /** Inject any queryable (reads + best-effort non-transactional writes). */
46
+ queryable?: IPgQueryable;
47
+ /** Table prefix (default `cns_`). */
48
+ tablePrefix?: string;
49
+ /** Lazily ensure the schema on first use (default true). */
50
+ autoMigrate?: boolean;
51
+ }
52
+ declare class CNSPostgresStimulationRepository implements ICNSStimulationRepository {
53
+ private readonly db;
54
+ private readonly pool?;
55
+ private readonly ownsPool;
56
+ private readonly p;
57
+ private readonly autoMigrate;
58
+ private ready;
59
+ constructor(opts: TCNSPostgresStimulationRepositoryOptions);
60
+ private ensure;
61
+ /** Run `fn` in a transaction when a Pool is available; else best-effort inline. */
62
+ private tx;
63
+ saveStimulation(s: TCNSStimulationPersisted): Promise<void>;
64
+ saveAttempt(a: TCNSStimulationAttemptPersisted): Promise<void>;
65
+ appendTask(t: TCNSStimulationTaskPersisted): Promise<void>;
66
+ getStimulation(stimulationId: string): Promise<TCNSStimulationPersisted | undefined>;
67
+ listStimulations(filter?: {
68
+ scopeName?: string;
69
+ }): Promise<TCNSStimulationPersisted[]>;
70
+ getAttempts(stimulationId: string): Promise<TCNSStimulationAttemptPersisted[]>;
71
+ getTasks(stimulationAttemptId: string): Promise<TCNSStimulationTaskPersisted[]>;
72
+ delete(stimulationId: string): Promise<void>;
73
+ close(): Promise<void>;
74
+ private buildProgress;
75
+ }
76
+
77
+ export { CNSPostgresStimulationRepository, DEFAULT_PREFIX, type IPgQueryable, type TCNSPostgresStimulationRepositoryOptions, assertSafePrefix, ensureSchema, persistPostgresSchemaSql };