@letsrunit/store 0.7.0 → 0.8.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 ADDED
@@ -0,0 +1,58 @@
1
+ ![letsrunit](https://cdn.jsdelivr.net/gh/letsrunit-hq/letsrunit@main/docs/.gitbook/assets/logo-light.svg)
2
+
3
+ # Store
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @letsrunit/store
9
+ # or
10
+ yarn add @letsrunit/store
11
+ ```
12
+
13
+ SQLite-backed persistence layer for letsrunit run history and artifacts. It manages the database schema and provides write functions for recording test sessions, features, scenarios, steps, runs, and file artifacts.
14
+
15
+ ## Schema
16
+
17
+ The store maintains six tables:
18
+
19
+ | Table | Purpose |
20
+ |---|---|
21
+ | `sessions` | One row per test run invocation, keyed by UUID with optional git commit |
22
+ | `features` | Gherkin feature files, keyed by a content hash of the file |
23
+ | `scenarios` | Gherkin scenarios, linked to their feature |
24
+ | `steps` | Individual steps within a scenario, ordered by index |
25
+ | `runs` | One row per scenario execution within a session; status is `running`, `passed`, or `failed` |
26
+ | `artifacts` | Files (screenshots, HTML snapshots) produced during a step |
27
+
28
+ ## API
29
+
30
+ ### `openStore(path?)`
31
+
32
+ Opens (or creates) the SQLite database at the given path. Defaults to `.letsrunit/letsrunit.db`. Enables WAL mode and foreign key enforcement, and runs the schema migration.
33
+
34
+ ```ts
35
+ import { openStore } from '@letsrunit/store';
36
+
37
+ const db = openStore(); // .letsrunit/letsrunit.db
38
+ ```
39
+
40
+ ### Write functions
41
+
42
+ All write functions take a `db` instance as their first argument.
43
+
44
+ | Function | Description |
45
+ |---|---|
46
+ | `insertSession(db, id, gitCommit, startedAt)` | Records a new test session |
47
+ | `upsertFeature(db, id, path, name)` | Inserts or replaces a feature file record |
48
+ | `upsertScenario(db, id, featureId, name)` | Inserts or replaces a scenario record |
49
+ | `upsertStep(db, id, scenarioId, idx, text)` | Inserts or replaces a step record |
50
+ | `insertRun(db, id, sessionId, scenarioId, startedAt)` | Starts a new run with status `running` |
51
+ | `finaliseRun(db, id, status, failedStepId?, error?)` | Updates a run's final status and optional failure details |
52
+ | `insertArtifact(db, id, runId, stepId, filename)` | Links a saved file to a specific step in a run |
53
+
54
+ ## Testing
55
+
56
+ ```bash
57
+ yarn test --project @letsrunit/store
58
+ ```
package/dist/index.d.ts CHANGED
@@ -1,13 +1,28 @@
1
- import Database from 'better-sqlite3';
1
+ import nodeWasm from 'node-sqlite3-wasm';
2
2
 
3
- declare function openStore(path?: string): Database.Database;
3
+ type Database = InstanceType<typeof nodeWasm.Database>;
4
+ declare function openStore(path?: string): Database;
4
5
 
5
- declare function insertSession(db: Database.Database, id: string, gitCommit: string | null, startedAt: number): void;
6
- declare function upsertFeature(db: Database.Database, id: string, path: string, name: string): void;
7
- declare function upsertScenario(db: Database.Database, id: string, featureId: string, name: string): void;
8
- declare function upsertStep(db: Database.Database, id: string, scenarioId: string, idx: number, text: string): void;
9
- declare function insertRun(db: Database.Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void;
10
- declare function finaliseRun(db: Database.Database, id: string, status: string, failedStepId?: string, error?: string): void;
11
- declare function insertArtifact(db: Database.Database, id: string, runId: string, stepId: string, filename: string): void;
6
+ declare function insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void;
7
+ declare function upsertFeature(db: Database, id: string, path: string, name: string): void;
8
+ declare function upsertScenario(db: Database, id: string, featureId: string, name: string): void;
9
+ declare function upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void;
10
+ declare function insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void;
11
+ declare function finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void;
12
+ declare function insertArtifact(db: Database, id: string, runId: string, stepId: string, filename: string): void;
12
13
 
13
- export { finaliseRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
14
+ declare function computeStepId(normalizedText: string): string;
15
+ declare function computeScenarioId(stepIds: string[]): string;
16
+ declare function computeFeatureId(uri: string): string;
17
+
18
+ declare function findLastRun(db: Database, scenarioId: string, status?: string, allowedCommits?: string[]): {
19
+ id: string;
20
+ gitCommit: string | null;
21
+ } | null;
22
+ declare function findArtifacts(db: Database, runId: string, stepId?: string): Array<{
23
+ filename: string;
24
+ stepId: string;
25
+ stepIdx: number;
26
+ }>;
27
+
28
+ export { computeFeatureId, computeScenarioId, computeStepId, finaliseRun, findArtifacts, findLastRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import Database from 'better-sqlite3';
1
+ import nodeWasm from 'node-sqlite3-wasm';
2
+ import { v5 } from 'uuid';
2
3
 
3
4
  // src/db.ts
5
+ var { Database } = nodeWasm;
4
6
  var SCHEMA = `
5
7
  CREATE TABLE IF NOT EXISTS sessions (
6
8
  id TEXT PRIMARY KEY,
@@ -41,35 +43,87 @@ CREATE TABLE IF NOT EXISTS artifacts (
41
43
  `;
42
44
  function openStore(path = ".letsrunit/letsrunit.db") {
43
45
  const db = new Database(path);
44
- db.pragma("journal_mode = WAL");
45
- db.pragma("foreign_keys = ON");
46
+ db.run("PRAGMA journal_mode = WAL");
47
+ db.run("PRAGMA foreign_keys = ON");
46
48
  db.exec(SCHEMA);
47
49
  return db;
48
50
  }
49
51
 
50
52
  // src/write.ts
51
53
  function insertSession(db, id, gitCommit, startedAt) {
52
- db.prepare("INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)").run(id, startedAt, gitCommit);
54
+ db.run("INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)", [id, startedAt, gitCommit]);
53
55
  }
54
56
  function upsertFeature(db, id, path, name) {
55
- db.prepare("INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)").run(id, path, name);
57
+ db.run("INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)", [id, path, name]);
56
58
  }
57
59
  function upsertScenario(db, id, featureId, name) {
58
- db.prepare("INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)").run(id, featureId, name);
60
+ db.run("INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)", [id, featureId, name]);
59
61
  }
60
62
  function upsertStep(db, id, scenarioId, idx, text) {
61
- db.prepare("INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)").run(id, scenarioId, idx, text);
63
+ db.run("INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)", [id, scenarioId, idx, text]);
62
64
  }
63
65
  function insertRun(db, id, sessionId, scenarioId, startedAt) {
64
- db.prepare("INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)").run(id, sessionId, scenarioId, startedAt);
66
+ db.run("INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)", [id, sessionId, scenarioId, startedAt]);
65
67
  }
66
68
  function finaliseRun(db, id, status, failedStepId, error) {
67
- db.prepare("UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?").run(status, failedStepId ?? null, error ?? null, id);
69
+ db.run("UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?", [status, failedStepId ?? null, error ?? null, id]);
68
70
  }
69
71
  function insertArtifact(db, id, runId, stepId, filename) {
70
- db.prepare("INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)").run(id, runId, stepId, filename);
72
+ db.run("INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)", [id, runId, stepId, filename]);
73
+ }
74
+ var UUID_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
75
+ function computeStepId(normalizedText) {
76
+ return v5(normalizedText, UUID_NAMESPACE);
77
+ }
78
+ function computeScenarioId(stepIds) {
79
+ return v5(stepIds.join(":"), UUID_NAMESPACE);
80
+ }
81
+ function computeFeatureId(uri) {
82
+ return v5(uri, UUID_NAMESPACE);
83
+ }
84
+
85
+ // src/read.ts
86
+ function findLastRun(db, scenarioId, status, allowedCommits) {
87
+ const conditions = ["r.scenario_id = ?"];
88
+ const params = [scenarioId];
89
+ if (status !== void 0) {
90
+ conditions.push("r.status = ?");
91
+ params.push(status);
92
+ }
93
+ if (allowedCommits !== void 0) {
94
+ conditions.push("s.git_commit IN (SELECT value FROM json_each(?))");
95
+ params.push(JSON.stringify(allowedCommits));
96
+ }
97
+ const sql = `
98
+ SELECT r.id, s.git_commit
99
+ FROM runs r
100
+ JOIN sessions s ON r.session_id = s.id
101
+ WHERE ${conditions.join(" AND ")}
102
+ ORDER BY r.started_at DESC
103
+ LIMIT 1
104
+ `;
105
+ const row = db.get(sql, params);
106
+ if (!row) return null;
107
+ return { id: row.id, gitCommit: row.git_commit };
108
+ }
109
+ function findArtifacts(db, runId, stepId) {
110
+ const conditions = ["a.run_id = ?"];
111
+ const params = [runId];
112
+ if (stepId !== void 0) {
113
+ conditions.push("a.step_id = ?");
114
+ params.push(stepId);
115
+ }
116
+ const sql = `
117
+ SELECT a.filename, a.step_id, st.idx
118
+ FROM artifacts a
119
+ JOIN steps st ON a.step_id = st.id
120
+ WHERE ${conditions.join(" AND ")}
121
+ ORDER BY st.idx ASC
122
+ `;
123
+ const rows = db.all(sql, params);
124
+ return rows.map((r) => ({ filename: r.filename, stepId: r.step_id, stepIdx: r.idx }));
71
125
  }
72
126
 
73
- export { finaliseRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
127
+ export { computeFeatureId, computeScenarioId, computeStepId, finaliseRun, findArtifacts, findLastRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
74
128
  //# sourceMappingURL=index.js.map
75
129
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db.ts","../src/write.ts"],"names":[],"mappings":";;;AAEA,IAAM,MAAA,GAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAuCR,SAAS,SAAA,CAAU,OAAO,yBAAA,EAA8C;AAC7E,EAAA,MAAM,EAAA,GAAK,IAAI,QAAA,CAAS,IAAI,CAAA;AAC5B,EAAA,EAAA,CAAG,OAAO,oBAAoB,CAAA;AAC9B,EAAA,EAAA,CAAG,OAAO,mBAAmB,CAAA;AAC7B,EAAA,EAAA,CAAG,KAAK,MAAM,CAAA;AACd,EAAA,OAAO,EAAA;AACT;;;AC7CO,SAAS,aAAA,CAAc,EAAA,EAAuB,EAAA,EAAY,SAAA,EAA0B,SAAA,EAAyB;AAClH,EAAA,EAAA,CAAG,QAAQ,8EAA8E,CAAA,CAAE,GAAA,CAAI,EAAA,EAAI,WAAW,SAAS,CAAA;AACzH;AAEO,SAAS,aAAA,CAAc,EAAA,EAAuB,EAAA,EAAY,IAAA,EAAc,IAAA,EAAoB;AACjG,EAAA,EAAA,CAAG,QAAQ,mEAAmE,CAAA,CAAE,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,CAAA;AACpG;AAEO,SAAS,cAAA,CAAe,EAAA,EAAuB,EAAA,EAAY,SAAA,EAAmB,IAAA,EAAoB;AACvG,EAAA,EAAA,CAAG,QAAQ,0EAA0E,CAAA,CAAE,GAAA,CAAI,EAAA,EAAI,WAAW,IAAI,CAAA;AAChH;AAEO,SAAS,UAAA,CAAW,EAAA,EAAuB,EAAA,EAAY,UAAA,EAAoB,KAAa,IAAA,EAAoB;AACjH,EAAA,EAAA,CAAG,QAAQ,+EAA+E,CAAA,CAAE,IAAI,EAAA,EAAI,UAAA,EAAY,KAAK,IAAI,CAAA;AAC3H;AAEO,SAAS,SAAA,CAAU,EAAA,EAAuB,EAAA,EAAY,SAAA,EAAmB,YAAoB,SAAA,EAAyB;AAC3H,EAAA,EAAA,CAAG,QAAQ,gFAAgF,CAAA,CAAE,IAAI,EAAA,EAAI,SAAA,EAAW,YAAY,SAAS,CAAA;AACvI;AAEO,SAAS,WAAA,CAAY,EAAA,EAAuB,EAAA,EAAY,MAAA,EAAgB,cAAuB,KAAA,EAAsB;AAC1H,EAAA,EAAA,CAAG,OAAA,CAAQ,wEAAwE,CAAA,CAAE,GAAA,CAAI,QAAQ,YAAA,IAAgB,IAAA,EAAM,KAAA,IAAS,IAAA,EAAM,EAAE,CAAA;AAC1I;AAEO,SAAS,cAAA,CAAe,EAAA,EAAuB,EAAA,EAAY,KAAA,EAAe,QAAgB,QAAA,EAAwB;AACvH,EAAA,EAAA,CAAG,QAAQ,2EAA2E,CAAA,CAAE,IAAI,EAAA,EAAI,KAAA,EAAO,QAAQ,QAAQ,CAAA;AACzH","file":"index.js","sourcesContent":["import Database from 'better-sqlite3';\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n started_at INTEGER NOT NULL,\n git_commit TEXT\n);\nCREATE TABLE IF NOT EXISTS features (\n id TEXT PRIMARY KEY,\n path TEXT NOT NULL,\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS scenarios (\n id TEXT PRIMARY KEY,\n feature_id TEXT NOT NULL REFERENCES features(id),\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS steps (\n id TEXT PRIMARY KEY,\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n idx INTEGER NOT NULL,\n text TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS runs (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n status TEXT NOT NULL DEFAULT 'running',\n failed_step_id TEXT REFERENCES steps(id),\n error TEXT,\n started_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS artifacts (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL REFERENCES runs(id),\n step_id TEXT NOT NULL REFERENCES steps(id),\n filename TEXT NOT NULL\n);\n`;\n\nexport function openStore(path = '.letsrunit/letsrunit.db'): Database.Database {\n const db = new Database(path);\n db.pragma('journal_mode = WAL');\n db.pragma('foreign_keys = ON');\n db.exec(SCHEMA);\n return db;\n}\n","import type Database from 'better-sqlite3';\n\nexport function insertSession(db: Database.Database, id: string, gitCommit: string | null, startedAt: number): void {\n db.prepare('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)').run(id, startedAt, gitCommit);\n}\n\nexport function upsertFeature(db: Database.Database, id: string, path: string, name: string): void {\n db.prepare('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)').run(id, path, name);\n}\n\nexport function upsertScenario(db: Database.Database, id: string, featureId: string, name: string): void {\n db.prepare('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)').run(id, featureId, name);\n}\n\nexport function upsertStep(db: Database.Database, id: string, scenarioId: string, idx: number, text: string): void {\n db.prepare('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)').run(id, scenarioId, idx, text);\n}\n\nexport function insertRun(db: Database.Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {\n db.prepare('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)').run(id, sessionId, scenarioId, startedAt);\n}\n\nexport function finaliseRun(db: Database.Database, id: string, status: string, failedStepId?: string, error?: string): void {\n db.prepare('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?').run(status, failedStepId ?? null, error ?? null, id);\n}\n\nexport function insertArtifact(db: Database.Database, id: string, runId: string, stepId: string, filename: string): void {\n db.prepare('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)').run(id, runId, stepId, filename);\n}\n"]}
1
+ {"version":3,"sources":["../src/db.ts","../src/write.ts","../src/ids.ts","../src/read.ts"],"names":["uuidv5"],"mappings":"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","file":"index.js","sourcesContent":["import nodeWasm from 'node-sqlite3-wasm';\nconst { Database } = nodeWasm;\nexport type Database = InstanceType<typeof nodeWasm.Database>;\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n started_at INTEGER NOT NULL,\n git_commit TEXT\n);\nCREATE TABLE IF NOT EXISTS features (\n id TEXT PRIMARY KEY,\n path TEXT NOT NULL,\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS scenarios (\n id TEXT PRIMARY KEY,\n feature_id TEXT NOT NULL REFERENCES features(id),\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS steps (\n id TEXT PRIMARY KEY,\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n idx INTEGER NOT NULL,\n text TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS runs (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n status TEXT NOT NULL DEFAULT 'running',\n failed_step_id TEXT REFERENCES steps(id),\n error TEXT,\n started_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS artifacts (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL REFERENCES runs(id),\n step_id TEXT NOT NULL REFERENCES steps(id),\n filename TEXT NOT NULL\n);\n`;\n\nexport function openStore(path = '.letsrunit/letsrunit.db'): Database {\n const db = new Database(path);\n db.run('PRAGMA journal_mode = WAL');\n db.run('PRAGMA foreign_keys = ON');\n db.exec(SCHEMA);\n return db;\n}\n","import type { Database } from './db';\n\nexport function insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void {\n db.run('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)', [id, startedAt, gitCommit]);\n}\n\nexport function upsertFeature(db: Database, id: string, path: string, name: string): void {\n db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [id, path, name]);\n}\n\nexport function upsertScenario(db: Database, id: string, featureId: string, name: string): void {\n db.run('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)', [id, featureId, name]);\n}\n\nexport function upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void {\n db.run('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)', [id, scenarioId, idx, text]);\n}\n\nexport function insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {\n db.run('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)', [id, sessionId, scenarioId, startedAt]);\n}\n\nexport function finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void {\n db.run('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?', [status, failedStepId ?? null, error ?? null, id]);\n}\n\nexport function insertArtifact(db: Database, id: string, runId: string, stepId: string, filename: string): void {\n db.run('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)', [id, runId, stepId, filename]);\n}\n","import { v5 as uuidv5 } from 'uuid';\n\nconst UUID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';\n\nexport function computeStepId(normalizedText: string): string {\n return uuidv5(normalizedText, UUID_NAMESPACE);\n}\n\nexport function computeScenarioId(stepIds: string[]): string {\n return uuidv5(stepIds.join(':'), UUID_NAMESPACE);\n}\n\nexport function computeFeatureId(uri: string): string {\n return uuidv5(uri, UUID_NAMESPACE);\n}\n","import type { Database } from './db';\n\nexport function findLastRun(\n db: Database,\n scenarioId: string,\n status?: string,\n allowedCommits?: string[],\n): { id: string; gitCommit: string | null } | null {\n const conditions: string[] = ['r.scenario_id = ?'];\n const params: (string | number | null)[] = [scenarioId];\n\n if (status !== undefined) {\n conditions.push('r.status = ?');\n params.push(status);\n }\n\n if (allowedCommits !== undefined) {\n conditions.push('s.git_commit IN (SELECT value FROM json_each(?))');\n params.push(JSON.stringify(allowedCommits));\n }\n\n const sql = `\n SELECT r.id, s.git_commit\n FROM runs r\n JOIN sessions s ON r.session_id = s.id\n WHERE ${conditions.join(' AND ')}\n ORDER BY r.started_at DESC\n LIMIT 1\n `;\n\n const row = db.get(sql, params) as { id: string; git_commit: string | null } | undefined;\n if (!row) return null;\n return { id: row.id, gitCommit: row.git_commit };\n}\n\nexport function findArtifacts(\n db: Database,\n runId: string,\n stepId?: string,\n): Array<{ filename: string; stepId: string; stepIdx: number }> {\n const conditions: string[] = ['a.run_id = ?'];\n const params: (string | number | null)[] = [runId];\n\n if (stepId !== undefined) {\n conditions.push('a.step_id = ?');\n params.push(stepId);\n }\n\n const sql = `\n SELECT a.filename, a.step_id, st.idx\n FROM artifacts a\n JOIN steps st ON a.step_id = st.id\n WHERE ${conditions.join(' AND ')}\n ORDER BY st.idx ASC\n `;\n\n const rows = db.all(sql, params) as Array<{ filename: string; step_id: string; idx: number }>;\n return rows.map((r) => ({ filename: r.filename, stepId: r.step_id, stepIdx: r.idx }));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsrunit/store",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "SQLite-backed artifact store for letsrunit",
5
5
  "keywords": [
6
6
  "testing",
@@ -51,10 +51,7 @@
51
51
  },
52
52
  "packageManager": "yarn@4.10.3",
53
53
  "dependencies": {
54
- "better-sqlite3": "^12.8.0",
54
+ "node-sqlite3-wasm": "^0.8.55",
55
55
  "uuid": "^13.0.0"
56
- },
57
- "devDependencies": {
58
- "@types/better-sqlite3": "^7.6.13"
59
56
  }
60
57
  }
package/src/db.ts CHANGED
@@ -1,4 +1,6 @@
1
- import Database from 'better-sqlite3';
1
+ import nodeWasm from 'node-sqlite3-wasm';
2
+ const { Database } = nodeWasm;
3
+ export type Database = InstanceType<typeof nodeWasm.Database>;
2
4
 
3
5
  const SCHEMA = `
4
6
  CREATE TABLE IF NOT EXISTS sessions (
@@ -39,10 +41,10 @@ CREATE TABLE IF NOT EXISTS artifacts (
39
41
  );
40
42
  `;
41
43
 
42
- export function openStore(path = '.letsrunit/letsrunit.db'): Database.Database {
44
+ export function openStore(path = '.letsrunit/letsrunit.db'): Database {
43
45
  const db = new Database(path);
44
- db.pragma('journal_mode = WAL');
45
- db.pragma('foreign_keys = ON');
46
+ db.run('PRAGMA journal_mode = WAL');
47
+ db.run('PRAGMA foreign_keys = ON');
46
48
  db.exec(SCHEMA);
47
49
  return db;
48
50
  }
package/src/ids.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { v5 as uuidv5 } from 'uuid';
2
+
3
+ const UUID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
4
+
5
+ export function computeStepId(normalizedText: string): string {
6
+ return uuidv5(normalizedText, UUID_NAMESPACE);
7
+ }
8
+
9
+ export function computeScenarioId(stepIds: string[]): string {
10
+ return uuidv5(stepIds.join(':'), UUID_NAMESPACE);
11
+ }
12
+
13
+ export function computeFeatureId(uri: string): string {
14
+ return uuidv5(uri, UUID_NAMESPACE);
15
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { openStore } from './db.js';
2
2
  export { insertSession, upsertFeature, upsertScenario, upsertStep, insertRun, finaliseRun, insertArtifact } from './write.js';
3
+ export { computeStepId, computeScenarioId, computeFeatureId } from './ids.js';
4
+ export { findLastRun, findArtifacts } from './read.js';
package/src/read.ts CHANGED
@@ -1,2 +1,59 @@
1
- // Read queries will be added when CLI/MCP integration is implemented.
2
- export {};
1
+ import type { Database } from './db';
2
+
3
+ export function findLastRun(
4
+ db: Database,
5
+ scenarioId: string,
6
+ status?: string,
7
+ allowedCommits?: string[],
8
+ ): { id: string; gitCommit: string | null } | null {
9
+ const conditions: string[] = ['r.scenario_id = ?'];
10
+ const params: (string | number | null)[] = [scenarioId];
11
+
12
+ if (status !== undefined) {
13
+ conditions.push('r.status = ?');
14
+ params.push(status);
15
+ }
16
+
17
+ if (allowedCommits !== undefined) {
18
+ conditions.push('s.git_commit IN (SELECT value FROM json_each(?))');
19
+ params.push(JSON.stringify(allowedCommits));
20
+ }
21
+
22
+ const sql = `
23
+ SELECT r.id, s.git_commit
24
+ FROM runs r
25
+ JOIN sessions s ON r.session_id = s.id
26
+ WHERE ${conditions.join(' AND ')}
27
+ ORDER BY r.started_at DESC
28
+ LIMIT 1
29
+ `;
30
+
31
+ const row = db.get(sql, params) as { id: string; git_commit: string | null } | undefined;
32
+ if (!row) return null;
33
+ return { id: row.id, gitCommit: row.git_commit };
34
+ }
35
+
36
+ export function findArtifacts(
37
+ db: Database,
38
+ runId: string,
39
+ stepId?: string,
40
+ ): Array<{ filename: string; stepId: string; stepIdx: number }> {
41
+ const conditions: string[] = ['a.run_id = ?'];
42
+ const params: (string | number | null)[] = [runId];
43
+
44
+ if (stepId !== undefined) {
45
+ conditions.push('a.step_id = ?');
46
+ params.push(stepId);
47
+ }
48
+
49
+ const sql = `
50
+ SELECT a.filename, a.step_id, st.idx
51
+ FROM artifacts a
52
+ JOIN steps st ON a.step_id = st.id
53
+ WHERE ${conditions.join(' AND ')}
54
+ ORDER BY st.idx ASC
55
+ `;
56
+
57
+ const rows = db.all(sql, params) as Array<{ filename: string; step_id: string; idx: number }>;
58
+ return rows.map((r) => ({ filename: r.filename, stepId: r.step_id, stepIdx: r.idx }));
59
+ }
package/src/write.ts CHANGED
@@ -1,29 +1,29 @@
1
- import type Database from 'better-sqlite3';
1
+ import type { Database } from './db';
2
2
 
3
- export function insertSession(db: Database.Database, id: string, gitCommit: string | null, startedAt: number): void {
4
- db.prepare('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)').run(id, startedAt, gitCommit);
3
+ export function insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void {
4
+ db.run('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)', [id, startedAt, gitCommit]);
5
5
  }
6
6
 
7
- export function upsertFeature(db: Database.Database, id: string, path: string, name: string): void {
8
- db.prepare('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)').run(id, path, name);
7
+ export function upsertFeature(db: Database, id: string, path: string, name: string): void {
8
+ db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [id, path, name]);
9
9
  }
10
10
 
11
- export function upsertScenario(db: Database.Database, id: string, featureId: string, name: string): void {
12
- db.prepare('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)').run(id, featureId, name);
11
+ export function upsertScenario(db: Database, id: string, featureId: string, name: string): void {
12
+ db.run('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)', [id, featureId, name]);
13
13
  }
14
14
 
15
- export function upsertStep(db: Database.Database, id: string, scenarioId: string, idx: number, text: string): void {
16
- db.prepare('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)').run(id, scenarioId, idx, text);
15
+ export function upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void {
16
+ db.run('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)', [id, scenarioId, idx, text]);
17
17
  }
18
18
 
19
- export function insertRun(db: Database.Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {
20
- db.prepare('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)').run(id, sessionId, scenarioId, startedAt);
19
+ export function insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {
20
+ db.run('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)', [id, sessionId, scenarioId, startedAt]);
21
21
  }
22
22
 
23
- export function finaliseRun(db: Database.Database, id: string, status: string, failedStepId?: string, error?: string): void {
24
- db.prepare('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?').run(status, failedStepId ?? null, error ?? null, id);
23
+ export function finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void {
24
+ db.run('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?', [status, failedStepId ?? null, error ?? null, id]);
25
25
  }
26
26
 
27
- export function insertArtifact(db: Database.Database, id: string, runId: string, stepId: string, filename: string): void {
28
- db.prepare('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)').run(id, runId, stepId, filename);
27
+ export function insertArtifact(db: Database, id: string, runId: string, stepId: string, filename: string): void {
28
+ db.run('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)', [id, runId, stepId, filename]);
29
29
  }