@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 +58 -0
- package/dist/index.d.ts +25 -10
- package/dist/index.js +65 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/src/db.ts +6 -4
- package/src/ids.ts +15 -0
- package/src/index.ts +2 -0
- package/src/read.ts +59 -2
- package/src/write.ts +15 -15
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+

|
|
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
|
|
1
|
+
import nodeWasm from 'node-sqlite3-wasm';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type Database = InstanceType<typeof nodeWasm.Database>;
|
|
4
|
+
declare function openStore(path?: string): Database;
|
|
4
5
|
|
|
5
|
-
declare function insertSession(db: Database
|
|
6
|
-
declare function upsertFeature(db: Database
|
|
7
|
-
declare function upsertScenario(db: Database
|
|
8
|
-
declare function upsertStep(db: Database
|
|
9
|
-
declare function insertRun(db: Database
|
|
10
|
-
declare function finaliseRun(db: Database
|
|
11
|
-
declare function insertArtifact(db: Database
|
|
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
|
-
|
|
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
|
|
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.
|
|
45
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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":"
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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
|
|
44
|
+
export function openStore(path = '.letsrunit/letsrunit.db'): Database {
|
|
43
45
|
const db = new Database(path);
|
|
44
|
-
db.
|
|
45
|
-
db.
|
|
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
|
-
|
|
2
|
-
|
|
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 '
|
|
1
|
+
import type { Database } from './db';
|
|
2
2
|
|
|
3
|
-
export function insertSession(db: Database
|
|
4
|
-
db.
|
|
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
|
|
8
|
-
db.
|
|
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
|
|
12
|
-
db.
|
|
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
|
|
16
|
-
db.
|
|
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
|
|
20
|
-
db.
|
|
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
|
|
24
|
-
db.
|
|
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
|
|
28
|
-
db.
|
|
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
|
}
|