@cronwatch/sdk 0.1.0 → 0.3.1

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -12
  3. package/dist/anthropic.cjs +19 -5
  4. package/dist/anthropic.cjs.map +1 -1
  5. package/dist/anthropic.d.cts +1 -1
  6. package/dist/anthropic.d.ts +1 -1
  7. package/dist/anthropic.js +19 -5
  8. package/dist/anthropic.js.map +1 -1
  9. package/dist/discord.cjs +14 -2
  10. package/dist/discord.cjs.map +1 -1
  11. package/dist/discord.d.cts +6 -2
  12. package/dist/discord.d.ts +6 -2
  13. package/dist/discord.js +13 -3
  14. package/dist/discord.js.map +1 -1
  15. package/dist/index.cjs +835 -293
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +133 -45
  18. package/dist/index.d.ts +133 -45
  19. package/dist/index.js +836 -291
  20. package/dist/index.js.map +1 -1
  21. package/dist/postgres.cjs +152 -73
  22. package/dist/postgres.cjs.map +1 -1
  23. package/dist/postgres.d.cts +2 -2
  24. package/dist/postgres.d.ts +2 -2
  25. package/dist/postgres.js +152 -73
  26. package/dist/postgres.js.map +1 -1
  27. package/dist/slack.cjs +12 -6
  28. package/dist/slack.cjs.map +1 -1
  29. package/dist/slack.d.cts +1 -1
  30. package/dist/slack.d.ts +1 -1
  31. package/dist/slack.js +12 -6
  32. package/dist/slack.js.map +1 -1
  33. package/dist/sqlite.cjs +184 -74
  34. package/dist/sqlite.cjs.map +1 -1
  35. package/dist/sqlite.d.cts +3 -1
  36. package/dist/sqlite.d.ts +3 -1
  37. package/dist/sqlite.js +185 -75
  38. package/dist/sqlite.js.map +1 -1
  39. package/dist/{types-BQ1P8z55.d.cts → types-DUt2apl0.d.cts} +84 -7
  40. package/dist/{types-BQ1P8z55.d.ts → types-DUt2apl0.d.ts} +84 -7
  41. package/dist/webhook.cjs +10 -2
  42. package/dist/webhook.cjs.map +1 -1
  43. package/dist/webhook.d.cts +1 -1
  44. package/dist/webhook.d.ts +1 -1
  45. package/dist/webhook.js +10 -2
  46. package/dist/webhook.js.map +1 -1
  47. package/package.json +97 -34
package/dist/postgres.cjs CHANGED
@@ -7,7 +7,110 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
7
  var pg__default = /*#__PURE__*/_interopDefault(pg);
8
8
 
9
9
  // src/stores/postgres.ts
