@cnstra/pg-boss 0.4.0 → 0.6.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/dist/cli.js CHANGED
@@ -1,37 +1,269 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
4
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
6
 
5
7
  // src/cli.ts
6
8
  var import_pg = require("pg");
7
9
 
8
- // src/migrate.ts
9
- var DEFAULT_TABLE = "cns_pgboss_progress";
10
- function assertSafeTable(table) {
11
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
12
- throw new Error(`[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`);
10
+ // ../postgres-schema/dist/index.mjs
11
+ var __defProp2 = Object.defineProperty;
12
+ var __defNormalProp2 = /* @__PURE__ */ __name((obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value, "__defNormalProp");
13
+ var __name2 = /* @__PURE__ */ __name((target, value) => __defProp2(target, "name", { value, configurable: true }), "__name");
14
+ var __publicField2 = /* @__PURE__ */ __name((obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value), "__publicField");
15
+ var _a;
16
+ var _CNSPostgresSchemaMigrator = (_a = class {
17
+ constructor(options) {
18
+ __publicField2(this, "options");
19
+ __publicField2(this, "steps");
20
+ var _a4;
21
+ this.options = options;
22
+ this.steps = [
23
+ ...options.steps
24
+ ].sort((left, right) => left.step - right.step);
25
+ this.steps.forEach((step, index) => {
26
+ if (step.step !== index + 1) throw new Error(`[${options.name}] schema steps must be numbered 1, 2, 3\u2026; found ${step.step} at position ${index + 1}`);
27
+ });
28
+ if (((_a4 = this.steps[0]) == null ? void 0 : _a4.kind) !== "expand") throw new Error(`[${options.name}] schema step 1 must be an expand step`);
13
29
  }
14
- return table;
15
- }
16
- __name(assertSafeTable, "assertSafeTable");
17
- function pgBossProgressSchemaSql(table = DEFAULT_TABLE) {
18
- assertSafeTable(table);
19
- return `CREATE TABLE IF NOT EXISTS ${table} (
20
- key text PRIMARY KEY,
30
+ migrationTable(tablePrefix) {
31
+ return `${tablePrefix}${this.options.name}_migration`;
32
+ }
33
+ async migrate(database, tablePrefix, options = {}) {
34
+ await database.transaction(async (connection) => {
35
+ await connection.query("SELECT pg_advisory_xact_lock($1, hashtext($2))", [
36
+ _a.LOCK_NAMESPACE,
37
+ tablePrefix + this.options.name
38
+ ]);
39
+ await connection.query(`SET LOCAL lock_timeout = '${_a.LOCK_TIMEOUT}'`);
40
+ const applied = await this.readAppliedSteps(connection, tablePrefix);
41
+ if (applied === void 0) await this.assertNoUntrackedTables(connection, tablePrefix);
42
+ const pending = this.pendingSteps(applied != null ? applied : /* @__PURE__ */ new Map(), applied === void 0 || options.contract === true);
43
+ if (pending.length > 0) await connection.query(this.sqlFor(tablePrefix, pending, applied === void 0));
44
+ });
45
+ }
46
+ async verify(connection, tablePrefix) {
47
+ const applied = await this.readAppliedSteps(connection, tablePrefix);
48
+ if (applied === void 0) {
49
+ throw new Error(`[${this.options.name}] the schema is not installed under prefix "${tablePrefix}": run migrate()`);
50
+ }
51
+ const missing = this.steps.filter((step) => step.kind === "expand" && !applied.has(step.step)).map((step) => step.step);
52
+ if (missing.length > 0) {
53
+ throw new Error(`[${this.options.name}] schema steps ${missing.join(", ")} are not applied under prefix "${tablePrefix}": run migrate()`);
54
+ }
55
+ const unknown = [
56
+ ...applied
57
+ ].filter(([step, kind]) => kind === "contract" && step > this.steps.length).map(([step]) => step);
58
+ if (unknown.length > 0) {
59
+ throw new Error(`[${this.options.name}] the database has contract steps ${unknown.join(", ")} that this version does not know: upgrade the package`);
60
+ }
61
+ }
62
+ migrationSql(tablePrefix, appliedSteps = [], options = {}) {
63
+ const fresh = appliedSteps.length === 0;
64
+ const applied = new Map(appliedSteps.map((step) => [
65
+ step,
66
+ "expand"
67
+ ]));
68
+ return this.sqlFor(tablePrefix, this.pendingSteps(applied, fresh || options.contract === true), fresh);
69
+ }
70
+ assertTablePrefix(tablePrefix, longestSuffix) {
71
+ const longest = Math.max(longestSuffix.length, `${this.options.name}_migration`.length);
72
+ const limit = _a.MAX_IDENTIFIER_LENGTH - longest;
73
+ if (!new RegExp(`^[A-Za-z_][A-Za-z0-9_]{0,${limit - 1}}$`).test(tablePrefix)) {
74
+ throw new Error(`[${this.options.name}] unsafe table prefix ${JSON.stringify(tablePrefix)}: letters, digits and _ only, at most ${limit} characters`);
75
+ }
76
+ return tablePrefix;
77
+ }
78
+ pendingSteps(applied, withContract) {
79
+ return this.steps.filter((step) => !applied.has(step.step) && (step.kind === "expand" || withContract));
80
+ }
81
+ sqlFor(tablePrefix, steps, createMigrationTable) {
82
+ const table = this.migrationTable(tablePrefix);
83
+ const parts = createMigrationTable ? [
84
+ `CREATE TABLE ${table} (step integer PRIMARY KEY, kind text NOT NULL CHECK (kind IN ('expand', 'contract')));`
85
+ ] : [];
86
+ for (const step of steps) {
87
+ parts.push(step.sql(tablePrefix), `INSERT INTO ${table} (step, kind) VALUES (${step.step}, '${step.kind}');`);
88
+ }
89
+ return parts.join("\n");
90
+ }
91
+ async readAppliedSteps(connection, tablePrefix) {
92
+ var _a4;
93
+ const table = this.migrationTable(tablePrefix);
94
+ const exists = await connection.query("SELECT to_regclass($1)::text AS found", [
95
+ table
96
+ ]);
97
+ if (((_a4 = exists.rows[0]) == null ? void 0 : _a4.found) == null) return void 0;
98
+ const rows = await connection.query(`SELECT step, kind FROM ${table}`);
99
+ return new Map(rows.rows.map((row) => [
100
+ Number(row.step),
101
+ row.kind
102
+ ]));
103
+ }
104
+ async assertNoUntrackedTables(connection, tablePrefix) {
105
+ const tables = this.options.tables(tablePrefix);
106
+ const found = await connection.query("SELECT name FROM unnest($1::text[]) AS name WHERE to_regclass(name) IS NOT NULL", [
107
+ tables
108
+ ]);
109
+ if (found.rows.length > 0) {
110
+ throw new Error(`[${this.options.name}] tables ${found.rows.map((row) => row.name).join(", ")} exist without ${this.migrationTable(tablePrefix)}: they were created by an earlier unversioned release; drop them or move the data before migrate()`);
111
+ }
112
+ }
113
+ }, __name(_a, "_CNSPostgresSchemaMigrator"), _a);
114
+ __name2(_CNSPostgresSchemaMigrator, "CNSPostgresSchemaMigrator");
115
+ __publicField2(_CNSPostgresSchemaMigrator, "LOCK_NAMESPACE", 6);
116
+ __publicField2(_CNSPostgresSchemaMigrator, "LOCK_TIMEOUT", "5s");
117
+ __publicField2(_CNSPostgresSchemaMigrator, "MAX_IDENTIFIER_LENGTH", 63);
118
+ var CNSPostgresSchemaMigrator = _CNSPostgresSchemaMigrator;
119
+ var _a2;
120
+ var _CNSPostgresSchemaPgDatabase = (_a2 = class {
121
+ constructor(open) {
122
+ __publicField2(this, "open");
123
+ this.open = open;
124
+ }
125
+ static fromPool(pool) {
126
+ return new _a2(async () => {
127
+ const client = await pool.connect();
128
+ return {
129
+ connection: client,
130
+ release: /* @__PURE__ */ __name2((error) => client.release(error), "release")
131
+ };
132
+ });
133
+ }
134
+ async transaction(work) {
135
+ const { connection, release } = await this.open();
136
+ try {
137
+ await connection.query("BEGIN");
138
+ const result = await work(connection);
139
+ await connection.query("COMMIT");
140
+ release();
141
+ return result;
142
+ } catch (error) {
143
+ const rollback = await connection.query("ROLLBACK").then(() => void 0, (rollbackError) => rollbackError);
144
+ release(rollback instanceof Error ? rollback : void 0);
145
+ throw error;
146
+ }
147
+ }
148
+ }, __name(_a2, "_CNSPostgresSchemaPgDatabase"), _a2);
149
+ __name2(_CNSPostgresSchemaPgDatabase, "CNSPostgresSchemaPgDatabase");
150
+ var CNSPostgresSchemaPgDatabase = _CNSPostgresSchemaPgDatabase;
151
+ var _a3;
152
+ var _CNSPostgresSchemaPartitionRotation = (_a3 = class {
153
+ constructor(options) {
154
+ __publicField2(this, "options");
155
+ __publicField2(this, "rotatedFor", /* @__PURE__ */ new Map());
156
+ this.options = options;
157
+ if (!(options.premakeDays >= 1) || !Number.isInteger(options.premakeDays)) {
158
+ throw new Error(`[partition rotation] premakeDays must be a positive integer, got ${options.premakeDays}`);
159
+ }
160
+ }
161
+ async rotate(database, table, now, dropBefore, beforeDrop) {
162
+ const today = Math.floor(now / _a3.DAY_MS);
163
+ const days = `${today}:${Math.floor(dropBefore / _a3.DAY_MS)}`;
164
+ if (this.rotatedFor.get(table) === days) return [];
165
+ const dropped = await this.underLock(database, table, async (connection) => {
166
+ const expired = (await this.readRangePartitions(connection, table)).filter((partition) => partition.to <= dropBefore);
167
+ if (beforeDrop) for (const partition of expired) await beforeDrop(connection, partition.name);
168
+ for (const partition of expired) await connection.query(`DROP TABLE ${partition.name}`);
169
+ return expired.map((partition) => partition.name);
170
+ });
171
+ await this.underLock(database, table, async (connection) => {
172
+ const partitions = await this.readRangePartitions(connection, table);
173
+ for (let ahead = 1; ahead <= this.options.premakeDays; ahead++) {
174
+ const from = (today + ahead) * _a3.DAY_MS;
175
+ const to = from + _a3.DAY_MS;
176
+ if (partitions.some((partition) => partition.from < to && from < partition.to)) continue;
177
+ const name = `${table}_p${new Date(from).toISOString().slice(0, 10).replace(/-/g, "")}`;
178
+ await connection.query(`CREATE TABLE ${name} (LIKE ${table} INCLUDING DEFAULTS INCLUDING CONSTRAINTS)`);
179
+ await connection.query(`ALTER TABLE ${table} ATTACH PARTITION ${name} FOR VALUES FROM (${from}) TO (${to})`);
180
+ }
181
+ });
182
+ this.rotatedFor.set(table, days);
183
+ return dropped;
184
+ }
185
+ underLock(database, table, work) {
186
+ return database.transaction(async (connection) => {
187
+ await connection.query("SELECT pg_advisory_xact_lock($1, hashtext($2))", [
188
+ _a3.LOCK_NAMESPACE,
189
+ table
190
+ ]);
191
+ await connection.query(`SET LOCAL lock_timeout = '${_a3.LOCK_TIMEOUT}'`);
192
+ return work(connection);
193
+ });
194
+ }
195
+ async readRangePartitions(connection, table) {
196
+ const rows = await connection.query(`SELECT child.oid::regclass::text AS name, pg_get_expr(child.relpartbound, child.oid) AS bound
197
+ FROM pg_inherits
198
+ JOIN pg_class child ON child.oid = pg_inherits.inhrelid
199
+ WHERE pg_inherits.inhparent = $1::regclass`, [
200
+ table
201
+ ]);
202
+ const partitions = [];
203
+ for (const row of rows.rows) {
204
+ const range = /^FOR VALUES FROM \('?(-?[\d.e+]+)'?\) TO \('?(-?[\d.e+]+)'?\)$/.exec(row.bound);
205
+ if (range) partitions.push({
206
+ name: row.name,
207
+ from: Number(range[1]),
208
+ to: Number(range[2])
209
+ });
210
+ }
211
+ return partitions;
212
+ }
213
+ }, __name(_a3, "_CNSPostgresSchemaPartitionRotation"), _a3);
214
+ __name2(_CNSPostgresSchemaPartitionRotation, "CNSPostgresSchemaPartitionRotation");
215
+ __publicField2(_CNSPostgresSchemaPartitionRotation, "LOCK_NAMESPACE", 8);
216
+ __publicField2(_CNSPostgresSchemaPartitionRotation, "LOCK_TIMEOUT", "5s");
217
+ __publicField2(_CNSPostgresSchemaPartitionRotation, "DAY_MS", 24 * 60 * 60 * 1e3);
218
+
219
+ // src/schema/CNSPgBossProgressSchema.ts
220
+ var _CNSPgBossProgressSchema = class _CNSPgBossProgressSchema {
221
+ static assertTablePrefix(tablePrefix) {
222
+ return _CNSPgBossProgressSchema.migrator.assertTablePrefix(tablePrefix, _CNSPgBossProgressSchema.LONGEST_SUFFIX);
223
+ }
224
+ static table(tablePrefix = _CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX) {
225
+ return `${_CNSPgBossProgressSchema.assertTablePrefix(tablePrefix)}pgboss_progress`;
226
+ }
227
+ static migrationTable(tablePrefix = _CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX) {
228
+ return _CNSPgBossProgressSchema.migrator.migrationTable(_CNSPgBossProgressSchema.assertTablePrefix(tablePrefix));
229
+ }
230
+ static migrationSql(tablePrefix = _CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX, appliedSteps = [], options) {
231
+ return _CNSPgBossProgressSchema.migrator.migrationSql(_CNSPgBossProgressSchema.assertTablePrefix(tablePrefix), appliedSteps, options);
232
+ }
233
+ static async migrate(pool, tablePrefix = _CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX, options) {
234
+ await _CNSPgBossProgressSchema.migrator.migrate(CNSPostgresSchemaPgDatabase.fromPool(pool), _CNSPgBossProgressSchema.assertTablePrefix(tablePrefix), options);
235
+ }
236
+ static async verify(pool, tablePrefix = _CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX) {
237
+ await _CNSPgBossProgressSchema.migrator.verify(pool, _CNSPgBossProgressSchema.assertTablePrefix(tablePrefix));
238
+ }
239
+ };
240
+ __name(_CNSPgBossProgressSchema, "CNSPgBossProgressSchema");
241
+ __publicField(_CNSPgBossProgressSchema, "DEFAULT_TABLE_PREFIX", "cns_");
242
+ __publicField(_CNSPgBossProgressSchema, "LONGEST_SUFFIX", "pgboss_progress_pkey");
243
+ __publicField(_CNSPgBossProgressSchema, "migrator", new CNSPostgresSchemaMigrator({
244
+ name: "pgboss_progress",
245
+ tables: /* @__PURE__ */ __name((tablePrefix) => [
246
+ _CNSPgBossProgressSchema.table(tablePrefix)
247
+ ], "tables"),
248
+ steps: [
249
+ {
250
+ step: 1,
251
+ kind: "expand",
252
+ sql: /* @__PURE__ */ __name((p) => `
253
+ CREATE TABLE ${p}pgboss_progress (
254
+ job_id text PRIMARY KEY,
21
255
  progress jsonb NOT NULL,
22
- updated_at timestamptz NOT NULL DEFAULT now()
256
+ updated_at bigint NOT NULL
23
257
  );
24
- CREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;
25
- }
26
- __name(pgBossProgressSchemaSql, "pgBossProgressSchemaSql");
27
- async function ensureSchema(db, table = DEFAULT_TABLE) {
28
- await db.query(pgBossProgressSchemaSql(table));
29
- }
30
- __name(ensureSchema, "ensureSchema");
258
+ `, "sql")
259
+ }
260
+ ]
261
+ }));
262
+ var CNSPgBossProgressSchema = _CNSPgBossProgressSchema;
31
263
 
32
264
  // src/cli.ts
33
265
  async function main() {
34
- var _a;
266
+ var _a4;
35
267
  const cmd = process.argv[2];
36
268
  if (cmd !== "migrate") {
37
269
  console.error("usage: cnstra-pgboss-progress migrate");
@@ -42,13 +274,13 @@ async function main() {
42
274
  console.error("DATABASE_URL is required");
43
275
  process.exit(1);
44
276
  }
45
- const table = (_a = process.env.CNS_PGBOSS_PROGRESS_TABLE) != null ? _a : DEFAULT_TABLE;
277
+ const tablePrefix = (_a4 = process.env.CNS_TABLE_PREFIX) != null ? _a4 : CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX;
46
278
  const pool = new import_pg.Pool({
47
279
  connectionString: url
48
280
  });
49
281
  try {
50
- await ensureSchema(pool, table);
51
- console.log(`\u2713 ensured progress table "${table}"`);
282
+ await CNSPgBossProgressSchema.migrate(pool, tablePrefix);
283
+ console.log(`\u2713 progress table is up to date "${CNSPgBossProgressSchema.table(tablePrefix)}"`);
52
284
  } finally {
53
285
  await pool.end();
54
286
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/migrate.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_PGBOSS_PROGRESS_TABLE=my_table (optional; default cns_pgboss_progress)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_TABLE } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress 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 table = process.env.CNS_PGBOSS_PROGRESS_TABLE ?? DEFAULT_TABLE;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, table);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured progress table \"${table}\"`);\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 pg-boss progress table (`cns_pgboss_progress`).\n *\n * The package OWNS this schema so you can manage it explicitly (a migration step in\n * your own tooling) instead of relying on the repository's lazy auto-create. Run it\n * via the `cnstra-pgboss-progress migrate` CLI, or call `ensureSchema(db)` yourself\n * with any pg client/pool.\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_TABLE = 'cns_pgboss_progress';\n\n/** Guard the table identifier (it is interpolated into DDL/DML, not parameterised). */\nexport function assertSafeTable(table: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {\n throw new Error(\n `[@cnstra/pg-boss] unsafe progress table name: ${JSON.stringify(table)}`\n );\n }\n return table;\n}\n\n/** The CREATE TABLE for the progress store. One upserted row per key (job id). */\nexport function pgBossProgressSchemaSql(table: string = DEFAULT_TABLE): string {\n assertSafeTable(table);\n // `updated_at` + its index back `deleteStale()`: a row is deleted when its job\n // succeeds, but a job that fails PERMANENTLY (retries exhausted) leaves its row —\n // pg-boss has no terminal-failure hook to key off. Pruning by staleness collects\n // those, and the index keeps the sweep cheap.\n return `CREATE TABLE IF NOT EXISTS ${table} (\n key text PRIMARY KEY,\n progress jsonb NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS ${table}_updated_idx ON ${table} (updated_at)`;\n}\n\n/** Idempotently create the progress table. Safe to run repeatedly. */\nexport async function ensureSchema(\n db: IPgQueryable,\n table: string = DEFAULT_TABLE\n): Promise<void> {\n await db.query(pgBossProgressSchemaSql(table));\n}\n"],"mappings":";;;;;AAOA,gBAAqB;;;ACOd,IAAMA,gBAAgB;AAGtB,SAASC,gBAAgBC,OAAa;AACzC,MAAI,CAAC,2BAA2BC,KAAKD,KAAAA,GAAQ;AACzC,UAAM,IAAIE,MACN,iDAAiDC,KAAKC,UAAUJ,KAAAA,CAAAA,EAAQ;EAEhF;AACA,SAAOA;AACX;AAPgBD;AAUT,SAASM,wBAAwBL,QAAgBF,eAAa;AACjEC,kBAAgBC,KAAAA;AAKhB,SAAO,8BAA8BA,KAAAA;;;;;6BAKZA,KAAAA,mBAAwBA,KAAAA;AACrD;AAZgBK;AAehB,eAAsBC,aAClBC,IACAP,QAAgBF,eAAa;AAE7B,QAAMS,GAAGC,MAAMH,wBAAwBL,KAAAA,CAAAA;AAC3C;AALsBM;;;ADhCtB,eAAeG,OAAAA;;AACX,QAAMC,MAAMC,QAAQC,KAAK,CAAA;AACzB,MAAIF,QAAQ,WAAW;AACnBG,YAAQC,MAAM,uCAAA;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,SAAQR,aAAQM,IAAIG,8BAAZT,YAAyCU;AACvD,QAAMC,OAAO,IAAIC,eAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,KAAAA;AAEzBN,YAAQa,IAAI,kCAA6BP,KAAAA,GAAQ;EACrD,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_TABLE","assertSafeTable","table","test","Error","JSON","stringify","pgBossProgressSchemaSql","ensureSchema","db","query","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","table","CNS_PGBOSS_PROGRESS_TABLE","DEFAULT_TABLE","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
1
+ {"version":3,"sources":["../src/cli.ts","../../postgres-schema/src/CNSPostgresSchemaMigrator.ts","../../postgres-schema/src/CNSPostgresSchemaPgDatabase.ts","../../postgres-schema/src/CNSPostgresSchemaPartitionRotation.ts","../src/schema/CNSPgBossProgressSchema.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_TABLE_PREFIX=my_ (optional; default cns_)\n */\nimport { Pool } from 'pg';\nimport { CNSPgBossProgressSchema } from './schema/CNSPgBossProgressSchema';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress 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 tablePrefix = process.env.CNS_TABLE_PREFIX ?? CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX;\n const pool = new Pool({ connectionString: url });\n try {\n await CNSPgBossProgressSchema.migrate(pool, tablePrefix);\n // eslint-disable-next-line no-console\n console.log(`✓ progress table is up to date \"${CNSPgBossProgressSchema.table(tablePrefix)}\"`);\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","import type { ICNSPostgresSchemaConnection } from './interfaces/ICNSPostgresSchemaConnection';\nimport type { ICNSPostgresSchemaDatabase } from './interfaces/ICNSPostgresSchemaDatabase';\nimport type { ECNSPostgresSchemaStepKind } from './types/ECNSPostgresSchemaStepKind';\nimport type { TCNSPostgresSchemaMigrateOptions } from './types/TCNSPostgresSchemaMigrateOptions';\nimport type { TCNSPostgresSchemaMigratorOptions } from './types/TCNSPostgresSchemaMigratorOptions';\nimport type { TCNSPostgresSchemaStep } from './types/TCNSPostgresSchemaStep';\n\nexport class CNSPostgresSchemaMigrator {\n static readonly LOCK_NAMESPACE = 6;\n static readonly LOCK_TIMEOUT = '5s';\n static readonly MAX_IDENTIFIER_LENGTH = 63;\n\n private readonly steps: readonly TCNSPostgresSchemaStep[];\n\n constructor(private readonly options: TCNSPostgresSchemaMigratorOptions) {\n this.steps = [...options.steps].sort((left, right) => left.step - right.step);\n this.steps.forEach((step, index) => {\n if (step.step !== index + 1) throw new Error(`[${options.name}] schema steps must be numbered 1, 2, 3…; found ${step.step} at position ${index + 1}`);\n });\n if (this.steps[0]?.kind !== 'expand') throw new Error(`[${options.name}] schema step 1 must be an expand step`);\n }\n\n migrationTable(tablePrefix: string): string {\n return `${tablePrefix}${this.options.name}_migration`;\n }\n\n async migrate(database: ICNSPostgresSchemaDatabase, tablePrefix: string, options: TCNSPostgresSchemaMigrateOptions = {}): Promise<void> {\n await database.transaction(async connection => {\n await connection.query('SELECT pg_advisory_xact_lock($1, hashtext($2))', [CNSPostgresSchemaMigrator.LOCK_NAMESPACE, tablePrefix + this.options.name]);\n await connection.query(`SET LOCAL lock_timeout = '${CNSPostgresSchemaMigrator.LOCK_TIMEOUT}'`);\n const applied = await this.readAppliedSteps(connection, tablePrefix);\n if (applied === undefined) await this.assertNoUntrackedTables(connection, tablePrefix);\n const pending = this.pendingSteps(applied ?? new Map(), applied === undefined || options.contract === true);\n if (pending.length > 0) await connection.query(this.sqlFor(tablePrefix, pending, applied === undefined));\n });\n }\n\n async verify(connection: ICNSPostgresSchemaConnection, tablePrefix: string): Promise<void> {\n const applied = await this.readAppliedSteps(connection, tablePrefix);\n if (applied === undefined) {\n throw new Error(`[${this.options.name}] the schema is not installed under prefix \"${tablePrefix}\": run migrate()`);\n }\n const missing = this.steps.filter(step => step.kind === 'expand' && !applied.has(step.step)).map(step => step.step);\n if (missing.length > 0) {\n throw new Error(`[${this.options.name}] schema steps ${missing.join(', ')} are not applied under prefix \"${tablePrefix}\": run migrate()`);\n }\n const unknown = [...applied].filter(([step, kind]) => kind === 'contract' && step > this.steps.length).map(([step]) => step);\n if (unknown.length > 0) {\n throw new Error(`[${this.options.name}] the database has contract steps ${unknown.join(', ')} that this version does not know: upgrade the package`);\n }\n }\n\n migrationSql(tablePrefix: string, appliedSteps: readonly number[] = [], options: TCNSPostgresSchemaMigrateOptions = {}): string {\n const fresh = appliedSteps.length === 0;\n const applied = new Map<number, ECNSPostgresSchemaStepKind>(appliedSteps.map(step => [step, 'expand']));\n return this.sqlFor(tablePrefix, this.pendingSteps(applied, fresh || options.contract === true), fresh);\n }\n\n assertTablePrefix(tablePrefix: string, longestSuffix: string): string {\n const longest = Math.max(longestSuffix.length, `${this.options.name}_migration`.length);\n const limit = CNSPostgresSchemaMigrator.MAX_IDENTIFIER_LENGTH - longest;\n if (!new RegExp(`^[A-Za-z_][A-Za-z0-9_]{0,${limit - 1}}$`).test(tablePrefix)) {\n throw new Error(`[${this.options.name}] unsafe table prefix ${JSON.stringify(tablePrefix)}: letters, digits and _ only, at most ${limit} characters`);\n }\n return tablePrefix;\n }\n\n private pendingSteps(applied: ReadonlyMap<number, ECNSPostgresSchemaStepKind>, withContract: boolean): TCNSPostgresSchemaStep[] {\n return this.steps.filter(step => !applied.has(step.step) && (step.kind === 'expand' || withContract));\n }\n\n private sqlFor(tablePrefix: string, steps: readonly TCNSPostgresSchemaStep[], createMigrationTable: boolean): string {\n const table = this.migrationTable(tablePrefix);\n const parts = createMigrationTable\n ? [`CREATE TABLE ${table} (step integer PRIMARY KEY, kind text NOT NULL CHECK (kind IN ('expand', 'contract')));`]\n : [];\n for (const step of steps) {\n parts.push(step.sql(tablePrefix), `INSERT INTO ${table} (step, kind) VALUES (${step.step}, '${step.kind}');`);\n }\n return parts.join('\\n');\n }\n\n private async readAppliedSteps(connection: ICNSPostgresSchemaConnection, tablePrefix: string): Promise<Map<number, ECNSPostgresSchemaStepKind> | undefined> {\n const table = this.migrationTable(tablePrefix);\n const exists = await connection.query<{ found: string | null }>('SELECT to_regclass($1)::text AS found', [table]);\n if (exists.rows[0]?.found == null) return undefined;\n const rows = await connection.query<{ step: number; kind: ECNSPostgresSchemaStepKind }>(`SELECT step, kind FROM ${table}`);\n return new Map(rows.rows.map(row => [Number(row.step), row.kind]));\n }\n\n private async assertNoUntrackedTables(connection: ICNSPostgresSchemaConnection, tablePrefix: string): Promise<void> {\n const tables = this.options.tables(tablePrefix);\n const found = await connection.query<{ name: string }>(\n 'SELECT name FROM unnest($1::text[]) AS name WHERE to_regclass(name) IS NOT NULL',\n [tables]\n );\n if (found.rows.length > 0) {\n throw new Error(\n `[${this.options.name}] tables ${found.rows.map(row => row.name).join(', ')} exist without ${this.migrationTable(tablePrefix)}: they were created by an earlier unversioned release; drop them or move the data before migrate()`\n );\n }\n }\n}\n","import type { ICNSPostgresSchemaConnection } from './interfaces/ICNSPostgresSchemaConnection';\nimport type { ICNSPostgresSchemaDatabase } from './interfaces/ICNSPostgresSchemaDatabase';\nimport type { ICNSPostgresSchemaPgPool } from './interfaces/ICNSPostgresSchemaPgPool';\n\nexport class CNSPostgresSchemaPgDatabase implements ICNSPostgresSchemaDatabase {\n private constructor(\n private readonly open: () => Promise<{ connection: ICNSPostgresSchemaConnection; release: (error?: Error) => void }>\n ) {}\n\n static fromPool(pool: ICNSPostgresSchemaPgPool): CNSPostgresSchemaPgDatabase {\n return new CNSPostgresSchemaPgDatabase(async () => {\n const client = await pool.connect();\n return { connection: client, release: error => client.release(error) };\n });\n }\n\n async transaction<TResult>(work: (connection: ICNSPostgresSchemaConnection) => Promise<TResult>): Promise<TResult> {\n const { connection, release } = await this.open();\n try {\n await connection.query('BEGIN');\n const result = await work(connection);\n await connection.query('COMMIT');\n release();\n return result;\n } catch (error) {\n const rollback = await connection.query('ROLLBACK').then(\n () => undefined,\n (rollbackError: unknown) => rollbackError\n );\n release(rollback instanceof Error ? rollback : undefined);\n throw error;\n }\n }\n}\n","import type { ICNSPostgresSchemaConnection } from './interfaces/ICNSPostgresSchemaConnection';\nimport type { ICNSPostgresSchemaDatabase } from './interfaces/ICNSPostgresSchemaDatabase';\nimport type { TCNSPostgresSchemaPartitionRotationOptions } from './types/TCNSPostgresSchemaPartitionRotationOptions';\n\ntype TRangePartition = { readonly name: string; readonly from: number; readonly to: number };\n\nexport class CNSPostgresSchemaPartitionRotation {\n static readonly LOCK_NAMESPACE = 8;\n static readonly LOCK_TIMEOUT = '5s';\n static readonly DAY_MS = 24 * 60 * 60 * 1000;\n\n private readonly rotatedFor = new Map<string, string>();\n\n constructor(private readonly options: TCNSPostgresSchemaPartitionRotationOptions) {\n if (!(options.premakeDays >= 1) || !Number.isInteger(options.premakeDays)) {\n throw new Error(`[partition rotation] premakeDays must be a positive integer, got ${options.premakeDays}`);\n }\n }\n\n async rotate(\n database: ICNSPostgresSchemaDatabase,\n table: string,\n now: number,\n dropBefore: number,\n beforeDrop?: (connection: ICNSPostgresSchemaConnection, partition: string) => Promise<void>\n ): Promise<string[]> {\n const today = Math.floor(now / CNSPostgresSchemaPartitionRotation.DAY_MS);\n const days = `${today}:${Math.floor(dropBefore / CNSPostgresSchemaPartitionRotation.DAY_MS)}`;\n if (this.rotatedFor.get(table) === days) return [];\n const dropped = await this.underLock(database, table, async connection => {\n const expired = (await this.readRangePartitions(connection, table)).filter(partition => partition.to <= dropBefore);\n if (beforeDrop) for (const partition of expired) await beforeDrop(connection, partition.name);\n for (const partition of expired) await connection.query(`DROP TABLE ${partition.name}`);\n return expired.map(partition => partition.name);\n });\n await this.underLock(database, table, async connection => {\n const partitions = await this.readRangePartitions(connection, table);\n for (let ahead = 1; ahead <= this.options.premakeDays; ahead++) {\n const from = (today + ahead) * CNSPostgresSchemaPartitionRotation.DAY_MS;\n const to = from + CNSPostgresSchemaPartitionRotation.DAY_MS;\n if (partitions.some(partition => partition.from < to && from < partition.to)) continue;\n const name = `${table}_p${new Date(from).toISOString().slice(0, 10).replace(/-/g, '')}`;\n await connection.query(`CREATE TABLE ${name} (LIKE ${table} INCLUDING DEFAULTS INCLUDING CONSTRAINTS)`);\n await connection.query(`ALTER TABLE ${table} ATTACH PARTITION ${name} FOR VALUES FROM (${from}) TO (${to})`);\n }\n });\n this.rotatedFor.set(table, days);\n return dropped;\n }\n\n private underLock<TResult>(database: ICNSPostgresSchemaDatabase, table: string, work: (connection: ICNSPostgresSchemaConnection) => Promise<TResult>): Promise<TResult> {\n return database.transaction(async connection => {\n await connection.query('SELECT pg_advisory_xact_lock($1, hashtext($2))', [CNSPostgresSchemaPartitionRotation.LOCK_NAMESPACE, table]);\n await connection.query(`SET LOCAL lock_timeout = '${CNSPostgresSchemaPartitionRotation.LOCK_TIMEOUT}'`);\n return work(connection);\n });\n }\n\n private async readRangePartitions(connection: ICNSPostgresSchemaConnection, table: string): Promise<TRangePartition[]> {\n const rows = await connection.query<{ name: string; bound: string }>(\n `SELECT child.oid::regclass::text AS name, pg_get_expr(child.relpartbound, child.oid) AS bound\n FROM pg_inherits\n JOIN pg_class child ON child.oid = pg_inherits.inhrelid\n WHERE pg_inherits.inhparent = $1::regclass`,\n [table]\n );\n const partitions: TRangePartition[] = [];\n for (const row of rows.rows) {\n const range = /^FOR VALUES FROM \\('?(-?[\\d.e+]+)'?\\) TO \\('?(-?[\\d.e+]+)'?\\)$/.exec(row.bound);\n if (range) partitions.push({ name: row.name, from: Number(range[1]), to: Number(range[2]) });\n }\n return partitions;\n }\n}\n","import { CNSPostgresSchemaMigrator, CNSPostgresSchemaPgDatabase } from '@cnstra/postgres-schema';\nimport type { ICNSPostgresSchemaConnection, ICNSPostgresSchemaPgPool } from '@cnstra/postgres-schema';\nimport type { Pool } from 'pg';\nimport type { TCNSPgBossProgressMigrateOptions } from '../types/TCNSPgBossProgressMigrateOptions';\n\nexport class CNSPgBossProgressSchema {\n static readonly DEFAULT_TABLE_PREFIX = 'cns_';\n private static readonly LONGEST_SUFFIX = 'pgboss_progress_pkey';\n\n private static readonly migrator = new CNSPostgresSchemaMigrator({\n name: 'pgboss_progress',\n tables: tablePrefix => [CNSPgBossProgressSchema.table(tablePrefix)],\n steps: [\n {\n step: 1,\n kind: 'expand',\n sql: p => `\nCREATE TABLE ${p}pgboss_progress (\n job_id text PRIMARY KEY,\n progress jsonb NOT NULL,\n updated_at bigint NOT NULL\n);\n`,\n },\n ],\n });\n\n static assertTablePrefix(tablePrefix: string): string {\n return CNSPgBossProgressSchema.migrator.assertTablePrefix(tablePrefix, CNSPgBossProgressSchema.LONGEST_SUFFIX);\n }\n\n static table(tablePrefix: string = CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX): string {\n return `${CNSPgBossProgressSchema.assertTablePrefix(tablePrefix)}pgboss_progress`;\n }\n\n static migrationTable(tablePrefix: string = CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX): string {\n return CNSPgBossProgressSchema.migrator.migrationTable(CNSPgBossProgressSchema.assertTablePrefix(tablePrefix));\n }\n\n static migrationSql(\n tablePrefix: string = CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX,\n appliedSteps: readonly number[] = [],\n options?: TCNSPgBossProgressMigrateOptions\n ): string {\n return CNSPgBossProgressSchema.migrator.migrationSql(CNSPgBossProgressSchema.assertTablePrefix(tablePrefix), appliedSteps, options);\n }\n\n static async migrate(\n pool: Pool,\n tablePrefix: string = CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX,\n options?: TCNSPgBossProgressMigrateOptions\n ): Promise<void> {\n await CNSPgBossProgressSchema.migrator.migrate(\n CNSPostgresSchemaPgDatabase.fromPool(pool as unknown as ICNSPostgresSchemaPgPool),\n CNSPgBossProgressSchema.assertTablePrefix(tablePrefix),\n options\n );\n }\n\n static async verify(pool: Pool, tablePrefix: string = CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX): Promise<void> {\n await CNSPgBossProgressSchema.migrator.verify(\n pool as unknown as ICNSPostgresSchemaConnection,\n CNSPgBossProgressSchema.assertTablePrefix(tablePrefix)\n );\n }\n}\n"],"mappings":"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","names":["CNSPostgresSchemaMigrator","options","steps","_a","sort","left","right","step","forEach","index","Error","name","kind","migrationTable","tablePrefix","migrate","database","transaction","connection","query","LOCK_NAMESPACE","LOCK_TIMEOUT","applied","readAppliedSteps","undefined","assertNoUntrackedTables","pending","pendingSteps","Map","contract","length","sqlFor","verify","missing","filter","has","map","join","unknown","migrationSql","appliedSteps","fresh","assertTablePrefix","longestSuffix","longest","Math","max","limit","MAX_IDENTIFIER_LENGTH","RegExp","test","JSON","stringify","withContract","createMigrationTable","table","parts","push","sql","exists","rows","found","row","Number","tables","__publicField","CNSPostgresSchemaPgDatabase","open","fromPool","pool","client","connect","release","error","work","result","rollback","then","rollbackError","CNSPostgresSchemaPartitionRotation","rotatedFor","premakeDays","isInteger","rotate","now","dropBefore","beforeDrop","today","floor","DAY_MS","days","get","dropped","underLock","expired","readRangePartitions","partition","to","partitions","ahead","from","some","Date","toISOString","slice","replace","set","range","exec","bound","CNSPgBossProgressSchema","assertTablePrefix","tablePrefix","migrator","LONGEST_SUFFIX","table","DEFAULT_TABLE_PREFIX","migrationTable","migrationSql","appliedSteps","options","migrate","pool","CNSPostgresSchemaPgDatabase","fromPool","verify","CNSPostgresSchemaMigrator","name","tables","steps","step","kind","sql","p","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","tablePrefix","CNS_TABLE_PREFIX","CNSPgBossProgressSchema","DEFAULT_TABLE_PREFIX","pool","Pool","connectionString","migrate","log","table","end","catch","e"]}
package/dist/cli.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- DEFAULT_TABLE,
4
- ensureSchema
5
- } from "./chunk-XJFC72LU.mjs";
3
+ CNSPgBossProgressSchema
4
+ } from "./chunk-OJUUT4VL.mjs";
6
5
  import {
7
6
  __name
8
7
  } from "./chunk-2JTKI4GS.mjs";
