@drej/sqlite 0.2.0 → 0.3.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.d.mts +23 -0
- package/dist/index.mjs +155 -0
- package/package.json +18 -5
- package/CHANGELOG.md +0 -69
- package/src/adapter.ts +0 -167
- package/src/index.ts +0 -1
- package/src/migrations.ts +0 -16
- package/test/adapter.test.ts +0 -189
- package/tsconfig.json +0 -13
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CheckpointInfo, EnvironmentRecord, IStorageAdapter, LedgerEntry, ListSandboxOptions, SandboxDetails } from "@drej/core";
|
|
2
|
+
|
|
3
|
+
//#region src/adapter.d.ts
|
|
4
|
+
declare class SQLiteAdapter implements IStorageAdapter {
|
|
5
|
+
private readonly db;
|
|
6
|
+
constructor(path: string);
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
append(entry: LedgerEntry): Promise<void>;
|
|
10
|
+
readAll(name: string, sandboxId: string): Promise<LedgerEntry[]>;
|
|
11
|
+
lastCheckpoint(name: string, sandboxId: string): Promise<LedgerEntry | null>;
|
|
12
|
+
listSandboxDetails(name: string, opts?: ListSandboxOptions): Promise<SandboxDetails[]>;
|
|
13
|
+
listAllSandboxDetails(opts?: ListSandboxOptions): Promise<SandboxDetails[]>;
|
|
14
|
+
getSandboxDetails(name: string, sandboxId: string): Promise<SandboxDetails | null>;
|
|
15
|
+
deleteSandbox(name: string, sandboxId: string): Promise<void>;
|
|
16
|
+
listCheckpoints(name: string, sandboxId: string): Promise<CheckpointInfo[]>;
|
|
17
|
+
getEnvironment(name: string): Promise<EnvironmentRecord | null>;
|
|
18
|
+
saveEnvironment(record: EnvironmentRecord): Promise<void>;
|
|
19
|
+
deleteEnvironment(name: string): Promise<void>;
|
|
20
|
+
listEnvironments(): Promise<EnvironmentRecord[]>;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { SQLiteAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { SandboxStatus } from "@drej/core";
|
|
3
|
+
//#region src/migrations.ts
|
|
4
|
+
const MIGRATION_SQL = `
|
|
5
|
+
CREATE TABLE IF NOT EXISTS drej_events (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
sandbox_id TEXT NOT NULL,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
step_idx INTEGER NOT NULL,
|
|
10
|
+
branch INTEGER,
|
|
11
|
+
event TEXT NOT NULL,
|
|
12
|
+
payload TEXT,
|
|
13
|
+
error TEXT,
|
|
14
|
+
ts INTEGER NOT NULL
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS drej_events_sandbox_id ON drej_events(sandbox_id);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS drej_events_name ON drej_events(name);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS drej_environments (
|
|
21
|
+
name TEXT PRIMARY KEY,
|
|
22
|
+
snapshot_id TEXT NOT NULL,
|
|
23
|
+
image TEXT NOT NULL,
|
|
24
|
+
built_at INTEGER NOT NULL
|
|
25
|
+
);
|
|
26
|
+
`;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/adapter.ts
|
|
29
|
+
const AGG_SQL = (whereClause) => `
|
|
30
|
+
WITH agg AS (
|
|
31
|
+
SELECT
|
|
32
|
+
name,
|
|
33
|
+
sandbox_id,
|
|
34
|
+
MIN(CASE WHEN event = 'sandbox_created' THEN ts END) AS started_at,
|
|
35
|
+
MAX(CASE WHEN event = 'sandbox_closed' THEN ts END) AS completed_at,
|
|
36
|
+
MAX(CASE WHEN event = 'sandbox_closed' THEN 1 ELSE 0 END) AS is_closed,
|
|
37
|
+
CAST(COUNT(CASE WHEN event = 'exec_complete' THEN 1 END) AS INTEGER) AS exec_count
|
|
38
|
+
FROM drej_events
|
|
39
|
+
${whereClause}
|
|
40
|
+
GROUP BY name, sandbox_id
|
|
41
|
+
)
|
|
42
|
+
SELECT * FROM agg WHERE started_at IS NOT NULL ORDER BY started_at DESC
|
|
43
|
+
`;
|
|
44
|
+
function aggRowToDetails(row) {
|
|
45
|
+
return {
|
|
46
|
+
name: row.name,
|
|
47
|
+
sandboxId: row.sandbox_id,
|
|
48
|
+
status: row.is_closed ? SandboxStatus.Completed : SandboxStatus.Running,
|
|
49
|
+
startedAt: row.started_at,
|
|
50
|
+
completedAt: row.completed_at ?? void 0,
|
|
51
|
+
execCount: row.exec_count
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function applyOpts(details, opts) {
|
|
55
|
+
let result = details;
|
|
56
|
+
if (opts?.before != null) result = result.filter((d) => d.startedAt < opts.before);
|
|
57
|
+
if (opts?.status != null) result = result.filter((d) => d.status === opts.status);
|
|
58
|
+
if (opts?.limit != null) result = result.slice(0, opts.limit);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function rowToEntry(row) {
|
|
62
|
+
return {
|
|
63
|
+
sandboxId: row.sandbox_id,
|
|
64
|
+
name: row.name,
|
|
65
|
+
stepIndex: row.step_idx,
|
|
66
|
+
branch: row.branch ?? void 0,
|
|
67
|
+
event: row.event,
|
|
68
|
+
payload: row.payload !== null ? JSON.parse(row.payload) : void 0,
|
|
69
|
+
error: row.error ?? void 0,
|
|
70
|
+
ts: row.ts
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var SQLiteAdapter = class {
|
|
74
|
+
db;
|
|
75
|
+
constructor(path) {
|
|
76
|
+
this.db = new Database(path, { create: true });
|
|
77
|
+
}
|
|
78
|
+
async connect() {
|
|
79
|
+
this.db.exec(MIGRATION_SQL);
|
|
80
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
81
|
+
}
|
|
82
|
+
async close() {
|
|
83
|
+
this.db.close();
|
|
84
|
+
}
|
|
85
|
+
async append(entry) {
|
|
86
|
+
this.db.prepare(`INSERT INTO drej_events (sandbox_id, name, step_idx, branch, event, payload, error, ts)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.sandboxId, entry.name, entry.stepIndex, entry.branch ?? null, entry.event, entry.payload !== void 0 ? JSON.stringify(entry.payload) : null, entry.error ?? null, entry.ts);
|
|
88
|
+
}
|
|
89
|
+
async readAll(name, sandboxId) {
|
|
90
|
+
return this.db.prepare(`SELECT sandbox_id, name, step_idx, branch, event, payload, error, ts
|
|
91
|
+
FROM drej_events
|
|
92
|
+
WHERE name = ? AND sandbox_id = ?
|
|
93
|
+
ORDER BY ts ASC`).all(name, sandboxId).map(rowToEntry);
|
|
94
|
+
}
|
|
95
|
+
async lastCheckpoint(name, sandboxId) {
|
|
96
|
+
const row = this.db.prepare(`SELECT sandbox_id, name, step_idx, branch, event, payload, error, ts
|
|
97
|
+
FROM drej_events
|
|
98
|
+
WHERE name = ? AND sandbox_id = ? AND event = 'checkpoint_created'
|
|
99
|
+
ORDER BY ts DESC
|
|
100
|
+
LIMIT 1`).get(name, sandboxId);
|
|
101
|
+
return row ? rowToEntry(row) : null;
|
|
102
|
+
}
|
|
103
|
+
async listSandboxDetails(name, opts) {
|
|
104
|
+
return applyOpts(this.db.prepare(AGG_SQL("WHERE name = ?")).all(name).map(aggRowToDetails), opts);
|
|
105
|
+
}
|
|
106
|
+
async listAllSandboxDetails(opts) {
|
|
107
|
+
return applyOpts(this.db.prepare(AGG_SQL("")).all().map(aggRowToDetails), opts);
|
|
108
|
+
}
|
|
109
|
+
async getSandboxDetails(name, sandboxId) {
|
|
110
|
+
const row = this.db.prepare(AGG_SQL("WHERE name = ? AND sandbox_id = ?")).get(name, sandboxId);
|
|
111
|
+
return row ? aggRowToDetails(row) : null;
|
|
112
|
+
}
|
|
113
|
+
async deleteSandbox(name, sandboxId) {
|
|
114
|
+
this.db.prepare(`DELETE FROM drej_events WHERE name = ? AND sandbox_id = ?`).run(name, sandboxId);
|
|
115
|
+
}
|
|
116
|
+
async listCheckpoints(name, sandboxId) {
|
|
117
|
+
return this.db.prepare(`SELECT sandbox_id, name, step_idx, branch, event, payload, error, ts
|
|
118
|
+
FROM drej_events
|
|
119
|
+
WHERE name = ? AND sandbox_id = ? AND event = 'checkpoint_created'
|
|
120
|
+
ORDER BY ts ASC`).all(name, sandboxId).map((r) => {
|
|
121
|
+
const p = r.payload !== null ? JSON.parse(r.payload) : { snapshotId: "" };
|
|
122
|
+
return {
|
|
123
|
+
snapshotId: p.snapshotId,
|
|
124
|
+
tag: p.name,
|
|
125
|
+
createdAt: r.ts
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async getEnvironment(name) {
|
|
130
|
+
const row = this.db.prepare("SELECT name, snapshot_id, image, built_at FROM drej_environments WHERE name = ?").get(name);
|
|
131
|
+
return row ? {
|
|
132
|
+
name: row.name,
|
|
133
|
+
snapshotId: row.snapshot_id,
|
|
134
|
+
image: row.image,
|
|
135
|
+
builtAt: row.built_at
|
|
136
|
+
} : null;
|
|
137
|
+
}
|
|
138
|
+
async saveEnvironment(record) {
|
|
139
|
+
this.db.prepare(`INSERT INTO drej_environments (name, snapshot_id, image, built_at) VALUES (?, ?, ?, ?)
|
|
140
|
+
ON CONFLICT(name) DO UPDATE SET snapshot_id = excluded.snapshot_id, image = excluded.image, built_at = excluded.built_at`).run(record.name, record.snapshotId, record.image, record.builtAt);
|
|
141
|
+
}
|
|
142
|
+
async deleteEnvironment(name) {
|
|
143
|
+
this.db.prepare("DELETE FROM drej_environments WHERE name = ?").run(name);
|
|
144
|
+
}
|
|
145
|
+
async listEnvironments() {
|
|
146
|
+
return this.db.prepare("SELECT name, snapshot_id, image, built_at FROM drej_environments ORDER BY built_at DESC").all().map((r) => ({
|
|
147
|
+
name: r.name,
|
|
148
|
+
snapshotId: r.snapshot_id,
|
|
149
|
+
image: r.image,
|
|
150
|
+
builtAt: r.built_at
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
export { SQLiteAdapter };
|
package/package.json
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drej/sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
4
16
|
"publishConfig": {
|
|
5
17
|
"access": "public"
|
|
6
18
|
},
|
|
7
|
-
"main": "./src/index.ts",
|
|
8
|
-
"dependencies": {
|
|
9
|
-
"@drej/core": "workspace:*"
|
|
10
|
-
},
|
|
11
19
|
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
12
21
|
"test": "bun test"
|
|
13
22
|
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@drej/core": "workspace:*"
|
|
25
|
+
},
|
|
14
26
|
"devDependencies": {
|
|
15
27
|
"bun-types": "1.3.14",
|
|
28
|
+
"tsdown": "0.22.3",
|
|
16
29
|
"typescript": "6.0.3"
|
|
17
30
|
}
|
|
18
31
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# @drej/sqlite
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- 2fd33e0: feat: run management API
|
|
8
|
-
|
|
9
|
-
Add `RunStatus` enum, `RunDetails` type, and `ListRunsOptions` for filtering. Replace `listRuns()` with `listRunDetails()`, `listAllRunDetails()`, `getRunDetails()`, and `deleteRun()` on both `IStorageAdapter` and `DrejClient`. Add `WorkflowRun.status` property that tracks execution state as events are consumed.
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- Updated dependencies [22b8a32]
|
|
14
|
-
- Updated dependencies [799b6dd]
|
|
15
|
-
- Updated dependencies [8d9d8bb]
|
|
16
|
-
- Updated dependencies [2fd33e0]
|
|
17
|
-
- Updated dependencies [0d94c2a]
|
|
18
|
-
- @drej/core@0.3.0
|
|
19
|
-
|
|
20
|
-
## 0.1.2
|
|
21
|
-
|
|
22
|
-
### Patch Changes
|
|
23
|
-
|
|
24
|
-
- Updated dependencies [4c0ad93]
|
|
25
|
-
- Updated dependencies [b04f8eb]
|
|
26
|
-
- Updated dependencies [a971b7b]
|
|
27
|
-
- Updated dependencies [ce173be]
|
|
28
|
-
- Updated dependencies [86c2dde]
|
|
29
|
-
- Updated dependencies [82094ae]
|
|
30
|
-
- @drej/core@0.2.0
|
|
31
|
-
|
|
32
|
-
## 0.1.1
|
|
33
|
-
|
|
34
|
-
### Patch Changes
|
|
35
|
-
|
|
36
|
-
- 0ea4c33: Rename npm scope from `@drej/*` to `@drej/*` and add TSDoc to all public API surfaces.
|
|
37
|
-
|
|
38
|
-
- All workspace packages now published under `@drej/*` (e.g. `@drej/sqlite`, `@drej/postgres`)
|
|
39
|
-
- `DrejClient`, `WorkflowBuilder`, `SandboxStepBuilder`, `IStorageAdapter`, `LedgerEvent`, `SandboxOpts` and all their members now have hover documentation visible in VS Code
|
|
40
|
-
|
|
41
|
-
- Updated dependencies [0ea4c33]
|
|
42
|
-
- @drej/core@0.1.1
|
|
43
|
-
|
|
44
|
-
## 0.1.0
|
|
45
|
-
|
|
46
|
-
### Minor Changes
|
|
47
|
-
|
|
48
|
-
- 5d77498: Introduce `@drej/sqlite` storage adapter.
|
|
49
|
-
|
|
50
|
-
`SQLiteAdapter` implements `IStorageAdapter` via Bun's built-in `bun:sqlite` — zero extra dependencies and no infrastructure required. Data persists across restarts, making it suitable for local development and single-process production workloads.
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { DrejClient } from "drej";
|
|
54
|
-
import { SQLiteAdapter } from "@drej/sqlite";
|
|
55
|
-
|
|
56
|
-
const client = new DrejClient({
|
|
57
|
-
baseUrl: "http://localhost:8080",
|
|
58
|
-
adapter: new SQLiteAdapter("./drej.db"),
|
|
59
|
-
});
|
|
60
|
-
await client.connect();
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
WAL mode is enabled on `connect()` for safe concurrent reads alongside ongoing writes.
|
|
64
|
-
|
|
65
|
-
### Patch Changes
|
|
66
|
-
|
|
67
|
-
- Updated dependencies [82e77fd]
|
|
68
|
-
- Updated dependencies [5d77498]
|
|
69
|
-
- @drej/core@0.1.0
|
package/src/adapter.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import type { IStorageAdapter, LedgerEntry, LedgerEvent, RunDetails, ListRunsOptions } from "@drej/core";
|
|
3
|
-
import { RunStatus } from "@drej/core";
|
|
4
|
-
import { MIGRATION_SQL } from "./migrations";
|
|
5
|
-
|
|
6
|
-
type Row = {
|
|
7
|
-
run_id: string;
|
|
8
|
-
wf_name: string;
|
|
9
|
-
step_idx: number;
|
|
10
|
-
branch: number | null;
|
|
11
|
-
event: string;
|
|
12
|
-
payload: string | null;
|
|
13
|
-
error: string | null;
|
|
14
|
-
ts: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type AggRow = {
|
|
18
|
-
wf_name: string;
|
|
19
|
-
run_id: string;
|
|
20
|
-
started_at: number | null;
|
|
21
|
-
completed_at: number | null;
|
|
22
|
-
terminal_event: string | null;
|
|
23
|
-
error_msg: string | null;
|
|
24
|
-
step_count: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const AGG_SQL = (whereClause: string) => `
|
|
28
|
-
WITH agg AS (
|
|
29
|
-
SELECT
|
|
30
|
-
wf_name,
|
|
31
|
-
run_id,
|
|
32
|
-
MIN(CASE WHEN event = 'run_started' THEN ts END) AS started_at,
|
|
33
|
-
MAX(CASE WHEN event IN ('workflow_complete', 'workflow_failed') THEN ts END) AS completed_at,
|
|
34
|
-
MAX(CASE WHEN event IN ('workflow_complete', 'workflow_failed') THEN event END) AS terminal_event,
|
|
35
|
-
MAX(CASE WHEN event = 'workflow_failed' THEN error END) AS error_msg,
|
|
36
|
-
CAST(COUNT(CASE WHEN event = 'step_complete' THEN 1 END) AS INTEGER) AS step_count
|
|
37
|
-
FROM drej_events
|
|
38
|
-
${whereClause}
|
|
39
|
-
GROUP BY wf_name, run_id
|
|
40
|
-
)
|
|
41
|
-
SELECT * FROM agg WHERE started_at IS NOT NULL ORDER BY started_at DESC
|
|
42
|
-
`;
|
|
43
|
-
|
|
44
|
-
function terminalToStatus(event: string | null): RunStatus {
|
|
45
|
-
if (event === "workflow_complete") return RunStatus.Completed;
|
|
46
|
-
if (event === "workflow_failed") return RunStatus.Failed;
|
|
47
|
-
return RunStatus.Running;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function aggRowToDetails(row: AggRow): RunDetails {
|
|
51
|
-
return {
|
|
52
|
-
workflowName: row.wf_name,
|
|
53
|
-
runId: row.run_id,
|
|
54
|
-
status: terminalToStatus(row.terminal_event),
|
|
55
|
-
startedAt: row.started_at!,
|
|
56
|
-
completedAt: row.completed_at ?? undefined,
|
|
57
|
-
stepCount: row.step_count,
|
|
58
|
-
error: row.error_msg ?? undefined,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function applyOpts(details: RunDetails[], opts?: ListRunsOptions): RunDetails[] {
|
|
63
|
-
let result = details;
|
|
64
|
-
if (opts?.before != null) result = result.filter((d) => d.startedAt < opts.before!);
|
|
65
|
-
if (opts?.status != null) result = result.filter((d) => d.status === opts.status);
|
|
66
|
-
if (opts?.limit != null) result = result.slice(0, opts.limit);
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function rowToEntry(row: Row): LedgerEntry {
|
|
71
|
-
return {
|
|
72
|
-
runId: row.run_id,
|
|
73
|
-
workflowName: row.wf_name,
|
|
74
|
-
stepIndex: row.step_idx,
|
|
75
|
-
branch: row.branch ?? undefined,
|
|
76
|
-
event: row.event as LedgerEvent,
|
|
77
|
-
payload: row.payload !== null ? (JSON.parse(row.payload) as unknown) : undefined,
|
|
78
|
-
error: row.error ?? undefined,
|
|
79
|
-
ts: row.ts,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class SQLiteAdapter implements IStorageAdapter {
|
|
84
|
-
private readonly db: Database;
|
|
85
|
-
|
|
86
|
-
constructor(path: string) {
|
|
87
|
-
this.db = new Database(path, { create: true });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async connect(): Promise<void> {
|
|
91
|
-
this.db.exec(MIGRATION_SQL);
|
|
92
|
-
// WAL mode prevents writer from blocking readers on concurrent access
|
|
93
|
-
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async close(): Promise<void> {
|
|
97
|
-
this.db.close();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async append(entry: LedgerEntry): Promise<void> {
|
|
101
|
-
this.db
|
|
102
|
-
.prepare(
|
|
103
|
-
`INSERT INTO drej_events (run_id, wf_name, step_idx, branch, event, payload, error, ts)
|
|
104
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
105
|
-
)
|
|
106
|
-
.run(
|
|
107
|
-
entry.runId,
|
|
108
|
-
entry.workflowName,
|
|
109
|
-
entry.stepIndex,
|
|
110
|
-
entry.branch ?? null,
|
|
111
|
-
entry.event,
|
|
112
|
-
entry.payload !== undefined ? JSON.stringify(entry.payload) : null,
|
|
113
|
-
entry.error ?? null,
|
|
114
|
-
entry.ts,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async readAll(workflowName: string, runId: string): Promise<LedgerEntry[]> {
|
|
119
|
-
const rows = this.db
|
|
120
|
-
.prepare<Row, [string, string]>(
|
|
121
|
-
`SELECT run_id, wf_name, step_idx, branch, event, payload, error, ts
|
|
122
|
-
FROM drej_events
|
|
123
|
-
WHERE wf_name = ? AND run_id = ?
|
|
124
|
-
ORDER BY ts ASC`,
|
|
125
|
-
)
|
|
126
|
-
.all(workflowName, runId);
|
|
127
|
-
return rows.map(rowToEntry);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async lastCheckpoint(workflowName: string, runId: string): Promise<LedgerEntry | null> {
|
|
131
|
-
const row = this.db
|
|
132
|
-
.prepare<Row, [string, string]>(
|
|
133
|
-
`SELECT run_id, wf_name, step_idx, branch, event, payload, error, ts
|
|
134
|
-
FROM drej_events
|
|
135
|
-
WHERE wf_name = ? AND run_id = ? AND event = 'checkpoint'
|
|
136
|
-
ORDER BY ts DESC
|
|
137
|
-
LIMIT 1`,
|
|
138
|
-
)
|
|
139
|
-
.get(workflowName, runId);
|
|
140
|
-
return row ? rowToEntry(row) : null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async listRunDetails(workflowName: string, opts?: ListRunsOptions): Promise<RunDetails[]> {
|
|
144
|
-
const rows = this.db
|
|
145
|
-
.prepare<AggRow, [string]>(AGG_SQL("WHERE wf_name = ?"))
|
|
146
|
-
.all(workflowName);
|
|
147
|
-
return applyOpts(rows.map(aggRowToDetails), opts);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async listAllRunDetails(opts?: ListRunsOptions): Promise<RunDetails[]> {
|
|
151
|
-
const rows = this.db.prepare<AggRow, []>(AGG_SQL("")).all();
|
|
152
|
-
return applyOpts(rows.map(aggRowToDetails), opts);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async getRunDetails(workflowName: string, runId: string): Promise<RunDetails | null> {
|
|
156
|
-
const row = this.db
|
|
157
|
-
.prepare<AggRow, [string, string]>(AGG_SQL("WHERE wf_name = ? AND run_id = ?"))
|
|
158
|
-
.get(workflowName, runId);
|
|
159
|
-
return row ? aggRowToDetails(row) : null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async deleteRun(workflowName: string, runId: string): Promise<void> {
|
|
163
|
-
this.db
|
|
164
|
-
.prepare<void, [string, string]>(`DELETE FROM drej_events WHERE wf_name = ? AND run_id = ?`)
|
|
165
|
-
.run(workflowName, runId);
|
|
166
|
-
}
|
|
167
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SQLiteAdapter } from "./adapter";
|
package/src/migrations.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export const MIGRATION_SQL = `
|
|
2
|
-
CREATE TABLE IF NOT EXISTS drej_events (
|
|
3
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
-
run_id TEXT NOT NULL,
|
|
5
|
-
wf_name TEXT NOT NULL,
|
|
6
|
-
step_idx INTEGER NOT NULL,
|
|
7
|
-
branch INTEGER,
|
|
8
|
-
event TEXT NOT NULL,
|
|
9
|
-
payload TEXT,
|
|
10
|
-
error TEXT,
|
|
11
|
-
ts INTEGER NOT NULL
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE INDEX IF NOT EXISTS drej_events_run_id ON drej_events(run_id);
|
|
15
|
-
CREATE INDEX IF NOT EXISTS drej_events_wf_name ON drej_events(wf_name);
|
|
16
|
-
`;
|
package/test/adapter.test.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { LedgerEvent, RunStatus, type LedgerEntry } from "@drej/core";
|
|
3
|
-
import { SQLiteAdapter } from "../src/adapter.ts";
|
|
4
|
-
|
|
5
|
-
function entry(overrides?: Partial<LedgerEntry>): LedgerEntry {
|
|
6
|
-
return {
|
|
7
|
-
ts: Date.now(),
|
|
8
|
-
workflowName: "test-wf",
|
|
9
|
-
runId: "run-1",
|
|
10
|
-
stepIndex: 0,
|
|
11
|
-
event: LedgerEvent.StepStart,
|
|
12
|
-
...overrides,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("SQLiteAdapter", () => {
|
|
17
|
-
let db: SQLiteAdapter;
|
|
18
|
-
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
db = new SQLiteAdapter(":memory:");
|
|
21
|
-
await db.connect();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
await db.close();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// ── append / readAll ────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
describe("append / readAll", () => {
|
|
31
|
-
it("stores and retrieves an entry", async () => {
|
|
32
|
-
await db.append(entry({ event: LedgerEvent.StepStart }));
|
|
33
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
34
|
-
expect(rows).toHaveLength(1);
|
|
35
|
-
expect(rows[0].event).toBe(LedgerEvent.StepStart);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns entries in ascending timestamp order", async () => {
|
|
39
|
-
await db.append(entry({ ts: 2000, event: LedgerEvent.StepComplete }));
|
|
40
|
-
await db.append(entry({ ts: 1000, event: LedgerEvent.StepStart }));
|
|
41
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
42
|
-
expect(rows[0].ts).toBe(1000);
|
|
43
|
-
expect(rows[1].ts).toBe(2000);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("scopes to the given workflow name and run id", async () => {
|
|
47
|
-
await db.append(entry({ workflowName: "wf-a", runId: "r1" }));
|
|
48
|
-
await db.append(entry({ workflowName: "wf-b", runId: "r2" }));
|
|
49
|
-
expect(await db.readAll("wf-a", "r1")).toHaveLength(1);
|
|
50
|
-
expect(await db.readAll("wf-b", "r2")).toHaveLength(1);
|
|
51
|
-
expect(await db.readAll("wf-a", "r2")).toHaveLength(0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("serialises and deserialises JSON payload", async () => {
|
|
55
|
-
await db.append(entry({ payload: { key: "value", n: 42 } }));
|
|
56
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
57
|
-
expect(rows[0].payload).toEqual({ key: "value", n: 42 });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("preserves null payload as undefined", async () => {
|
|
61
|
-
await db.append(entry({ payload: undefined }));
|
|
62
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
63
|
-
expect(rows[0].payload).toBeUndefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("preserves the branch field", async () => {
|
|
67
|
-
await db.append(entry({ branch: 2 }));
|
|
68
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
69
|
-
expect(rows[0].branch).toBe(2);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("returns undefined branch when not set", async () => {
|
|
73
|
-
await db.append(entry());
|
|
74
|
-
const rows = await db.readAll("test-wf", "run-1");
|
|
75
|
-
expect(rows[0].branch).toBeUndefined();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ── lastCheckpoint ──────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
describe("lastCheckpoint", () => {
|
|
82
|
-
it("returns null when no checkpoint exists", async () => {
|
|
83
|
-
expect(await db.lastCheckpoint("test-wf", "run-1")).toBeNull();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("returns the most recent checkpoint", async () => {
|
|
87
|
-
await db.append(entry({ ts: 1000, event: LedgerEvent.Checkpoint, payload: { step: 1 } }));
|
|
88
|
-
await db.append(entry({ ts: 2000, event: LedgerEvent.Checkpoint, payload: { step: 2 } }));
|
|
89
|
-
const cp = await db.lastCheckpoint("test-wf", "run-1");
|
|
90
|
-
expect((cp!.payload as any).step).toBe(2);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("ignores non-checkpoint events", async () => {
|
|
94
|
-
await db.append(entry({ event: LedgerEvent.StepStart }));
|
|
95
|
-
expect(await db.lastCheckpoint("test-wf", "run-1")).toBeNull();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ── run details ─────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
async function seedRun(wf: string, runId: string, terminal: LedgerEvent.WorkflowComplete | LedgerEvent.WorkflowFailed, error?: string) {
|
|
102
|
-
const ts = Date.now();
|
|
103
|
-
await db.append(entry({ workflowName: wf, runId, ts, stepIndex: -1, event: LedgerEvent.RunStarted }));
|
|
104
|
-
await db.append(entry({ workflowName: wf, runId, ts: ts + 1, stepIndex: 0, event: LedgerEvent.StepComplete }));
|
|
105
|
-
await db.append(entry({ workflowName: wf, runId, ts: ts + 2, stepIndex: -1, event: terminal, error }));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
describe("getRunDetails", () => {
|
|
109
|
-
it("returns null for an unknown run", async () => {
|
|
110
|
-
expect(await db.getRunDetails("wf", "no-such-run")).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns Completed status", async () => {
|
|
114
|
-
await seedRun("wf", "run-1", LedgerEvent.WorkflowComplete);
|
|
115
|
-
const d = await db.getRunDetails("wf", "run-1");
|
|
116
|
-
expect(d?.status).toBe(RunStatus.Completed);
|
|
117
|
-
expect(d?.stepCount).toBe(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("returns Failed status with error message", async () => {
|
|
121
|
-
await seedRun("wf", "run-1", LedgerEvent.WorkflowFailed, "boom");
|
|
122
|
-
const d = await db.getRunDetails("wf", "run-1");
|
|
123
|
-
expect(d?.status).toBe(RunStatus.Failed);
|
|
124
|
-
expect(d?.error).toBe("boom");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("returns Running when no terminal event present", async () => {
|
|
128
|
-
await db.append(entry({ stepIndex: -1, event: LedgerEvent.RunStarted }));
|
|
129
|
-
const d = await db.getRunDetails("test-wf", "run-1");
|
|
130
|
-
expect(d?.status).toBe(RunStatus.Running);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("listRunDetails", () => {
|
|
135
|
-
it("scopes to a single workflow", async () => {
|
|
136
|
-
await seedRun("wf-a", "r1", LedgerEvent.WorkflowComplete);
|
|
137
|
-
await seedRun("wf-b", "r2", LedgerEvent.WorkflowComplete);
|
|
138
|
-
const results = await db.listRunDetails("wf-a");
|
|
139
|
-
expect(results).toHaveLength(1);
|
|
140
|
-
expect(results[0].workflowName).toBe("wf-a");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("filters by status", async () => {
|
|
144
|
-
await seedRun("wf", "r1", LedgerEvent.WorkflowComplete);
|
|
145
|
-
await seedRun("wf", "r2", LedgerEvent.WorkflowFailed);
|
|
146
|
-
const completed = await db.listRunDetails("wf", { status: RunStatus.Completed });
|
|
147
|
-
expect(completed).toHaveLength(1);
|
|
148
|
-
expect(completed[0].runId).toBe("r1");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("respects the limit option", async () => {
|
|
152
|
-
await seedRun("wf", "r1", LedgerEvent.WorkflowComplete);
|
|
153
|
-
await seedRun("wf", "r2", LedgerEvent.WorkflowComplete);
|
|
154
|
-
expect(await db.listRunDetails("wf", { limit: 1 })).toHaveLength(1);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("listAllRunDetails", () => {
|
|
159
|
-
it("returns runs across all workflows", async () => {
|
|
160
|
-
await seedRun("wf-a", "r1", LedgerEvent.WorkflowComplete);
|
|
161
|
-
await seedRun("wf-b", "r2", LedgerEvent.WorkflowComplete);
|
|
162
|
-
expect(await db.listAllRunDetails()).toHaveLength(2);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("filters by status across workflows", async () => {
|
|
166
|
-
await seedRun("wf-a", "r1", LedgerEvent.WorkflowComplete);
|
|
167
|
-
await seedRun("wf-b", "r2", LedgerEvent.WorkflowFailed);
|
|
168
|
-
const failed = await db.listAllRunDetails({ status: RunStatus.Failed });
|
|
169
|
-
expect(failed).toHaveLength(1);
|
|
170
|
-
expect(failed[0].workflowName).toBe("wf-b");
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe("deleteRun", () => {
|
|
175
|
-
it("removes all entries for the run", async () => {
|
|
176
|
-
await seedRun("wf", "run-1", LedgerEvent.WorkflowComplete);
|
|
177
|
-
await db.deleteRun("wf", "run-1");
|
|
178
|
-
expect(await db.readAll("wf", "run-1")).toHaveLength(0);
|
|
179
|
-
expect(await db.getRunDetails("wf", "run-1")).toBeNull();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("does not affect other runs", async () => {
|
|
183
|
-
await seedRun("wf", "r1", LedgerEvent.WorkflowComplete);
|
|
184
|
-
await seedRun("wf", "r2", LedgerEvent.WorkflowComplete);
|
|
185
|
-
await db.deleteRun("wf", "r1");
|
|
186
|
-
expect(await db.getRunDetails("wf", "r2")).not.toBeNull();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"noEmit": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"allowImportingTsExtensions": true,
|
|
10
|
-
"types": ["bun-types"]
|
|
11
|
-
},
|
|
12
|
-
"include": ["src"]
|
|
13
|
-
}
|