@cronwatch/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -12
- package/dist/anthropic.cjs +19 -5
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +19 -5
- package/dist/anthropic.js.map +1 -1
- package/dist/discord.cjs +14 -2
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +6 -2
- package/dist/discord.d.ts +6 -2
- package/dist/discord.js +13 -3
- package/dist/discord.js.map +1 -1
- package/dist/index.cjs +676 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -44
- package/dist/index.d.ts +100 -44
- package/dist/index.js +677 -283
- package/dist/index.js.map +1 -1
- package/dist/postgres.cjs +140 -73
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +2 -2
- package/dist/postgres.d.ts +2 -2
- package/dist/postgres.js +140 -73
- package/dist/postgres.js.map +1 -1
- package/dist/slack.cjs +12 -6
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +12 -6
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +142 -71
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +3 -1
- package/dist/sqlite.d.ts +3 -1
- package/dist/sqlite.js +143 -72
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-BQ1P8z55.d.cts → types-C1PyRjI5.d.cts} +63 -5
- package/dist/{types-BQ1P8z55.d.ts → types-C1PyRjI5.d.ts} +63 -5
- package/dist/webhook.cjs +10 -2
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +10 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +95 -32
package/dist/sqlite.js
CHANGED
|
@@ -1,136 +1,207 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import { mkdirSync, chmodSync } from 'fs';
|
|
2
|
+
import { mkdirSync, closeSync, openSync, chmodSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
// src/stores/sqlite.ts
|
|
6
|
+
|
|
7
|
+
// src/stores/sql.ts
|
|
8
|
+
var DEFAULT_PREFIX = "cronwatch_";
|
|
9
|
+
var MAX_PREFIX = 63 - "runs_job_started".length;
|
|
10
|
+
function tablePrefix(prefix = DEFAULT_PREFIX) {
|
|
11
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits and underscores, not starting with a digit, at most ${MAX_PREFIX} characters.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return prefix;
|
|
17
|
+
}
|
|
18
|
+
function schema(dialect, p) {
|
|
19
|
+
const int = "INTEGER";
|
|
20
|
+
const json2 = "TEXT";
|
|
21
|
+
return `
|
|
22
|
+
CREATE TABLE IF NOT EXISTS ${p}jobs (
|
|
23
|
+
name TEXT PRIMARY KEY,
|
|
24
|
+
definition ${json2} NOT NULL,
|
|
25
|
+
created_at ${int} NOT NULL,
|
|
26
|
+
updated_at ${int} NOT NULL
|
|
27
|
+
);
|
|
28
|
+
CREATE TABLE IF NOT EXISTS ${p}runs (${""}
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
job TEXT NOT NULL,
|
|
31
|
+
status TEXT NOT NULL,
|
|
32
|
+
started_at ${int} NOT NULL,
|
|
33
|
+
finished_at ${int},
|
|
34
|
+
duration_ms ${int},
|
|
35
|
+
error TEXT,
|
|
36
|
+
output TEXT,
|
|
37
|
+
metrics ${json2} NOT NULL DEFAULT '{}',
|
|
38
|
+
trigger TEXT NOT NULL DEFAULT 'run'
|
|
39
|
+
);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';
|
|
42
|
+
CREATE TABLE IF NOT EXISTS ${p}state (
|
|
43
|
+
job TEXT PRIMARY KEY,
|
|
44
|
+
state ${json2} NOT NULL
|
|
45
|
+
);
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
function statements(dialect, p) {
|
|
49
|
+
const seq = "rowid";
|
|
50
|
+
const byName = "name";
|
|
51
|
+
const sql = {
|
|
52
|
+
upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
53
|
+
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,
|
|
54
|
+
getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,
|
|
55
|
+
listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,
|
|
56
|
+
deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,
|
|
57
|
+
deleteState: `DELETE FROM ${p}state WHERE job = ?`,
|
|
58
|
+
deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,
|
|
59
|
+
insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
61
|
+
updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,
|
|
62
|
+
getRun: `SELECT * FROM ${p}runs WHERE id = ?`,
|
|
63
|
+
listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,
|
|
64
|
+
runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,
|
|
65
|
+
getState: `SELECT state FROM ${p}state WHERE job = ?`,
|
|
66
|
+
setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,
|
|
67
|
+
// Each job's newest run is kept whatever its age: without it, a job that
|
|
68
|
+
// runs less often than the retention looks like it never ran.
|
|
69
|
+
prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?
|
|
70
|
+
AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`
|
|
71
|
+
};
|
|
72
|
+
return sql;
|
|
73
|
+
}
|
|
74
|
+
var params = {
|
|
75
|
+
upsertJob: (definition, now) => [definition.name, JSON.stringify(definition), now, now],
|
|
76
|
+
insertRun: (run) => [
|
|
77
|
+
run.id,
|
|
78
|
+
run.job,
|
|
79
|
+
run.status,
|
|
80
|
+
run.startedAt,
|
|
81
|
+
run.finishedAt,
|
|
82
|
+
run.durationMs,
|
|
83
|
+
run.error,
|
|
84
|
+
run.output,
|
|
85
|
+
JSON.stringify(run.metrics),
|
|
86
|
+
run.trigger
|
|
87
|
+
],
|
|
88
|
+
updateRun: (run) => [run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id],
|
|
89
|
+
setState: (state) => [state.job, JSON.stringify(state)]
|
|
90
|
+
};
|
|
91
|
+
var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
|
|
92
|
+
var num = (v) => v === null ? null : Number(v);
|
|
93
|
+
function rowToJob(r) {
|
|
94
|
+
return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
|
|
95
|
+
}
|
|
6
96
|
function rowToRun(r) {
|
|
7
97
|
return {
|
|
8
98
|
id: r.id,
|
|
9
99
|
job: r.job,
|
|
10
100
|
status: r.status,
|
|
11
|
-
startedAt: r.started_at,
|
|
12
|
-
finishedAt: r.finished_at,
|
|
13
|
-
durationMs: r.duration_ms,
|
|
101
|
+
startedAt: Number(r.started_at),
|
|
102
|
+
finishedAt: num(r.finished_at),
|
|
103
|
+
durationMs: num(r.duration_ms),
|
|
14
104
|
error: r.error,
|
|
15
105
|
output: r.output,
|
|
16
|
-
metrics:
|
|
106
|
+
metrics: r.metrics === null ? {} : json(r.metrics),
|
|
17
107
|
trigger: r.trigger
|
|
18
108
|
};
|
|
19
109
|
}
|
|
110
|
+
function rowToState(r) {
|
|
111
|
+
return json(r.state);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/stores/sqlite.ts
|
|
20
115
|
function sqlite(options = {}) {
|
|
116
|
+
const p = tablePrefix(options.prefix);
|
|
117
|
+
const sql = statements("sqlite", p);
|
|
21
118
|
let db = options.database ?? null;
|
|
22
119
|
const file = options.path ?? "./data/cronwatch.db";
|
|
120
|
+
const cache = /* @__PURE__ */ new Map();
|
|
23
121
|
const open = () => {
|
|
24
122
|
if (db) return db;
|
|
25
|
-
|
|
123
|
+
const onDisk = file !== ":memory:" && file !== "";
|
|
124
|
+
if (onDisk) {
|
|
26
125
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
db = new Database(file);
|
|
29
|
-
if (file !== ":memory:") {
|
|
30
126
|
try {
|
|
31
|
-
|
|
127
|
+
closeSync(openSync(file, "a", 384));
|
|
128
|
+
for (const f of [file, `${file}-wal`, `${file}-shm`]) {
|
|
129
|
+
try {
|
|
130
|
+
chmodSync(f, 384);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
32
134
|
} catch {
|
|
33
135
|
}
|
|
34
136
|
}
|
|
137
|
+
db = new Database(file);
|
|
35
138
|
db.pragma("journal_mode = WAL");
|
|
36
139
|
db.pragma("busy_timeout = 5000");
|
|
37
140
|
db.pragma("synchronous = NORMAL");
|
|
38
141
|
return db;
|
|
39
142
|
};
|
|
143
|
+
const stmt = (text) => {
|
|
144
|
+
let s = cache.get(text);
|
|
145
|
+
if (!s) {
|
|
146
|
+
s = open().prepare(text);
|
|
147
|
+
cache.set(text, s);
|
|
148
|
+
}
|
|
149
|
+
return s;
|
|
150
|
+
};
|
|
151
|
+
const forget = (name) => {
|
|
152
|
+
stmt(sql.deleteRuns).run(name);
|
|
153
|
+
stmt(sql.deleteState).run(name);
|
|
154
|
+
stmt(sql.deleteJob).run(name);
|
|
155
|
+
};
|
|
40
156
|
return {
|
|
41
157
|
async init() {
|
|
42
|
-
|
|
43
|
-
d.exec(`
|
|
44
|
-
CREATE TABLE IF NOT EXISTS cronwatch_jobs (
|
|
45
|
-
name TEXT PRIMARY KEY,
|
|
46
|
-
definition TEXT NOT NULL,
|
|
47
|
-
created_at INTEGER NOT NULL,
|
|
48
|
-
updated_at INTEGER NOT NULL
|
|
49
|
-
);
|
|
50
|
-
CREATE TABLE IF NOT EXISTS cronwatch_runs (
|
|
51
|
-
id TEXT PRIMARY KEY,
|
|
52
|
-
job TEXT NOT NULL,
|
|
53
|
-
status TEXT NOT NULL,
|
|
54
|
-
started_at INTEGER NOT NULL,
|
|
55
|
-
finished_at INTEGER,
|
|
56
|
-
duration_ms INTEGER,
|
|
57
|
-
error TEXT,
|
|
58
|
-
output TEXT,
|
|
59
|
-
metrics TEXT NOT NULL DEFAULT '{}',
|
|
60
|
-
trigger TEXT NOT NULL DEFAULT 'run'
|
|
61
|
-
);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS cronwatch_runs_job_started ON cronwatch_runs (job, started_at DESC);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS cronwatch_runs_running ON cronwatch_runs (status) WHERE status = 'running';
|
|
64
|
-
CREATE TABLE IF NOT EXISTS cronwatch_state (
|
|
65
|
-
job TEXT PRIMARY KEY,
|
|
66
|
-
state TEXT NOT NULL
|
|
67
|
-
);
|
|
68
|
-
`);
|
|
158
|
+
open().exec(schema("sqlite", p));
|
|
69
159
|
},
|
|
70
160
|
async upsertJob(definition, now) {
|
|
71
|
-
|
|
72
|
-
INSERT INTO cronwatch_jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
73
|
-
ON CONFLICT(name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at
|
|
74
|
-
`).run(definition.name, JSON.stringify(definition), now, now);
|
|
161
|
+
stmt(sql.upsertJob).run(...params.upsertJob(definition, now));
|
|
75
162
|
},
|
|
76
163
|
async getJob(name) {
|
|
77
|
-
const r =
|
|
78
|
-
return r ?
|
|
164
|
+
const r = stmt(sql.getJob).get(name);
|
|
165
|
+
return r ? rowToJob(r) : null;
|
|
79
166
|
},
|
|
80
167
|
async listJobs() {
|
|
81
|
-
|
|
82
|
-
return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));
|
|
168
|
+
return stmt(sql.listJobs).all().map(rowToJob);
|
|
83
169
|
},
|
|
84
170
|
async deleteJob(name) {
|
|
85
|
-
|
|
86
|
-
d.transaction(() => {
|
|
87
|
-
d.prepare("DELETE FROM cronwatch_runs WHERE job = ?").run(name);
|
|
88
|
-
d.prepare("DELETE FROM cronwatch_state WHERE job = ?").run(name);
|
|
89
|
-
d.prepare("DELETE FROM cronwatch_jobs WHERE name = ?").run(name);
|
|
90
|
-
})();
|
|
171
|
+
open().transaction(forget)(name);
|
|
91
172
|
},
|
|
92
173
|
async insertRun(run) {
|
|
93
|
-
|
|
94
|
-
INSERT INTO cronwatch_runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
|
|
95
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
-
`).run(run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger);
|
|
174
|
+
stmt(sql.insertRun).run(...params.insertRun(run));
|
|
97
175
|
},
|
|
98
176
|
async updateRun(run) {
|
|
99
|
-
|
|
100
|
-
UPDATE cronwatch_runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?
|
|
101
|
-
`).run(run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id);
|
|
177
|
+
stmt(sql.updateRun).run(...params.updateRun(run));
|
|
102
178
|
},
|
|
103
179
|
async getRun(id) {
|
|
104
|
-
const r =
|
|
180
|
+
const r = stmt(sql.getRun).get(id);
|
|
105
181
|
return r ? rowToRun(r) : null;
|
|
106
182
|
},
|
|
107
183
|
async listRuns(job, limit) {
|
|
108
|
-
|
|
109
|
-
return rows.map(rowToRun);
|
|
184
|
+
return stmt(sql.listRuns).all(job, limit).map(rowToRun);
|
|
110
185
|
},
|
|
111
186
|
async lastRun(job) {
|
|
112
|
-
const r =
|
|
187
|
+
const r = stmt(sql.listRuns).get(job, 1);
|
|
113
188
|
return r ? rowToRun(r) : null;
|
|
114
189
|
},
|
|
115
190
|
async runningRuns() {
|
|
116
|
-
|
|
117
|
-
return rows.map(rowToRun);
|
|
191
|
+
return stmt(sql.runningRuns).all().map(rowToRun);
|
|
118
192
|
},
|
|
119
193
|
async getState(job) {
|
|
120
|
-
const r =
|
|
121
|
-
return r ?
|
|
194
|
+
const r = stmt(sql.getState).get(job);
|
|
195
|
+
return r ? rowToState(r) : null;
|
|
122
196
|
},
|
|
123
197
|
async setState(state) {
|
|
124
|
-
|
|
125
|
-
INSERT INTO cronwatch_state (job, state) VALUES (?, ?)
|
|
126
|
-
ON CONFLICT(job) DO UPDATE SET state = excluded.state
|
|
127
|
-
`).run(state.job, JSON.stringify(state));
|
|
198
|
+
stmt(sql.setState).run(...params.setState(state));
|
|
128
199
|
},
|
|
129
200
|
async prune(before) {
|
|
130
|
-
|
|
131
|
-
return info.changes;
|
|
201
|
+
return stmt(sql.prune).run(before).changes;
|
|
132
202
|
},
|
|
133
203
|
async close() {
|
|
204
|
+
cache.clear();
|
|
134
205
|
if (db && !options.database) {
|
|
135
206
|
db.close();
|
|
136
207
|
db = null;
|
package/dist/sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/sqlite.ts"],"names":[],"mappings":"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","file":"sqlite.js","sourcesContent":["import Database from \"better-sqlite3\";\nimport { chmodSync, mkdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { JobState, Run, Store, StoredJob, StoredJobDefinition } from \"../types.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n}\n\ninterface JobRow { name: string; definition: string; created_at: number; updated_at: number }\ninterface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: number; finished_at: number | null;\n duration_ms: number | null; error: string | null; output: string | null; metrics: string; trigger: string;\n}\n\nfunction rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: r.started_at, finishedAt: r.finished_at,\n durationMs: r.duration_ms, error: r.error, output: r.output, metrics: JSON.parse(r.metrics), trigger: r.trigger,\n };\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n\n const open = (): Database.Database => {\n if (db) return db;\n if (file !== \":memory:\") {\n mkdirSync(path.dirname(file), { recursive: true });\n }\n db = new Database(file);\n if (file !== \":memory:\") {\n try { chmodSync(file, 0o600); } catch { /* best effort: not every filesystem cares */ }\n }\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n return db;\n };\n\n return {\n async init() {\n const d = open();\n d.exec(`\n CREATE TABLE IF NOT EXISTS cronwatch_jobs (\n name TEXT PRIMARY KEY,\n definition TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS cronwatch_runs (\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n finished_at INTEGER,\n duration_ms INTEGER,\n error TEXT,\n output TEXT,\n metrics TEXT NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS cronwatch_runs_job_started ON cronwatch_runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS cronwatch_runs_running ON cronwatch_runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS cronwatch_state (\n job TEXT PRIMARY KEY,\n state TEXT NOT NULL\n );\n `);\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n open().prepare(`\n INSERT INTO cronwatch_jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at\n `).run(definition.name, JSON.stringify(definition), now, now);\n },\n async getJob(name) {\n const r = open().prepare(\"SELECT * FROM cronwatch_jobs WHERE name = ?\").get(name) as JobRow | undefined;\n return r ? { name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at } : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n const rows = open().prepare(\"SELECT * FROM cronwatch_jobs ORDER BY name\").all() as JobRow[];\n return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));\n },\n async deleteJob(name) {\n const d = open();\n d.transaction(() => {\n d.prepare(\"DELETE FROM cronwatch_runs WHERE job = ?\").run(name);\n d.prepare(\"DELETE FROM cronwatch_state WHERE job = ?\").run(name);\n d.prepare(\"DELETE FROM cronwatch_jobs WHERE name = ?\").run(name);\n })();\n },\n async insertRun(run) {\n open().prepare(`\n INSERT INTO cronwatch_runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger);\n },\n async updateRun(run) {\n open().prepare(`\n UPDATE cronwatch_runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?\n `).run(run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id);\n },\n async getRun(id) {\n const r = open().prepare(\"SELECT * FROM cronwatch_runs WHERE id = ?\").get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n const rows = open().prepare(\"SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT ?\").all(job, limit) as RunRow[];\n return rows.map(rowToRun);\n },\n async lastRun(job) {\n const r = open().prepare(\"SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT 1\").get(job) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n const rows = open().prepare(\"SELECT * FROM cronwatch_runs WHERE status = 'running' ORDER BY started_at\").all() as RunRow[];\n return rows.map(rowToRun);\n },\n async getState(job) {\n const r = open().prepare(\"SELECT state FROM cronwatch_state WHERE job = ?\").get(job) as { state: string } | undefined;\n return r ? (JSON.parse(r.state) as JobState) : null;\n },\n async setState(state) {\n open().prepare(`\n INSERT INTO cronwatch_state (job, state) VALUES (?, ?)\n ON CONFLICT(job) DO UPDATE SET state = excluded.state\n `).run(state.job, JSON.stringify(state));\n },\n async prune(before) {\n const info = open().prepare(\"DELETE FROM cronwatch_runs WHERE status != 'running' AND started_at < ?\").run(before);\n return info.changes;\n },\n async close() {\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/stores/sql.ts","../src/stores/sqlite.ts"],"names":["json"],"mappings":";;;;;;;AAUO,IAAM,cAAA,GAAiB,YAAA;AAG9B,IAAM,UAAA,GAAa,KAAK,kBAAA,CAAmB,MAAA;AAOpC,SAAS,WAAA,CAAY,SAAiB,cAAA,EAAwB;AACnE,EAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,MAAM,CAAA,IAAK,MAAA,CAAO,SAAS,UAAA,EAAY;AACpE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,mCAAmC,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,uFACjB,UAAU,CAAA,YAAA;AAAA,KACpD;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,MAAA,CAAO,SAAkB,CAAA,EAAmB;AAE1D,EAAA,MAAM,GAAA,GAAsB,SAAA;AAC5B,EAAA,MAAMA,KAAAA,GAAsB,MAAA;AAC5B,EAAA,OAAO;AAAA,+BAAA,EACwB,CAAC,CAAA;AAAA;AAAA,iBAAA,EAEfA,KAAI,CAAA;AAAA,iBAAA,EACJ,GAAG,CAAA;AAAA,iBAAA,EACH,GAAG,CAAA;AAAA;AAAA,+BAAA,EAEW,CAAC,CAAA,MAAA,EAAyC,EAAE;AAAA;AAAA;AAAA;AAAA,iBAAA,EAI1D,GAAG,CAAA;AAAA,kBAAA,EACF,GAAG,CAAA;AAAA,kBAAA,EACH,GAAG,CAAA;AAAA;AAAA;AAAA,cAAA,EAGPA,KAAI,CAAA;AAAA;AAAA;AAAA,+BAAA,EAGa,CAAC,uBAAuB,CAAC,CAAA;AAAA,+BAAA,EACzB,CAAC,mBAAmB,CAAC,CAAA;AAAA,+BAAA,EACrB,CAAC,CAAA;AAAA;AAAA,YAAA,EAEpBA,KAAI,CAAA;AAAA;AAAA,EAAA,CAAA;AAGlB;AAEO,SAAS,UAAA,CAAW,SAAkB,CAAA,EAAW;AAGtD,EAAA,MAAM,GAAA,GAAmB,OAAA;AAEzB,EAAA,MAAM,MAAA,GAAmC,MAAA;AACzC,EAAA,MAAM,GAAA,GAAM;AAAA,IACV,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,yGAAA,CAAA;AAAA,IAE3B,MAAA,EAAQ,iBAAiB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,cAAA,EAAiB,MAAM,CAAA,CAAA;AAAA,IACnD,UAAA,EAAY,eAAe,CAAC,CAAA,kBAAA,CAAA;AAAA,IAC5B,WAAA,EAAa,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC7B,SAAA,EAAW,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC3B,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,2CAAA,CAAA;AAAA,IAE3B,SAAA,EAAW,UAAU,CAAC,CAAA,sGAAA,CAAA;AAAA,IACtB,MAAA,EAAQ,iBAAiB,CAAC,CAAA,iBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,6CAAA,EAAgD,GAAG,CAAA,aAAA,CAAA;AAAA,IAC/E,WAAA,EAAa,CAAA,cAAA,EAAiB,CAAC,CAAA,mDAAA,EAAsD,GAAG,CAAA,CAAA;AAAA,IACxF,QAAA,EAAU,qBAAqB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAChC,QAAA,EAAU,eAAe,CAAC,CAAA,uFAAA,CAAA;AAAA;AAAA;AAAA,IAG1B,KAAA,EAAO,eAAe,CAAC,CAAA;AAAA,sDAAA,EAC6B,CAAC,wBAAwB,CAAC,CAAA,SAAA;AAAA,GAChF;AAOA,EAAA,OAAO,GAAA;AACT;AAGO,IAAM,MAAA,GAAS;AAAA,EACpB,SAAA,EAAW,CAAC,UAAA,EAAiC,GAAA,KAAgB,CAAC,UAAA,CAAW,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,UAAU,CAAA,EAAG,GAAA,EAAK,GAAG,CAAA;AAAA,EACnH,SAAA,EAAW,CAAC,GAAA,KAAa;AAAA,IACvB,GAAA,CAAI,EAAA;AAAA,IAAI,GAAA,CAAI,GAAA;AAAA,IAAK,GAAA,CAAI,MAAA;AAAA,IAAQ,GAAA,CAAI,SAAA;AAAA,IAAW,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,KAAA;AAAA,IAAO,GAAA,CAAI,MAAA;AAAA,IAAQ,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,OAAO,CAAA;AAAA,IAAG,GAAA,CAAI;AAAA,GACtI;AAAA,EACA,SAAA,EAAW,CAAC,GAAA,KAAa,CAAC,IAAI,MAAA,EAAQ,GAAA,CAAI,YAAY,GAAA,CAAI,UAAA,EAAY,IAAI,KAAA,EAAO,GAAA,CAAI,QAAQ,IAAA,CAAK,SAAA,CAAU,IAAI,OAAO,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EAChI,QAAA,EAAU,CAAC,KAAA,KAAoB,CAAC,MAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC;AAClE,CAAA;AAaA,IAAM,IAAA,GAAO,CAAI,CAAA,KAAmB,OAAO,MAAM,QAAA,GAAY,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,GAAU,CAAA;AACnF,IAAM,MAAM,CAAC,CAAA,KAAkC,MAAM,IAAA,GAAO,IAAA,GAAO,OAAO,CAAC,CAAA;AAEpE,SAAS,SAAS,CAAA,EAAsB;AAC7C,EAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,MAAM,UAAA,EAAY,IAAA,CAAK,EAAE,UAAU,CAAA,EAAG,SAAA,EAAW,MAAA,CAAO,EAAE,UAAU,CAAA,EAAG,WAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA,EAAE;AAC1H;AAEO,SAAS,SAAS,CAAA,EAAgB;AACvC,EAAA,OAAO;AAAA,IACL,IAAI,CAAA,CAAE,EAAA;AAAA,IAAI,KAAK,CAAA,CAAE,GAAA;AAAA,IAAK,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,SAAA,EAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA;AAAA,IAAG,UAAA,EAAY,GAAA,CAAI,CAAA,CAAE,WAAW,CAAA;AAAA,IACtG,UAAA,EAAY,GAAA,CAAI,CAAA,CAAE,WAAW,CAAA;AAAA,IAAG,OAAO,CAAA,CAAE,KAAA;AAAA,IAAO,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,OAAA,EAAS,EAAE,OAAA,KAAY,IAAA,GAAO,EAAC,GAAI,IAAA,CAAK,EAAE,OAAO,CAAA;AAAA,IAAG,SAAS,CAAA,CAAE;AAAA,GACnI;AACF;AAEO,SAAS,WAAW,CAAA,EAAuB;AAChD,EAAA,OAAO,IAAA,CAAK,EAAE,KAAK,CAAA;AACrB;;;ACnHO,SAAS,MAAA,CAAO,OAAA,GAAyB,EAAC,EAAU;AACzD,EAAA,MAAM,CAAA,GAAI,WAAA,CAAY,OAAA,CAAQ,MAAM,CAAA;AACpC,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,QAAA,EAAU,CAAC,CAAA;AAClC,EAAA,IAAI,EAAA,GAA+B,QAAQ,QAAA,IAAY,IAAA;AACvD,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,qBAAA;AAC7B,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAgC;AAElD,EAAA,MAAM,OAAO,MAAyB;AACpC,IAAA,IAAI,IAAI,OAAO,EAAA;AACf,IAAA,MAAM,MAAA,GAAS,IAAA,KAAS,UAAA,IAAc,IAAA,KAAS,EAAA;AAC/C,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,SAAA,CAAU,KAAK,OAAA,CAAQ,IAAI,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAGjD,MAAA,IAAI;AACF,QAAA,SAAA,CAAU,QAAA,CAAS,IAAA,EAAM,GAAA,EAAK,GAAK,CAAC,CAAA;AACpC,QAAA,KAAA,MAAW,CAAA,IAAK,CAAC,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA,IAAA,CAAA,EAAQ,CAAA,EAAG,IAAI,CAAA,IAAA,CAAM,CAAA,EAAG;AACpD,UAAA,IAAI;AAAE,YAAA,SAAA,CAAU,GAAG,GAAK,CAAA;AAAA,UAAG,CAAA,CAAA,MAAQ;AAAA,UAAsC;AAAA,QAC3E;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAAgD;AAAA,IAC1D;AACA,IAAA,EAAA,GAAK,IAAI,SAAS,IAAI,CAAA;AACtB,IAAA,EAAA,CAAG,OAAO,oBAAoB,CAAA;AAC9B,IAAA,EAAA,CAAG,OAAO,qBAAqB,CAAA;AAC/B,IAAA,EAAA,CAAG,OAAO,sBAAsB,CAAA;AAChC,IAAA,OAAO,EAAA;AAAA,EACT,CAAA;AAGA,EAAA,MAAM,IAAA,GAAO,CAAC,IAAA,KAAqC;AACjD,IAAA,IAAI,CAAA,GAAI,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AACtB,IAAA,IAAI,CAAC,CAAA,EAAG;AACN,MAAA,CAAA,GAAI,IAAA,EAAK,CAAE,OAAA,CAAQ,IAAI,CAAA;AACvB,MAAA,KAAA,CAAM,GAAA,CAAI,MAAM,CAAC,CAAA;AAAA,IACnB;AACA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,IAAA,KAAiB;AAC/B,IAAA,IAAA,CAAK,GAAA,CAAI,UAAU,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAC7B,IAAA,IAAA,CAAK,GAAA,CAAI,WAAW,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAC9B,IAAA,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,CAAE,GAAA,CAAI,IAAI,CAAA;AAAA,EAC9B,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAO;AACX,MAAA,IAAA,EAAK,CAAE,IAAA,CAAK,MAAA,CAAO,QAAA,EAAU,CAAC,CAAC,CAAA;AAAA,IACjC,CAAA;AAAA,IACA,MAAM,SAAA,CAAU,UAAA,EAAiC,GAAA,EAAa;AAC5D,MAAA,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,CAAE,GAAA,CAAI,GAAG,MAAA,CAAO,SAAA,CAAU,UAAA,EAAY,GAAG,CAAC,CAAA;AAAA,IAC9D,CAAA;AAAA,IACA,MAAM,OAAO,IAAA,EAAM;AACjB,MAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,IAAI,IAAI,CAAA;AACnC,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,QAAA,GAAiC;AACrC,MAAA,OAAQ,KAAK,GAAA,CAAI,QAAQ,EAAE,GAAA,EAAI,CAAe,IAAI,QAAQ,CAAA;AAAA,IAC5D,CAAA;AAAA,IACA,MAAM,UAAU,IAAA,EAAM;AACpB,MAAA,IAAA,EAAK,CAAE,WAAA,CAAY,MAAM,CAAA,CAAE,IAAI,CAAA;AAAA,IACjC,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,CAAE,GAAA,CAAI,GAAG,MAAA,CAAO,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IAClD,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,CAAE,GAAA,CAAI,GAAG,MAAA,CAAO,SAAA,CAAU,GAAG,CAAC,CAAA;AAAA,IAClD,CAAA;AAAA,IACA,MAAM,OAAO,EAAA,EAAI;AACf,MAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,IAAI,EAAE,CAAA;AACjC,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,GAAA,EAAK,KAAA,EAAO;AACzB,MAAA,OAAQ,IAAA,CAAK,IAAI,QAAQ,CAAA,CAAE,IAAI,GAAA,EAAK,KAAK,CAAA,CAAe,GAAA,CAAI,QAAQ,CAAA;AAAA,IACtE,CAAA;AAAA,IACA,MAAM,QAAQ,GAAA,EAAK;AACjB,MAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA,CAAE,GAAA,CAAI,KAAK,CAAC,CAAA;AACvC,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,WAAA,GAAc;AAClB,MAAA,OAAQ,KAAK,GAAA,CAAI,WAAW,EAAE,GAAA,EAAI,CAAe,IAAI,QAAQ,CAAA;AAAA,IAC/D,CAAA;AAAA,IACA,MAAM,SAAS,GAAA,EAAK;AAClB,MAAA,MAAM,IAAI,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA,CAAE,IAAI,GAAG,CAAA;AACpC,MAAA,OAAO,CAAA,GAAI,UAAA,CAAW,CAAC,CAAA,GAAI,IAAA;AAAA,IAC7B,CAAA;AAAA,IACA,MAAM,SAAS,KAAA,EAAO;AACpB,MAAA,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA,CAAE,GAAA,CAAI,GAAG,MAAA,CAAO,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,IAClD,CAAA;AAAA,IACA,MAAM,MAAM,MAAA,EAAQ;AAClB,MAAA,OAAO,KAAK,GAAA,CAAI,KAAK,CAAA,CAAE,GAAA,CAAI,MAAM,CAAA,CAAE,OAAA;AAAA,IACrC,CAAA;AAAA,IACA,MAAM,KAAA,GAAQ;AACZ,MAAA,KAAA,CAAM,KAAA,EAAM;AACZ,MAAA,IAAI,EAAA,IAAM,CAAC,OAAA,CAAQ,QAAA,EAAU;AAC3B,QAAA,EAAA,CAAG,KAAA,EAAM;AACT,QAAA,EAAA,GAAK,IAAA;AAAA,MACP;AAAA,IACF;AAAA,GACF;AACF","file":"sqlite.js","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 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 // 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};\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 Database from \"better-sqlite3\";\nimport { chmodSync, closeSync, mkdirSync, openSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Store, StoredJob, StoredJobDefinition } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\nexport interface SqliteOptions {\n /** File path. The directory is created if missing. \":memory:\" works too. */\n path?: string;\n /** Bring your own open better-sqlite3 database instead. */\n database?: Database.Database;\n /** Table name prefix: lowercase letters, digits and underscores. Default \"cronwatch_\". */\n prefix?: string;\n}\n\n/**\n * Keeps everything in one SQLite file through better-sqlite3. The right\n * choice for a single server. WAL mode, so the app's reads never block on a\n * run being written.\n */\nexport function sqlite(options: SqliteOptions = {}): Store {\n const p = tablePrefix(options.prefix);\n const sql = statements(\"sqlite\", p);\n let db: Database.Database | null = options.database ?? null;\n const file = options.path ?? \"./data/cronwatch.db\";\n const cache = new Map<string, Database.Statement>();\n\n const open = (): Database.Database => {\n if (db) return db;\n const onDisk = file !== \":memory:\" && file !== \"\";\n if (onDisk) {\n mkdirSync(path.dirname(file), { recursive: true });\n // Create the file private before SQLite opens it. SQLite gives the -wal and -shm files the\n // main file's mode, so the whole set stays 0600; the chmods cover files left by an earlier open.\n try {\n closeSync(openSync(file, \"a\", 0o600));\n for (const f of [file, `${file}-wal`, `${file}-shm`]) {\n try { chmodSync(f, 0o600); } catch { /* the sidecar may not exist yet */ }\n }\n } catch { /* best effort: not every filesystem cares */ }\n }\n db = new Database(file);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n return db;\n };\n\n // Each statement is prepared once per open database.\n const stmt = (text: string): Database.Statement => {\n let s = cache.get(text);\n if (!s) {\n s = open().prepare(text);\n cache.set(text, s);\n }\n return s;\n };\n\n const forget = (name: string) => {\n stmt(sql.deleteRuns).run(name);\n stmt(sql.deleteState).run(name);\n stmt(sql.deleteJob).run(name);\n };\n\n return {\n async init() {\n open().exec(schema(\"sqlite\", p));\n },\n async upsertJob(definition: StoredJobDefinition, now: number) {\n stmt(sql.upsertJob).run(...params.upsertJob(definition, now));\n },\n async getJob(name) {\n const r = stmt(sql.getJob).get(name) as JobRow | undefined;\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (stmt(sql.listJobs).all() as JobRow[]).map(rowToJob);\n },\n async deleteJob(name) {\n open().transaction(forget)(name);\n },\n async insertRun(run) {\n stmt(sql.insertRun).run(...params.insertRun(run));\n },\n async updateRun(run) {\n stmt(sql.updateRun).run(...params.updateRun(run));\n },\n async getRun(id) {\n const r = stmt(sql.getRun).get(id) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (stmt(sql.listRuns).all(job, limit) as RunRow[]).map(rowToRun);\n },\n async lastRun(job) {\n const r = stmt(sql.listRuns).get(job, 1) as RunRow | undefined;\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (stmt(sql.runningRuns).all() as RunRow[]).map(rowToRun);\n },\n async getState(job) {\n const r = stmt(sql.getState).get(job) as StateRow | undefined;\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n stmt(sql.setState).run(...params.setState(state));\n },\n async prune(before) {\n return stmt(sql.prune).run(before).changes;\n },\n async close() {\n cache.clear();\n if (db && !options.database) {\n db.close();\n db = null;\n }\n },\n };\n}\n"]}
|
|
@@ -84,11 +84,61 @@ interface JobState {
|
|
|
84
84
|
open: Partial<Record<Condition, number>>;
|
|
85
85
|
consecutiveFailures: number;
|
|
86
86
|
silencedUntil: number | null;
|
|
87
|
+
/** When an alert last reached at least one channel. */
|
|
87
88
|
lastAlertAt: number | null;
|
|
89
|
+
/**
|
|
90
|
+
* Conditions that alerted and have since closed, waiting for the recovered
|
|
91
|
+
* message that the next successful run sends. Absent in state written
|
|
92
|
+
* before this field existed.
|
|
93
|
+
*/
|
|
94
|
+
pendingRecovery?: Condition[];
|
|
95
|
+
/** Alerts that no channel accepted. Each check retries them once. */
|
|
96
|
+
undelivered?: Alert[];
|
|
88
97
|
}
|
|
89
98
|
type AlertType = Condition | "recovered";
|
|
90
|
-
interface
|
|
91
|
-
|
|
99
|
+
interface BudgetBreach {
|
|
100
|
+
metric: string;
|
|
101
|
+
value: number;
|
|
102
|
+
limit: number;
|
|
103
|
+
basis: string;
|
|
104
|
+
}
|
|
105
|
+
/** What each alert type carries in `details`. */
|
|
106
|
+
interface AlertDetails {
|
|
107
|
+
missed: {
|
|
108
|
+
dueAt: number;
|
|
109
|
+
deadline: number;
|
|
110
|
+
graceMs: number;
|
|
111
|
+
lastRunAt: number | null;
|
|
112
|
+
};
|
|
113
|
+
failed: {
|
|
114
|
+
consecutiveFailures: number;
|
|
115
|
+
threshold: number;
|
|
116
|
+
};
|
|
117
|
+
stuck: {
|
|
118
|
+
consecutiveFailures: number;
|
|
119
|
+
threshold: number;
|
|
120
|
+
};
|
|
121
|
+
slow: {
|
|
122
|
+
durationMs: number;
|
|
123
|
+
thresholdMs: number;
|
|
124
|
+
basis: string;
|
|
125
|
+
};
|
|
126
|
+
over_budget: {
|
|
127
|
+
breaches: BudgetBreach[];
|
|
128
|
+
};
|
|
129
|
+
recovered: {
|
|
130
|
+
after: Condition[];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** An alert before it has a title and message. See composeAlert(). */
|
|
134
|
+
type AlertDraft = {
|
|
135
|
+
[K in AlertType]: {
|
|
136
|
+
type: K;
|
|
137
|
+
run: Run | null;
|
|
138
|
+
details: AlertDetails[K];
|
|
139
|
+
};
|
|
140
|
+
}[AlertType];
|
|
141
|
+
interface AlertBase {
|
|
92
142
|
job: string;
|
|
93
143
|
definition: StoredJobDefinition;
|
|
94
144
|
run: Run | null;
|
|
@@ -96,11 +146,17 @@ interface Alert {
|
|
|
96
146
|
title: string;
|
|
97
147
|
/** A few lines of plain text with the specifics. */
|
|
98
148
|
message: string;
|
|
99
|
-
details: Record<string, unknown>;
|
|
100
149
|
/** A short diagnosis from the triage function, when one is configured. */
|
|
101
150
|
triage?: string;
|
|
102
151
|
at: number;
|
|
103
152
|
}
|
|
153
|
+
/** Narrow on `type` to read `details`: `if (alert.type === "slow") alert.details.durationMs`. */
|
|
154
|
+
type Alert = {
|
|
155
|
+
[K in AlertType]: AlertBase & {
|
|
156
|
+
type: K;
|
|
157
|
+
details: AlertDetails[K];
|
|
158
|
+
};
|
|
159
|
+
}[AlertType];
|
|
104
160
|
interface AlertChannel {
|
|
105
161
|
name: string;
|
|
106
162
|
send(alert: Alert): Promise<void>;
|
|
@@ -136,7 +192,7 @@ interface JobSummary {
|
|
|
136
192
|
nextExpectedAt: number | null;
|
|
137
193
|
consecutiveFailures: number;
|
|
138
194
|
silencedUntil: number | null;
|
|
139
|
-
/** From the last twenty successful
|
|
195
|
+
/** From the last twenty runs of any status; p50Ms and p95Ms are over the successful ones among them. */
|
|
140
196
|
stats: {
|
|
141
197
|
runs: number;
|
|
142
198
|
okRate: number;
|
|
@@ -153,7 +209,9 @@ interface CheckResult {
|
|
|
153
209
|
interface TriageContext {
|
|
154
210
|
alert: Alert;
|
|
155
211
|
recentRuns: Run[];
|
|
212
|
+
/** Aborts when the client stops waiting for a diagnosis. Pass it to any request you make. */
|
|
213
|
+
signal: AbortSignal;
|
|
156
214
|
}
|
|
157
215
|
type TriageFn = (context: TriageContext) => Promise<string | null>;
|
|
158
216
|
|
|
159
|
-
export type { Alert as A, CheckResult as C, Duration as D, ExpectRule as E, JobOptions as J, Run as R, Store as S, TriageFn as T, AlertChannel as a, JobDefinition as b, JobSummary as c, JobState as d,
|
|
217
|
+
export type { Alert as A, BudgetBreach as B, CheckResult as C, Duration as D, ExpectRule as E, JobOptions as J, Run as R, Store as S, TriageFn as T, AlertChannel as a, JobDefinition as b, JobSummary as c, JobState as d, AlertDraft as e, StoredJobDefinition as f, AlertDetails as g, AlertType as h, Condition as i, JobHealth as j, RunStatus as k, StoredJob as l, TriageContext as m };
|
|
@@ -84,11 +84,61 @@ interface JobState {
|
|
|
84
84
|
open: Partial<Record<Condition, number>>;
|
|
85
85
|
consecutiveFailures: number;
|
|
86
86
|
silencedUntil: number | null;
|
|
87
|
+
/** When an alert last reached at least one channel. */
|
|
87
88
|
lastAlertAt: number | null;
|
|
89
|
+
/**
|
|
90
|
+
* Conditions that alerted and have since closed, waiting for the recovered
|
|
91
|
+
* message that the next successful run sends. Absent in state written
|
|
92
|
+
* before this field existed.
|
|
93
|
+
*/
|
|
94
|
+
pendingRecovery?: Condition[];
|
|
95
|
+
/** Alerts that no channel accepted. Each check retries them once. */
|
|
96
|
+
undelivered?: Alert[];
|
|
88
97
|
}
|
|
89
98
|
type AlertType = Condition | "recovered";
|
|
90
|
-
interface
|
|
91
|
-
|
|
99
|
+
interface BudgetBreach {
|
|
100
|
+
metric: string;
|
|
101
|
+
value: number;
|
|
102
|
+
limit: number;
|
|
103
|
+
basis: string;
|
|
104
|
+
}
|
|
105
|
+
/** What each alert type carries in `details`. */
|
|
106
|
+
interface AlertDetails {
|
|
107
|
+
missed: {
|
|
108
|
+
dueAt: number;
|
|
109
|
+
deadline: number;
|
|
110
|
+
graceMs: number;
|
|
111
|
+
lastRunAt: number | null;
|
|
112
|
+
};
|
|
113
|
+
failed: {
|
|
114
|
+
consecutiveFailures: number;
|
|
115
|
+
threshold: number;
|
|
116
|
+
};
|
|
117
|
+
stuck: {
|
|
118
|
+
consecutiveFailures: number;
|
|
119
|
+
threshold: number;
|
|
120
|
+
};
|
|
121
|
+
slow: {
|
|
122
|
+
durationMs: number;
|
|
123
|
+
thresholdMs: number;
|
|
124
|
+
basis: string;
|
|
125
|
+
};
|
|
126
|
+
over_budget: {
|
|
127
|
+
breaches: BudgetBreach[];
|
|
128
|
+
};
|
|
129
|
+
recovered: {
|
|
130
|
+
after: Condition[];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** An alert before it has a title and message. See composeAlert(). */
|
|
134
|
+
type AlertDraft = {
|
|
135
|
+
[K in AlertType]: {
|
|
136
|
+
type: K;
|
|
137
|
+
run: Run | null;
|
|
138
|
+
details: AlertDetails[K];
|
|
139
|
+
};
|
|
140
|
+
}[AlertType];
|
|
141
|
+
interface AlertBase {
|
|
92
142
|
job: string;
|
|
93
143
|
definition: StoredJobDefinition;
|
|
94
144
|
run: Run | null;
|
|
@@ -96,11 +146,17 @@ interface Alert {
|
|
|
96
146
|
title: string;
|
|
97
147
|
/** A few lines of plain text with the specifics. */
|
|
98
148
|
message: string;
|
|
99
|
-
details: Record<string, unknown>;
|
|
100
149
|
/** A short diagnosis from the triage function, when one is configured. */
|
|
101
150
|
triage?: string;
|
|
102
151
|
at: number;
|
|
103
152
|
}
|
|
153
|
+
/** Narrow on `type` to read `details`: `if (alert.type === "slow") alert.details.durationMs`. */
|
|
154
|
+
type Alert = {
|
|
155
|
+
[K in AlertType]: AlertBase & {
|
|
156
|
+
type: K;
|
|
157
|
+
details: AlertDetails[K];
|
|
158
|
+
};
|
|
159
|
+
}[AlertType];
|
|
104
160
|
interface AlertChannel {
|
|
105
161
|
name: string;
|
|
106
162
|
send(alert: Alert): Promise<void>;
|
|
@@ -136,7 +192,7 @@ interface JobSummary {
|
|
|
136
192
|
nextExpectedAt: number | null;
|
|
137
193
|
consecutiveFailures: number;
|
|
138
194
|
silencedUntil: number | null;
|
|
139
|
-
/** From the last twenty successful
|
|
195
|
+
/** From the last twenty runs of any status; p50Ms and p95Ms are over the successful ones among them. */
|
|
140
196
|
stats: {
|
|
141
197
|
runs: number;
|
|
142
198
|
okRate: number;
|
|
@@ -153,7 +209,9 @@ interface CheckResult {
|
|
|
153
209
|
interface TriageContext {
|
|
154
210
|
alert: Alert;
|
|
155
211
|
recentRuns: Run[];
|
|
212
|
+
/** Aborts when the client stops waiting for a diagnosis. Pass it to any request you make. */
|
|
213
|
+
signal: AbortSignal;
|
|
156
214
|
}
|
|
157
215
|
type TriageFn = (context: TriageContext) => Promise<string | null>;
|
|
158
216
|
|
|
159
|
-
export type { Alert as A, CheckResult as C, Duration as D, ExpectRule as E, JobOptions as J, Run as R, Store as S, TriageFn as T, AlertChannel as a, JobDefinition as b, JobSummary as c, JobState as d,
|
|
217
|
+
export type { Alert as A, BudgetBreach as B, CheckResult as C, Duration as D, ExpectRule as E, JobOptions as J, Run as R, Store as S, TriageFn as T, AlertChannel as a, JobDefinition as b, JobSummary as c, JobState as d, AlertDraft as e, StoredJobDefinition as f, AlertDetails as g, AlertType as h, Condition as i, JobHealth as j, RunStatus as k, StoredJob as l, TriageContext as m };
|
package/dist/webhook.cjs
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
|
|
5
5
|
// src/alerts/webhook.ts
|
|
6
|
+
var TIMEOUT_MS = 1e4;
|
|
7
|
+
function origin(url) {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url).origin;
|
|
10
|
+
} catch {
|
|
11
|
+
return "(invalid URL)";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
6
14
|
function webhook(options) {
|
|
7
15
|
if (!options.url) throw new Error("webhook() needs a url");
|
|
8
16
|
return {
|
|
@@ -13,8 +21,8 @@ function webhook(options) {
|
|
|
13
21
|
if (options.secret) {
|
|
14
22
|
headers["x-cronwatch-signature"] = `sha256=${crypto.createHmac("sha256", options.secret).update(body).digest("hex")}`;
|
|
15
23
|
}
|
|
16
|
-
const response = await fetch(options.url, { method: "POST", headers, body });
|
|
17
|
-
if (!response.ok) throw new Error(`Webhook ${options.url} answered ${response.status}`);
|
|
24
|
+
const response = await fetch(options.url, { method: "POST", headers, body, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
25
|
+
if (!response.ok) throw new Error(`Webhook ${origin(options.url)} answered ${response.status}`);
|
|
18
26
|
}
|
|
19
27
|
};
|
|
20
28
|
}
|
package/dist/webhook.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/webhook.ts"],"names":["createHmac"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"sources":["../src/alerts/webhook.ts"],"names":["createHmac"],"mappings":";;;;;AAeA,IAAM,UAAA,GAAa,GAAA;AAEnB,SAAS,OAAO,GAAA,EAAqB;AACnC,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAMO,SAAS,QAAQ,OAAA,EAAuC;AAC7D,EAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,EAAK,MAAM,IAAI,MAAM,uBAAuB,CAAA;AACzD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACjC,MAAA,MAAM,OAAA,GAAkC,EAAE,cAAA,EAAgB,kBAAA,EAAoB,cAAc,WAAA,EAAa,GAAG,QAAQ,OAAA,EAAQ;AAC5H,MAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,QAAA,OAAA,CAAQ,uBAAuB,CAAA,GAAI,CAAA,OAAA,EAAUA,iBAAA,CAAW,QAAA,EAAU,OAAA,CAAQ,MAAM,CAAA,CAAE,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,MAC9G;AACA,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,KAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,MAAM,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,GAAG,CAAA;AAEpH,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAC,CAAA,UAAA,EAAa,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,IAChG;AAAA,GACF;AACF","file":"webhook.cjs","sourcesContent":["import { createHmac } from \"node:crypto\";\nimport type { AlertChannel } from \"../types.js\";\n\nexport interface WebhookOptions {\n url: string;\n /** Extra request headers, for an Authorization header say. */\n headers?: Record<string, string>;\n /**\n * When set, each request carries `X-CronWatch-Signature: sha256=<hex>`,\n * the HMAC-SHA256 of the raw body with this secret, so the receiver can\n * verify it.\n */\n secret?: string;\n}\n\nconst TIMEOUT_MS = 10_000;\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * POSTs the alert as JSON to any URL. The body is the Alert object:\n * { type, job, title, message, run, details, triage, at, definition }.\n */\nexport function webhook(options: WebhookOptions): AlertChannel {\n if (!options.url) throw new Error(\"webhook() needs a url\");\n return {\n name: \"webhook\",\n async send(alert) {\n const body = JSON.stringify(alert);\n const headers: Record<string, string> = { \"content-type\": \"application/json\", \"user-agent\": \"cronwatch\", ...options.headers };\n if (options.secret) {\n headers[\"x-cronwatch-signature\"] = `sha256=${createHmac(\"sha256\", options.secret).update(body).digest(\"hex\")}`;\n }\n const response = await fetch(options.url, { method: \"POST\", headers, body, signal: AbortSignal.timeout(TIMEOUT_MS) });\n // Only the origin: a webhook URL's path or query often is the credential.\n if (!response.ok) throw new Error(`Webhook ${origin(options.url)} answered ${response.status}`);\n },\n };\n}\n"]}
|
package/dist/webhook.d.cts
CHANGED
package/dist/webhook.d.ts
CHANGED
package/dist/webhook.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { createHmac } from 'crypto';
|
|
2
2
|
|
|
3
3
|
// src/alerts/webhook.ts
|
|
4
|
+
var TIMEOUT_MS = 1e4;
|
|
5
|
+
function origin(url) {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(url).origin;
|
|
8
|
+
} catch {
|
|
9
|
+
return "(invalid URL)";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
4
12
|
function webhook(options) {
|
|
5
13
|
if (!options.url) throw new Error("webhook() needs a url");
|
|
6
14
|
return {
|
|
@@ -11,8 +19,8 @@ function webhook(options) {
|
|
|
11
19
|
if (options.secret) {
|
|
12
20
|
headers["x-cronwatch-signature"] = `sha256=${createHmac("sha256", options.secret).update(body).digest("hex")}`;
|
|
13
21
|
}
|
|
14
|
-
const response = await fetch(options.url, { method: "POST", headers, body });
|
|
15
|
-
if (!response.ok) throw new Error(`Webhook ${options.url} answered ${response.status}`);
|
|
22
|
+
const response = await fetch(options.url, { method: "POST", headers, body, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
23
|
+
if (!response.ok) throw new Error(`Webhook ${origin(options.url)} answered ${response.status}`);
|
|
16
24
|
}
|
|
17
25
|
};
|
|
18
26
|
}
|
package/dist/webhook.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/webhook.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/alerts/webhook.ts"],"names":[],"mappings":";;;AAeA,IAAM,UAAA,GAAa,GAAA;AAEnB,SAAS,OAAO,GAAA,EAAqB;AACnC,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAMO,SAAS,QAAQ,OAAA,EAAuC;AAC7D,EAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,EAAK,MAAM,IAAI,MAAM,uBAAuB,CAAA;AACzD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACjC,MAAA,MAAM,OAAA,GAAkC,EAAE,cAAA,EAAgB,kBAAA,EAAoB,cAAc,WAAA,EAAa,GAAG,QAAQ,OAAA,EAAQ;AAC5H,MAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,QAAA,OAAA,CAAQ,uBAAuB,CAAA,GAAI,CAAA,OAAA,EAAU,UAAA,CAAW,QAAA,EAAU,OAAA,CAAQ,MAAM,CAAA,CAAE,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA;AAAA,MAC9G;AACA,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,KAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,MAAM,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,GAAG,CAAA;AAEpH,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAC,CAAA,UAAA,EAAa,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,IAChG;AAAA,GACF;AACF","file":"webhook.js","sourcesContent":["import { createHmac } from \"node:crypto\";\nimport type { AlertChannel } from \"../types.js\";\n\nexport interface WebhookOptions {\n url: string;\n /** Extra request headers, for an Authorization header say. */\n headers?: Record<string, string>;\n /**\n * When set, each request carries `X-CronWatch-Signature: sha256=<hex>`,\n * the HMAC-SHA256 of the raw body with this secret, so the receiver can\n * verify it.\n */\n secret?: string;\n}\n\nconst TIMEOUT_MS = 10_000;\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * POSTs the alert as JSON to any URL. The body is the Alert object:\n * { type, job, title, message, run, details, triage, at, definition }.\n */\nexport function webhook(options: WebhookOptions): AlertChannel {\n if (!options.url) throw new Error(\"webhook() needs a url\");\n return {\n name: \"webhook\",\n async send(alert) {\n const body = JSON.stringify(alert);\n const headers: Record<string, string> = { \"content-type\": \"application/json\", \"user-agent\": \"cronwatch\", ...options.headers };\n if (options.secret) {\n headers[\"x-cronwatch-signature\"] = `sha256=${createHmac(\"sha256\", options.secret).update(body).digest(\"hex\")}`;\n }\n const response = await fetch(options.url, { method: \"POST\", headers, body, signal: AbortSignal.timeout(TIMEOUT_MS) });\n // Only the origin: a webhook URL's path or query often is the credential.\n if (!response.ok) throw new Error(`Webhook ${origin(options.url)} answered ${response.status}`);\n },\n };\n}\n"]}
|