@wopr-network/defcon 1.8.0 → 1.10.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/src/api/router.d.ts +2 -1
- package/dist/src/api/server.d.ts +3 -0
- package/dist/src/api/server.js +80 -0
- package/dist/src/engine/domain-event-adapter.d.ts +8 -0
- package/dist/src/engine/domain-event-adapter.js +14 -0
- package/dist/src/execution/admin-schemas.d.ts +5 -0
- package/dist/src/execution/admin-schemas.js +5 -0
- package/dist/src/execution/cli.js +16 -0
- package/dist/src/execution/mcp-server.d.ts +2 -1
- package/dist/src/execution/mcp-server.js +29 -1
- package/dist/src/repositories/drizzle/domain-event.repo.d.ts +14 -0
- package/dist/src/repositories/drizzle/domain-event.repo.js +44 -0
- package/dist/src/repositories/drizzle/event.repo.d.ts +3 -1
- package/dist/src/repositories/drizzle/event.repo.js +13 -0
- package/dist/src/repositories/drizzle/index.d.ts +1 -0
- package/dist/src/repositories/drizzle/index.js +1 -0
- package/dist/src/repositories/drizzle/schema.d.ts +115 -0
- package/dist/src/repositories/drizzle/schema.js +13 -0
- package/dist/src/repositories/interfaces.d.ts +32 -0
- package/dist/src/ui/index.html.d.ts +1 -0
- package/dist/src/ui/index.html.js +465 -0
- package/dist/src/ui/sse.d.ts +8 -0
- package/dist/src/ui/sse.js +23 -0
- package/drizzle/0015_aberrant_luminals.sql +2 -0
- package/drizzle/0016_domain_events.sql +11 -0
- package/drizzle/meta/0015_snapshot.json +1169 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
package/dist/src/api/router.d.ts
CHANGED
package/dist/src/api/server.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import type { Engine } from "../engine/engine.js";
|
|
3
3
|
import type { McpServerDeps } from "../execution/mcp-server.js";
|
|
4
4
|
import type { Logger } from "../logger.js";
|
|
5
|
+
import type { UiSseAdapter } from "../ui/sse.js";
|
|
5
6
|
export interface HttpServerDeps {
|
|
6
7
|
engine: Engine;
|
|
7
8
|
mcpDeps: McpServerDeps;
|
|
@@ -9,5 +10,7 @@ export interface HttpServerDeps {
|
|
|
9
10
|
workerToken?: string;
|
|
10
11
|
corsOrigins?: string[];
|
|
11
12
|
logger?: Logger;
|
|
13
|
+
enableUi?: boolean;
|
|
14
|
+
uiSseAdapter?: UiSseAdapter;
|
|
12
15
|
}
|
|
13
16
|
export declare function createHttpServer(deps: HttpServerDeps): http.Server;
|
package/dist/src/api/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import { callToolHandler } from "../execution/mcp-server.js";
|
|
4
4
|
import { consoleLogger } from "../logger.js";
|
|
5
|
+
import { UI_HTML } from "../ui/index.html.js";
|
|
5
6
|
import { Router } from "./router.js";
|
|
6
7
|
function extractBearerToken(header) {
|
|
7
8
|
if (!header)
|
|
@@ -274,6 +275,52 @@ export function createHttpServer(deps) {
|
|
|
274
275
|
const result = await callToolHandler(deps.mcpDeps, "admin.gate.rerun", { entity_id: req.params.id, gate_name: req.params.gateName }, { adminToken: deps.adminToken, callerToken });
|
|
275
276
|
return mcpResultToApi(result);
|
|
276
277
|
});
|
|
278
|
+
// --- UI routes (optional, enabled via enableUi) ---
|
|
279
|
+
if (deps.enableUi) {
|
|
280
|
+
router.add("GET", "/ui", async () => {
|
|
281
|
+
// No auth here: browsers navigating to /ui can't send Authorization headers.
|
|
282
|
+
// Auth is handled in the browser (token prompt) and enforced on API calls.
|
|
283
|
+
return { status: 200, html: "UI" };
|
|
284
|
+
});
|
|
285
|
+
router.add("GET", "/api/ui/entity/:id/events", async (req) => {
|
|
286
|
+
const authErr = requireAdminToken(deps, req);
|
|
287
|
+
if (authErr)
|
|
288
|
+
return authErr;
|
|
289
|
+
const limitStr = req.query.get("limit");
|
|
290
|
+
const limit = limitStr !== null ? parseInt(limitStr, 10) : 100;
|
|
291
|
+
if (limitStr !== null && (!Number.isFinite(limit) || limit <= 0)) {
|
|
292
|
+
return { status: 400, body: { error: "invalid limit parameter" } };
|
|
293
|
+
}
|
|
294
|
+
const evts = await deps.mcpDeps.eventRepo.findByEntity(req.params.id, limit);
|
|
295
|
+
return { status: 200, body: evts };
|
|
296
|
+
});
|
|
297
|
+
router.add("GET", "/api/ui/entity/:id/invocations", async (req) => {
|
|
298
|
+
const authErr = requireAdminToken(deps, req);
|
|
299
|
+
if (authErr)
|
|
300
|
+
return authErr;
|
|
301
|
+
const invocations = await deps.mcpDeps.invocations.findByEntity(req.params.id);
|
|
302
|
+
return { status: 200, body: invocations };
|
|
303
|
+
});
|
|
304
|
+
router.add("GET", "/api/ui/entity/:id/gates", async (req) => {
|
|
305
|
+
const authErr = requireAdminToken(deps, req);
|
|
306
|
+
if (authErr)
|
|
307
|
+
return authErr;
|
|
308
|
+
const results = await deps.mcpDeps.gates.resultsFor(req.params.id);
|
|
309
|
+
return { status: 200, body: results };
|
|
310
|
+
});
|
|
311
|
+
router.add("GET", "/api/ui/events/recent", async (req) => {
|
|
312
|
+
const authErr = requireAdminToken(deps, req);
|
|
313
|
+
if (authErr)
|
|
314
|
+
return authErr;
|
|
315
|
+
const limitStr = req.query.get("limit");
|
|
316
|
+
const limit = limitStr !== null ? parseInt(limitStr, 10) : 200;
|
|
317
|
+
if (limitStr !== null && (!Number.isFinite(limit) || limit <= 0)) {
|
|
318
|
+
return { status: 400, body: { error: "invalid limit parameter" } };
|
|
319
|
+
}
|
|
320
|
+
const evts = await deps.mcpDeps.eventRepo.findRecent(limit);
|
|
321
|
+
return { status: 200, body: evts };
|
|
322
|
+
});
|
|
323
|
+
}
|
|
277
324
|
// --- HTTP server ---
|
|
278
325
|
const server = http.createServer(async (req, res) => {
|
|
279
326
|
// CORS
|
|
@@ -297,6 +344,35 @@ export function createHttpServer(deps) {
|
|
|
297
344
|
return;
|
|
298
345
|
}
|
|
299
346
|
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
347
|
+
// SSE endpoint — handled before router (holds connection open)
|
|
348
|
+
if (deps.enableUi && url.pathname === "/api/ui/events" && req.method === "GET") {
|
|
349
|
+
const queryToken = url.searchParams.get("token") ?? undefined;
|
|
350
|
+
const headerToken = extractBearerToken(req.headers.authorization);
|
|
351
|
+
const callerToken = headerToken ?? queryToken;
|
|
352
|
+
const configuredToken = deps.adminToken?.trim() || undefined;
|
|
353
|
+
if (configuredToken) {
|
|
354
|
+
if (!callerToken) {
|
|
355
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
356
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const hashA = createHash("sha256").update(configuredToken).digest();
|
|
360
|
+
const hashB = createHash("sha256").update(callerToken).digest();
|
|
361
|
+
if (!timingSafeEqual(hashA, hashB)) {
|
|
362
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
363
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
res.writeHead(200, {
|
|
368
|
+
"Content-Type": "text/event-stream",
|
|
369
|
+
"Cache-Control": "no-cache",
|
|
370
|
+
Connection: "keep-alive",
|
|
371
|
+
});
|
|
372
|
+
res.write(":\n\n"); // SSE comment to establish connection
|
|
373
|
+
deps.uiSseAdapter?.addClient(res);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
300
376
|
const match = router.match(req.method ?? "GET", url.pathname);
|
|
301
377
|
if (!match) {
|
|
302
378
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -337,6 +413,10 @@ export function createHttpServer(deps) {
|
|
|
337
413
|
if (apiRes.status === 204) {
|
|
338
414
|
res.writeHead(204).end();
|
|
339
415
|
}
|
|
416
|
+
else if (apiRes.html !== undefined) {
|
|
417
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
418
|
+
res.end(UI_HTML);
|
|
419
|
+
}
|
|
340
420
|
else {
|
|
341
421
|
res.writeHead(apiRes.status, { "Content-Type": "application/json" });
|
|
342
422
|
res.end(JSON.stringify(apiRes.body));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IDomainEventRepository } from "../repositories/interfaces.js";
|
|
2
|
+
import type { EngineEvent, IEventBusAdapter } from "./event-types.js";
|
|
3
|
+
/** IEventBusAdapter that persists every engine event (except definition.changed) to the domain_events table. */
|
|
4
|
+
export declare class DomainEventPersistAdapter implements IEventBusAdapter {
|
|
5
|
+
private readonly repo;
|
|
6
|
+
constructor(repo: IDomainEventRepository);
|
|
7
|
+
emit(event: EngineEvent): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** IEventBusAdapter that persists every engine event (except definition.changed) to the domain_events table. */
|
|
2
|
+
export class DomainEventPersistAdapter {
|
|
3
|
+
repo;
|
|
4
|
+
constructor(repo) {
|
|
5
|
+
this.repo = repo;
|
|
6
|
+
}
|
|
7
|
+
async emit(event) {
|
|
8
|
+
// Skip events that have no entityId (e.g. definition.changed)
|
|
9
|
+
if (!("entityId" in event) || !event.entityId)
|
|
10
|
+
return;
|
|
11
|
+
const { type, entityId, emittedAt, ...rest } = event;
|
|
12
|
+
await this.repo.append(type, entityId, rest);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -136,3 +136,8 @@ export declare const AdminGateRerunSchema: z.ZodObject<{
|
|
|
136
136
|
entity_id: z.ZodString;
|
|
137
137
|
gate_name: z.ZodString;
|
|
138
138
|
}, z.core.$strip>;
|
|
139
|
+
export declare const AdminEventsListSchema: z.ZodObject<{
|
|
140
|
+
entity_id: z.ZodString;
|
|
141
|
+
type: z.ZodOptional<z.ZodString>;
|
|
142
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
143
|
+
}, z.core.$strip>;
|
|
@@ -145,3 +145,8 @@ export const AdminGateRerunSchema = z.object({
|
|
|
145
145
|
entity_id: z.string().min(1),
|
|
146
146
|
gate_name: z.string().min(1),
|
|
147
147
|
});
|
|
148
|
+
export const AdminEventsListSchema = z.object({
|
|
149
|
+
entity_id: z.string().min(1),
|
|
150
|
+
type: z.string().min(1).optional(),
|
|
151
|
+
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
152
|
+
});
|
|
@@ -12,9 +12,11 @@ import { createHttpServer } from "../api/server.js";
|
|
|
12
12
|
import { exportSeed } from "../config/exporter.js";
|
|
13
13
|
import { loadSeed } from "../config/seed-loader.js";
|
|
14
14
|
import { resolveCorsOrigin } from "../cors.js";
|
|
15
|
+
import { DomainEventPersistAdapter } from "../engine/domain-event-adapter.js";
|
|
15
16
|
import { Engine } from "../engine/engine.js";
|
|
16
17
|
import { EventEmitter } from "../engine/event-emitter.js";
|
|
17
18
|
import { withTransaction } from "../main.js";
|
|
19
|
+
import { DrizzleDomainEventRepository } from "../repositories/drizzle/domain-event.repo.js";
|
|
18
20
|
import { DrizzleEntityRepository } from "../repositories/drizzle/entity.repo.js";
|
|
19
21
|
import { DrizzleEventRepository } from "../repositories/drizzle/event.repo.js";
|
|
20
22
|
import { DrizzleFlowRepository } from "../repositories/drizzle/flow.repo.js";
|
|
@@ -23,6 +25,7 @@ import { DrizzleInvocationRepository } from "../repositories/drizzle/invocation.
|
|
|
23
25
|
import * as schema from "../repositories/drizzle/schema.js";
|
|
24
26
|
import { entities, entityHistory, flowDefinitions, flowVersions, gateDefinitions, gateResults, invocations, stateDefinitions, transitionRules, } from "../repositories/drizzle/schema.js";
|
|
25
27
|
import { DrizzleTransitionLogRepository } from "../repositories/drizzle/transition-log.repo.js";
|
|
28
|
+
import { UiSseAdapter } from "../ui/sse.js";
|
|
26
29
|
import { WebSocketBroadcaster } from "../ws/broadcast.js";
|
|
27
30
|
import { createMcpServer, startStdioServer } from "./mcp-server.js";
|
|
28
31
|
import { provisionWorktree } from "./provision-worktree.js";
|
|
@@ -153,6 +156,7 @@ program
|
|
|
153
156
|
.option("--mcp-only", "Start MCP stdio only (no HTTP REST server)")
|
|
154
157
|
.option("--http-port <number>", "Port for HTTP REST API", "3000")
|
|
155
158
|
.option("--http-host <address>", "Host for HTTP REST API", "127.0.0.1")
|
|
159
|
+
.option("--ui", "Enable built-in web UI at /ui")
|
|
156
160
|
.action(async (opts) => {
|
|
157
161
|
const { db, sqlite } = openDb(opts.db);
|
|
158
162
|
const entityRepo = new DrizzleEntityRepository(db);
|
|
@@ -160,12 +164,14 @@ program
|
|
|
160
164
|
const invocationRepo = new DrizzleInvocationRepository(db);
|
|
161
165
|
const gateRepo = new DrizzleGateRepository(db);
|
|
162
166
|
const transitionLogRepo = new DrizzleTransitionLogRepository(db);
|
|
167
|
+
const domainEventRepo = new DrizzleDomainEventRepository(db);
|
|
163
168
|
const eventEmitter = new EventEmitter();
|
|
164
169
|
eventEmitter.register({
|
|
165
170
|
emit: async (event) => {
|
|
166
171
|
process.stderr.write(`[event] ${event.type} ${JSON.stringify(event)}\n`);
|
|
167
172
|
},
|
|
168
173
|
});
|
|
174
|
+
eventEmitter.register(new DomainEventPersistAdapter(domainEventRepo));
|
|
169
175
|
const engine = new Engine({
|
|
170
176
|
entityRepo,
|
|
171
177
|
flowRepo,
|
|
@@ -183,6 +189,7 @@ program
|
|
|
183
189
|
gates: gateRepo,
|
|
184
190
|
transitions: transitionLogRepo,
|
|
185
191
|
eventRepo: new DrizzleEventRepository(db),
|
|
192
|
+
domainEvents: domainEventRepo,
|
|
186
193
|
engine,
|
|
187
194
|
withTransaction: (fn) => withTransaction(sqlite, fn),
|
|
188
195
|
};
|
|
@@ -209,6 +216,9 @@ program
|
|
|
209
216
|
const workerToken = process.env.DEFCON_WORKER_TOKEN || undefined;
|
|
210
217
|
const startHttp = !opts.mcpOnly;
|
|
211
218
|
const startMcp = !opts.httpOnly;
|
|
219
|
+
if (opts.mcpOnly && opts.ui) {
|
|
220
|
+
console.warn("Warning: --ui is ignored when --mcp-only is set (HTTP server is disabled)");
|
|
221
|
+
}
|
|
212
222
|
try {
|
|
213
223
|
validateAdminToken({ adminToken, startHttp, transport: opts.transport });
|
|
214
224
|
}
|
|
@@ -241,13 +251,19 @@ program
|
|
|
241
251
|
sqlite.close();
|
|
242
252
|
process.exit(1);
|
|
243
253
|
}
|
|
254
|
+
const uiSseAdapter = opts.ui ? new UiSseAdapter() : undefined;
|
|
244
255
|
restHttpServer = createHttpServer({
|
|
245
256
|
engine,
|
|
246
257
|
mcpDeps: deps,
|
|
247
258
|
adminToken,
|
|
248
259
|
workerToken,
|
|
249
260
|
corsOrigins: restCorsResult.origins ?? undefined,
|
|
261
|
+
enableUi: !!opts.ui,
|
|
262
|
+
uiSseAdapter,
|
|
250
263
|
});
|
|
264
|
+
if (uiSseAdapter) {
|
|
265
|
+
eventEmitter.register(uiSseAdapter);
|
|
266
|
+
}
|
|
251
267
|
if (adminToken) {
|
|
252
268
|
const wsBroadcaster = new WebSocketBroadcaster({
|
|
253
269
|
server: restHttpServer,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import type { Engine } from "../engine/engine.js";
|
|
3
3
|
import type { Logger } from "../logger.js";
|
|
4
|
-
import type { IEntityRepository, IEventRepository, IFlowRepository, IGateRepository, IInvocationRepository, ITransitionLogRepository } from "../repositories/interfaces.js";
|
|
4
|
+
import type { IDomainEventRepository, IEntityRepository, IEventRepository, IFlowRepository, IGateRepository, IInvocationRepository, ITransitionLogRepository } from "../repositories/interfaces.js";
|
|
5
5
|
export interface McpServerDeps {
|
|
6
6
|
entities: IEntityRepository;
|
|
7
7
|
flows: IFlowRepository;
|
|
@@ -9,6 +9,7 @@ export interface McpServerDeps {
|
|
|
9
9
|
gates: IGateRepository;
|
|
10
10
|
transitions: ITransitionLogRepository;
|
|
11
11
|
eventRepo: IEventRepository;
|
|
12
|
+
domainEvents?: IDomainEventRepository;
|
|
12
13
|
engine?: Engine;
|
|
13
14
|
logger?: Logger;
|
|
14
15
|
withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
@@ -6,7 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
6
6
|
import { DEFAULT_TIMEOUT_PROMPT } from "../engine/constants.js";
|
|
7
7
|
import { isTerminal } from "../engine/state-machine.js";
|
|
8
8
|
import { consoleLogger } from "../logger.js";
|
|
9
|
-
import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
9
|
+
import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminEventsListSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
10
10
|
import { handleFlowClaim } from "./handlers/flow.js";
|
|
11
11
|
import { FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
|
|
12
12
|
function getSystemDefaultGateTimeoutMs() {
|
|
@@ -394,6 +394,19 @@ const TOOL_DEFINITIONS = [
|
|
|
394
394
|
required: ["entity_id", "gate_name"],
|
|
395
395
|
},
|
|
396
396
|
},
|
|
397
|
+
{
|
|
398
|
+
name: "admin.events.list",
|
|
399
|
+
description: "List domain events for an entity. Returns append-only audit log entries ordered by sequence.",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
entity_id: { type: "string", description: "Entity ID to list events for" },
|
|
404
|
+
type: { type: "string", description: "Optional event type filter (e.g. 'entity.transitioned')" },
|
|
405
|
+
limit: { type: "number", description: "Max events to return (default 100, max 500)" },
|
|
406
|
+
},
|
|
407
|
+
required: ["entity_id"],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
397
410
|
];
|
|
398
411
|
export function createMcpServer(deps, opts) {
|
|
399
412
|
const server = new Server({ name: "defcon", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
@@ -486,6 +499,8 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
|
|
|
486
499
|
return await handleAdminWorkerUndrain(deps, safeArgs);
|
|
487
500
|
case "admin.gate.rerun":
|
|
488
501
|
return await handleAdminGateRerun(deps, safeArgs);
|
|
502
|
+
case "admin.events.list":
|
|
503
|
+
return await handleAdminEventsList(deps, safeArgs);
|
|
489
504
|
default:
|
|
490
505
|
return errorResult(`Unknown tool: ${name}`);
|
|
491
506
|
}
|
|
@@ -1121,3 +1136,16 @@ async function handleAdminFlowRestore(deps, args) {
|
|
|
1121
1136
|
emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.restore", { version: v.data.version });
|
|
1122
1137
|
return jsonResult({ restored: true, version: v.data.version });
|
|
1123
1138
|
}
|
|
1139
|
+
async function handleAdminEventsList(deps, args) {
|
|
1140
|
+
if (!deps.domainEvents) {
|
|
1141
|
+
return errorResult("Domain events repository not available");
|
|
1142
|
+
}
|
|
1143
|
+
const parsed = validateInput(AdminEventsListSchema, args);
|
|
1144
|
+
if (!parsed.ok)
|
|
1145
|
+
return parsed.result;
|
|
1146
|
+
const events = await deps.domainEvents.list(parsed.data.entity_id, {
|
|
1147
|
+
type: parsed.data.type,
|
|
1148
|
+
limit: parsed.data.limit,
|
|
1149
|
+
});
|
|
1150
|
+
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
|
|
1151
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
|
2
|
+
import type { DomainEvent, IDomainEventRepository } from "../interfaces.js";
|
|
3
|
+
import type * as schema from "./schema.js";
|
|
4
|
+
type Db = BetterSQLite3Database<typeof schema>;
|
|
5
|
+
export declare class DrizzleDomainEventRepository implements IDomainEventRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
constructor(db: Db);
|
|
8
|
+
append(type: string, entityId: string, payload: Record<string, unknown>): Promise<DomainEvent>;
|
|
9
|
+
list(entityId: string, opts?: {
|
|
10
|
+
type?: string;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}): Promise<DomainEvent[]>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { domainEvents } from "./schema.js";
|
|
4
|
+
export class DrizzleDomainEventRepository {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async append(type, entityId, payload) {
|
|
10
|
+
return this.db.transaction((tx) => {
|
|
11
|
+
const maxRow = tx
|
|
12
|
+
.select({ maxSeq: sql `coalesce(max(${domainEvents.sequence}), 0)` })
|
|
13
|
+
.from(domainEvents)
|
|
14
|
+
.where(eq(domainEvents.entityId, entityId))
|
|
15
|
+
.get();
|
|
16
|
+
const sequence = (maxRow?.maxSeq ?? 0) + 1;
|
|
17
|
+
const id = randomUUID();
|
|
18
|
+
const emittedAt = Date.now();
|
|
19
|
+
tx.insert(domainEvents).values({ id, type, entityId, payload, sequence, emittedAt }).run();
|
|
20
|
+
return { id, type, entityId, payload, sequence, emittedAt };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async list(entityId, opts) {
|
|
24
|
+
const conditions = [eq(domainEvents.entityId, entityId)];
|
|
25
|
+
if (opts?.type) {
|
|
26
|
+
conditions.push(eq(domainEvents.type, opts.type));
|
|
27
|
+
}
|
|
28
|
+
const rows = this.db
|
|
29
|
+
.select()
|
|
30
|
+
.from(domainEvents)
|
|
31
|
+
.where(and(...conditions))
|
|
32
|
+
.orderBy(domainEvents.sequence)
|
|
33
|
+
.limit(opts?.limit ?? 100)
|
|
34
|
+
.all();
|
|
35
|
+
return rows.map((r) => ({
|
|
36
|
+
id: r.id,
|
|
37
|
+
type: r.type,
|
|
38
|
+
entityId: r.entityId,
|
|
39
|
+
payload: r.payload,
|
|
40
|
+
sequence: r.sequence,
|
|
41
|
+
emittedAt: r.emittedAt,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
|
2
|
-
import type { IEventRepository } from "../interfaces.js";
|
|
2
|
+
import type { EventRow, IEventRepository } from "../interfaces.js";
|
|
3
3
|
import type * as schema from "./schema.js";
|
|
4
4
|
import { events } from "./schema.js";
|
|
5
5
|
type Db = BetterSQLite3Database<typeof schema>;
|
|
@@ -8,5 +8,7 @@ export declare class DrizzleEventRepository implements IEventRepository {
|
|
|
8
8
|
constructor(db: Db);
|
|
9
9
|
emitDefinitionChanged(flowId: string | null, tool: string, payload: Record<string, unknown>): Promise<void>;
|
|
10
10
|
findAll(): (typeof events.$inferSelect)[];
|
|
11
|
+
findByEntity(entityId: string, limit?: number): Promise<EventRow[]>;
|
|
12
|
+
findRecent(limit?: number): Promise<EventRow[]>;
|
|
11
13
|
}
|
|
12
14
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { desc, eq } from "drizzle-orm";
|
|
2
3
|
import { events } from "./schema.js";
|
|
3
4
|
export class DrizzleEventRepository {
|
|
4
5
|
db;
|
|
@@ -21,4 +22,16 @@ export class DrizzleEventRepository {
|
|
|
21
22
|
findAll() {
|
|
22
23
|
return this.db.select().from(events).all();
|
|
23
24
|
}
|
|
25
|
+
findByEntity(entityId, limit = 100) {
|
|
26
|
+
return Promise.resolve(this.db
|
|
27
|
+
.select()
|
|
28
|
+
.from(events)
|
|
29
|
+
.where(eq(events.entityId, entityId))
|
|
30
|
+
.orderBy(desc(events.emittedAt))
|
|
31
|
+
.limit(limit)
|
|
32
|
+
.all());
|
|
33
|
+
}
|
|
34
|
+
findRecent(limit = 100) {
|
|
35
|
+
return Promise.resolve(this.db.select().from(events).orderBy(desc(events.emittedAt)).limit(limit).all());
|
|
36
|
+
}
|
|
24
37
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Drizzle ORM implementations of repository interfaces
|
|
2
|
+
export { DrizzleDomainEventRepository } from "./domain-event.repo.js";
|
|
2
3
|
export { DrizzleEntityRepository } from "./entity.repo.js";
|
|
3
4
|
export { DrizzleEventRepository } from "./event.repo.js";
|
|
4
5
|
export { DrizzleFlowRepository } from "./flow.repo.js";
|
|
@@ -2034,3 +2034,118 @@ export declare const events: import("drizzle-orm/sqlite-core").SQLiteTableWithCo
|
|
|
2034
2034
|
};
|
|
2035
2035
|
dialect: "sqlite";
|
|
2036
2036
|
}>;
|
|
2037
|
+
export declare const domainEvents: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
2038
|
+
name: "domain_events";
|
|
2039
|
+
schema: undefined;
|
|
2040
|
+
columns: {
|
|
2041
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2042
|
+
name: "id";
|
|
2043
|
+
tableName: "domain_events";
|
|
2044
|
+
dataType: "string";
|
|
2045
|
+
columnType: "SQLiteText";
|
|
2046
|
+
data: string;
|
|
2047
|
+
driverParam: string;
|
|
2048
|
+
notNull: true;
|
|
2049
|
+
hasDefault: false;
|
|
2050
|
+
isPrimaryKey: true;
|
|
2051
|
+
isAutoincrement: false;
|
|
2052
|
+
hasRuntimeDefault: false;
|
|
2053
|
+
enumValues: [string, ...string[]];
|
|
2054
|
+
baseColumn: never;
|
|
2055
|
+
identity: undefined;
|
|
2056
|
+
generated: undefined;
|
|
2057
|
+
}, {}, {
|
|
2058
|
+
length: number | undefined;
|
|
2059
|
+
}>;
|
|
2060
|
+
type: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2061
|
+
name: "type";
|
|
2062
|
+
tableName: "domain_events";
|
|
2063
|
+
dataType: "string";
|
|
2064
|
+
columnType: "SQLiteText";
|
|
2065
|
+
data: string;
|
|
2066
|
+
driverParam: string;
|
|
2067
|
+
notNull: true;
|
|
2068
|
+
hasDefault: false;
|
|
2069
|
+
isPrimaryKey: false;
|
|
2070
|
+
isAutoincrement: false;
|
|
2071
|
+
hasRuntimeDefault: false;
|
|
2072
|
+
enumValues: [string, ...string[]];
|
|
2073
|
+
baseColumn: never;
|
|
2074
|
+
identity: undefined;
|
|
2075
|
+
generated: undefined;
|
|
2076
|
+
}, {}, {
|
|
2077
|
+
length: number | undefined;
|
|
2078
|
+
}>;
|
|
2079
|
+
entityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2080
|
+
name: "entity_id";
|
|
2081
|
+
tableName: "domain_events";
|
|
2082
|
+
dataType: "string";
|
|
2083
|
+
columnType: "SQLiteText";
|
|
2084
|
+
data: string;
|
|
2085
|
+
driverParam: string;
|
|
2086
|
+
notNull: true;
|
|
2087
|
+
hasDefault: false;
|
|
2088
|
+
isPrimaryKey: false;
|
|
2089
|
+
isAutoincrement: false;
|
|
2090
|
+
hasRuntimeDefault: false;
|
|
2091
|
+
enumValues: [string, ...string[]];
|
|
2092
|
+
baseColumn: never;
|
|
2093
|
+
identity: undefined;
|
|
2094
|
+
generated: undefined;
|
|
2095
|
+
}, {}, {
|
|
2096
|
+
length: number | undefined;
|
|
2097
|
+
}>;
|
|
2098
|
+
payload: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2099
|
+
name: "payload";
|
|
2100
|
+
tableName: "domain_events";
|
|
2101
|
+
dataType: "json";
|
|
2102
|
+
columnType: "SQLiteTextJson";
|
|
2103
|
+
data: unknown;
|
|
2104
|
+
driverParam: string;
|
|
2105
|
+
notNull: true;
|
|
2106
|
+
hasDefault: false;
|
|
2107
|
+
isPrimaryKey: false;
|
|
2108
|
+
isAutoincrement: false;
|
|
2109
|
+
hasRuntimeDefault: false;
|
|
2110
|
+
enumValues: undefined;
|
|
2111
|
+
baseColumn: never;
|
|
2112
|
+
identity: undefined;
|
|
2113
|
+
generated: undefined;
|
|
2114
|
+
}, {}, {}>;
|
|
2115
|
+
sequence: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2116
|
+
name: "sequence";
|
|
2117
|
+
tableName: "domain_events";
|
|
2118
|
+
dataType: "number";
|
|
2119
|
+
columnType: "SQLiteInteger";
|
|
2120
|
+
data: number;
|
|
2121
|
+
driverParam: number;
|
|
2122
|
+
notNull: true;
|
|
2123
|
+
hasDefault: false;
|
|
2124
|
+
isPrimaryKey: false;
|
|
2125
|
+
isAutoincrement: false;
|
|
2126
|
+
hasRuntimeDefault: false;
|
|
2127
|
+
enumValues: undefined;
|
|
2128
|
+
baseColumn: never;
|
|
2129
|
+
identity: undefined;
|
|
2130
|
+
generated: undefined;
|
|
2131
|
+
}, {}, {}>;
|
|
2132
|
+
emittedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2133
|
+
name: "emitted_at";
|
|
2134
|
+
tableName: "domain_events";
|
|
2135
|
+
dataType: "number";
|
|
2136
|
+
columnType: "SQLiteInteger";
|
|
2137
|
+
data: number;
|
|
2138
|
+
driverParam: number;
|
|
2139
|
+
notNull: true;
|
|
2140
|
+
hasDefault: false;
|
|
2141
|
+
isPrimaryKey: false;
|
|
2142
|
+
isAutoincrement: false;
|
|
2143
|
+
hasRuntimeDefault: false;
|
|
2144
|
+
enumValues: undefined;
|
|
2145
|
+
baseColumn: never;
|
|
2146
|
+
identity: undefined;
|
|
2147
|
+
generated: undefined;
|
|
2148
|
+
}, {}, {}>;
|
|
2149
|
+
};
|
|
2150
|
+
dialect: "sqlite";
|
|
2151
|
+
}>;
|
|
@@ -159,4 +159,17 @@ export const events = sqliteTable("events", {
|
|
|
159
159
|
emittedAt: integer("emitted_at").notNull(),
|
|
160
160
|
}, (table) => ({
|
|
161
161
|
typeEmittedIdx: index("events_type_emitted_idx").on(table.type, table.emittedAt),
|
|
162
|
+
entityIdIdx: index("events_entity_id_idx").on(table.entityId),
|
|
163
|
+
emittedAtIdx: index("events_emitted_at_idx").on(table.emittedAt),
|
|
164
|
+
}));
|
|
165
|
+
export const domainEvents = sqliteTable("domain_events", {
|
|
166
|
+
id: text("id").primaryKey(),
|
|
167
|
+
type: text("type").notNull(),
|
|
168
|
+
entityId: text("entity_id").notNull(),
|
|
169
|
+
payload: text("payload", { mode: "json" }).notNull(),
|
|
170
|
+
sequence: integer("sequence").notNull(),
|
|
171
|
+
emittedAt: integer("emitted_at").notNull(),
|
|
172
|
+
}, (table) => ({
|
|
173
|
+
entitySeqIdx: uniqueIndex("domain_events_entity_seq_idx").on(table.entityId, table.sequence),
|
|
174
|
+
typeIdx: index("domain_events_type_idx").on(table.type, table.emittedAt),
|
|
162
175
|
}));
|
|
@@ -338,10 +338,42 @@ export interface ITransitionLogRepository {
|
|
|
338
338
|
/** Get full transition history for an entity, ordered by timestamp. */
|
|
339
339
|
historyFor(entityId: string): Promise<TransitionLog[]>;
|
|
340
340
|
}
|
|
341
|
+
/** A raw row from the events table. */
|
|
342
|
+
export interface EventRow {
|
|
343
|
+
id: string;
|
|
344
|
+
type: string;
|
|
345
|
+
entityId: string | null;
|
|
346
|
+
flowId: string | null;
|
|
347
|
+
payload: Record<string, unknown> | null;
|
|
348
|
+
emittedAt: number;
|
|
349
|
+
}
|
|
341
350
|
/** Data-access contract for emitting definition-change events. */
|
|
342
351
|
export interface IEventRepository {
|
|
343
352
|
/** Emit a definition change event for a tool action. */
|
|
344
353
|
emitDefinitionChanged(flowId: string | null, tool: string, payload: Record<string, unknown>): Promise<void>;
|
|
354
|
+
/** Get events for a specific entity, ordered by emittedAt descending. */
|
|
355
|
+
findByEntity(entityId: string, limit?: number): Promise<EventRow[]>;
|
|
356
|
+
/** Get the most recent events across all entities, ordered by emittedAt descending. */
|
|
357
|
+
findRecent(limit?: number): Promise<EventRow[]>;
|
|
358
|
+
}
|
|
359
|
+
/** A persisted domain event from the append-only audit log. */
|
|
360
|
+
export interface DomainEvent {
|
|
361
|
+
id: string;
|
|
362
|
+
type: string;
|
|
363
|
+
entityId: string;
|
|
364
|
+
payload: Record<string, unknown>;
|
|
365
|
+
sequence: number;
|
|
366
|
+
emittedAt: number;
|
|
367
|
+
}
|
|
368
|
+
/** Data-access contract for the append-only domain_events table. */
|
|
369
|
+
export interface IDomainEventRepository {
|
|
370
|
+
/** Append a domain event. Sequence is computed as max(sequence)+1 for the entity. */
|
|
371
|
+
append(type: string, entityId: string, payload: Record<string, unknown>): Promise<DomainEvent>;
|
|
372
|
+
/** List domain events for an entity, optionally filtered by type, ordered by sequence ascending. */
|
|
373
|
+
list(entityId: string, opts?: {
|
|
374
|
+
type?: string;
|
|
375
|
+
limit?: number;
|
|
376
|
+
}): Promise<DomainEvent[]>;
|
|
345
377
|
}
|
|
346
378
|
/** Data-access contract for gate definitions and result recording. */
|
|
347
379
|
export interface IGateRepository {
|