10
+
11
+ // src/stores/sql.ts
12
+ var DEFAULT_PREFIX = "cronwatch_";
13
+ var MAX_PREFIX = 63 - "runs_job_started".length;
14
+ function tablePrefix(prefix = DEFAULT_PREFIX) {
15
+ if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {
16
+ throw new Error(
17
+ `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, not starting with a digit, at most ${MAX_PREFIX} characters.`
18
+ );
19
+ }
20
+ return prefix;
21
+ }
22
+ function schema(dialect, p) {
23
+ const int = "BIGINT" ;
24
+ const json2 = "JSONB" ;
25
+ return `
26
+ CREATE TABLE IF NOT EXISTS ${p}jobs (
27
+ name TEXT PRIMARY KEY,
28
+ definition ${json2} NOT NULL,
29
+ created_at ${int} NOT NULL,
30
+ updated_at ${int} NOT NULL
31
+ );
32
+ CREATE TABLE IF NOT EXISTS ${p}runs (${"\n seq BIGSERIAL," }
33
+ id TEXT PRIMARY KEY,
34
+ job TEXT NOT NULL,
35
+ status TEXT NOT NULL,
36
+ started_at ${int} NOT NULL,
37
+ finished_at ${int},
38
+ duration_ms ${int},
39
+ error TEXT,
40
+ output TEXT,
41
+ metrics ${json2} NOT NULL DEFAULT '{}',
42
+ trigger TEXT NOT NULL DEFAULT 'run'
43
+ );
44
+ CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);
45
+ CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';
46
+ CREATE TABLE IF NOT EXISTS ${p}state (
47
+ job TEXT PRIMARY KEY,
48
+ state ${json2} NOT NULL
49
+ );
50
+ `;
51
+ }
52
+ function statements(dialect, p) {
53
+ const seq = "seq" ;
54
+ const byName = `name COLLATE "C"` ;
55
+ const version = (column) => `COALESCE((${column}->>'version')::bigint, 0)` ;
56
+ const sql = {
57
+ upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
58
+ ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
59
+ getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,
60
+ listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,
61
+ deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,
62
+ deleteState: `DELETE FROM ${p}state WHERE job = ?`,
63
+ deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,
64
+ insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
65
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
66
+ updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,
67
+ getRun: `SELECT * FROM ${p}runs WHERE id = ?`,
68
+ listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,
69
+ runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
70
+ getState: `SELECT state FROM ${p}state WHERE job = ?`,
71
+ setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,
72
+ // compareAndSetState. Expecting version 0 also matches a missing row, so
73
+ // that case inserts; any other version must find its row.
74
+ casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)
75
+ ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,
76
+ casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version("state")} = ?`,
77
+ // Each job's newest run is kept whatever its age: without it, a job that
78
+ // runs less often than the retention looks like it never ran.
79
+ prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
80
+ AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`
81
+ };
82
+ {
83
+ for (const key of Object.keys(sql)) {
84
+ let n = 0;
85
+ sql[key] = sql[key].replace(/\?/g, () => `$${++n}`);
86
+ }
87
+ }
88
+ return sql;
89
+ }
90
+ var params = {
91
+ upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
92
+ insertRun: (run) => [
93
+ run.id,
94
+ run.job,
95
+ run.status,
96
+ run.startedAt,
97
+ run.finishedAt,
98
+ run.durationMs,
99
+ run.error,
100
+ run.output,
101
+ JSON.stringify(run.metrics),
102
+ run.trigger
103
+ ],
104
+ updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
105
+ setState: (state) => [state.job, JSON.stringify(state)],
106
+ casInsert: (state) => [state.job, JSON.stringify(state)],
107
+ casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
108
+ };
109
+ var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
10
110
  var num = (v) => v === null ? null : Number(v);
111
+ function rowToJob(r) {
112
+ return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
113
+ }
11
114
  function rowToRun(r) {
12
115
  return {
13
116
  id: r.id,
@@ -18,117 +121,93 @@ function rowToRun(r) {
18
121
  durationMs: num(r.duration_ms),
19
122
  error: r.error,
20
123
  output: r.output,
21
- metrics: r.metrics ?? {},
124
+ metrics: r.metrics === null ? {} : json(r.metrics),
22
125
  trigger: r.trigger
23
126
  };
24
127
  }
128
+ function rowToState(r) {
129
+ return json(r.state);
130
+ }
131
+
132
+ // src/stores/postgres.ts
25
133
  function postgres(options = {}) {
134
+ const p = tablePrefix(options.prefix);
135
+ const sql = statements("postgres", p);
26
136
  const owned = !options.pool;
27
137
  const pool = options.pool ?? new pg__default.default.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });
28
- const p = (options.prefix ?? "cronwatch_").replace(/[^a-z0-9_]/gi, "");
29
- const JOBS = `${p}jobs`, RUNS = `${p}runs`, STATE = `${p}state`;
138
+ const transaction = async (work) => {
139
+ const client = await pool.connect();
140
+ try {
141
+ await client.query("BEGIN");
142
+ await work(client);
143
+ await client.query("COMMIT");
144
+ } catch (e) {
145
+ await client.query("ROLLBACK");
146
+ throw e;
147
+ } finally {
148
+ client.release();
149
+ }
150
+ };
30
151
  return {
31
152
  async init() {
32
- await pool.query(`
33
- CREATE TABLE IF NOT EXISTS ${JOBS} (
34
- name TEXT PRIMARY KEY,
35
- definition JSONB NOT NULL,
36
- created_at BIGINT NOT NULL,
37
- updated_at BIGINT NOT NULL
38
- );
39
- CREATE TABLE IF NOT EXISTS ${RUNS} (
40
- seq BIGSERIAL,
41
- id TEXT PRIMARY KEY,
42
- job TEXT NOT NULL,
43
- status TEXT NOT NULL,
44
- started_at BIGINT NOT NULL,
45
- finished_at BIGINT,
46
- duration_ms BIGINT,
47
- error TEXT,
48
- output TEXT,
49
- metrics JSONB NOT NULL DEFAULT '{}'::jsonb,
50
- trigger TEXT NOT NULL DEFAULT 'run'
51
- );
52
- CREATE INDEX IF NOT EXISTS ${RUNS}_job_started ON ${RUNS} (job, started_at DESC);
53
- CREATE INDEX IF NOT EXISTS ${RUNS}_running ON ${RUNS} (status) WHERE status = 'running';
54
- CREATE TABLE IF NOT EXISTS ${STATE} (
55
- job TEXT PRIMARY KEY,
56
- state JSONB NOT NULL
57
- );
58
- `);
153
+ await transaction(async (client) => {
154
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`cronwatch:${p}`]);
155
+ await client.query(schema("postgres", p));
156
+ });
59
157
  },