@@ -21,13 +20,13 @@ async function main() {
21
20
  console.error("DATABASE_URL is required");
22
21
  process.exit(1);
23
22
  }
24
- const table = (_a = process.env.CNS_PGBOSS_PROGRESS_TABLE) != null ? _a : DEFAULT_TABLE;
23
+ const tablePrefix = (_a = process.env.CNS_TABLE_PREFIX) != null ? _a : CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX;
25
24
  const pool = new Pool({
26
25
  connectionString: url
27
26
  });
28
27
  try {
29
- await ensureSchema(pool, table);
30
- console.log(`\u2713 ensured progress table "${table}"`);
28
+ await CNSPgBossProgressSchema.migrate(pool, tablePrefix);
29
+ console.log(`\u2713 progress table is up to date "${CNSPgBossProgressSchema.table(tablePrefix)}"`);
31
30
  } finally {
32
31
  await pool.end();
33
32
  }
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_PGBOSS_PROGRESS_TABLE=my_table (optional; default cns_pgboss_progress)\n */\nimport { Pool } from 'pg';\nimport { ensureSchema, DEFAULT_TABLE } from './migrate';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress 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 table = process.env.CNS_PGBOSS_PROGRESS_TABLE ?? DEFAULT_TABLE;\n const pool = new Pool({ connectionString: url });\n try {\n await ensureSchema(pool, table);\n // eslint-disable-next-line no-console\n console.log(`✓ ensured progress table \"${table}\"`);\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,uCAAA;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,SAAQR,aAAQM,IAAIG,8BAAZT,YAAyCU;AACvD,QAAMC,OAAO,IAAIC,KAAK;IAAEC,kBAAkBR;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMS,aAAaH,MAAMH,KAAAA;AAEzBN,YAAQa,IAAI,kCAA6BP,KAAAA,GAAQ;EACrD,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","table","CNS_PGBOSS_PROGRESS_TABLE","DEFAULT_TABLE","pool","Pool","connectionString","ensureSchema","log","end","catch","e"]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `cnstra-pgboss-progress migrate` — create the pg-boss progress table.\n *\n * DATABASE_URL=postgres://... cnstra-pgboss-progress migrate\n * CNS_TABLE_PREFIX=my_ (optional; default cns_)\n */\nimport { Pool } from 'pg';\nimport { CNSPgBossProgressSchema } from './schema/CNSPgBossProgressSchema';\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2];\n if (cmd !== 'migrate') {\n console.error('usage: cnstra-pgboss-progress 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 tablePrefix = process.env.CNS_TABLE_PREFIX ?? CNSPgBossProgressSchema.DEFAULT_TABLE_PREFIX;\n const pool = new Pool({ connectionString: url });\n try {\n await CNSPgBossProgressSchema.migrate(pool, tablePrefix);\n // eslint-disable-next-line no-console\n console.log(`✓ progress table is up to date \"${CNSPgBossProgressSchema.table(tablePrefix)}\"`);\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,uCAAA;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,eAAcR,aAAQM,IAAIG,qBAAZT,YAAgCU,wBAAwBC;AAC5E,QAAMC,OAAO,IAAIC,KAAK;IAAEC,kBAAkBT;EAAI,CAAA;AAC9C,MAAI;AACA,UAAMK,wBAAwBK,QAAQH,MAAMJ,WAAAA;AAE5CN,YAAQc,IAAI,wCAAmCN,wBAAwBO,MAAMT,WAAAA,CAAAA,GAAe;EAChG,UAAA;AACI,UAAMI,KAAKM,IAAG;EAClB;AACJ;AApBepB;AAsBf,KAAKA,KAAAA,EAAOqB,MAAMC,CAAAA,MAAAA;AACdlB,UAAQC,MAAMiB,CAAAA;AACdpB,UAAQI,KAAK,CAAA;AACjB,CAAA;","names":["Pool","main","cmd","process","argv","console","error","exit","url","env","DATABASE_URL","tablePrefix","CNS_TABLE_PREFIX","CNSPgBossProgressSchema","DEFAULT_TABLE_PREFIX","pool","Pool","connectionString","migrate","log","table","end","catch","e"]}
package/dist/index.d.mts CHANGED
@@ -1,8 +1,9 @@
1
- import { CNSPersistOptionsRegistry, ICNSStimulationRepository, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
1
+ import { CNSPersistOptionsRegistry, ICNSStimulationWriter, ICNSProgressRepository, ICNSProgressRecordingStrategy } from '@cnstra/persist';
2
2
  import { ICNS } from '@cnstra/types';
3
3
  import { CNSCollateral } from '@cnstra/core';
4
- import { TCNSStimulationJobData } from '@cnstra/broker';
4
+ import { TCNSStimulationJobOptions, TCNSStimulationJobData } from '@cnstra/broker';
5
5
  export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
6
+ import { TCNSDevToolsActionMiddleware } from '@cnstra/persist-dto';
6
7
 
7
8
  /**
8
9
  * Minimal, structural view of the pg-boss surface this package touches.
@@ -12,22 +13,29 @@ export { ICNSResponseLike, TCNSStimulationJobData } from '@cnstra/broker';
12
13
  * `work` handlers receive an array of jobs (batch). Pass your real `PgBoss`
13
14
  * instance — it satisfies this shape structurally.
14
15
  */
15
- interface IPgBossJob<TData = unknown> {
16
+ interface ICNSPgBossJob<TData = unknown> {
16
17
  id: string;
17
18
  name: string;
18
19
  data: TData;
19
20
  /** 0-based retry count (pg-boss `Job.retryCount`); drives the attempt number. */
20
21
  retryCount?: number;
21
22
  }
22
- interface IPgBossLike {
23
+ interface ICNSPgBossLike {
23
24
  send(name: string, data: object, options?: Record<string, unknown>): Promise<string | null>;
24
- work<TData extends object>(name: string, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
25
- work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: IPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
25
+ work<TData extends object>(name: string, handler: (jobs: ICNSPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
26
+ work<TData extends object>(name: string, options: Record<string, unknown>, handler: (jobs: ICNSPgBossJob<TData>[]) => Promise<unknown>): Promise<string>;
27
+ /**
28
+ * Mark jobs finished by id (pg-boss `complete(name, ids)`). The batch handler
29
+ * calls it for the jobs that succeeded alongside one that did not, so a single
30
+ * failure does not re-run its neighbours. Optional: a stand-in that never
31
+ * batches has nothing to settle.
32
+ */
33
+ complete?(name: string, ids: string[]): Promise<unknown>;
26
34
  }
27
35
 
28
- interface TCNSWorkerOptions {
36
+ type TCNSWorkerOptions = {
29
37
  /** Your pg-boss instance (already started). */
30
- boss: IPgBossLike;
38
+ boss: ICNSPgBossLike;
31
39
  /** The CNS the jobs run against. */
32
40
  cns: ICNS<any, any>;
33
41
  /** Name ↔ object registry used to reconstruct signals from job data. */
@@ -43,7 +51,7 @@ interface TCNSWorkerOptions {
43
51
  * that is `resume` below. One job = one stimulation (`stimulationId` = job id),
44
52
  * each pg-boss retry = a new attempt.
45
53
  */
46
- observe?: ICNSStimulationRepository;
54
+ observe?: ICNSStimulationWriter;
47
55
  /**
48
56
  * Optional resumable **resume**. When set, the worker records the run's
49
57
  * outstanding frontier (by name) into `repository` as it advances, and a
@@ -63,7 +71,7 @@ interface TCNSWorkerOptions {
63
71
  };
64
72
  /** pg-boss `work` options (batchSize, pollingIntervalSeconds, …). */
65
73
  workOptions?: Record<string, unknown>;
66
- }
74
+ };
67
75
  /**
68
76
  * Register a pg-boss worker that turns each job into a CNStra stimulation.
69
77
  *
@@ -74,7 +82,7 @@ interface TCNSWorkerOptions {
74
82
  * own retry/backoff policy.
75
83
  */
76
84
  /** A registered worker + a graceful `shutdown` (see {@link createCNSWorker}). */
77
- interface TCNSPgBossWorkerHandle {
85
+ type TCNSPgBossWorkerHandle = {
78
86
  /** The pg-boss `work()` registration id. */
79
87
  workId: string;
80
88
  /**
@@ -88,11 +96,11 @@ interface TCNSPgBossWorkerHandle {
88
96
  shutdown(opts?: {
89
97
  drainMs?: number;
90
98
  }): Promise<void>;
91
- }
99
+ };
92
100
  declare function createCNSWorker(options: TCNSWorkerOptions): Promise<TCNSPgBossWorkerHandle>;
93
101
 
94
102
  /** Commonly used pg-boss send options (a superset is passed through). */
95
- interface TCNSSendOptions {
103
+ type TCNSSendOptions = {
96
104
  retryLimit?: number;
97
105
  retryBackoff?: boolean;
98
106
  retryDelay?: number;
@@ -102,13 +110,34 @@ interface TCNSSendOptions {
102
110
  expireInSeconds?: number;
103
111
  priority?: number;
104
112
  [key: string]: unknown;
105
- }
113
+ };
106
114
  /**
107
115
  * Build a queue job from a **registered** collateral. Throws if the collateral
108
116
  * is not in the registry, so a typo can't reach the queue.
109
117
  */
110
- declare function stimulationJob(registry: CNSPersistOptionsRegistry, collateral: CNSCollateral<unknown>, payload: unknown, extra?: Pick<TCNSStimulationJobData, 'maxNeuronHops'>): TCNSStimulationJobData;
118
+ declare function cnsStimulationJob(registry: CNSPersistOptionsRegistry, collateral: CNSCollateral<unknown>, payload: unknown, stimulationJobOptions?: TCNSStimulationJobOptions): TCNSStimulationJobData;
111
119
  /** Send a stimulation job to a pg-boss queue. Returns the job id. */
112
- declare function enqueueStimulation(boss: IPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
120
+ declare function enqueueCNSStimulation(boss: ICNSPgBossLike, queue: string, job: TCNSStimulationJobData, options?: TCNSSendOptions): Promise<string | null>;
121
+
122
+ /** The two calls this needs beyond `send` — kept structural, like the rest. */
123
+ interface ICNSPgBossActionsLike extends ICNSPgBossLike {
124
+ getJobById(name: string, id: string): Promise<{
125
+ id: string;
126
+ state?: string;
127
+ } | null | undefined>;
128
+ retry(name: string, id: string): Promise<unknown>;
129
+ }
130
+ /**
131
+ * Carries out DevTools actions against a pg-boss queue.
132
+ *
133
+ * Same shape as the BullMQ adapter and for the same reason: **retry is the
134
+ * broker's own retry**, keyed by the job id — which is our `stimulationId` — so
135
+ * the run keeps its identity, its history and its resume checkpoint. A clone or
136
+ * a replay is a new job under a new id.
137
+ *
138
+ * Ownership is decided by asking pg-boss whether it holds the job; anything else
139
+ * falls through to the next link in the chain.
140
+ */
141
+ declare const createCNSPgBossActions: (boss: ICNSPgBossActionsLike, queue: string) => TCNSDevToolsActionMiddleware;
113
142
 
114
- export { type IPgBossJob, type IPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, createCNSWorker, enqueueStimulation, stimulationJob };
143
+ export { type ICNSPgBossActionsLike, type ICNSPgBossJob, type ICNSPgBossLike, type TCNSPgBossWorkerHandle, type TCNSSendOptions, type TCNSWorkerOptions, cnsStimulationJob, createCNSPgBossActions, createCNSWorker, enqueueCNSStimulation };