@cronwatch/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/anthropic.cjs +84 -0
- package/dist/anthropic.cjs.map +1 -0
- package/dist/anthropic.d.cts +35 -0
- package/dist/anthropic.d.ts +35 -0
- package/dist/anthropic.js +78 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/discord.cjs +42 -0
- package/dist/discord.cjs.map +1 -0
- package/dist/discord.d.cts +11 -0
- package/dist/discord.d.ts +11 -0
- package/dist/discord.js +40 -0
- package/dist/discord.js.map +1 -0
- package/dist/index.cjs +1317 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +219 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +1302 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres.cjs +142 -0
- package/dist/postgres.cjs.map +1 -0
- package/dist/postgres.d.cts +19 -0
- package/dist/postgres.d.ts +19 -0
- package/dist/postgres.js +136 -0
- package/dist/postgres.js.map +1 -0
- package/dist/slack.cjs +44 -0
- package/dist/slack.cjs.map +1 -0
- package/dist/slack.d.cts +12 -0
- package/dist/slack.d.ts +12 -0
- package/dist/slack.js +42 -0
- package/dist/slack.js.map +1 -0
- package/dist/sqlite.cjs +151 -0
- package/dist/sqlite.cjs.map +1 -0
- package/dist/sqlite.d.cts +17 -0
- package/dist/sqlite.d.ts +17 -0
- package/dist/sqlite.js +144 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types-BQ1P8z55.d.cts +159 -0
- package/dist/types-BQ1P8z55.d.ts +159 -0
- package/dist/webhook.cjs +24 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +20 -0
- package/dist/webhook.d.ts +20 -0
- package/dist/webhook.js +22 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +114 -0
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
|
|
3
|
+
// src/stores/postgres.ts
|
|
4
|
+
var num = (v) => v === null ? null : Number(v);
|
|
5
|
+
function rowToRun(r) {
|
|
6
|
+
return {
|
|
7
|
+
id: r.id,
|
|
8
|
+
job: r.job,
|
|
9
|
+
status: r.status,
|
|
10
|
+
startedAt: Number(r.started_at),
|
|
11
|
+
finishedAt: num(r.finished_at),
|
|
12
|
+
durationMs: num(r.duration_ms),
|
|
13
|
+
error: r.error,
|
|
14
|
+
output: r.output,
|
|
15
|
+
metrics: r.metrics ?? {},
|
|
16
|
+
trigger: r.trigger
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function postgres(options = {}) {
|
|
20
|
+
const owned = !options.pool;
|
|
21
|
+
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`;
|
|
24
|
+
return {
|
|
25
|
+
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
|
+
`);
|
|
53
|
+
},
|
|
54
|
+
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
|
+
);
|
|
60
|
+
},
|
|
61
|
+
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;
|
|
65
|
+
},
|
|
66
|
+
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) }));
|
|
69
|
+
},
|
|
70
|
+
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
|
+
}
|
|
84
|
+
},
|
|
85
|
+
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
|
+
);
|
|
91
|
+
},
|
|
92
|
+
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
|
+
);
|
|
97
|
+
},
|
|
98
|
+
async getRun(id) {
|
|
99
|
+
const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE id = $1`, [id]);
|
|
100
|
+
return rows[0] ? rowToRun(rows[0]) : null;
|
|
101
|
+
},
|
|
102
|
+
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]);
|
|
104
|
+
return rows.map(rowToRun);
|
|
105
|
+
},
|
|
106
|
+
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]);
|
|
108
|
+
return rows[0] ? rowToRun(rows[0]) : null;
|
|
109
|
+
},
|
|
110
|
+
async runningRuns() {
|
|
111
|
+
const { rows } = await pool.query(`SELECT * FROM ${RUNS} WHERE status = 'running' ORDER BY started_at`);
|
|
112
|
+
return rows.map(rowToRun);
|
|
113
|
+
},
|
|
114
|
+
async getState(job) {
|
|
115
|
+
const { rows } = await pool.query(`SELECT state FROM ${STATE} WHERE job = $1`, [job]);
|
|
116
|
+
return rows[0]?.state ?? null;
|
|
117
|
+
},
|
|
118
|
+
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
|
+
);
|
|
123
|
+
},
|
|
124
|
+
async prune(before) {
|
|
125
|
+
const result = await pool.query(`DELETE FROM ${RUNS} WHERE status <> 'running' AND started_at < $1`, [before]);
|
|
126
|
+
return result.rowCount ?? 0;
|
|
127
|
+
},
|
|
128
|
+
async close() {
|
|
129
|
+
if (owned) await pool.end();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { postgres };
|
|
135
|
+
//# sourceMappingURL=postgres.js.map
|
|
136
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/postgres.ts"],"names":[],"mappings":"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","file":"postgres.js","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"]}
|
package/dist/slack.cjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/alerts/slack.ts
|
|
4
|
+
var EMOJI = {
|
|
5
|
+
missed: ":hourglass_flowing_sand:",
|
|
6
|
+
failed: ":x:",
|
|
7
|
+
stuck: ":no_entry:",
|
|
8
|
+
slow: ":turtle:",
|
|
9
|
+
over_budget: ":moneybag:",
|
|
10
|
+
recovered: ":white_check_mark:"
|
|
11
|
+
};
|
|
12
|
+
function slack(options) {
|
|
13
|
+
if (!options.webhookUrl) throw new Error("slack() needs a webhookUrl");
|
|
14
|
+
return {
|
|
15
|
+
name: "slack",
|
|
16
|
+
async send(alert) {
|
|
17
|
+
const url = options.link?.(alert);
|
|
18
|
+
const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : ""}`;
|
|
19
|
+
const body = escape(alert.message);
|
|
20
|
+
const triage = alert.triage ? `
|
|
21
|
+
_Triage:_ ${escape(alert.triage)}` : "";
|
|
22
|
+
const response = await fetch(options.webhookUrl, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
text: `${alert.title}
|
|
27
|
+
${alert.message}`,
|
|
28
|
+
blocks: [
|
|
29
|
+
{ type: "section", text: { type: "mrkdwn", text: title } },
|
|
30
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + body.slice(0, 2800) + "```" + triage } }
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function escape(text) {
|
|
39
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.slack = slack;
|
|
43
|
+
//# sourceMappingURL=slack.cjs.map
|
|
44
|
+
//# sourceMappingURL=slack.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";;;AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAGO,SAAS,MAAM,OAAA,EAAqC;AACzD,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACrE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAQ,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,IAAI,CAAC,CAAA,EAAA,EAAK,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,GAAM,CAAA,GAAA,EAAM,GAAG,YAAY,EAAE,CAAA,CAAA;AAC3F,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA;AACjC,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,GAAS;AAAA,UAAA,EAAe,MAAA,CAAO,KAAA,CAAM,MAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AACtE,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,IAAA,EAAM,CAAA,EAAG,KAAA,CAAM,KAAK;AAAA,EAAK,MAAM,OAAO,CAAA,CAAA;AAAA,UACtC,MAAA,EAAQ;AAAA,YACN,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,EAAM,EAAE;AAAA,YACzD,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,EAAE,MAAM,QAAA,EAAU,IAAA,EAAM,KAAA,GAAQ,IAAA,CAAK,MAAM,CAAA,EAAG,IAAI,CAAA,GAAI,KAAA,GAAQ,QAAO;AAAE;AAClG,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,0BAA0B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IACzH;AAAA,GACF;AACF;AAEA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA;AAC/E","file":"slack.cjs","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = escape(alert.message);\n const triage = alert.triage ? `\\n_Triage:_ ${escape(alert.triage)}` : \"\";\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n text: `${alert.title}\\n${alert.message}`,\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body.slice(0, 2800) + \"```\" + triage } },\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n"]}
|
package/dist/slack.d.cts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { A as Alert, a as AlertChannel } from './types-BQ1P8z55.cjs';
|
|
2
|
+
|
|
3
|
+
interface SlackOptions {
|
|
4
|
+
/** An incoming webhook URL from api.slack.com/messaging/webhooks. */
|
|
5
|
+
webhookUrl: string;
|
|
6
|
+
/** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */
|
|
7
|
+
link?: (alert: Alert) => string;
|
|
8
|
+
}
|
|
9
|
+
/** Sends alerts to a Slack channel through an incoming webhook. */
|
|
10
|
+
declare function slack(options: SlackOptions): AlertChannel;
|
|
11
|
+
|
|
12
|
+
export { type SlackOptions, slack };
|
package/dist/slack.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { A as Alert, a as AlertChannel } from './types-BQ1P8z55.js';
|
|
2
|
+
|
|
3
|
+
interface SlackOptions {
|
|
4
|
+
/** An incoming webhook URL from api.slack.com/messaging/webhooks. */
|
|
5
|
+
webhookUrl: string;
|
|
6
|
+
/** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */
|
|
7
|
+
link?: (alert: Alert) => string;
|
|
8
|
+
}
|
|
9
|
+
/** Sends alerts to a Slack channel through an incoming webhook. */
|
|
10
|
+
declare function slack(options: SlackOptions): AlertChannel;
|
|
11
|
+
|
|
12
|
+
export { type SlackOptions, slack };
|
package/dist/slack.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/alerts/slack.ts
|
|
2
|
+
var EMOJI = {
|
|
3
|
+
missed: ":hourglass_flowing_sand:",
|
|
4
|
+
failed: ":x:",
|
|
5
|
+
stuck: ":no_entry:",
|
|
6
|
+
slow: ":turtle:",
|
|
7
|
+
over_budget: ":moneybag:",
|
|
8
|
+
recovered: ":white_check_mark:"
|
|
9
|
+
};
|
|
10
|
+
function slack(options) {
|
|
11
|
+
if (!options.webhookUrl) throw new Error("slack() needs a webhookUrl");
|
|
12
|
+
return {
|
|
13
|
+
name: "slack",
|
|
14
|
+
async send(alert) {
|
|
15
|
+
const url = options.link?.(alert);
|
|
16
|
+
const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : ""}`;
|
|
17
|
+
const body = escape(alert.message);
|
|
18
|
+
const triage = alert.triage ? `
|
|
19
|
+
_Triage:_ ${escape(alert.triage)}` : "";
|
|
20
|
+
const response = await fetch(options.webhookUrl, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
text: `${alert.title}
|
|
25
|
+
${alert.message}`,
|
|
26
|
+
blocks: [
|
|
27
|
+
{ type: "section", text: { type: "mrkdwn", text: title } },
|
|
28
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + body.slice(0, 2800) + "```" + triage } }
|
|
29
|
+
]
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function escape(text) {
|
|
37
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { slack };
|
|
41
|
+
//# sourceMappingURL=slack.js.map
|
|
42
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/alerts/slack.ts"],"names":[],"mappings":";AASA,IAAM,KAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,0BAAA;AAAA,EACR,MAAA,EAAQ,KAAA;AAAA,EACR,KAAA,EAAO,YAAA;AAAA,EACP,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,YAAA;AAAA,EACb,SAAA,EAAW;AACb,CAAA;AAGO,SAAS,MAAM,OAAA,EAAqC;AACzD,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,EAAY,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACrE,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AAChC,MAAA,MAAM,QAAQ,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,IAAI,CAAC,CAAA,EAAA,EAAK,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA,EAAI,GAAA,GAAM,CAAA,GAAA,EAAM,GAAG,YAAY,EAAE,CAAA,CAAA;AAC3F,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA;AACjC,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,GAAS;AAAA,UAAA,EAAe,MAAA,CAAO,KAAA,CAAM,MAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AACtE,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY;AAAA,QAC/C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,IAAA,EAAM,CAAA,EAAG,KAAA,CAAM,KAAK;AAAA,EAAK,MAAM,OAAO,CAAA,CAAA;AAAA,UACtC,MAAA,EAAQ;AAAA,YACN,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,EAAM,EAAE;AAAA,YACzD,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,EAAE,MAAM,QAAA,EAAU,IAAA,EAAM,KAAA,GAAQ,IAAA,CAAK,MAAM,CAAA,EAAG,IAAI,CAAA,GAAI,KAAA,GAAQ,QAAO;AAAE;AAClG,SACD;AAAA,OACF,CAAA;AACD,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,QAAU,IAAI,KAAA,CAAM,0BAA0B,QAAA,CAAS,MAAM,CAAA,EAAA,EAAA,CAAM,MAAM,SAAS,IAAA,EAAK,EAAG,MAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IACzH;AAAA,GACF;AACF;AAEA,SAAS,OAAO,IAAA,EAAsB;AACpC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA;AAC/E","file":"slack.js","sourcesContent":["import type { Alert, AlertChannel } from \"../types.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = escape(alert.message);\n const triage = alert.triage ? `\\n_Triage:_ ${escape(alert.triage)}` : \"\";\n const response = await fetch(options.webhookUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n text: `${alert.title}\\n${alert.message}`,\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body.slice(0, 2800) + \"```\" + triage } },\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n"]}
|
package/dist/sqlite.cjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Database = require('better-sqlite3');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
10
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
|
|
12
|
+
// src/stores/sqlite.ts
|
|
13
|
+
function rowToRun(r) {
|
|
14
|
+
return {
|
|
15
|
+
id: r.id,
|
|
16
|
+
job: r.job,
|
|
17
|
+
status: r.status,
|
|
18
|
+
startedAt: r.started_at,
|
|
19
|
+
finishedAt: r.finished_at,
|
|
20
|
+
durationMs: r.duration_ms,
|
|
21
|
+
error: r.error,
|
|
22
|
+
output: r.output,
|
|
23
|
+
metrics: JSON.parse(r.metrics),
|
|
24
|
+
trigger: r.trigger
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function sqlite(options = {}) {
|
|
28
|
+
let db = options.database ?? null;
|
|
29
|
+
const file = options.path ?? "./data/cronwatch.db";
|
|
30
|
+
const open = () => {
|
|
31
|
+
if (db) return db;
|
|
32
|
+
if (file !== ":memory:") {
|
|
33
|
+
fs.mkdirSync(path__default.default.dirname(file), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
db = new Database__default.default(file);
|
|
36
|
+
if (file !== ":memory:") {
|
|
37
|
+
try {
|
|
38
|
+
fs.chmodSync(file, 384);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
db.pragma("journal_mode = WAL");
|
|
43
|
+
db.pragma("busy_timeout = 5000");
|
|
44
|
+
db.pragma("synchronous = NORMAL");
|
|
45
|
+
return db;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
async init() {
|
|
49
|
+
const d = open();
|
|
50
|
+
d.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS cronwatch_jobs (
|
|
52
|
+
name TEXT PRIMARY KEY,
|
|
53
|
+
definition TEXT NOT NULL,
|
|
54
|
+
created_at INTEGER NOT NULL,
|
|
55
|
+
updated_at INTEGER NOT NULL
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS cronwatch_runs (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
job TEXT NOT NULL,
|
|
60
|
+
status TEXT NOT NULL,
|
|
61
|
+
started_at INTEGER NOT NULL,
|
|
62
|
+
finished_at INTEGER,
|
|
63
|
+
duration_ms INTEGER,
|
|
64
|
+
error TEXT,
|
|
65
|
+
output TEXT,
|
|
66
|
+
metrics TEXT NOT NULL DEFAULT '{}',
|
|
67
|
+
trigger TEXT NOT NULL DEFAULT 'run'
|
|
68
|
+
);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS cronwatch_runs_job_started ON cronwatch_runs (job, started_at DESC);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS cronwatch_runs_running ON cronwatch_runs (status) WHERE status = 'running';
|
|
71
|
+
CREATE TABLE IF NOT EXISTS cronwatch_state (
|
|
72
|
+
job TEXT PRIMARY KEY,
|
|
73
|
+
state TEXT NOT NULL
|
|
74
|
+
);
|
|
75
|
+
`);
|
|
76
|
+
},
|
|
77
|
+
async upsertJob(definition, now) {
|
|
78
|
+
open().prepare(`
|
|
79
|
+
INSERT INTO cronwatch_jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
80
|
+
ON CONFLICT(name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at
|
|
81
|
+
`).run(definition.name, JSON.stringify(definition), now, now);
|
|
82
|
+
},
|
|
83
|
+
async getJob(name) {
|
|
84
|
+
const r = open().prepare("SELECT * FROM cronwatch_jobs WHERE name = ?").get(name);
|
|
85
|
+
return r ? { name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at } : null;
|
|
86
|
+
},
|
|
87
|
+
async listJobs() {
|
|
88
|
+
const rows = open().prepare("SELECT * FROM cronwatch_jobs ORDER BY name").all();
|
|
89
|
+
return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));
|
|
90
|
+
},
|
|
91
|
+
async deleteJob(name) {
|
|
92
|
+
const d = open();
|
|
93
|
+
d.transaction(() => {
|
|
94
|
+
d.prepare("DELETE FROM cronwatch_runs WHERE job = ?").run(name);
|
|
95
|
+
d.prepare("DELETE FROM cronwatch_state WHERE job = ?").run(name);
|
|
96
|
+
d.prepare("DELETE FROM cronwatch_jobs WHERE name = ?").run(name);
|
|
97
|
+
})();
|
|
98
|
+
},
|
|
99
|
+
async insertRun(run) {
|
|
100
|
+
open().prepare(`
|
|
101
|
+
INSERT INTO cronwatch_runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
103
|
+
`).run(run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.trigger);
|
|
104
|
+
},
|
|
105
|
+
async updateRun(run) {
|
|
106
|
+
open().prepare(`
|
|
107
|
+
UPDATE cronwatch_runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?
|
|
108
|
+
`).run(run.status, run.finishedAt, run.durationMs, run.error, run.output, JSON.stringify(run.metrics), run.id);
|
|
109
|
+
},
|
|
110
|
+
async getRun(id) {
|
|
111
|
+
const r = open().prepare("SELECT * FROM cronwatch_runs WHERE id = ?").get(id);
|
|
112
|
+
return r ? rowToRun(r) : null;
|
|
113
|
+
},
|
|
114
|
+
async listRuns(job, limit) {
|
|
115
|
+
const rows = open().prepare("SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT ?").all(job, limit);
|
|
116
|
+
return rows.map(rowToRun);
|
|
117
|
+
},
|
|
118
|
+
async lastRun(job) {
|
|
119
|
+
const r = open().prepare("SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT 1").get(job);
|
|
120
|
+
return r ? rowToRun(r) : null;
|
|
121
|
+
},
|
|
122
|
+
async runningRuns() {
|
|
123
|
+
const rows = open().prepare("SELECT * FROM cronwatch_runs WHERE status = 'running' ORDER BY started_at").all();
|
|
124
|
+
return rows.map(rowToRun);
|
|
125
|
+
},
|
|
126
|
+
async getState(job) {
|
|
127
|
+
const r = open().prepare("SELECT state FROM cronwatch_state WHERE job = ?").get(job);
|
|
128
|
+
return r ? JSON.parse(r.state) : null;
|
|
129
|
+
},
|
|
130
|
+
async setState(state) {
|
|
131
|
+
open().prepare(`
|
|
132
|
+
INSERT INTO cronwatch_state (job, state) VALUES (?, ?)
|
|
133
|
+
ON CONFLICT(job) DO UPDATE SET state = excluded.state
|
|
134
|
+
`).run(state.job, JSON.stringify(state));
|
|
135
|
+
},
|
|
136
|
+
async prune(before) {
|
|
137
|
+
const info = open().prepare("DELETE FROM cronwatch_runs WHERE status != 'running' AND started_at < ?").run(before);
|
|
138
|
+
return info.changes;
|
|
139
|
+
},
|
|
140
|
+
async close() {
|
|
141
|
+
if (db && !options.database) {
|
|
142
|
+
db.close();
|
|
143
|
+
db = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
exports.sqlite = sqlite;
|
|
150
|
+
//# sourceMappingURL=sqlite.cjs.map
|
|
151
|
+
//# sourceMappingURL=sqlite.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/sqlite.ts"],"names":["mkdirSync","path","Database","chmodSync"],"mappings":"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","file":"sqlite.cjs","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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { S as Store } from './types-BQ1P8z55.cjs';
|
|
3
|
+
|
|
4
|
+
interface SqliteOptions {
|
|
5
|
+
/** File path. The directory is created if missing. ":memory:" works too. */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** Bring your own open better-sqlite3 database instead. */
|
|
8
|
+
database?: Database.Database;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Keeps everything in one SQLite file through better-sqlite3. The right
|
|
12
|
+
* choice for a single server. WAL mode, so the app's reads never block on a
|
|
13
|
+
* run being written.
|
|
14
|
+
*/
|
|
15
|
+
declare function sqlite(options?: SqliteOptions): Store;
|
|
16
|
+
|
|
17
|
+
export { type SqliteOptions, sqlite };
|
package/dist/sqlite.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { S as Store } from './types-BQ1P8z55.js';
|
|
3
|
+
|
|
4
|
+
interface SqliteOptions {
|
|
5
|
+
/** File path. The directory is created if missing. ":memory:" works too. */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** Bring your own open better-sqlite3 database instead. */
|
|
8
|
+
database?: Database.Database;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Keeps everything in one SQLite file through better-sqlite3. The right
|
|
12
|
+
* choice for a single server. WAL mode, so the app's reads never block on a
|
|
13
|
+
* run being written.
|
|
14
|
+
*/
|
|
15
|
+
declare function sqlite(options?: SqliteOptions): Store;
|
|
16
|
+
|
|
17
|
+
export { type SqliteOptions, sqlite };
|
package/dist/sqlite.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { mkdirSync, chmodSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// src/stores/sqlite.ts
|
|
6
|
+
function rowToRun(r) {
|
|
7
|
+
return {
|
|
8
|
+
id: r.id,
|
|
9
|
+
job: r.job,
|
|
10
|
+
status: r.status,
|
|
11
|
+
startedAt: r.started_at,
|
|
12
|
+
finishedAt: r.finished_at,
|
|
13
|
+
durationMs: r.duration_ms,
|
|
14
|
+
error: r.error,
|
|
15
|
+
output: r.output,
|
|
16
|
+
metrics: JSON.parse(r.metrics),
|
|
17
|
+
trigger: r.trigger
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function sqlite(options = {}) {
|
|
21
|
+
let db = options.database ?? null;
|
|
22
|
+
const file = options.path ?? "./data/cronwatch.db";
|
|
23
|
+
const open = () => {
|
|
24
|
+
if (db) return db;
|
|
25
|
+
if (file !== ":memory:") {
|
|
26
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
db = new Database(file);
|
|
29
|
+
if (file !== ":memory:") {
|
|
30
|
+
try {
|
|
31
|
+
chmodSync(file, 384);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
db.pragma("journal_mode = WAL");
|
|
36
|
+
db.pragma("busy_timeout = 5000");
|
|
37
|
+
db.pragma("synchronous = NORMAL");
|
|
38
|
+
return db;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
async init() {
|
|
42
|
+
const d = open();
|
|
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
|
+
`);
|
|
69
|
+
},
|
|
70
|
+
async upsertJob(definition, now) {
|
|
71
|
+
open().prepare(`
|
|
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);
|
|
75
|
+
},
|
|
76
|
+
async getJob(name) {
|
|
77
|
+
const r = open().prepare("SELECT * FROM cronwatch_jobs WHERE name = ?").get(name);
|
|
78
|
+
return r ? { name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at } : null;
|
|
79
|
+
},
|
|
80
|
+
async listJobs() {
|
|
81
|
+
const rows = open().prepare("SELECT * FROM cronwatch_jobs ORDER BY name").all();
|
|
82
|
+
return rows.map((r) => ({ name: r.name, definition: JSON.parse(r.definition), createdAt: r.created_at, updatedAt: r.updated_at }));
|
|
83
|
+
},
|
|
84
|
+
async deleteJob(name) {
|
|
85
|
+
const d = open();
|
|
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
|
+
})();
|
|
91
|
+
},
|
|
92
|
+
async insertRun(run) {
|
|
93
|
+
open().prepare(`
|
|
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);
|
|
97
|
+
},
|
|
98
|
+
async updateRun(run) {
|
|
99
|
+
open().prepare(`
|
|
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);
|
|
102
|
+
},
|
|
103
|
+
async getRun(id) {
|
|
104
|
+
const r = open().prepare("SELECT * FROM cronwatch_runs WHERE id = ?").get(id);
|
|
105
|
+
return r ? rowToRun(r) : null;
|
|
106
|
+
},
|
|
107
|
+
async listRuns(job, limit) {
|
|
108
|
+
const rows = open().prepare("SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT ?").all(job, limit);
|
|
109
|
+
return rows.map(rowToRun);
|
|
110
|
+
},
|
|
111
|
+
async lastRun(job) {
|
|
112
|
+
const r = open().prepare("SELECT * FROM cronwatch_runs WHERE job = ? ORDER BY started_at DESC, rowid DESC LIMIT 1").get(job);
|
|
113
|
+
return r ? rowToRun(r) : null;
|
|
114
|
+
},
|
|
115
|
+
async runningRuns() {
|
|
116
|
+
const rows = open().prepare("SELECT * FROM cronwatch_runs WHERE status = 'running' ORDER BY started_at").all();
|
|
117
|
+
return rows.map(rowToRun);
|
|
118
|
+
},
|
|
119
|
+
async getState(job) {
|
|
120
|
+
const r = open().prepare("SELECT state FROM cronwatch_state WHERE job = ?").get(job);
|
|
121
|
+
return r ? JSON.parse(r.state) : null;
|
|
122
|
+
},
|
|
123
|
+
async setState(state) {
|
|
124
|
+
open().prepare(`
|
|
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));
|
|
128
|
+
},
|
|
129
|
+
async prune(before) {
|
|
130
|
+
const info = open().prepare("DELETE FROM cronwatch_runs WHERE status != 'running' AND started_at < ?").run(before);
|
|
131
|
+
return info.changes;
|
|
132
|
+
},
|
|
133
|
+
async close() {
|
|
134
|
+
if (db && !options.database) {
|
|
135
|
+
db.close();
|
|
136
|
+
db = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { sqlite };
|
|
143
|
+
//# sourceMappingURL=sqlite.js.map
|
|
144
|
+
//# sourceMappingURL=sqlite.js.map
|