station-adapter-sqlite 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import Database from "better-sqlite3";
3
+ import { registerAdapter } from "station-signal";
4
+ const MODULE_URL = import.meta.url;
5
+ import { validateTableName, dateToStr, createColumnMapper, rowToObject } from "./shared.js";
6
+ const { toColumn, toField } = createColumnMapper({
7
+ signalName: "signal_name",
8
+ maxAttempts: "max_attempts",
9
+ nextRunAt: "next_run_at",
10
+ lastRunAt: "last_run_at",
11
+ startedAt: "started_at",
12
+ completedAt: "completed_at",
13
+ createdAt: "created_at",
14
+ });
15
+ const DATE_FIELDS = new Set(["nextRunAt", "lastRunAt", "startedAt", "completedAt", "createdAt"]);
16
+ const { toColumn: toStepColumn, toField: toStepField } = createColumnMapper({
17
+ runId: "run_id",
18
+ startedAt: "started_at",
19
+ completedAt: "completed_at",
20
+ });
21
+ const STEP_DATE_FIELDS = new Set(["startedAt", "completedAt"]);
22
+ function rowToRun(row) {
23
+ return rowToObject(row, toField, DATE_FIELDS);
24
+ }
25
+ function rowToStep(row) {
26
+ return rowToObject(row, toStepField, STEP_DATE_FIELDS);
27
+ }
28
+ export class SqliteAdapter {
29
+ db;
30
+ tableName;
31
+ options;
32
+ constructor(options = {}) {
33
+ this.options = options;
34
+ const dbPath = options.dbPath ?? "station.db";
35
+ this.tableName = validateTableName(options.tableName ?? "runs");
36
+ this.db = new Database(dbPath);
37
+ // Enable WAL mode and foreign keys
38
+ this.db.pragma("journal_mode = WAL");
39
+ this.db.pragma("foreign_keys = ON");
40
+ this.db.exec(`
41
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
42
+ id TEXT PRIMARY KEY,
43
+ signal_name TEXT NOT NULL,
44
+ kind TEXT NOT NULL,
45
+ input TEXT NOT NULL,
46
+ status TEXT NOT NULL DEFAULT 'pending',
47
+ attempts INTEGER NOT NULL DEFAULT 0,
48
+ max_attempts INTEGER NOT NULL DEFAULT 1,
49
+ timeout INTEGER NOT NULL,
50
+ interval TEXT,
51
+ next_run_at TEXT,
52
+ last_run_at TEXT,
53
+ started_at TEXT,
54
+ completed_at TEXT,
55
+ created_at TEXT NOT NULL,
56
+ output TEXT,
57
+ error TEXT
58
+ )
59
+ `);
60
+ // Migrate existing databases: add columns if missing
61
+ try {
62
+ this.db.exec(`ALTER TABLE ${this.tableName} ADD COLUMN output TEXT`);
63
+ }
64
+ catch { /* already exists */ }
65
+ try {
66
+ this.db.exec(`ALTER TABLE ${this.tableName} ADD COLUMN error TEXT`);
67
+ }
68
+ catch { /* already exists */ }
69
+ // Indexes for the two hot queries (getRunsDue / getRunsRunning)
70
+ this.db.exec(`
71
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status_next
72
+ ON ${this.tableName} (status, next_run_at)
73
+ `);
74
+ this.db.exec(`
75
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status_running
76
+ ON ${this.tableName} (status) WHERE status = 'running'
77
+ `);
78
+ // M3: Index on signal_name for listRuns queries
79
+ this.db.exec(`
80
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_signal_name
81
+ ON ${this.tableName} (signal_name)
82
+ `);
83
+ // Steps table with foreign key
84
+ this.db.exec(`
85
+ CREATE TABLE IF NOT EXISTS ${this.tableName}_steps (
86
+ id TEXT PRIMARY KEY,
87
+ run_id TEXT NOT NULL REFERENCES ${this.tableName}(id) ON DELETE CASCADE,
88
+ name TEXT NOT NULL,
89
+ status TEXT NOT NULL DEFAULT 'pending',
90
+ input TEXT,
91
+ output TEXT,
92
+ error TEXT,
93
+ started_at TEXT,
94
+ completed_at TEXT
95
+ )
96
+ `);
97
+ // Migrate existing step tables: add error column if missing
98
+ try {
99
+ this.db.exec(`ALTER TABLE ${this.tableName}_steps ADD COLUMN error TEXT`);
100
+ }
101
+ catch { /* already exists */ }
102
+ this.db.exec(`
103
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_steps_run_id
104
+ ON ${this.tableName}_steps (run_id)
105
+ `);
106
+ }
107
+ toManifest() {
108
+ return {
109
+ name: "sqlite",
110
+ options: this.options,
111
+ moduleUrl: MODULE_URL,
112
+ };
113
+ }
114
+ async addRun(run) {
115
+ this.db
116
+ .prepare(`INSERT INTO ${this.tableName}
117
+ (id, signal_name, kind, input, status, attempts, max_attempts,
118
+ timeout, interval, next_run_at, last_run_at, started_at,
119
+ completed_at, created_at, output, error)
120
+ VALUES
121
+ (@id, @signal_name, @kind, @input, @status, @attempts, @max_attempts,
122
+ @timeout, @interval, @next_run_at, @last_run_at, @started_at,
123
+ @completed_at, @created_at, @output, @error)`)
124
+ .run({
125
+ id: run.id,
126
+ signal_name: run.signalName,
127
+ kind: run.kind,
128
+ input: run.input,
129
+ status: run.status,
130
+ attempts: run.attempts,
131
+ max_attempts: run.maxAttempts,
132
+ timeout: run.timeout,
133
+ interval: run.interval ?? null,
134
+ next_run_at: dateToStr(run.nextRunAt),
135
+ last_run_at: dateToStr(run.lastRunAt),
136
+ started_at: dateToStr(run.startedAt),
137
+ completed_at: dateToStr(run.completedAt),
138
+ created_at: dateToStr(run.createdAt),
139
+ output: run.output ?? null,
140
+ error: run.error ?? null,
141
+ });
142
+ }
143
+ async removeRun(id) {
144
+ // Steps cascade via FOREIGN KEY ON DELETE CASCADE
145
+ this.db
146
+ .prepare(`DELETE FROM ${this.tableName} WHERE id = ?`)
147
+ .run(id);
148
+ }
149
+ async getRunsDue() {
150
+ const now = new Date().toISOString();
151
+ const rows = this.db
152
+ .prepare(`SELECT * FROM ${this.tableName}
153
+ WHERE status = 'pending'
154
+ AND (next_run_at IS NULL OR next_run_at <= ?)
155
+ ORDER BY created_at ASC`)
156
+ .all(now);
157
+ return rows.map(rowToRun);
158
+ }
159
+ async getRunsRunning() {
160
+ const rows = this.db
161
+ .prepare(`SELECT * FROM ${this.tableName} WHERE status = 'running'`)
162
+ .all();
163
+ return rows.map(rowToRun);
164
+ }
165
+ async getRun(id) {
166
+ const row = this.db
167
+ .prepare(`SELECT * FROM ${this.tableName} WHERE id = ?`)
168
+ .get(id);
169
+ return row ? rowToRun(row) : null;
170
+ }
171
+ /** Allowed RunPatch keys (L13: whitelist to prevent injection via unexpected keys). */
172
+ static RUN_PATCH_KEYS = new Set([
173
+ "input", "output", "error", "status", "attempts", "maxAttempts",
174
+ "timeout", "interval", "nextRunAt", "lastRunAt", "startedAt", "completedAt",
175
+ ]);
176
+ async updateRun(id, patch) {
177
+ const setClauses = [];
178
+ const values = { id };
179
+ for (const [key, value] of Object.entries(patch)) {
180
+ if (!SqliteAdapter.RUN_PATCH_KEYS.has(key))
181
+ continue;
182
+ if (value === undefined) {
183
+ const col = toColumn(key);
184
+ const param = `p_${col}`;
185
+ setClauses.push(`${col} = @${param}`);
186
+ values[param] = null;
187
+ }
188
+ else {
189
+ const col = toColumn(key);
190
+ const param = `p_${col}`;
191
+ setClauses.push(`${col} = @${param}`);
192
+ values[param] = DATE_FIELDS.has(key) ? dateToStr(value) : value;
193
+ }
194
+ }
195
+ if (setClauses.length === 0)
196
+ return;
197
+ this.db
198
+ .prepare(`UPDATE ${this.tableName} SET ${setClauses.join(", ")} WHERE id = @id`)
199
+ .run(values);
200
+ }
201
+ async listRuns(signalName) {
202
+ const rows = this.db
203
+ .prepare(`SELECT * FROM ${this.tableName} WHERE signal_name = ? ORDER BY created_at DESC`)
204
+ .all(signalName);
205
+ return rows.map(rowToRun);
206
+ }
207
+ async hasRunWithStatus(signalName, statuses) {
208
+ if (statuses.length === 0)
209
+ return false;
210
+ const placeholders = statuses.map(() => "?").join(", ");
211
+ const row = this.db
212
+ .prepare(`SELECT 1 FROM ${this.tableName} WHERE signal_name = ? AND status IN (${placeholders}) LIMIT 1`)
213
+ .get(signalName, ...statuses);
214
+ return row !== undefined;
215
+ }
216
+ async purgeRuns(olderThan, statuses) {
217
+ if (statuses.length === 0)
218
+ return 0;
219
+ const placeholders = statuses.map(() => "?").join(", ");
220
+ const cutoff = olderThan.toISOString();
221
+ // Steps cascade via FOREIGN KEY ON DELETE CASCADE
222
+ const result = this.db
223
+ .prepare(`DELETE FROM ${this.tableName} WHERE status IN (${placeholders}) AND completed_at IS NOT NULL AND completed_at < ?`)
224
+ .run(...statuses, cutoff);
225
+ return result.changes;
226
+ }
227
+ async addStep(step) {
228
+ this.db
229
+ .prepare(`INSERT INTO ${this.tableName}_steps
230
+ (id, run_id, name, status, input, output, error, started_at, completed_at)
231
+ VALUES
232
+ (@id, @run_id, @name, @status, @input, @output, @error, @started_at, @completed_at)`)
233
+ .run({
234
+ id: step.id,
235
+ run_id: step.runId,
236
+ name: step.name,
237
+ status: step.status,
238
+ input: step.input ?? null,
239
+ output: step.output ?? null,
240
+ error: step.error ?? null,
241
+ started_at: dateToStr(step.startedAt),
242
+ completed_at: dateToStr(step.completedAt),
243
+ });
244
+ }
245
+ /** Allowed StepPatch keys. */
246
+ static STEP_PATCH_KEYS = new Set([
247
+ "status", "input", "output", "error", "startedAt", "completedAt",
248
+ ]);
249
+ async updateStep(id, patch) {
250
+ const setClauses = [];
251
+ const values = { id };
252
+ for (const [key, value] of Object.entries(patch)) {
253
+ if (!SqliteAdapter.STEP_PATCH_KEYS.has(key))
254
+ continue;
255
+ if (value === undefined) {
256
+ const col = toStepColumn(key);
257
+ const param = `p_${col}`;
258
+ setClauses.push(`${col} = @${param}`);
259
+ values[param] = null;
260
+ }
261
+ else {
262
+ const col = toStepColumn(key);
263
+ const param = `p_${col}`;
264
+ setClauses.push(`${col} = @${param}`);
265
+ values[param] = STEP_DATE_FIELDS.has(key) ? dateToStr(value) : value;
266
+ }
267
+ }
268
+ if (setClauses.length === 0)
269
+ return;
270
+ this.db
271
+ .prepare(`UPDATE ${this.tableName}_steps SET ${setClauses.join(", ")} WHERE id = @id`)
272
+ .run(values);
273
+ }
274
+ async getSteps(runId) {
275
+ const rows = this.db
276
+ .prepare(`SELECT * FROM ${this.tableName}_steps WHERE run_id = ?`)
277
+ .all(runId);
278
+ return rows.map(rowToStep);
279
+ }
280
+ async removeSteps(runId) {
281
+ this.db
282
+ .prepare(`DELETE FROM ${this.tableName}_steps WHERE run_id = ?`)
283
+ .run(runId);
284
+ }
285
+ async ping() {
286
+ try {
287
+ this.db.prepare("SELECT 1").get();
288
+ return true;
289
+ }
290
+ catch {
291
+ return false;
292
+ }
293
+ }
294
+ generateId() {
295
+ return randomUUID();
296
+ }
297
+ async close() {
298
+ this.db.close();
299
+ }
300
+ }
301
+ // Register in the adapter factory for cross-process reconstruction
302
+ registerAdapter("sqlite", (options) => new SqliteAdapter(options));
303
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** Validate table name to prevent SQL injection (alphanumeric + underscores only). */
2
+ export declare function validateTableName(name: string): string;
3
+ /** Serialise a Date to ISO string, or pass through null/undefined. */
4
+ export declare function dateToStr(value: unknown): string | null;
5
+ /** Deserialise an ISO string back to Date, or return undefined. */
6
+ export declare function strToDate(value: unknown): Date | undefined;
7
+ /** Create forward and reverse column mappers from a camelCase→snake_case mapping. */
8
+ export declare function createColumnMapper(mappings: Record<string, string>): {
9
+ toColumn: (key: string) => string;
10
+ toField: (col: string) => string;
11
+ };
12
+ /** Map a raw SQLite row to a typed object, converting date fields. */
13
+ export declare function rowToObject<T>(row: Record<string, unknown>, toField: (col: string) => string, dateFields: Set<string>): T;
14
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIvD;AAED,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAG1D;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;oBAK/C,MAAM,KAAG,MAAM;mBAChB,MAAM,KAAG,MAAM;EAEjC;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,CAAC,EAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAChC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB,CAAC,CAWH"}
package/dist/shared.js ADDED
@@ -0,0 +1,44 @@
1
+ /** Validate table name to prevent SQL injection (alphanumeric + underscores only). */
2
+ export function validateTableName(name) {
3
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
4
+ throw new Error(`Invalid table name "${name}". Only alphanumeric characters and underscores are allowed.`);
5
+ }
6
+ return name;
7
+ }
8
+ /** Serialise a Date to ISO string, or pass through null/undefined. */
9
+ export function dateToStr(value) {
10
+ if (value instanceof Date)
11
+ return value.toISOString();
12
+ if (value === undefined || value === null)
13
+ return null;
14
+ return String(value);
15
+ }
16
+ /** Deserialise an ISO string back to Date, or return undefined. */
17
+ export function strToDate(value) {
18
+ if (typeof value === "string")
19
+ return new Date(value);
20
+ return undefined;
21
+ }
22
+ /** Create forward and reverse column mappers from a camelCase→snake_case mapping. */
23
+ export function createColumnMapper(mappings) {
24
+ const reverse = Object.fromEntries(Object.entries(mappings).map(([k, v]) => [v, k]));
25
+ return {
26
+ toColumn: (key) => mappings[key] ?? key,
27
+ toField: (col) => reverse[col] ?? col,
28
+ };
29
+ }
30
+ /** Map a raw SQLite row to a typed object, converting date fields. */
31
+ export function rowToObject(row, toField, dateFields) {
32
+ const obj = {};
33
+ for (const [col, value] of Object.entries(row)) {
34
+ const field = toField(col);
35
+ if (dateFields.has(field)) {
36
+ obj[field] = value != null ? strToDate(value) : undefined;
37
+ }
38
+ else {
39
+ obj[field] = value;
40
+ }
41
+ }
42
+ return obj;
43
+ }
44
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,8DAA8D,CAAC,CAAC;IAC7G,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IACtD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IACtD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,QAAgC;IACjE,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAChC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CACjD,CAAC;IACF,OAAO;QACL,QAAQ,EAAE,CAAC,GAAW,EAAU,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG;QACvD,OAAO,EAAE,CAAC,GAAW,EAAU,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG;KACtD,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,WAAW,CACzB,GAA4B,EAC5B,OAAgC,EAChC,UAAuB;IAEvB,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5D,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,GAAmB,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "station-adapter-sqlite",
3
+ "version": "1.0.0",
4
+ "description": "SQLite adapter for station-signal using better-sqlite3",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./broadcast": {
13
+ "types": "./dist/broadcast.d.ts",
14
+ "import": "./dist/broadcast.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "better-sqlite3": "^11.9.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/better-sqlite3": "^7.6.13"
31
+ },
32
+ "peerDependencies": {
33
+ "station-signal": "1.0.0",
34
+ "station-broadcast": "1.0.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "typecheck": "tsc --noEmit"
39
+ }
40
+ }