60
158
  async upsertJob(definition, now) {
61
- await pool.query(
62
- `INSERT INTO ${JOBS} (name, definition, created_at, updated_at) VALUES ($1, $2, $3, $3)
63
- ON CONFLICT (name) DO UPDATE SET definition = EXCLUDED.definition, updated_at = EXCLUDED.updated_at`,
64
- [definition.name, JSON.stringify(definition), now]
65
- );
159
+ await pool.query(sql.upsertJob, params.upsertJob(definition, now));
66
160
  },
67
161
  async getJob(name) {
68
- const { rows } = await pool.query(`SELECT * FROM ${JOBS} WHERE name = $1`, [name]);
69
- const r = rows[0];
70
- return r ? { name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) } : null;
162
+ const { rows } = await pool.query(sql.getJob, [name]);
163
+ return rows[0] ? rowToJob(rows[0]) : null;
71
164
  },
72
165
  async listJobs() {
73
- const { rows } = await pool.query(`SELECT * FROM ${JOBS} ORDER BY name`);
74
- return rows.map((r) => ({ name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) }));
166
+ const { rows } = await pool.query(sql.listJobs);
167
+ return rows.map(rowToJob);
75
168
  },
76
169
  async deleteJob(name) {
77
- const client = await pool.connect();
78
- try {
79
- await client.query("BEGIN");
80
- await client.query(`DELETE FROM ${RUNS} WHERE job = $1`, [name]);
81
- await client.query(`DELETE FROM ${STATE} WHERE job = $1`, [name]);
82
- await client.query(`DELETE FROM ${JOBS} WHERE name = $1`, [name]);
83
- await client.query("COMMIT");
84
- } catch (e) {
85
- await client.query("ROLLBACK");
86
- throw e;
87
- } finally {
88
- client.release();
89
- }
170
+ await transaction(async (client) => {
171
+ await client.query(sql.deleteRuns, [name]);
172
+ await client.query(sql.deleteState, [name]);
173
+ await client.query(sql.deleteJob, [name]);
174
+ });
90
175
  },
91
176
  async insertRun(run) {
92
- await pool.query(
93
- `INSERT INTO ${RUNS} (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
94
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
95
- [run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger]
96
- );
177
+ await pool.query(sql.insertRun, params.insertRun(run));
97
178
  },
98
179
  async updateRun(run) {
99
- await pool.query(
100
- `UPDATE ${RUNS} SET status = $1, finished_at = $2, duration_ms = $3, error = $4, output = $5, metrics = $6 WHERE id = $7`,
101
- [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id]
102
- );
180
+ await pool.query(sql.updateRun, params.updateRun(run));
103
181
  },
104
182
  async getRun(id) {
105
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE id = $1`, [id]);
183
+ const { rows } = await pool.query(sql.getRun, [id]);
106
184
  return rows[0] ? rowToRun(rows[0]) : null;
107
185
  },
108
186
  async listRuns(job, limit) {
109
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT $2`, [job, limit]);
187
+ const { rows } = await pool.query(sql.listRuns, [job, limit]);
110
188
  return rows.map(rowToRun);
111
189
  },
112
190
  async lastRun(job) {
113
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT 1`, [job]);
191
+ const { rows } = await pool.query(sql.listRuns, [job, 1]);
114
192
  return rows[0] ? rowToRun(rows[0]) : null;
115
193
  },
