sermon-js 0.0.0-stage → 0.2.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 +239 -2
- package/dist/api/index.cjs +174 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +77 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.js +171 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-2M4UBCDM.js +178 -0
- package/dist/chunk-2M4UBCDM.js.map +1 -0
- package/dist/chunk-AH372HLN.js +98 -0
- package/dist/chunk-AH372HLN.js.map +1 -0
- package/dist/chunk-APC2HVSI.js +154 -0
- package/dist/chunk-APC2HVSI.js.map +1 -0
- package/dist/chunk-D7BBJTYU.cjs +335 -0
- package/dist/chunk-D7BBJTYU.cjs.map +1 -0
- package/dist/chunk-ELV4ZU6G.cjs +51 -0
- package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
- package/dist/chunk-EMP7XVKC.js +334 -0
- package/dist/chunk-EMP7XVKC.js.map +1 -0
- package/dist/chunk-I3346MUL.js +49 -0
- package/dist/chunk-I3346MUL.js.map +1 -0
- package/dist/chunk-NP3YYVRZ.cjs +163 -0
- package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
- package/dist/chunk-PARE6EUZ.cjs +184 -0
- package/dist/chunk-PARE6EUZ.cjs.map +1 -0
- package/dist/chunk-XHXEKB3Q.cjs +241 -0
- package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
- package/dist/chunk-XPXIDJUU.js +229 -0
- package/dist/chunk-XPXIDJUU.js.map +1 -0
- package/dist/chunk-ZI442ZA5.cjs +100 -0
- package/dist/chunk-ZI442ZA5.cjs.map +1 -0
- package/dist/config-BEbP-uLf.d.cts +125 -0
- package/dist/config-w1tm1bdu.d.ts +125 -0
- package/dist/generic-BJmlgtUM.d.ts +31 -0
- package/dist/generic-lmrGxMDC.d.cts +31 -0
- package/dist/index.cjs +407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/index.cjs +1012 -0
- package/dist/presets/index.cjs.map +1 -0
- package/dist/presets/index.d.cts +516 -0
- package/dist/presets/index.d.ts +516 -0
- package/dist/presets/index.js +971 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/react/heroui/index.cjs +822 -0
- package/dist/react/heroui/index.cjs.map +1 -0
- package/dist/react/heroui/index.d.cts +122 -0
- package/dist/react/heroui/index.d.ts +122 -0
- package/dist/react/heroui/index.js +821 -0
- package/dist/react/heroui/index.js.map +1 -0
- package/dist/react/heroui/styles.css +11 -0
- package/dist/react/index.cjs +71 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +53 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/status-DsWzgb9Q.d.cts +146 -0
- package/dist/status-zd04ZQ98.d.ts +146 -0
- package/dist/store-DybhDphS.d.cts +77 -0
- package/dist/store-_GuzSP0z.d.ts +77 -0
- package/dist/stores/mongodb.cjs +99 -0
- package/dist/stores/mongodb.cjs.map +1 -0
- package/dist/stores/mongodb.d.cts +40 -0
- package/dist/stores/mongodb.d.ts +40 -0
- package/dist/stores/mongodb.js +97 -0
- package/dist/stores/mongodb.js.map +1 -0
- package/dist/stores/redis.cjs +23 -0
- package/dist/stores/redis.cjs.map +1 -0
- package/dist/stores/redis.d.cts +31 -0
- package/dist/stores/redis.d.ts +31 -0
- package/dist/stores/redis.js +21 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/sql.cjs +164 -0
- package/dist/stores/sql.cjs.map +1 -0
- package/dist/stores/sql.d.cts +39 -0
- package/dist/stores/sql.d.ts +39 -0
- package/dist/stores/sql.js +162 -0
- package/dist/stores/sql.js.map +1 -0
- package/dist/stores/upstash.cjs +27 -0
- package/dist/stores/upstash.cjs.map +1 -0
- package/dist/stores/upstash.d.cts +19 -0
- package/dist/stores/upstash.d.ts +19 -0
- package/dist/stores/upstash.js +25 -0
- package/dist/stores/upstash.js.map +1 -0
- package/dist/testing/index.cjs +209 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +205 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tree-BXbXfjF1.d.cts +236 -0
- package/dist/tree-BXbXfjF1.d.ts +236 -0
- package/dist/types-CctvUAs2.d.cts +192 -0
- package/dist/types-CctvUAs2.d.ts +192 -0
- package/package.json +233 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-DybhDphS.cjs';
|
|
2
|
+
import '../types-CctvUAs2.cjs';
|
|
3
|
+
|
|
4
|
+
/** ioredis: `client.call("GET", "key")` */
|
|
5
|
+
type CallClient = {
|
|
6
|
+
call: (command: string, ...args: Array<string | number>) => Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
/** node-redis: `client.sendCommand(["GET", "key"])` */
|
|
9
|
+
type SendCommandClient = {
|
|
10
|
+
sendCommand: (args: string[]) => Promise<unknown>;
|
|
11
|
+
};
|
|
12
|
+
type RedisStoreOptions = {
|
|
13
|
+
/** An ioredis or node-redis client you created and connected */
|
|
14
|
+
client?: CallClient | SendCommandClient;
|
|
15
|
+
/** Or any function that sends one raw command, for other clients */
|
|
16
|
+
command?: (args: string[]) => Promise<unknown>;
|
|
17
|
+
/** Prefix of every key (default "sermon"): lets several status pages share one database */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Store on any Redis-compatible server (Redis, Valkey, KeyDB, Dragonfly…), using the client
|
|
22
|
+
* you already have:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* redisStore({ client: new Redis(process.env.REDIS_URL) }) // ioredis
|
|
26
|
+
* redisStore({ client: await createClient({ url }).connect() }) // node-redis
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function redisStore({ client, command, prefix }: RedisStoreOptions): StatusStore;
|
|
30
|
+
|
|
31
|
+
export { type RedisStoreOptions, redisStore };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-_GuzSP0z.js';
|
|
2
|
+
import '../types-CctvUAs2.js';
|
|
3
|
+
|
|
4
|
+
/** ioredis: `client.call("GET", "key")` */
|
|
5
|
+
type CallClient = {
|
|
6
|
+
call: (command: string, ...args: Array<string | number>) => Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
/** node-redis: `client.sendCommand(["GET", "key"])` */
|
|
9
|
+
type SendCommandClient = {
|
|
10
|
+
sendCommand: (args: string[]) => Promise<unknown>;
|
|
11
|
+
};
|
|
12
|
+
type RedisStoreOptions = {
|
|
13
|
+
/** An ioredis or node-redis client you created and connected */
|
|
14
|
+
client?: CallClient | SendCommandClient;
|
|
15
|
+
/** Or any function that sends one raw command, for other clients */
|
|
16
|
+
command?: (args: string[]) => Promise<unknown>;
|
|
17
|
+
/** Prefix of every key (default "sermon"): lets several status pages share one database */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Store on any Redis-compatible server (Redis, Valkey, KeyDB, Dragonfly…), using the client
|
|
22
|
+
* you already have:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* redisStore({ client: new Redis(process.env.REDIS_URL) }) // ioredis
|
|
26
|
+
* redisStore({ client: await createClient({ url }).connect() }) // node-redis
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function redisStore({ client, command, prefix }: RedisStoreOptions): StatusStore;
|
|
30
|
+
|
|
31
|
+
export { type RedisStoreOptions, redisStore };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRedisStore } from '../chunk-AH372HLN.js';
|
|
2
|
+
|
|
3
|
+
// src/stores/redis.ts
|
|
4
|
+
function redisStore({ client, command, prefix }) {
|
|
5
|
+
let send = command;
|
|
6
|
+
if (!send && client) {
|
|
7
|
+
if ("call" in client && typeof client.call === "function") {
|
|
8
|
+
send = ([name, ...args]) => client.call(name, ...args);
|
|
9
|
+
} else if ("sendCommand" in client && typeof client.sendCommand === "function") {
|
|
10
|
+
send = (args) => client.sendCommand(args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (!send) throw new Error("sermon-js: redisStore needs a `client` (ioredis or node-redis) or a `command` function");
|
|
14
|
+
const run = send;
|
|
15
|
+
const exec = (commands) => Promise.all(commands.map((args) => run(args.map(String))));
|
|
16
|
+
return createRedisStore({ exec, prefix });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { redisStore };
|
|
20
|
+
//# sourceMappingURL=redis.js.map
|
|
21
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stores/redis.ts"],"names":[],"mappings":";;;AA0BO,SAAS,UAAA,CAAW,EAAE,MAAA,EAAQ,OAAA,EAAS,QAAO,EAAmC;AACtF,EAAA,IAAI,IAAA,GAAO,OAAA;AACX,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,IAAI,MAAA,IAAU,MAAA,IAAU,OAAO,MAAA,CAAO,SAAS,UAAA,EAAY;AACzD,MAAA,IAAA,GAAO,CAAC,CAAC,IAAA,EAAS,GAAA,IAAI,MAAM,MAAA,CAAO,IAAA,CAAK,IAAA,EAAgB,GAAI,IAAiB,CAAA;AAAA,IAC/E,WAAW,aAAA,IAAiB,MAAA,IAAU,OAAO,MAAA,CAAO,gBAAgB,UAAA,EAAY;AAC9E,MAAA,IAAA,GAAO,CAAC,IAAA,KAAS,MAAA,CAAO,WAAA,CAAY,IAAI,CAAA;AAAA,IAC1C;AAAA,EACF;AACA,EAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,wFAAwF,CAAA;AAEnH,EAAA,MAAM,GAAA,GAAM,IAAA;AACZ,EAAA,MAAM,IAAA,GAAkB,CAAC,QAAA,KAAa,OAAA,CAAQ,IAAI,QAAA,CAAS,GAAA,CAAI,CAAC,IAAA,KAAS,IAAI,IAAA,CAAK,GAAA,CAAI,MAAM,CAAC,CAAC,CAAC,CAAA;AAC/F,EAAA,OAAO,gBAAA,CAAiB,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA;AAC1C","file":"redis.js","sourcesContent":["import type { StatusStore } from \"../store\";\nimport { createRedisStore, type RedisExec } from \"./redis-core\";\n\n/** ioredis: `client.call(\"GET\", \"key\")` */\ntype CallClient = { call: (command: string, ...args: Array<string | number>) => Promise<unknown> };\n/** node-redis: `client.sendCommand([\"GET\", \"key\"])` */\ntype SendCommandClient = { sendCommand: (args: string[]) => Promise<unknown> };\n\nexport type RedisStoreOptions = {\n /** An ioredis or node-redis client you created and connected */\n client?: CallClient | SendCommandClient;\n /** Or any function that sends one raw command, for other clients */\n command?: (args: string[]) => Promise<unknown>;\n /** Prefix of every key (default \"sermon\"): lets several status pages share one database */\n prefix?: string;\n};\n\n/**\n * Store on any Redis-compatible server (Redis, Valkey, KeyDB, Dragonfly…), using the client\n * you already have:\n *\n * ```ts\n * redisStore({ client: new Redis(process.env.REDIS_URL) }) // ioredis\n * redisStore({ client: await createClient({ url }).connect() }) // node-redis\n * ```\n */\nexport function redisStore({ client, command, prefix }: RedisStoreOptions): StatusStore {\n let send = command;\n if (!send && client) {\n if (\"call\" in client && typeof client.call === \"function\") {\n send = ([name, ...args]) => client.call(name as string, ...(args as string[]));\n } else if (\"sendCommand\" in client && typeof client.sendCommand === \"function\") {\n send = (args) => client.sendCommand(args);\n }\n }\n if (!send) throw new Error(\"sermon-js: redisStore needs a `client` (ioredis or node-redis) or a `command` function\");\n\n const run = send;\n const exec: RedisExec = (commands) => Promise.all(commands.map((args) => run(args.map(String))));\n return createRedisStore({ exec, prefix });\n}\n"]}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/stores/sql.ts
|
|
4
|
+
function schemaFor(dialect, t) {
|
|
5
|
+
const text = dialect === "mysql" ? "MEDIUMTEXT" : "TEXT";
|
|
6
|
+
const key = "VARCHAR(255)";
|
|
7
|
+
if (dialect === "mysql") {
|
|
8
|
+
return [
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,
|
|
10
|
+
`CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL, INDEX ${t.runs}_at (at_ms))`,
|
|
11
|
+
`CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms), INDEX ${t.samples}_at (at_ms))`,
|
|
12
|
+
`CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
`CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,
|
|
17
|
+
`CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL)`,
|
|
18
|
+
`CREATE INDEX IF NOT EXISTS ${t.runs}_at ON ${t.runs} (at_ms)`,
|
|
19
|
+
`CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms))`,
|
|
20
|
+
`CREATE INDEX IF NOT EXISTS ${t.samples}_at ON ${t.samples} (at_ms)`,
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
function parse(value) {
|
|
25
|
+
if (typeof value !== "string") return value && typeof value === "object" ? value : void 0;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sqlStore({ query, dialect, tablePrefix = "sermon_", autoMigrate = true }) {
|
|
33
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tablePrefix)) {
|
|
34
|
+
throw new Error("sermon-js: sqlStore tablePrefix may only contain letters, digits and underscores");
|
|
35
|
+
}
|
|
36
|
+
const t = {
|
|
37
|
+
latest: `${tablePrefix}latest`,
|
|
38
|
+
runs: `${tablePrefix}runs`,
|
|
39
|
+
samples: `${tablePrefix}samples`,
|
|
40
|
+
kv: `${tablePrefix}kv`
|
|
41
|
+
};
|
|
42
|
+
const schema = schemaFor(dialect, t);
|
|
43
|
+
const run = (sql, params = []) => {
|
|
44
|
+
let index = 0;
|
|
45
|
+
return query(dialect === "postgres" ? sql.replace(/\?/g, () => `$${++index}`) : sql, params);
|
|
46
|
+
};
|
|
47
|
+
const upsert = (table, columns, conflict) => {
|
|
48
|
+
const updates = columns.filter((column) => !conflict.includes(column));
|
|
49
|
+
const insert = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
50
|
+
return dialect === "mysql" ? `${insert} ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(", ")}` : `${insert} ON CONFLICT (${conflict.join(", ")}) DO UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(", ")}`;
|
|
51
|
+
};
|
|
52
|
+
const placeholders = (count) => Array.from({ length: count }, () => "?").join(", ");
|
|
53
|
+
let migrated;
|
|
54
|
+
const migrate = async () => {
|
|
55
|
+
for (const statement of schema) await query(statement, []);
|
|
56
|
+
};
|
|
57
|
+
const ready = () => {
|
|
58
|
+
if (!autoMigrate) return Promise.resolve();
|
|
59
|
+
migrated ??= migrate().catch((error) => {
|
|
60
|
+
migrated = void 0;
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
return migrated;
|
|
64
|
+
};
|
|
65
|
+
let sequence = 0;
|
|
66
|
+
const runId = (at) => `${at}-${(sequence++).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
67
|
+
return {
|
|
68
|
+
schema,
|
|
69
|
+
migrate,
|
|
70
|
+
async saveRun({ summary, results, samples }) {
|
|
71
|
+
await ready();
|
|
72
|
+
for (const result of results) {
|
|
73
|
+
await run(upsert(t.latest, ["service_key", "data"], ["service_key"]), [result.key, JSON.stringify(result)]);
|
|
74
|
+
}
|
|
75
|
+
for (const sample of samples) {
|
|
76
|
+
await run(upsert(t.samples, ["service_key", "at_ms", "data"], ["service_key", "at_ms"]), [
|
|
77
|
+
sample.key,
|
|
78
|
+
sample.at,
|
|
79
|
+
JSON.stringify(sample)
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
await run(`INSERT INTO ${t.runs} (id, at_ms, data) VALUES (?, ?, ?)`, [
|
|
83
|
+
runId(summary.at),
|
|
84
|
+
summary.at,
|
|
85
|
+
JSON.stringify(summary)
|
|
86
|
+
]);
|
|
87
|
+
},
|
|
88
|
+
async latest() {
|
|
89
|
+
await ready();
|
|
90
|
+
const rows = await run(`SELECT service_key, data FROM ${t.latest}`);
|
|
91
|
+
const latest = {};
|
|
92
|
+
for (const row of rows) {
|
|
93
|
+
const result = parse(row.data);
|
|
94
|
+
if (result) latest[String(row.service_key)] = result;
|
|
95
|
+
}
|
|
96
|
+
return latest;
|
|
97
|
+
},
|
|
98
|
+
async runs({ since, until }) {
|
|
99
|
+
await ready();
|
|
100
|
+
const rows = await run(`SELECT data FROM ${t.runs} WHERE at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`, [since, until]);
|
|
101
|
+
return rows.map((row) => parse(row.data)).filter((item) => !!item);
|
|
102
|
+
},
|
|
103
|
+
async samples(key, { since, until }) {
|
|
104
|
+
await ready();
|
|
105
|
+
const rows = await run(
|
|
106
|
+
`SELECT data FROM ${t.samples} WHERE service_key = ? AND at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`,
|
|
107
|
+
[key, since, until]
|
|
108
|
+
);
|
|
109
|
+
return rows.map((row) => parse(row.data)).filter((item) => !!item);
|
|
110
|
+
},
|
|
111
|
+
async prune({ before, keepKeys }) {
|
|
112
|
+
await ready();
|
|
113
|
+
await run(`DELETE FROM ${t.runs} WHERE at_ms < ?`, [before]);
|
|
114
|
+
await run(`DELETE FROM ${t.samples} WHERE at_ms < ?`, [before]);
|
|
115
|
+
if (!keepKeys) return;
|
|
116
|
+
if (keepKeys.length === 0) {
|
|
117
|
+
await run(`DELETE FROM ${t.latest}`);
|
|
118
|
+
await run(`DELETE FROM ${t.samples}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await run(`DELETE FROM ${t.latest} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);
|
|
122
|
+
await run(`DELETE FROM ${t.samples} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);
|
|
123
|
+
},
|
|
124
|
+
async lock(name, ttlMs) {
|
|
125
|
+
await ready();
|
|
126
|
+
const key = `lock:${name}`;
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
await run(`DELETE FROM ${t.kv} WHERE k = ? AND expires_at <= ?`, [key, now]);
|
|
129
|
+
try {
|
|
130
|
+
await run(`INSERT INTO ${t.kv} (k, v, expires_at) VALUES (?, ?, ?)`, [key, "1", now + Math.max(1, Math.round(ttlMs))]);
|
|
131
|
+
return { ok: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const rows = await run(`SELECT expires_at FROM ${t.kv} WHERE k = ?`, [key]);
|
|
134
|
+
const expiresAt = Number(rows[0]?.expires_at);
|
|
135
|
+
if (!rows.length || !Number.isFinite(expiresAt)) throw error;
|
|
136
|
+
return { ok: false, retryAfterMs: Math.max(1, expiresAt - Date.now()) };
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async unlock(name) {
|
|
140
|
+
await ready();
|
|
141
|
+
await run(`DELETE FROM ${t.kv} WHERE k = ?`, [`lock:${name}`]);
|
|
142
|
+
},
|
|
143
|
+
cache: {
|
|
144
|
+
async get(key) {
|
|
145
|
+
await ready();
|
|
146
|
+
const rows = await run(`SELECT v FROM ${t.kv} WHERE k = ? AND expires_at > ?`, [`cache:${key}`, Date.now()]);
|
|
147
|
+
const value = rows[0]?.v;
|
|
148
|
+
return value === void 0 || value === null ? null : String(value);
|
|
149
|
+
},
|
|
150
|
+
async set(key, value, ttlMs) {
|
|
151
|
+
await ready();
|
|
152
|
+
await run(upsert(t.kv, ["k", "v", "expires_at"], ["k"]), [
|
|
153
|
+
`cache:${key}`,
|
|
154
|
+
value,
|
|
155
|
+
Date.now() + Math.max(1, Math.round(ttlMs))
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
exports.sqlStore = sqlStore;
|
|
163
|
+
//# sourceMappingURL=sql.cjs.map
|
|
164
|
+
//# sourceMappingURL=sql.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stores/sql.ts"],"names":[],"mappings":"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","file":"sql.cjs","sourcesContent":["import type { CheckSample, LockResult, RunSummary, StatusStore } from \"../store\";\nimport type { CheckResult } from \"../types\";\n\nexport type SqlDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\n/**\n * Runs one statement and resolves with its rows (an empty array for writes). Placeholders\n * are `$1, $2…` for postgres and `?` for mysql and sqlite, matching each driver's own style.\n */\nexport type SqlQuery = (sql: string, params: unknown[]) => Promise<Array<Record<string, unknown>>>;\n\nexport type SqlStoreOptions = {\n query: SqlQuery;\n dialect: SqlDialect;\n /** Prefix of the four tables (default \"sermon_\") */\n tablePrefix?: string;\n /** Create the tables on first use (default true). Turn off to run `migrate()` yourself. */\n autoMigrate?: boolean;\n};\n\nexport type SqlStore = StatusStore & {\n /** Create the tables and indexes if they don't exist. Safe to call repeatedly. */\n migrate(): Promise<void>;\n /** The statements `migrate()` runs, for migration tools */\n readonly schema: string[];\n};\n\nfunction schemaFor(dialect: SqlDialect, t: Record<\"latest\" | \"runs\" | \"samples\" | \"kv\", string>): string[] {\n const text = dialect === \"mysql\" ? \"MEDIUMTEXT\" : \"TEXT\";\n const key = \"VARCHAR(255)\";\n if (dialect === \"mysql\") {\n // MySQL has no CREATE INDEX IF NOT EXISTS: indexes are declared with the tables\n return [\n `CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,\n `CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL, INDEX ${t.runs}_at (at_ms))`,\n `CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms), INDEX ${t.samples}_at (at_ms))`,\n `CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`,\n ];\n }\n return [\n `CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,\n `CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL)`,\n `CREATE INDEX IF NOT EXISTS ${t.runs}_at ON ${t.runs} (at_ms)`,\n `CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms))`,\n `CREATE INDEX IF NOT EXISTS ${t.samples}_at ON ${t.samples} (at_ms)`,\n `CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`,\n ];\n}\n\nfunction parse<T>(value: unknown): T | undefined {\n if (typeof value !== \"string\") return value && typeof value === \"object\" ? (value as T) : undefined;\n try {\n return JSON.parse(value) as T;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Store on any SQL database, with the driver you already use. You provide a `query` function;\n * the store only needs four small tables, which it creates on first use.\n *\n * ```ts\n * // PostgreSQL (pg)\n * sqlStore({ dialect: \"postgres\", query: async (sql, params) => (await pool.query(sql, params)).rows });\n * // MySQL / MariaDB (mysql2)\n * sqlStore({ dialect: \"mysql\", query: async (sql, params) => { const [rows] = await pool.query(sql, params); return Array.isArray(rows) ? rows : []; } });\n * // SQLite (better-sqlite3)\n * sqlStore({ dialect: \"sqlite\", query: async (sql, params) => { const s = db.prepare(sql); return s.reader ? s.all(...params) : (s.run(...params), []); } });\n * ```\n */\nexport function sqlStore({ query, dialect, tablePrefix = \"sermon_\", autoMigrate = true }: SqlStoreOptions): SqlStore {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tablePrefix)) {\n throw new Error(\"sermon-js: sqlStore tablePrefix may only contain letters, digits and underscores\");\n }\n const t = {\n latest: `${tablePrefix}latest`,\n runs: `${tablePrefix}runs`,\n samples: `${tablePrefix}samples`,\n kv: `${tablePrefix}kv`,\n };\n const schema = schemaFor(dialect, t);\n\n /** Write statements with `?` and convert to `$n` for postgres */\n const run = (sql: string, params: unknown[] = []) => {\n let index = 0;\n return query(dialect === \"postgres\" ? sql.replace(/\\?/g, () => `$${++index}`) : sql, params);\n };\n const upsert = (table: string, columns: string[], conflict: string[]) => {\n const updates = columns.filter((column) => !conflict.includes(column));\n const insert = `INSERT INTO ${table} (${columns.join(\", \")}) VALUES (${columns.map(() => \"?\").join(\", \")})`;\n return dialect === \"mysql\"\n ? `${insert} ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(\", \")}`\n : `${insert} ON CONFLICT (${conflict.join(\", \")}) DO UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(\", \")}`;\n };\n const placeholders = (count: number) => Array.from({ length: count }, () => \"?\").join(\", \");\n\n let migrated: Promise<void> | undefined;\n const migrate = async () => {\n for (const statement of schema) await query(statement, []);\n };\n const ready = () => {\n if (!autoMigrate) return Promise.resolve();\n migrated ??= migrate().catch((error) => {\n // Let the next call try again\n migrated = undefined;\n throw error;\n });\n return migrated;\n };\n\n let sequence = 0;\n const runId = (at: number) => `${at}-${(sequence++).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n\n return {\n schema,\n migrate,\n\n async saveRun({ summary, results, samples }) {\n await ready();\n for (const result of results) {\n await run(upsert(t.latest, [\"service_key\", \"data\"], [\"service_key\"]), [result.key, JSON.stringify(result)]);\n }\n for (const sample of samples) {\n await run(upsert(t.samples, [\"service_key\", \"at_ms\", \"data\"], [\"service_key\", \"at_ms\"]), [\n sample.key,\n sample.at,\n JSON.stringify(sample),\n ]);\n }\n await run(`INSERT INTO ${t.runs} (id, at_ms, data) VALUES (?, ?, ?)`, [\n runId(summary.at),\n summary.at,\n JSON.stringify(summary),\n ]);\n },\n\n async latest() {\n await ready();\n const rows = await run(`SELECT service_key, data FROM ${t.latest}`);\n const latest: Record<string, CheckResult> = {};\n for (const row of rows) {\n const result = parse<CheckResult>(row.data);\n if (result) latest[String(row.service_key)] = result;\n }\n return latest;\n },\n\n async runs({ since, until }) {\n await ready();\n const rows = await run(`SELECT data FROM ${t.runs} WHERE at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`, [since, until]);\n return rows.map((row) => parse<RunSummary>(row.data)).filter((item): item is RunSummary => !!item);\n },\n\n async samples(key, { since, until }) {\n await ready();\n const rows = await run(\n `SELECT data FROM ${t.samples} WHERE service_key = ? AND at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`,\n [key, since, until]\n );\n return rows.map((row) => parse<CheckSample>(row.data)).filter((item): item is CheckSample => !!item);\n },\n\n async prune({ before, keepKeys }) {\n await ready();\n await run(`DELETE FROM ${t.runs} WHERE at_ms < ?`, [before]);\n await run(`DELETE FROM ${t.samples} WHERE at_ms < ?`, [before]);\n if (!keepKeys) return;\n if (keepKeys.length === 0) {\n await run(`DELETE FROM ${t.latest}`);\n await run(`DELETE FROM ${t.samples}`);\n return;\n }\n await run(`DELETE FROM ${t.latest} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);\n await run(`DELETE FROM ${t.samples} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);\n },\n\n async lock(name, ttlMs): Promise<LockResult> {\n await ready();\n const key = `lock:${name}`;\n const now = Date.now();\n await run(`DELETE FROM ${t.kv} WHERE k = ? AND expires_at <= ?`, [key, now]);\n try {\n // The primary key makes this atomic: only one caller can insert the row\n await run(`INSERT INTO ${t.kv} (k, v, expires_at) VALUES (?, ?, ?)`, [key, \"1\", now + Math.max(1, Math.round(ttlMs))]);\n return { ok: true };\n } catch (error) {\n const rows = await run(`SELECT expires_at FROM ${t.kv} WHERE k = ?`, [key]);\n const expiresAt = Number(rows[0]?.expires_at);\n // No row means the insert failed for another reason than \"already locked\"\n if (!rows.length || !Number.isFinite(expiresAt)) throw error;\n return { ok: false, retryAfterMs: Math.max(1, expiresAt - Date.now()) };\n }\n },\n\n async unlock(name) {\n await ready();\n await run(`DELETE FROM ${t.kv} WHERE k = ?`, [`lock:${name}`]);\n },\n\n cache: {\n async get(key) {\n await ready();\n const rows = await run(`SELECT v FROM ${t.kv} WHERE k = ? AND expires_at > ?`, [`cache:${key}`, Date.now()]);\n const value = rows[0]?.v;\n return value === undefined || value === null ? null : String(value);\n },\n async set(key, value, ttlMs) {\n await ready();\n await run(upsert(t.kv, [\"k\", \"v\", \"expires_at\"], [\"k\"]), [\n `cache:${key}`,\n value,\n Date.now() + Math.max(1, Math.round(ttlMs)),\n ]);\n },\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-DybhDphS.cjs';
|
|
2
|
+
import '../types-CctvUAs2.cjs';
|
|
3
|
+
|
|
4
|
+
type SqlDialect = "postgres" | "mysql" | "sqlite";
|
|
5
|
+
/**
|
|
6
|
+
* Runs one statement and resolves with its rows (an empty array for writes). Placeholders
|
|
7
|
+
* are `$1, $2…` for postgres and `?` for mysql and sqlite, matching each driver's own style.
|
|
8
|
+
*/
|
|
9
|
+
type SqlQuery = (sql: string, params: unknown[]) => Promise<Array<Record<string, unknown>>>;
|
|
10
|
+
type SqlStoreOptions = {
|
|
11
|
+
query: SqlQuery;
|
|
12
|
+
dialect: SqlDialect;
|
|
13
|
+
/** Prefix of the four tables (default "sermon_") */
|
|
14
|
+
tablePrefix?: string;
|
|
15
|
+
/** Create the tables on first use (default true). Turn off to run `migrate()` yourself. */
|
|
16
|
+
autoMigrate?: boolean;
|
|
17
|
+
};
|
|
18
|
+
type SqlStore = StatusStore & {
|
|
19
|
+
/** Create the tables and indexes if they don't exist. Safe to call repeatedly. */
|
|
20
|
+
migrate(): Promise<void>;
|
|
21
|
+
/** The statements `migrate()` runs, for migration tools */
|
|
22
|
+
readonly schema: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Store on any SQL database, with the driver you already use. You provide a `query` function;
|
|
26
|
+
* the store only needs four small tables, which it creates on first use.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* // PostgreSQL (pg)
|
|
30
|
+
* sqlStore({ dialect: "postgres", query: async (sql, params) => (await pool.query(sql, params)).rows });
|
|
31
|
+
* // MySQL / MariaDB (mysql2)
|
|
32
|
+
* sqlStore({ dialect: "mysql", query: async (sql, params) => { const [rows] = await pool.query(sql, params); return Array.isArray(rows) ? rows : []; } });
|
|
33
|
+
* // SQLite (better-sqlite3)
|
|
34
|
+
* sqlStore({ dialect: "sqlite", query: async (sql, params) => { const s = db.prepare(sql); return s.reader ? s.all(...params) : (s.run(...params), []); } });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function sqlStore({ query, dialect, tablePrefix, autoMigrate }: SqlStoreOptions): SqlStore;
|
|
38
|
+
|
|
39
|
+
export { type SqlDialect, type SqlQuery, type SqlStore, type SqlStoreOptions, sqlStore };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-_GuzSP0z.js';
|
|
2
|
+
import '../types-CctvUAs2.js';
|
|
3
|
+
|
|
4
|
+
type SqlDialect = "postgres" | "mysql" | "sqlite";
|
|
5
|
+
/**
|
|
6
|
+
* Runs one statement and resolves with its rows (an empty array for writes). Placeholders
|
|
7
|
+
* are `$1, $2…` for postgres and `?` for mysql and sqlite, matching each driver's own style.
|
|
8
|
+
*/
|
|
9
|
+
type SqlQuery = (sql: string, params: unknown[]) => Promise<Array<Record<string, unknown>>>;
|
|
10
|
+
type SqlStoreOptions = {
|
|
11
|
+
query: SqlQuery;
|
|
12
|
+
dialect: SqlDialect;
|
|
13
|
+
/** Prefix of the four tables (default "sermon_") */
|
|
14
|
+
tablePrefix?: string;
|
|
15
|
+
/** Create the tables on first use (default true). Turn off to run `migrate()` yourself. */
|
|
16
|
+
autoMigrate?: boolean;
|
|
17
|
+
};
|
|
18
|
+
type SqlStore = StatusStore & {
|
|
19
|
+
/** Create the tables and indexes if they don't exist. Safe to call repeatedly. */
|
|
20
|
+
migrate(): Promise<void>;
|
|
21
|
+
/** The statements `migrate()` runs, for migration tools */
|
|
22
|
+
readonly schema: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Store on any SQL database, with the driver you already use. You provide a `query` function;
|
|
26
|
+
* the store only needs four small tables, which it creates on first use.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* // PostgreSQL (pg)
|
|
30
|
+
* sqlStore({ dialect: "postgres", query: async (sql, params) => (await pool.query(sql, params)).rows });
|
|
31
|
+
* // MySQL / MariaDB (mysql2)
|
|
32
|
+
* sqlStore({ dialect: "mysql", query: async (sql, params) => { const [rows] = await pool.query(sql, params); return Array.isArray(rows) ? rows : []; } });
|
|
33
|
+
* // SQLite (better-sqlite3)
|
|
34
|
+
* sqlStore({ dialect: "sqlite", query: async (sql, params) => { const s = db.prepare(sql); return s.reader ? s.all(...params) : (s.run(...params), []); } });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function sqlStore({ query, dialect, tablePrefix, autoMigrate }: SqlStoreOptions): SqlStore;
|
|
38
|
+
|
|
39
|
+
export { type SqlDialect, type SqlQuery, type SqlStore, type SqlStoreOptions, sqlStore };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/stores/sql.ts
|
|
2
|
+
function schemaFor(dialect, t) {
|
|
3
|
+
const text = dialect === "mysql" ? "MEDIUMTEXT" : "TEXT";
|
|
4
|
+
const key = "VARCHAR(255)";
|
|
5
|
+
if (dialect === "mysql") {
|
|
6
|
+
return [
|
|
7
|
+
`CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,
|
|
8
|
+
`CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL, INDEX ${t.runs}_at (at_ms))`,
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms), INDEX ${t.samples}_at (at_ms))`,
|
|
10
|
+
`CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
`CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,
|
|
15
|
+
`CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL)`,
|
|
16
|
+
`CREATE INDEX IF NOT EXISTS ${t.runs}_at ON ${t.runs} (at_ms)`,
|
|
17
|
+
`CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms))`,
|
|
18
|
+
`CREATE INDEX IF NOT EXISTS ${t.samples}_at ON ${t.samples} (at_ms)`,
|
|
19
|
+
`CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function parse(value) {
|
|
23
|
+
if (typeof value !== "string") return value && typeof value === "object" ? value : void 0;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function sqlStore({ query, dialect, tablePrefix = "sermon_", autoMigrate = true }) {
|
|
31
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tablePrefix)) {
|
|
32
|
+
throw new Error("sermon-js: sqlStore tablePrefix may only contain letters, digits and underscores");
|
|
33
|
+
}
|
|
34
|
+
const t = {
|
|
35
|
+
latest: `${tablePrefix}latest`,
|
|
36
|
+
runs: `${tablePrefix}runs`,
|
|
37
|
+
samples: `${tablePrefix}samples`,
|
|
38
|
+
kv: `${tablePrefix}kv`
|
|
39
|
+
};
|
|
40
|
+
const schema = schemaFor(dialect, t);
|
|
41
|
+
const run = (sql, params = []) => {
|
|
42
|
+
let index = 0;
|
|
43
|
+
return query(dialect === "postgres" ? sql.replace(/\?/g, () => `$${++index}`) : sql, params);
|
|
44
|
+
};
|
|
45
|
+
const upsert = (table, columns, conflict) => {
|
|
46
|
+
const updates = columns.filter((column) => !conflict.includes(column));
|
|
47
|
+
const insert = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
48
|
+
return dialect === "mysql" ? `${insert} ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(", ")}` : `${insert} ON CONFLICT (${conflict.join(", ")}) DO UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(", ")}`;
|
|
49
|
+
};
|
|
50
|
+
const placeholders = (count) => Array.from({ length: count }, () => "?").join(", ");
|
|
51
|
+
let migrated;
|
|
52
|
+
const migrate = async () => {
|
|
53
|
+
for (const statement of schema) await query(statement, []);
|
|
54
|
+
};
|
|
55
|
+
const ready = () => {
|
|
56
|
+
if (!autoMigrate) return Promise.resolve();
|
|
57
|
+
migrated ??= migrate().catch((error) => {
|
|
58
|
+
migrated = void 0;
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
return migrated;
|
|
62
|
+
};
|
|
63
|
+
let sequence = 0;
|
|
64
|
+
const runId = (at) => `${at}-${(sequence++).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
65
|
+
return {
|
|
66
|
+
schema,
|
|
67
|
+
migrate,
|
|
68
|
+
async saveRun({ summary, results, samples }) {
|
|
69
|
+
await ready();
|
|
70
|
+
for (const result of results) {
|
|
71
|
+
await run(upsert(t.latest, ["service_key", "data"], ["service_key"]), [result.key, JSON.stringify(result)]);
|
|
72
|
+
}
|
|
73
|
+
for (const sample of samples) {
|
|
74
|
+
await run(upsert(t.samples, ["service_key", "at_ms", "data"], ["service_key", "at_ms"]), [
|
|
75
|
+
sample.key,
|
|
76
|
+
sample.at,
|
|
77
|
+
JSON.stringify(sample)
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
await run(`INSERT INTO ${t.runs} (id, at_ms, data) VALUES (?, ?, ?)`, [
|
|
81
|
+
runId(summary.at),
|
|
82
|
+
summary.at,
|
|
83
|
+
JSON.stringify(summary)
|
|
84
|
+
]);
|
|
85
|
+
},
|
|
86
|
+
async latest() {
|
|
87
|
+
await ready();
|
|
88
|
+
const rows = await run(`SELECT service_key, data FROM ${t.latest}`);
|
|
89
|
+
const latest = {};
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
const result = parse(row.data);
|
|
92
|
+
if (result) latest[String(row.service_key)] = result;
|
|
93
|
+
}
|
|
94
|
+
return latest;
|
|
95
|
+
},
|
|
96
|
+
async runs({ since, until }) {
|
|
97
|
+
await ready();
|
|
98
|
+
const rows = await run(`SELECT data FROM ${t.runs} WHERE at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`, [since, until]);
|
|
99
|
+
return rows.map((row) => parse(row.data)).filter((item) => !!item);
|
|
100
|
+
},
|
|
101
|
+
async samples(key, { since, until }) {
|
|
102
|
+
await ready();
|
|
103
|
+
const rows = await run(
|
|
104
|
+
`SELECT data FROM ${t.samples} WHERE service_key = ? AND at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`,
|
|
105
|
+
[key, since, until]
|
|
106
|
+
);
|
|
107
|
+
return rows.map((row) => parse(row.data)).filter((item) => !!item);
|
|
108
|
+
},
|
|
109
|
+
async prune({ before, keepKeys }) {
|
|
110
|
+
await ready();
|
|
111
|
+
await run(`DELETE FROM ${t.runs} WHERE at_ms < ?`, [before]);
|
|
112
|
+
await run(`DELETE FROM ${t.samples} WHERE at_ms < ?`, [before]);
|
|
113
|
+
if (!keepKeys) return;
|
|
114
|
+
if (keepKeys.length === 0) {
|
|
115
|
+
await run(`DELETE FROM ${t.latest}`);
|
|
116
|
+
await run(`DELETE FROM ${t.samples}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await run(`DELETE FROM ${t.latest} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);
|
|
120
|
+
await run(`DELETE FROM ${t.samples} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);
|
|
121
|
+
},
|
|
122
|
+
async lock(name, ttlMs) {
|
|
123
|
+
await ready();
|
|
124
|
+
const key = `lock:${name}`;
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
await run(`DELETE FROM ${t.kv} WHERE k = ? AND expires_at <= ?`, [key, now]);
|
|
127
|
+
try {
|
|
128
|
+
await run(`INSERT INTO ${t.kv} (k, v, expires_at) VALUES (?, ?, ?)`, [key, "1", now + Math.max(1, Math.round(ttlMs))]);
|
|
129
|
+
return { ok: true };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const rows = await run(`SELECT expires_at FROM ${t.kv} WHERE k = ?`, [key]);
|
|
132
|
+
const expiresAt = Number(rows[0]?.expires_at);
|
|
133
|
+
if (!rows.length || !Number.isFinite(expiresAt)) throw error;
|
|
134
|
+
return { ok: false, retryAfterMs: Math.max(1, expiresAt - Date.now()) };
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async unlock(name) {
|
|
138
|
+
await ready();
|
|
139
|
+
await run(`DELETE FROM ${t.kv} WHERE k = ?`, [`lock:${name}`]);
|
|
140
|
+
},
|
|
141
|
+
cache: {
|
|
142
|
+
async get(key) {
|
|
143
|
+
await ready();
|
|
144
|
+
const rows = await run(`SELECT v FROM ${t.kv} WHERE k = ? AND expires_at > ?`, [`cache:${key}`, Date.now()]);
|
|
145
|
+
const value = rows[0]?.v;
|
|
146
|
+
return value === void 0 || value === null ? null : String(value);
|
|
147
|
+
},
|
|
148
|
+
async set(key, value, ttlMs) {
|
|
149
|
+
await ready();
|
|
150
|
+
await run(upsert(t.kv, ["k", "v", "expires_at"], ["k"]), [
|
|
151
|
+
`cache:${key}`,
|
|
152
|
+
value,
|
|
153
|
+
Date.now() + Math.max(1, Math.round(ttlMs))
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { sqlStore };
|
|
161
|
+
//# sourceMappingURL=sql.js.map
|
|
162
|
+
//# sourceMappingURL=sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stores/sql.ts"],"names":[],"mappings":"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","file":"sql.js","sourcesContent":["import type { CheckSample, LockResult, RunSummary, StatusStore } from \"../store\";\nimport type { CheckResult } from \"../types\";\n\nexport type SqlDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\n/**\n * Runs one statement and resolves with its rows (an empty array for writes). Placeholders\n * are `$1, $2…` for postgres and `?` for mysql and sqlite, matching each driver's own style.\n */\nexport type SqlQuery = (sql: string, params: unknown[]) => Promise<Array<Record<string, unknown>>>;\n\nexport type SqlStoreOptions = {\n query: SqlQuery;\n dialect: SqlDialect;\n /** Prefix of the four tables (default \"sermon_\") */\n tablePrefix?: string;\n /** Create the tables on first use (default true). Turn off to run `migrate()` yourself. */\n autoMigrate?: boolean;\n};\n\nexport type SqlStore = StatusStore & {\n /** Create the tables and indexes if they don't exist. Safe to call repeatedly. */\n migrate(): Promise<void>;\n /** The statements `migrate()` runs, for migration tools */\n readonly schema: string[];\n};\n\nfunction schemaFor(dialect: SqlDialect, t: Record<\"latest\" | \"runs\" | \"samples\" | \"kv\", string>): string[] {\n const text = dialect === \"mysql\" ? \"MEDIUMTEXT\" : \"TEXT\";\n const key = \"VARCHAR(255)\";\n if (dialect === \"mysql\") {\n // MySQL has no CREATE INDEX IF NOT EXISTS: indexes are declared with the tables\n return [\n `CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,\n `CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL, INDEX ${t.runs}_at (at_ms))`,\n `CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms), INDEX ${t.samples}_at (at_ms))`,\n `CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`,\n ];\n }\n return [\n `CREATE TABLE IF NOT EXISTS ${t.latest} (service_key ${key} NOT NULL PRIMARY KEY, data ${text} NOT NULL)`,\n `CREATE TABLE IF NOT EXISTS ${t.runs} (id ${key} NOT NULL PRIMARY KEY, at_ms BIGINT NOT NULL, data ${text} NOT NULL)`,\n `CREATE INDEX IF NOT EXISTS ${t.runs}_at ON ${t.runs} (at_ms)`,\n `CREATE TABLE IF NOT EXISTS ${t.samples} (service_key ${key} NOT NULL, at_ms BIGINT NOT NULL, data ${text} NOT NULL, PRIMARY KEY (service_key, at_ms))`,\n `CREATE INDEX IF NOT EXISTS ${t.samples}_at ON ${t.samples} (at_ms)`,\n `CREATE TABLE IF NOT EXISTS ${t.kv} (k ${key} NOT NULL PRIMARY KEY, v ${text} NOT NULL, expires_at BIGINT NOT NULL)`,\n ];\n}\n\nfunction parse<T>(value: unknown): T | undefined {\n if (typeof value !== \"string\") return value && typeof value === \"object\" ? (value as T) : undefined;\n try {\n return JSON.parse(value) as T;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Store on any SQL database, with the driver you already use. You provide a `query` function;\n * the store only needs four small tables, which it creates on first use.\n *\n * ```ts\n * // PostgreSQL (pg)\n * sqlStore({ dialect: \"postgres\", query: async (sql, params) => (await pool.query(sql, params)).rows });\n * // MySQL / MariaDB (mysql2)\n * sqlStore({ dialect: \"mysql\", query: async (sql, params) => { const [rows] = await pool.query(sql, params); return Array.isArray(rows) ? rows : []; } });\n * // SQLite (better-sqlite3)\n * sqlStore({ dialect: \"sqlite\", query: async (sql, params) => { const s = db.prepare(sql); return s.reader ? s.all(...params) : (s.run(...params), []); } });\n * ```\n */\nexport function sqlStore({ query, dialect, tablePrefix = \"sermon_\", autoMigrate = true }: SqlStoreOptions): SqlStore {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tablePrefix)) {\n throw new Error(\"sermon-js: sqlStore tablePrefix may only contain letters, digits and underscores\");\n }\n const t = {\n latest: `${tablePrefix}latest`,\n runs: `${tablePrefix}runs`,\n samples: `${tablePrefix}samples`,\n kv: `${tablePrefix}kv`,\n };\n const schema = schemaFor(dialect, t);\n\n /** Write statements with `?` and convert to `$n` for postgres */\n const run = (sql: string, params: unknown[] = []) => {\n let index = 0;\n return query(dialect === \"postgres\" ? sql.replace(/\\?/g, () => `$${++index}`) : sql, params);\n };\n const upsert = (table: string, columns: string[], conflict: string[]) => {\n const updates = columns.filter((column) => !conflict.includes(column));\n const insert = `INSERT INTO ${table} (${columns.join(\", \")}) VALUES (${columns.map(() => \"?\").join(\", \")})`;\n return dialect === \"mysql\"\n ? `${insert} ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(\", \")}`\n : `${insert} ON CONFLICT (${conflict.join(\", \")}) DO UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(\", \")}`;\n };\n const placeholders = (count: number) => Array.from({ length: count }, () => \"?\").join(\", \");\n\n let migrated: Promise<void> | undefined;\n const migrate = async () => {\n for (const statement of schema) await query(statement, []);\n };\n const ready = () => {\n if (!autoMigrate) return Promise.resolve();\n migrated ??= migrate().catch((error) => {\n // Let the next call try again\n migrated = undefined;\n throw error;\n });\n return migrated;\n };\n\n let sequence = 0;\n const runId = (at: number) => `${at}-${(sequence++).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n\n return {\n schema,\n migrate,\n\n async saveRun({ summary, results, samples }) {\n await ready();\n for (const result of results) {\n await run(upsert(t.latest, [\"service_key\", \"data\"], [\"service_key\"]), [result.key, JSON.stringify(result)]);\n }\n for (const sample of samples) {\n await run(upsert(t.samples, [\"service_key\", \"at_ms\", \"data\"], [\"service_key\", \"at_ms\"]), [\n sample.key,\n sample.at,\n JSON.stringify(sample),\n ]);\n }\n await run(`INSERT INTO ${t.runs} (id, at_ms, data) VALUES (?, ?, ?)`, [\n runId(summary.at),\n summary.at,\n JSON.stringify(summary),\n ]);\n },\n\n async latest() {\n await ready();\n const rows = await run(`SELECT service_key, data FROM ${t.latest}`);\n const latest: Record<string, CheckResult> = {};\n for (const row of rows) {\n const result = parse<CheckResult>(row.data);\n if (result) latest[String(row.service_key)] = result;\n }\n return latest;\n },\n\n async runs({ since, until }) {\n await ready();\n const rows = await run(`SELECT data FROM ${t.runs} WHERE at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`, [since, until]);\n return rows.map((row) => parse<RunSummary>(row.data)).filter((item): item is RunSummary => !!item);\n },\n\n async samples(key, { since, until }) {\n await ready();\n const rows = await run(\n `SELECT data FROM ${t.samples} WHERE service_key = ? AND at_ms >= ? AND at_ms <= ? ORDER BY at_ms ASC`,\n [key, since, until]\n );\n return rows.map((row) => parse<CheckSample>(row.data)).filter((item): item is CheckSample => !!item);\n },\n\n async prune({ before, keepKeys }) {\n await ready();\n await run(`DELETE FROM ${t.runs} WHERE at_ms < ?`, [before]);\n await run(`DELETE FROM ${t.samples} WHERE at_ms < ?`, [before]);\n if (!keepKeys) return;\n if (keepKeys.length === 0) {\n await run(`DELETE FROM ${t.latest}`);\n await run(`DELETE FROM ${t.samples}`);\n return;\n }\n await run(`DELETE FROM ${t.latest} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);\n await run(`DELETE FROM ${t.samples} WHERE service_key NOT IN (${placeholders(keepKeys.length)})`, keepKeys);\n },\n\n async lock(name, ttlMs): Promise<LockResult> {\n await ready();\n const key = `lock:${name}`;\n const now = Date.now();\n await run(`DELETE FROM ${t.kv} WHERE k = ? AND expires_at <= ?`, [key, now]);\n try {\n // The primary key makes this atomic: only one caller can insert the row\n await run(`INSERT INTO ${t.kv} (k, v, expires_at) VALUES (?, ?, ?)`, [key, \"1\", now + Math.max(1, Math.round(ttlMs))]);\n return { ok: true };\n } catch (error) {\n const rows = await run(`SELECT expires_at FROM ${t.kv} WHERE k = ?`, [key]);\n const expiresAt = Number(rows[0]?.expires_at);\n // No row means the insert failed for another reason than \"already locked\"\n if (!rows.length || !Number.isFinite(expiresAt)) throw error;\n return { ok: false, retryAfterMs: Math.max(1, expiresAt - Date.now()) };\n }\n },\n\n async unlock(name) {\n await ready();\n await run(`DELETE FROM ${t.kv} WHERE k = ?`, [`lock:${name}`]);\n },\n\n cache: {\n async get(key) {\n await ready();\n const rows = await run(`SELECT v FROM ${t.kv} WHERE k = ? AND expires_at > ?`, [`cache:${key}`, Date.now()]);\n const value = rows[0]?.v;\n return value === undefined || value === null ? null : String(value);\n },\n async set(key, value, ttlMs) {\n await ready();\n await run(upsert(t.kv, [\"k\", \"v\", \"expires_at\"], [\"k\"]), [\n `cache:${key}`,\n value,\n Date.now() + Math.max(1, Math.round(ttlMs)),\n ]);\n },\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkZI442ZA5_cjs = require('../chunk-ZI442ZA5.cjs');
|
|
4
|
+
|
|
5
|
+
// src/stores/upstash.ts
|
|
6
|
+
function upstashStore({ url, token, prefix, fetch: fetchImpl = fetch }) {
|
|
7
|
+
if (!url || !token) throw new Error("sermon-js: upstashStore needs a url and a token");
|
|
8
|
+
const endpoint = `${url.replace(/\/+$/, "")}/pipeline`;
|
|
9
|
+
const exec = async (commands) => {
|
|
10
|
+
const response = await fetchImpl(endpoint, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
13
|
+
body: JSON.stringify(commands.map((command) => command.map(String)))
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) throw new Error(`sermon-js: Upstash request failed (HTTP ${response.status})`);
|
|
16
|
+
const replies = await response.json();
|
|
17
|
+
return replies.map((reply) => {
|
|
18
|
+
if (reply.error) throw new Error(`sermon-js: Upstash command failed: ${reply.error}`);
|
|
19
|
+
return reply.result;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
return chunkZI442ZA5_cjs.createRedisStore({ exec, prefix });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.upstashStore = upstashStore;
|
|
26
|
+
//# sourceMappingURL=upstash.cjs.map
|
|
27
|
+
//# sourceMappingURL=upstash.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stores/upstash.ts"],"names":["createRedisStore"],"mappings":";;;;;AAiBO,SAAS,YAAA,CAAa,EAAE,GAAA,EAAK,KAAA,EAAO,QAAQ,KAAA,EAAO,SAAA,GAAY,OAAM,EAAqC;AAC/G,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,OAAO,MAAM,IAAI,MAAM,iDAAiD,CAAA;AACrF,EAAA,MAAM,WAAW,CAAA,EAAG,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,SAAA,CAAA;AAE3C,EAAA,MAAM,IAAA,GAAkB,OAAO,QAAA,KAAa;AAC1C,IAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,QAAA,EAAU;AAAA,MACzC,MAAA,EAAQ,MAAA;AAAA,MACR,SAAS,EAAE,aAAA,EAAe,UAAU,KAAK,CAAA,CAAA,EAAI,gBAAgB,kBAAA,EAAmB;AAAA,MAChF,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,QAAA,CAAS,GAAA,CAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAC,CAAC;AAAA,KACpE,CAAA;AACD,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,wCAAA,EAA2C,QAAA,CAAS,MAAM,CAAA,CAAA,CAAG,CAAA;AAC/F,IAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAC,KAAA,KAAU;AAC5B,MAAA,IAAI,KAAA,CAAM,OAAO,MAAM,IAAI,MAAM,CAAA,mCAAA,EAAsC,KAAA,CAAM,KAAK,CAAA,CAAE,CAAA;AACpF,MAAA,OAAO,KAAA,CAAM,MAAA;AAAA,IACf,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,OAAOA,kCAAA,CAAiB,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA;AAC1C","file":"upstash.cjs","sourcesContent":["import type { StatusStore } from \"../store\";\nimport { createRedisStore, type RedisExec } from \"./redis-core\";\n\nexport type UpstashStoreOptions = {\n /** REST URL of the database (`UPSTASH_REDIS_REST_URL` / Vercel KV's `KV_REST_API_URL`) */\n url: string;\n /** REST token (`UPSTASH_REDIS_REST_TOKEN` / `KV_REST_API_TOKEN`) */\n token: string;\n /** Prefix of every key (default \"sermon\"): lets several status pages share one database */\n prefix?: string;\n fetch?: typeof fetch;\n};\n\n/**\n * Store on Upstash Redis or Vercel KV, over their REST API. It only needs `fetch`, so it works\n * on serverless and edge runtimes, with no client library to install.\n */\nexport function upstashStore({ url, token, prefix, fetch: fetchImpl = fetch }: UpstashStoreOptions): StatusStore {\n if (!url || !token) throw new Error(\"sermon-js: upstashStore needs a url and a token\");\n const endpoint = `${url.replace(/\\/+$/, \"\")}/pipeline`;\n\n const exec: RedisExec = async (commands) => {\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { authorization: `Bearer ${token}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(commands.map((command) => command.map(String))),\n });\n if (!response.ok) throw new Error(`sermon-js: Upstash request failed (HTTP ${response.status})`);\n const replies = (await response.json()) as Array<{ result?: unknown; error?: string }>;\n return replies.map((reply) => {\n if (reply.error) throw new Error(`sermon-js: Upstash command failed: ${reply.error}`);\n return reply.result;\n });\n };\n\n return createRedisStore({ exec, prefix });\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-DybhDphS.cjs';
|
|
2
|
+
import '../types-CctvUAs2.cjs';
|
|
3
|
+
|
|
4
|
+
type UpstashStoreOptions = {
|
|
5
|
+
/** REST URL of the database (`UPSTASH_REDIS_REST_URL` / Vercel KV's `KV_REST_API_URL`) */
|
|
6
|
+
url: string;
|
|
7
|
+
/** REST token (`UPSTASH_REDIS_REST_TOKEN` / `KV_REST_API_TOKEN`) */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Prefix of every key (default "sermon"): lets several status pages share one database */
|
|
10
|
+
prefix?: string;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Store on Upstash Redis or Vercel KV, over their REST API. It only needs `fetch`, so it works
|
|
15
|
+
* on serverless and edge runtimes, with no client library to install.
|
|
16
|
+
*/
|
|
17
|
+
declare function upstashStore({ url, token, prefix, fetch: fetchImpl }: UpstashStoreOptions): StatusStore;
|
|
18
|
+
|
|
19
|
+
export { type UpstashStoreOptions, upstashStore };
|