@checkstack/automation-backend 0.2.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/CHANGELOG.md +453 -0
- package/drizzle/0000_acoustic_diamondback.sql +80 -0
- package/drizzle/0001_mute_vindicator.sql +12 -0
- package/drizzle/0002_silky_omega_red.sql +12 -0
- package/drizzle/meta/0000_snapshot.json +688 -0
- package/drizzle/meta/0001_snapshot.json +785 -0
- package/drizzle/meta/0002_snapshot.json +861 -0
- package/drizzle/meta/_journal.json +27 -0
- package/drizzle.config.ts +12 -0
- package/package.json +41 -0
- package/src/action-registry.ts +83 -0
- package/src/action-types.ts +324 -0
- package/src/artifact-store.ts +140 -0
- package/src/artifact-type-registry.ts +64 -0
- package/src/automation-store.ts +227 -0
- package/src/builtin-actions.test.ts +185 -0
- package/src/builtin-actions.ts +132 -0
- package/src/builtin-triggers.test.ts +264 -0
- package/src/builtin-triggers.ts +365 -0
- package/src/dispatch/action-kind.ts +44 -0
- package/src/dispatch/condition.ts +61 -0
- package/src/dispatch/delay-queue.ts +91 -0
- package/src/dispatch/engine.test.ts +1198 -0
- package/src/dispatch/engine.ts +1672 -0
- package/src/dispatch/path-nav.ts +65 -0
- package/src/dispatch/render.test.ts +75 -0
- package/src/dispatch/render.ts +136 -0
- package/src/dispatch/run-state-store.ts +143 -0
- package/src/dispatch/run-state.ts +298 -0
- package/src/dispatch/scope.test.ts +40 -0
- package/src/dispatch/scope.ts +125 -0
- package/src/dispatch/stalled-sweeper.ts +164 -0
- package/src/dispatch/test-fixtures.ts +558 -0
- package/src/dispatch/trigger-subscriber.ts +397 -0
- package/src/dispatch/types.ts +259 -0
- package/src/extension-points.ts +88 -0
- package/src/index.ts +379 -0
- package/src/migration/from-webhook-subscriptions.test.ts +237 -0
- package/src/migration/from-webhook-subscriptions.ts +398 -0
- package/src/registries.test.ts +357 -0
- package/src/router.test.ts +724 -0
- package/src/router.ts +556 -0
- package/src/schema.ts +310 -0
- package/src/trigger-registry.ts +99 -0
- package/src/validate-definition.test.ts +306 -0
- package/src/validate-definition.ts +304 -0
- package/tsconfig.json +41 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oRPC router for the Automation backend.
|
|
3
|
+
*
|
|
4
|
+
* Implements every procedure declared in `automationContract`
|
|
5
|
+
* (`@checkstack/automation-common`). Auth + access checks are wired up via
|
|
6
|
+
* `autoAuthMiddleware` driven by the contract's `meta.userType` /
|
|
7
|
+
* `meta.access`.
|
|
8
|
+
*/
|
|
9
|
+
import { implement, ORPCError } from "@orpc/server";
|
|
10
|
+
import { and, asc, desc, eq, sql } from "drizzle-orm";
|
|
11
|
+
import type { SafeDatabase, Logger } from "@checkstack/backend-api";
|
|
12
|
+
import {
|
|
13
|
+
autoAuthMiddleware,
|
|
14
|
+
correlationMiddleware,
|
|
15
|
+
resolveActor,
|
|
16
|
+
type RpcContext,
|
|
17
|
+
} from "@checkstack/backend-api";
|
|
18
|
+
import type { SignalService } from "@checkstack/signal-common";
|
|
19
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
20
|
+
import {
|
|
21
|
+
AUTOMATION_DEFINITION_CHANGED,
|
|
22
|
+
AUTOMATION_RUN_COMPLETED,
|
|
23
|
+
automationContract,
|
|
24
|
+
type Automation,
|
|
25
|
+
type AutomationArtifact,
|
|
26
|
+
type AutomationRun,
|
|
27
|
+
type AutomationRunStep,
|
|
28
|
+
type TriggerInfo,
|
|
29
|
+
} from "@checkstack/automation-common";
|
|
30
|
+
import {
|
|
31
|
+
evaluateBoolean,
|
|
32
|
+
parseCondition,
|
|
33
|
+
parseTemplate,
|
|
34
|
+
render,
|
|
35
|
+
TemplateError,
|
|
36
|
+
} from "@checkstack/template-engine";
|
|
37
|
+
import { ZodError } from "zod";
|
|
38
|
+
|
|
39
|
+
import type { ActionRegistry } from "./action-registry";
|
|
40
|
+
import type { ArtifactTypeRegistry } from "./artifact-type-registry";
|
|
41
|
+
import type { TriggerRegistry } from "./trigger-registry";
|
|
42
|
+
import type { AutomationStore } from "./automation-store";
|
|
43
|
+
import { dispatchTrigger } from "./dispatch/engine";
|
|
44
|
+
import type { DispatchDeps } from "./dispatch/types";
|
|
45
|
+
import { collectDefinitionIssues } from "./validate-definition";
|
|
46
|
+
import * as schema from "./schema";
|
|
47
|
+
|
|
48
|
+
interface RouterDeps {
|
|
49
|
+
db: SafeDatabase<typeof schema>;
|
|
50
|
+
automationStore: AutomationStore;
|
|
51
|
+
triggerRegistry: TriggerRegistry;
|
|
52
|
+
actionRegistry: ActionRegistry;
|
|
53
|
+
artifactTypeRegistry: ArtifactTypeRegistry;
|
|
54
|
+
dispatchDeps: DispatchDeps;
|
|
55
|
+
signalService: SignalService;
|
|
56
|
+
logger: Logger;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Map a `automation_runs` row to the wire `AutomationRun`.
|
|
61
|
+
*/
|
|
62
|
+
function mapRunRow(
|
|
63
|
+
row: typeof schema.automationRuns.$inferSelect,
|
|
64
|
+
): AutomationRun {
|
|
65
|
+
return {
|
|
66
|
+
id: row.id,
|
|
67
|
+
automationId: row.automationId,
|
|
68
|
+
triggerId: row.triggerId,
|
|
69
|
+
triggerEventId: row.triggerEventId,
|
|
70
|
+
triggerPayload: row.triggerPayload,
|
|
71
|
+
contextKey: row.contextKey,
|
|
72
|
+
status: row.status as AutomationRun["status"],
|
|
73
|
+
errorMessage: row.errorMessage ?? undefined,
|
|
74
|
+
startedAt: row.startedAt,
|
|
75
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Map a `automation_run_steps` row to the wire `AutomationRunStep`.
|
|
81
|
+
*/
|
|
82
|
+
function mapStepRow(
|
|
83
|
+
row: typeof schema.automationRunSteps.$inferSelect,
|
|
84
|
+
): AutomationRunStep {
|
|
85
|
+
return {
|
|
86
|
+
id: row.id,
|
|
87
|
+
runId: row.runId,
|
|
88
|
+
actionPath: row.actionPath,
|
|
89
|
+
actionId: row.actionId,
|
|
90
|
+
actionKind: row.actionKind,
|
|
91
|
+
providerActionId: row.providerActionId,
|
|
92
|
+
status: row.status as AutomationRunStep["status"],
|
|
93
|
+
attempts: row.attempts,
|
|
94
|
+
errorMessage: row.errorMessage ?? undefined,
|
|
95
|
+
resultPayload: row.resultPayload ?? undefined,
|
|
96
|
+
startedAt: row.startedAt,
|
|
97
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Map a `automation_artifacts` row to the wire `AutomationArtifact`.
|
|
103
|
+
*/
|
|
104
|
+
function mapArtifactRow(
|
|
105
|
+
row: typeof schema.automationArtifacts.$inferSelect,
|
|
106
|
+
): AutomationArtifact {
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
automationId: row.automationId,
|
|
110
|
+
runId: row.runId,
|
|
111
|
+
stepId: row.stepId,
|
|
112
|
+
actionId: row.actionId,
|
|
113
|
+
artifactType: row.artifactType,
|
|
114
|
+
data: row.data,
|
|
115
|
+
contextKey: row.contextKey,
|
|
116
|
+
closedAt: row.closedAt ?? undefined,
|
|
117
|
+
createdAt: row.createdAt,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export function createAutomationRouter(deps: RouterDeps) {
|
|
123
|
+
const {
|
|
124
|
+
db,
|
|
125
|
+
automationStore,
|
|
126
|
+
triggerRegistry,
|
|
127
|
+
actionRegistry,
|
|
128
|
+
artifactTypeRegistry,
|
|
129
|
+
dispatchDeps,
|
|
130
|
+
signalService,
|
|
131
|
+
logger,
|
|
132
|
+
} = deps;
|
|
133
|
+
|
|
134
|
+
const os = implement(automationContract)
|
|
135
|
+
.$context<RpcContext>()
|
|
136
|
+
.use(correlationMiddleware)
|
|
137
|
+
.use(autoAuthMiddleware);
|
|
138
|
+
|
|
139
|
+
return os.router({
|
|
140
|
+
// ─── Automations CRUD ────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
listAutomations: os.listAutomations.handler(async ({ input }) => {
|
|
143
|
+
const { limit, offset, status } = input;
|
|
144
|
+
const result = await automationStore.list({ limit, offset, status });
|
|
145
|
+
return {
|
|
146
|
+
items: result.items,
|
|
147
|
+
total: result.total,
|
|
148
|
+
limit,
|
|
149
|
+
offset,
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
getAutomation: os.getAutomation.handler(async ({ input }) => {
|
|
154
|
+
const automation = await automationStore.getById(input.id);
|
|
155
|
+
if (!automation) {
|
|
156
|
+
throw new ORPCError("NOT_FOUND", {
|
|
157
|
+
message: `Automation ${input.id} not found`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return automation;
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
createAutomation: os.createAutomation.handler(async ({ input }) => {
|
|
164
|
+
let created: Automation;
|
|
165
|
+
try {
|
|
166
|
+
created = await automationStore.create(input);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof ZodError) {
|
|
169
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
170
|
+
message: `Invalid automation definition: ${error.message}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
await signalService.broadcast(AUTOMATION_DEFINITION_CHANGED, {
|
|
176
|
+
action: "created",
|
|
177
|
+
automationId: created.id,
|
|
178
|
+
});
|
|
179
|
+
logger.info(`Created automation "${created.name}" (${created.id})`);
|
|
180
|
+
return created;
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
updateAutomation: os.updateAutomation.handler(async ({ input }) => {
|
|
184
|
+
const existing = await automationStore.getById(input.id);
|
|
185
|
+
if (!existing) {
|
|
186
|
+
throw new ORPCError("NOT_FOUND", {
|
|
187
|
+
message: `Automation ${input.id} not found`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let updated: Automation;
|
|
191
|
+
try {
|
|
192
|
+
updated = await automationStore.update(input);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error instanceof ZodError) {
|
|
195
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
196
|
+
message: `Invalid automation definition: ${error.message}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
await signalService.broadcast(AUTOMATION_DEFINITION_CHANGED, {
|
|
202
|
+
action: "updated",
|
|
203
|
+
automationId: updated.id,
|
|
204
|
+
});
|
|
205
|
+
return updated;
|
|
206
|
+
}),
|
|
207
|
+
|
|
208
|
+
deleteAutomation: os.deleteAutomation.handler(async ({ input }) => {
|
|
209
|
+
const existing = await automationStore.getById(input.id);
|
|
210
|
+
if (!existing) {
|
|
211
|
+
throw new ORPCError("NOT_FOUND", {
|
|
212
|
+
message: `Automation ${input.id} not found`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
await automationStore.delete(input.id);
|
|
216
|
+
await signalService.broadcast(AUTOMATION_DEFINITION_CHANGED, {
|
|
217
|
+
action: "deleted",
|
|
218
|
+
automationId: input.id,
|
|
219
|
+
});
|
|
220
|
+
logger.info(`Deleted automation ${input.id}`);
|
|
221
|
+
return { success: true };
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
toggleAutomation: os.toggleAutomation.handler(async ({ input }) => {
|
|
225
|
+
const existing = await automationStore.getById(input.id);
|
|
226
|
+
if (!existing) {
|
|
227
|
+
throw new ORPCError("NOT_FOUND", {
|
|
228
|
+
message: `Automation ${input.id} not found`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const updated = await automationStore.toggle(input.id, input.enabled);
|
|
232
|
+
await signalService.broadcast(AUTOMATION_DEFINITION_CHANGED, {
|
|
233
|
+
action: "updated",
|
|
234
|
+
automationId: input.id,
|
|
235
|
+
});
|
|
236
|
+
return updated;
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
// ─── Definition validation ───────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
validateDefinition: os.validateDefinition.handler(async ({ input }) => {
|
|
242
|
+
// Deep validation: structural shape PLUS each provider action's
|
|
243
|
+
// config against its registered schema, trigger configs, and
|
|
244
|
+
// unknown trigger/action ids — so the editor surfaces invalid
|
|
245
|
+
// values / keys, not just malformed structure.
|
|
246
|
+
const issues = collectDefinitionIssues(input.definition, {
|
|
247
|
+
triggerRegistry,
|
|
248
|
+
actionRegistry,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
valid: issues.length === 0,
|
|
252
|
+
errors: issues,
|
|
253
|
+
};
|
|
254
|
+
}),
|
|
255
|
+
|
|
256
|
+
// ─── Manual run ──────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
manualRun: os.manualRun.handler(async ({ input, context }) => {
|
|
259
|
+
const automation = await automationStore.getById(input.automationId);
|
|
260
|
+
if (!automation) {
|
|
261
|
+
throw new ORPCError("NOT_FOUND", {
|
|
262
|
+
message: `Automation ${input.automationId} not found`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const triggers = automation.definition.triggers;
|
|
267
|
+
const selectedTrigger = input.triggerId
|
|
268
|
+
? triggers.find((t) => (t.id ?? t.event) === input.triggerId)
|
|
269
|
+
: triggers[0];
|
|
270
|
+
if (!selectedTrigger) {
|
|
271
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
272
|
+
message: input.triggerId
|
|
273
|
+
? `Trigger ${input.triggerId} not found on automation ${automation.id}`
|
|
274
|
+
: `Automation ${automation.id} has no triggers`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const triggerDef = triggerRegistry.getTrigger(selectedTrigger.event);
|
|
279
|
+
const contextKey =
|
|
280
|
+
triggerDef?.contextKey?.(input.payload) ?? null;
|
|
281
|
+
|
|
282
|
+
const result = await dispatchTrigger(dispatchDeps, {
|
|
283
|
+
automation: {
|
|
284
|
+
id: automation.id,
|
|
285
|
+
name: automation.name,
|
|
286
|
+
status: automation.status,
|
|
287
|
+
definition: automation.definition,
|
|
288
|
+
},
|
|
289
|
+
triggerId: selectedTrigger.id ?? selectedTrigger.event,
|
|
290
|
+
triggerEventId: selectedTrigger.event,
|
|
291
|
+
payload: input.payload,
|
|
292
|
+
// Attribute the manual run to whoever invoked it.
|
|
293
|
+
actor: resolveActor(context.user),
|
|
294
|
+
contextKey,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await signalService.broadcast(AUTOMATION_RUN_COMPLETED, {
|
|
298
|
+
runId: result.runId,
|
|
299
|
+
automationId: automation.id,
|
|
300
|
+
status: result.status as AutomationRun["status"],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return { runId: result.runId };
|
|
304
|
+
}),
|
|
305
|
+
|
|
306
|
+
// ─── Runs / history ──────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
listRuns: os.listRuns.handler(async ({ input }) => {
|
|
309
|
+
const { limit, offset, automationId, status } = input;
|
|
310
|
+
|
|
311
|
+
const filters = [];
|
|
312
|
+
if (automationId) {
|
|
313
|
+
filters.push(eq(schema.automationRuns.automationId, automationId));
|
|
314
|
+
}
|
|
315
|
+
if (status) {
|
|
316
|
+
filters.push(eq(schema.automationRuns.status, status));
|
|
317
|
+
}
|
|
318
|
+
const whereClause =
|
|
319
|
+
filters.length > 0 ? and(...filters) : undefined;
|
|
320
|
+
|
|
321
|
+
const baseSelect = db.select().from(schema.automationRuns);
|
|
322
|
+
const rows = whereClause
|
|
323
|
+
? await baseSelect
|
|
324
|
+
.where(whereClause)
|
|
325
|
+
.orderBy(desc(schema.automationRuns.startedAt))
|
|
326
|
+
.limit(limit)
|
|
327
|
+
.offset(offset)
|
|
328
|
+
: await baseSelect
|
|
329
|
+
.orderBy(desc(schema.automationRuns.startedAt))
|
|
330
|
+
.limit(limit)
|
|
331
|
+
.offset(offset);
|
|
332
|
+
|
|
333
|
+
const baseCount = db
|
|
334
|
+
.select({ c: sql<number>`count(*)::int` })
|
|
335
|
+
.from(schema.automationRuns);
|
|
336
|
+
const countRows = whereClause
|
|
337
|
+
? await baseCount.where(whereClause)
|
|
338
|
+
: await baseCount;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
items: rows.map((row) => mapRunRow(row)),
|
|
342
|
+
total: countRows[0]?.c ?? 0,
|
|
343
|
+
limit,
|
|
344
|
+
offset,
|
|
345
|
+
};
|
|
346
|
+
}),
|
|
347
|
+
|
|
348
|
+
getRun: os.getRun.handler(async ({ input }) => {
|
|
349
|
+
const runRow = await db
|
|
350
|
+
.select()
|
|
351
|
+
.from(schema.automationRuns)
|
|
352
|
+
.where(eq(schema.automationRuns.id, input.id))
|
|
353
|
+
.limit(1);
|
|
354
|
+
const run = runRow[0];
|
|
355
|
+
if (!run) {
|
|
356
|
+
throw new ORPCError("NOT_FOUND", {
|
|
357
|
+
message: `Run ${input.id} not found`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const [stepRows, artifactRows] = await Promise.all([
|
|
362
|
+
db
|
|
363
|
+
.select()
|
|
364
|
+
.from(schema.automationRunSteps)
|
|
365
|
+
.where(eq(schema.automationRunSteps.runId, input.id))
|
|
366
|
+
.orderBy(asc(schema.automationRunSteps.startedAt)),
|
|
367
|
+
db
|
|
368
|
+
.select()
|
|
369
|
+
.from(schema.automationArtifacts)
|
|
370
|
+
.where(eq(schema.automationArtifacts.runId, input.id))
|
|
371
|
+
.orderBy(asc(schema.automationArtifacts.createdAt)),
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
run: mapRunRow(run),
|
|
376
|
+
steps: stepRows.map((row) => mapStepRow(row)),
|
|
377
|
+
artifacts: artifactRows.map((row) => mapArtifactRow(row)),
|
|
378
|
+
};
|
|
379
|
+
}),
|
|
380
|
+
|
|
381
|
+
cancelRun: os.cancelRun.handler(async ({ input }) => {
|
|
382
|
+
const runRow = await db
|
|
383
|
+
.select()
|
|
384
|
+
.from(schema.automationRuns)
|
|
385
|
+
.where(eq(schema.automationRuns.id, input.id))
|
|
386
|
+
.limit(1);
|
|
387
|
+
const run = runRow[0];
|
|
388
|
+
if (!run) {
|
|
389
|
+
throw new ORPCError("NOT_FOUND", {
|
|
390
|
+
message: `Run ${input.id} not found`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Idempotent: a run already in a terminal state is a no-op.
|
|
395
|
+
const terminal = new Set([
|
|
396
|
+
"success",
|
|
397
|
+
"failed",
|
|
398
|
+
"cancelled",
|
|
399
|
+
"skipped",
|
|
400
|
+
]);
|
|
401
|
+
if (terminal.has(run.status)) {
|
|
402
|
+
return { success: true };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const now = new Date();
|
|
406
|
+
await db
|
|
407
|
+
.update(schema.automationRuns)
|
|
408
|
+
.set({
|
|
409
|
+
status: "cancelled",
|
|
410
|
+
errorMessage: "Cancelled by operator",
|
|
411
|
+
finishedAt: now,
|
|
412
|
+
})
|
|
413
|
+
.where(eq(schema.automationRuns.id, input.id));
|
|
414
|
+
|
|
415
|
+
// Tear down any pending wait locks so a future trigger event
|
|
416
|
+
// doesn't accidentally resume a cancelled run.
|
|
417
|
+
await db
|
|
418
|
+
.delete(schema.automationWaitLocks)
|
|
419
|
+
.where(eq(schema.automationWaitLocks.runId, input.id));
|
|
420
|
+
|
|
421
|
+
// Drop the per-run durable state — cancellation is terminal.
|
|
422
|
+
await db
|
|
423
|
+
.delete(schema.automationRunState)
|
|
424
|
+
.where(eq(schema.automationRunState.runId, input.id));
|
|
425
|
+
|
|
426
|
+
await signalService.broadcast(AUTOMATION_RUN_COMPLETED, {
|
|
427
|
+
runId: input.id,
|
|
428
|
+
automationId: run.automationId,
|
|
429
|
+
status: "cancelled",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
logger.info(`Cancelled automation run ${input.id}`);
|
|
433
|
+
return { success: true };
|
|
434
|
+
}),
|
|
435
|
+
|
|
436
|
+
// ─── Registry introspection ──────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
listTriggers: os.listTriggers.handler(async () => {
|
|
439
|
+
const items: TriggerInfo[] = triggerRegistry
|
|
440
|
+
.getTriggers()
|
|
441
|
+
.map((t) => ({
|
|
442
|
+
qualifiedId: t.qualifiedId,
|
|
443
|
+
displayName: t.displayName,
|
|
444
|
+
description: t.description,
|
|
445
|
+
category: t.category ?? "Uncategorized",
|
|
446
|
+
ownerPluginId: t.ownerPluginId,
|
|
447
|
+
payloadSchema: t.payloadJsonSchema,
|
|
448
|
+
configSchema: t.configJsonSchema,
|
|
449
|
+
}));
|
|
450
|
+
return { items };
|
|
451
|
+
}),
|
|
452
|
+
|
|
453
|
+
listActions: os.listActions.handler(async () => {
|
|
454
|
+
const items = actionRegistry.getActions().map((a) => ({
|
|
455
|
+
qualifiedId: a.qualifiedId,
|
|
456
|
+
displayName: a.displayName,
|
|
457
|
+
description: a.description,
|
|
458
|
+
category: a.category ?? "Uncategorized",
|
|
459
|
+
ownerPluginId: a.ownerPluginId,
|
|
460
|
+
configSchema: a.configJsonSchema,
|
|
461
|
+
produces: a.produces,
|
|
462
|
+
consumes: a.consumes ?? [],
|
|
463
|
+
connectionProviderId: a.connectionProviderId,
|
|
464
|
+
}));
|
|
465
|
+
return { items };
|
|
466
|
+
}),
|
|
467
|
+
|
|
468
|
+
listArtifactTypes: os.listArtifactTypes.handler(async () => {
|
|
469
|
+
const items = artifactTypeRegistry.getArtifactTypes().map((t) => ({
|
|
470
|
+
qualifiedId: t.qualifiedId,
|
|
471
|
+
displayName: t.displayName,
|
|
472
|
+
description: t.description,
|
|
473
|
+
ownerPluginId: t.ownerPluginId,
|
|
474
|
+
schema: t.jsonSchema,
|
|
475
|
+
}));
|
|
476
|
+
return { items };
|
|
477
|
+
}),
|
|
478
|
+
|
|
479
|
+
// ─── Subscription-migration failures ────────────────────────────────
|
|
480
|
+
|
|
481
|
+
listMigrationFailures: os.listMigrationFailures.handler(async () => {
|
|
482
|
+
const rows = await db
|
|
483
|
+
.select()
|
|
484
|
+
.from(schema.automationMigrationFailures)
|
|
485
|
+
.orderBy(desc(schema.automationMigrationFailures.createdAt));
|
|
486
|
+
return {
|
|
487
|
+
items: rows.map((row) => ({
|
|
488
|
+
id: row.id,
|
|
489
|
+
subscriptionId: row.subscriptionId,
|
|
490
|
+
subscriptionName: row.subscriptionName,
|
|
491
|
+
providerId: row.providerId,
|
|
492
|
+
eventId: row.eventId,
|
|
493
|
+
reason: row.reason,
|
|
494
|
+
detail: row.detail ?? undefined,
|
|
495
|
+
providerConfig: row.providerConfig,
|
|
496
|
+
createdAt: row.createdAt,
|
|
497
|
+
})),
|
|
498
|
+
};
|
|
499
|
+
}),
|
|
500
|
+
|
|
501
|
+
acknowledgeMigrationFailure: os.acknowledgeMigrationFailure.handler(
|
|
502
|
+
async ({ input }) => {
|
|
503
|
+
const result = await db
|
|
504
|
+
.delete(schema.automationMigrationFailures)
|
|
505
|
+
.where(eq(schema.automationMigrationFailures.id, input.id))
|
|
506
|
+
.returning({ id: schema.automationMigrationFailures.id });
|
|
507
|
+
if (result.length === 0) {
|
|
508
|
+
throw new ORPCError("NOT_FOUND", {
|
|
509
|
+
message: `Migration failure ${input.id} not found`,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
logger.info(`Acknowledged migration failure ${input.id}`);
|
|
513
|
+
return { success: true };
|
|
514
|
+
},
|
|
515
|
+
),
|
|
516
|
+
|
|
517
|
+
// ─── Template playground ─────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
renderTemplate: os.renderTemplate.handler(async ({ input }) => {
|
|
520
|
+
const filters = dispatchDeps.filters;
|
|
521
|
+
try {
|
|
522
|
+
if (input.mode === "condition") {
|
|
523
|
+
const parsed = parseCondition(input.template);
|
|
524
|
+
const booleanResult = evaluateBoolean(parsed, input.context, {
|
|
525
|
+
filters,
|
|
526
|
+
});
|
|
527
|
+
return { success: true, booleanResult };
|
|
528
|
+
}
|
|
529
|
+
const parsed = parseTemplate(input.template);
|
|
530
|
+
const output = render(parsed, input.context, { filters });
|
|
531
|
+
return { success: true, output };
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if (error instanceof TemplateError) {
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
error: {
|
|
537
|
+
message: error.message,
|
|
538
|
+
line: error.range.start.line,
|
|
539
|
+
column: error.range.start.column,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
error: {
|
|
546
|
+
message: extractErrorMessage(error, "Template error"),
|
|
547
|
+
line: 1,
|
|
548
|
+
column: 1,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export type AutomationRouter = ReturnType<typeof createAutomationRouter>;
|