116
194
  async runningRuns() {
117
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE status = 'running' ORDER BY started_at`);
195
+ const { rows } = await pool.query(sql.runningRuns);
118
196
  return rows.map(rowToRun);
119
197
  },
120
198
  async getState(job) {
121
- const { rows } = await pool.query(`SELECT state FROM ${STATE} WHERE job = $1`, [job]);
122
- return rows[0]?.state ?? null;
199
+ const { rows } = await pool.query(sql.getState, [job]);
200
+ return rows[0] ? rowToState(rows[0]) : null;
123
201
  },
124
202
  async setState(state) {
125
- await pool.query(
126
- `INSERT INTO ${STATE} (job, state) VALUES ($1, $2) ON CONFLICT (job) DO UPDATE SET state = EXCLUDED.state`,
127
- [state.job, JSON.stringify(state)]
128
- );
203
+ await pool.query(sql.setState, params.setState(state));
204
+ },
205
+ async compareAndSetState(state, expectedVersion) {
206
+ const result = expectedVersion === 0 ? await pool.query(sql.casInsert, params.casInsert(state)) : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));
207
+ return (result.rowCount ?? 0) > 0;
129
208
  },
130
209
  async prune(before) {
131
- const result = await pool.query(`DELETE FROM ${RUNS} WHERE status <> 'running' AND started_at < $1`, [before]);
210
+ const result = await pool.query(sql.prune, [before]);
132
211
  return result.rowCount ?? 0;
133
212
  },
134
213
  async close() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stores/postgres.ts"],"names":["pg"],"mappings":"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","file":"postgres.cjs","sourcesContent":["import pg from \"pg\";\nimport type { JobState, Run, Store, StoredJob, StoredJobDefinition } from \"../types.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix. Default \"cronwatch_\". */\n prefix?: string;\n}\n\ninterface JobRow { name: string; definition: StoredJobDefinition; created_at: string | number; updated_at: string | number }\ninterface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: string | number; finished_at: string | number | null;\n duration_ms: string | number | null; error: string | null; output: string | null; metrics: Record<string, number>; trigger: string;\n}\n\nconst num = (v: string | number | null): number | null => (v === null ? null : Number(v));\n\nfunction rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics ?? {}, trigger: r.trigger,\n };\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n const p = (options.prefix ?? \"cronwatch_\").replace(/[^a-z0-9_]/gi, \"\");\n const JOBS = `${p}jobs`, RUNS = `${p}runs`, STATE = `${p}state`;\n\n return {\n async init() {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS ${JOBS} (\n name TEXT PRIMARY KEY,\n definition JSONB NOT NULL,\n created_at BIGINT NOT NULL,\n updated_at BIGINT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${RUNS} (\n seq BIGSERIAL,\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at BIGINT NOT NULL,\n finished_at BIGINT,\n duration_ms BIGINT,\n error TEXT,\n output TEXT,\n metrics JSONB NOT NULL DEFAULT '{}'::jsonb,\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${RUNS}_job_started ON ${RUNS} (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${RUNS}_running ON ${RUNS} (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${STATE} (\n job TEXT PRIMARY KEY,\n state JSONB NOT NULL\n );\n `);\n },\n async upsertJob(definition, now) {\n await pool.query(\n `INSERT INTO ${JOBS} (name, definition, created_at, updated_at) VALUES ($1, $2, $3, $3)\n ON CONFLICT (name) DO UPDATE SET definition = EXCLUDED.definition, updated_at = EXCLUDED.updated_at`,\n [definition.name, JSON.stringify(definition), now],\n );\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(`SELECT * FROM ${JOBS} WHERE name = $1`, [name]);\n const r = rows[0];\n return r ? { name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) } : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(`SELECT * FROM ${JOBS} ORDER BY name`);\n return rows.map((r) => ({ name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) }));\n },\n async deleteJob(name) {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(`DELETE FROM ${RUNS} WHERE job = $1`, [name]);\n await client.query(`DELETE FROM ${STATE} WHERE job = $1`, [name]);\n await client.query(`DELETE FROM ${JOBS} WHERE name = $1`, [name]);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n },\n async insertRun(run) {\n await pool.query(\n `INSERT INTO ${RUNS} (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,\n [run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger],\n );\n },\n async updateRun(run) {\n await pool.query(\n `UPDATE ${RUNS} SET status = $1, finished_at = $2, duration_ms = $3, error = $4, output = $5, metrics = $6 WHERE id = $7`,\n [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n );\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE id = $1`, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT $2`, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT 1`, [job]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(`SELECT * FROM ${RUNS} WHERE status = 'running' ORDER BY started_at`);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<{ state: JobState }>(`SELECT state FROM ${STATE} WHERE job = $1`, [job]);\n return rows[0]?.state ?? null;\n },\n async setState(state) {\n await pool.query(\n `INSERT INTO ${STATE} (job, state) VALUES ($1, $2) ON CONFLICT (job) DO UPDATE SET state = EXCLUDED.state`,\n [state.job, JSON.stringify(state)],\n );\n },\n async prune(before) {\n const result = await pool.query(`DELETE FROM ${RUNS} WHERE status <> 'running' AND started_at < $1`, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/stores/sql.ts","../src/stores/postgres.ts"],"names":["json","pg"],"mappings":"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","file":"postgres.cjs","sourcesContent":["import type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, 0 when it has none.\n const version = (column: string) => (pg ? `COALESCE((${column}->>'version')::bigint, 0)` : `COALESCE(json_extract(${column}, '$.version'), 0)`);\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, JSON.stringify(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger,\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],\n setState: (state: JobState) => [state.job, JSON.stringify(state)],\n casInsert: (state: JobState) => [state.job, JSON.stringify(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [JSON.stringify(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\n}\n","import pg from \"pg\";\nimport type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface PostgresOptions {\n /** postgres://... Defaults to process.env.DATABASE_URL. */\n connectionString?: string;\n /** Bring your own pool instead. It is not closed by close(). */\n pool?: pg.Pool;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in Postgres through the `pg` driver. For apps on Vercel,\n * Neon, Supabase, Railway and the like, where there is no disk to keep a\n * SQLite file on. Times are stored as BIGINT epoch milliseconds.\n */\nexport function postgres(options: PostgresOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"postgres\", p);\n const owned = !options.pool;\n const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });\n\n const transaction = async (work: (client: pg.PoolClient) => Promise<void>) => {\n const client = await pool.connect();\n try {\n await client.query(\"BEGIN\");\n await work(client);\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n };\n\n return {\n async init() {\n // Many instances starting at once would race CREATE TABLE IF NOT EXISTS, which Postgres can\n // reject with a unique violation on pg_type. A lock per prefix makes them take turns.\n await transaction(async (client) => {\n await client.query(\"SELECT pg_advisory_xact_lock(hashtext($1))\", [`cronwatch:${p}`]);\n await client.query(schema(\"postgres\", p));\n });\n },\n async upsertJob(definition, now) {\n await pool.query(sql.upsertJob, params.upsertJob(definition, now));\n },\n async getJob(name) {\n const { rows } = await pool.query<JobRow>(sql.getJob, [name]);\n return rows[0] ? rowToJob(rows[0]) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const { rows } = await pool.query<JobRow>(sql.listJobs);\n return rows.map(rowToJob);\n },\n async deleteJob(name) {\n await transaction(async (client) => {\n await client.query(sql.deleteRuns, [name]);\n await client.query(sql.deleteState, [name]);\n await client.query(sql.deleteJob, [name]);\n });\n },\n async insertRun(run) {\n await pool.query(sql.insertRun, params.insertRun(run));\n },\n async updateRun(run) {\n await pool.query(sql.updateRun, params.updateRun(run));\n },\n async getRun(id) {\n const { rows } = await pool.query<RunRow>(sql.getRun, [id]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async listRuns(job, limit) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, limit]);\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const { rows } = await pool.query<RunRow>(sql.listRuns, [job, 1]);\n return rows[0] ? rowToRun(rows[0]) : null;\n },\n async runningRuns() {\n const { rows } = await pool.query<RunRow>(sql.runningRuns);\n return rows.map(rowToRun);\n },\n async getState(job) {\n const { rows } = await pool.query<StateRow>(sql.getState, [job]);\n return rows[0] ? rowToState(rows[0]) : null;\n },\n async setState(state) {\n await pool.query(sql.setState, params.setState(state));\n },\n async compareAndSetState(state, expectedVersion) {\n const result = expectedVersion === 0\n ? await pool.query(sql.casInsert, params.casInsert(state))\n : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return (result.rowCount ?? 0) > 0;\n },\n async prune(before) {\n const result = await pool.query(sql.prune, [before]);\n return result.rowCount ?? 0;\n },\n async close() {\n if (owned) await pool.end();\n },\n };\n}\n"]}
@@ -1,12 +1,12 @@
1
1
  import pg from 'pg';
2
- import { S as Store } from './types-BQ1P8z55.cjs';
2
+ import { S as Store } from './types-DUt2apl0.cjs';
3
3
 
4
4
  interface PostgresOptions {
5
5
  /** postgres://... Defaults to process.env.DATABASE_URL. */
6
6
  connectionString?: string;
7
7
  /** Bring your own pool instead. It is not closed by close(). */
8
8
  pool?: pg.Pool;
9
- /** Table name prefix. Default "cronwatch_". */
9
+ /** Table name prefix: lowercase letters, digits and underscores. Default "cronwatch_". */
10
10
  prefix?: string;
11
11
  }
12
12
  /**
@@ -1,12 +1,12 @@
1
1
  import pg from 'pg';
2
- import { S as Store } from './types-BQ1P8z55.js';
2
+ import { S as Store } from './types-DUt2apl0.js';
3
3
 
4
4
  interface PostgresOptions {
5
5
  /** postgres://... Defaults to process.env.DATABASE_URL. */
6
6
  connectionString?: string;
7
7
  /** Bring your own pool instead. It is not closed by close(). */
8
8
  pool?: pg.Pool;
9
- /** Table name prefix. Default "cronwatch_". */
9
+ /** Table name prefix: lowercase letters, digits and underscores. Default "cronwatch_". */
10
10
  prefix?: string;
11
11
  }
12
12
  /**
package/dist/postgres.js CHANGED
@@ -1,7 +1,110 @@
1
1
  import pg from 'pg';
2
2
 
3
3
  // src/stores/postgres.ts
4
+
5
+ // src/stores/sql.ts
6
+ var DEFAULT_PREFIX = "cronwatch_";
7
+ var MAX_PREFIX = 63 - "runs_job_started".length;
8
+ function tablePrefix(prefix = DEFAULT_PREFIX) {
9
+ if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {
10
+ throw new Error(
11
+ `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, not starting with a digit, at most ${MAX_PREFIX} characters.`
12
+ );
13
+ }
14
+ return prefix;
15
+ }
16
+ function schema(dialect, p) {
17
+ const int = "BIGINT" ;
18
+ const json2 = "JSONB" ;
19
+ return `
20
+ CREATE TABLE IF NOT EXISTS ${p}jobs (
21
+ name TEXT PRIMARY KEY,
22
+ definition ${json2} NOT NULL,
23
+ created_at ${int} NOT NULL,
24
+ updated_at ${int} NOT NULL
25
+ );
26
+ CREATE TABLE IF NOT EXISTS ${p}runs (${"\n seq BIGSERIAL," }
27
+ id TEXT PRIMARY KEY,
28
+ job TEXT NOT NULL,
29
+ status TEXT NOT NULL,
30
+ started_at ${int} NOT NULL,
31
+ finished_at ${int},
32
+ duration_ms ${int},
33
+ error TEXT,
34
+ output TEXT,
35
+ metrics ${json2} NOT NULL DEFAULT '{}',
36
+ trigger TEXT NOT NULL DEFAULT 'run'
37
+ );
38
+ CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);
39
+ CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';
40
+ CREATE TABLE IF NOT EXISTS ${p}state (
41
+ job TEXT PRIMARY KEY,
42
+ state ${json2} NOT NULL
43
+ );
44
+ `;
45
+ }
46
+ function statements(dialect, p) {
47
+ const seq = "seq" ;
48
+ const byName = `name COLLATE "C"` ;
49
+ const version = (column) => `COALESCE((${column}->>'version')::bigint, 0)` ;
50
+ const sql = {
51
+ upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
52
+ ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
53
+ getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,
54
+ listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,
55
+ deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,
56
+ deleteState: `DELETE FROM ${p}state WHERE job = ?`,
57
+ deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,
58
+ insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
60
+ updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,
61
+ getRun: `SELECT * FROM ${p}runs WHERE id = ?`,
62
+ listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,
63
+ runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
64
+ getState: `SELECT state FROM ${p}state WHERE job = ?`,
65
+ setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,
66
+ // compareAndSetState. Expecting version 0 also matches a missing row, so
67
+ // that case inserts; any other version must find its row.
68
+ casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)
69
+ ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,
70
+ casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version("state")} = ?`,
71
+ // Each job's newest run is kept whatever its age: without it, a job that
72
+ // runs less often than the retention looks like it never ran.
73
+ prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
74
+ AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`
75
+ };
76
+ {
77
+ for (const key of Object.keys(sql)) {
78
+ let n = 0;
79
+ sql[key] = sql[key].replace(/\?/g, () => `$${++n}`);
80
+ }
81
+ }
82
+ return sql;
83
+ }
84
+ var params = {
85
+ upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
86
+ insertRun: (run) => [
87
+ run.id,
88
+ run.job,
89
+ run.status,
90
+ run.startedAt,
91
+ run.finishedAt,
92
+ run.durationMs,
93
+ run.error,
94
+ run.output,
95
+ JSON.stringify(run.metrics),
96
+ run.trigger
97
+ ],
98
+ updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
99
+ setState: (state) => [state.job, JSON.stringify(state)],
100
+ casInsert: (state) => [state.job, JSON.stringify(state)],
101
+ casUpdate: (state, expectedVersion) => [JSON.stringify(state), state.job, expectedVersion]
102
+ };
103
+ var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
4
104
  var num = (v) => v === null ? null : Number(v);
105
+ function rowToJob(r) {
106
+ return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
107
+ }
5
108
  function rowToRun(r) {
6
109
  return {
7
110
  id: r.id,
@@ -12,117 +115,93 @@ function rowToRun(r) {
12
115
  durationMs: num(r.duration_ms),
13
116
  error: r.error,
14
117
  output: r.output,
15
- metrics: r.metrics ?? {},
118
+ metrics: r.metrics === null ? {} : json(r.metrics),
16
119
  trigger: r.trigger
17
120
  };
18
121
  }
122
+ function rowToState(r) {
123
+ return json(r.state);
124
+ }
125
+
126
+ // src/stores/postgres.ts
19
127
  function postgres(options = {}) {
128
+ const p = tablePrefix(options.prefix);
129
+ const sql = statements("postgres", p);
20
130
  const owned = !options.pool;
21
131
  const pool = options.pool ?? new pg.Pool({ connectionString: options.connectionString ?? process.env.DATABASE_URL });
22
- const p = (options.prefix ?? "cronwatch_").replace(/[^a-z0-9_]/gi, "");
23
- const JOBS = `${p}jobs`, RUNS = `${p}runs`, STATE = `${p}state`;
132
+ const transaction = async (work) => {
133
+ const client = await pool.connect();
134
+ try {
135
+ await client.query("BEGIN");
136
+ await work(client);
137
+ await client.query("COMMIT");
138
+ } catch (e) {
139
+ await client.query("ROLLBACK");
140
+ throw e;
141
+ } finally {
142
+ client.release();
143
+ }
144
+ };
24
145
  return {
25
146
  async init() {
26
- await pool.query(`
27
- CREATE TABLE IF NOT EXISTS ${JOBS} (
28
- name TEXT PRIMARY KEY,
29
- definition JSONB NOT NULL,
30
- created_at BIGINT NOT NULL,
31
- updated_at BIGINT NOT NULL
32
- );
33
- CREATE TABLE IF NOT EXISTS ${RUNS} (
34
- seq BIGSERIAL,
35
- id TEXT PRIMARY KEY,
36
- job TEXT NOT NULL,
37
- status TEXT NOT NULL,
38
- started_at BIGINT NOT NULL,
39
- finished_at BIGINT,
40
- duration_ms BIGINT,
41
- error TEXT,
42
- output TEXT,
43
- metrics JSONB NOT NULL DEFAULT '{}'::jsonb,
44
- trigger TEXT NOT NULL DEFAULT 'run'
45
- );
46
- CREATE INDEX IF NOT EXISTS ${RUNS}_job_started ON ${RUNS} (job, started_at DESC);
47
- CREATE INDEX IF NOT EXISTS ${RUNS}_running ON ${RUNS} (status) WHERE status = 'running';
48
- CREATE TABLE IF NOT EXISTS ${STATE} (
49
- job TEXT PRIMARY KEY,
50
- state JSONB NOT NULL
51
- );
52
- `);
147
+ await transaction(async (client) => {
148
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`cronwatch:${p}`]);
149
+ await client.query(schema("postgres", p));
150
+ });
53
151
  },
54
152
  async upsertJob(definition, now) {
55
- await pool.query(
56
- `INSERT INTO ${JOBS} (name, definition, created_at, updated_at) VALUES ($1, $2, $3, $3)
57
- ON CONFLICT (name) DO UPDATE SET definition = EXCLUDED.definition, updated_at = EXCLUDED.updated_at`,
58
- [definition.name, JSON.stringify(definition), now]
59
- );
153
+ await pool.query(sql.upsertJob, params.upsertJob(definition, now));
60
154
  },
61
155
  async getJob(name) {
62
- const { rows } = await pool.query(`SELECT * FROM ${JOBS} WHERE name = $1`, [name]);
63
- const r = rows[0];
64
- return r ? { name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) } : null;
156
+ const { rows } = await pool.query(sql.getJob, [name]);
157
+ return rows[0] ? rowToJob(rows[0]) : null;
65
158
  },
66
159
  async listJobs() {
67
- const { rows } = await pool.query(`SELECT * FROM ${JOBS} ORDER BY name`);
68
- return rows.map((r) => ({ name: r.name, definition: r.definition, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) }));
160
+ const { rows } = await pool.query(sql.listJobs);
161
+ return rows.map(rowToJob);
69
162
  },
70
163
  async deleteJob(name) {
71
- const client = await pool.connect();
72
- try {
73
- await client.query("BEGIN");
74
- await client.query(`DELETE FROM ${RUNS} WHERE job = $1`, [name]);
75
- await client.query(`DELETE FROM ${STATE} WHERE job = $1`, [name]);
76
- await client.query(`DELETE FROM ${JOBS} WHERE name = $1`, [name]);
77
- await client.query("COMMIT");
78
- } catch (e) {
79
- await client.query("ROLLBACK");
80
- throw e;
81
- } finally {
82
- client.release();
83
- }
164
+ await transaction(async (client) => {
165
+ await client.query(sql.deleteRuns, [name]);
166
+ await client.query(sql.deleteState, [name]);
167
+ await client.query(sql.deleteJob, [name]);
168
+ });
84
169
  },
85
170
  async insertRun(run) {
86
- await pool.query(
87
- `INSERT INTO ${RUNS} (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
88
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
89
- [run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger]
90
- );
171
+ await pool.query(sql.insertRun, params.insertRun(run));
91
172
  },
92
173
  async updateRun(run) {
93
- await pool.query(
94
- `UPDATE ${RUNS} SET status = $1, finished_at = $2, duration_ms = $3, error = $4, output = $5, metrics = $6 WHERE id = $7`,
95
- [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id]
96
- );
174
+ await pool.query(sql.updateRun, params.updateRun(run));
97
175
  },
98
176
  async getRun(id) {
99
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE id = $1`, [id]);
177
+ const { rows } = await pool.query(sql.getRun, [id]);
100
178
  return rows[0] ? rowToRun(rows[0]) : null;
101
179
  },
102
180
  async listRuns(job, limit) {
103
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT $2`, [job, limit]);
181
+ const { rows } = await pool.query(sql.listRuns, [job, limit]);
104
182
  return rows.map(rowToRun);
105
183
  },
106
184
  async lastRun(job) {
107
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE job = $1 ORDER BY started_at DESC, seq DESC LIMIT 1`, [job]);
185
+ const { rows } = await pool.query(sql.listRuns, [job, 1]);
108
186
  return rows[0] ? rowToRun(rows[0]) : null;
109
187
  },
110
188
  async runningRuns() {
111
- const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE status = 'running' ORDER BY started_at`);
189
+ const { rows } = await pool.query(sql.runningRuns);
112
190
  return rows.map(rowToRun);
113
191
  },
114
192
  async getState(job) {
115
- const { rows } = await pool.query(`SELECT state FROM ${STATE} WHERE job = $1`, [job]);
116
- return rows[0]?.state ?? null;
193
+ const { rows } = await pool.query(sql.getState, [job]);
194
+ return rows[0] ? rowToState(rows[0]) : null;
117
195
  },
118
196
  async setState(state) {
119
- await pool.query(
120
- `INSERT INTO ${STATE} (job, state) VALUES ($1, $2) ON CONFLICT (job) DO UPDATE SET state = EXCLUDED.state`,
121
- [state.job, JSON.stringify(state)]
122
- );
197
+ await pool.query(sql.setState, params.setState(state));
198
+ },
199
+ async compareAndSetState(state, expectedVersion) {
200
+ const result = expectedVersion === 0 ? await pool.query(sql.casInsert, params.casInsert(state)) : await pool.query(sql.casUpdate, params.casUpdate(state, expectedVersion));
201
+ return (result.rowCount ?? 0) > 0;
123
202
  },
124
203
  async prune(before) {
125
- const result = await pool.query(`DELETE FROM ${RUNS} WHERE status <> 'running' AND started_at < $1`, [before]);
204
+ const result = await pool.query(sql.prune, [before]);
126
205
  return result.rowCount ?? 0;
127
206
  },
128
207
  async close() {