@polpo-ai/server 0.15.44 → 0.15.46
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/deps.d.ts +5 -3
- package/dist/deps.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/schedules.d.ts +2 -5
- package/dist/routes/schedules.d.ts.map +1 -1
- package/dist/routes/schedules.js +625 -121
- package/dist/routes/schedules.js.map +1 -1
- package/dist/routes/schedules.test.d.ts +2 -0
- package/dist/routes/schedules.test.d.ts.map +1 -0
- package/dist/routes/schedules.test.js +328 -0
- package/dist/routes/schedules.test.js.map +1 -0
- package/dist/schemas.d.ts +10 -10
- package/dist/schemas.js +4 -4
- package/dist/schemas.js.map +1 -1
- package/dist/services/schedules-migration.d.ts +44 -0
- package/dist/services/schedules-migration.d.ts.map +1 -0
- package/dist/services/schedules-migration.js +308 -0
- package/dist/services/schedules-migration.js.map +1 -0
- package/dist/services/schedules-migration.test.d.ts +2 -0
- package/dist/services/schedules-migration.test.d.ts.map +1 -0
- package/dist/services/schedules-migration.test.js +471 -0
- package/dist/services/schedules-migration.test.js.map +1 -0
- package/dist/services/schedules.d.ts +42 -0
- package/dist/services/schedules.d.ts.map +1 -0
- package/dist/services/schedules.js +183 -0
- package/dist/services/schedules.js.map +1 -0
- package/dist/services/schedules.test.d.ts +2 -0
- package/dist/services/schedules.test.d.ts.map +1 -0
- package/dist/services/schedules.test.js +261 -0
- package/dist/services/schedules.test.js.map +1 -0
- package/package.json +6 -6
package/dist/routes/schedules.js
CHANGED
|
@@ -1,193 +1,697 @@
|
|
|
1
1
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { ScheduleConflictError, ScheduleInvalidStateError, ScheduleNotFoundError, translateLegacyMissionSchedule, } from "@polpo-ai/core/scheduling";
|
|
3
|
+
import { ScheduleServiceError, } from "../services/schedules.js";
|
|
4
|
+
import { legacyMissionScheduleId } from "../services/schedules-migration.js";
|
|
5
|
+
const MetadataSchema = z.record(z.string(), z.unknown());
|
|
6
|
+
const TimingSchema = z.discriminatedUnion("kind", [
|
|
7
|
+
z.object({
|
|
8
|
+
kind: z.literal("cron"),
|
|
9
|
+
expression: z.string().min(1),
|
|
10
|
+
timezone: z.string().min(1),
|
|
11
|
+
}).passthrough(),
|
|
12
|
+
z.object({
|
|
13
|
+
kind: z.literal("once"),
|
|
14
|
+
at: z.string().min(1),
|
|
15
|
+
timezone: z.string().min(1),
|
|
16
|
+
}).passthrough(),
|
|
17
|
+
]);
|
|
18
|
+
const InvocationSchema = z.discriminatedUnion("surface", [
|
|
19
|
+
z.object({ surface: z.literal("agent") }).passthrough(),
|
|
20
|
+
z.object({ surface: z.literal("task") }).passthrough(),
|
|
21
|
+
z.object({ surface: z.literal("channel") }).passthrough(),
|
|
22
|
+
z.object({ surface: z.literal("webhook") }).passthrough(),
|
|
23
|
+
z.object({ surface: z.literal("legacy_mission") }).passthrough(),
|
|
24
|
+
]);
|
|
25
|
+
const PolicySchema = z.object({
|
|
26
|
+
catchUp: z.enum(["skip", "latest"]).optional(),
|
|
27
|
+
misfireGraceSeconds: z.number().int().nonnegative().optional(),
|
|
28
|
+
maxConcurrency: z.number().int().positive().optional(),
|
|
29
|
+
}).passthrough();
|
|
30
|
+
const V2CreateScheduleSchema = z.object({
|
|
31
|
+
id: z.string().min(1).optional(),
|
|
32
|
+
name: z.string().min(1).optional(),
|
|
33
|
+
description: z.string().min(1).optional(),
|
|
34
|
+
timing: TimingSchema,
|
|
35
|
+
invocation: InvocationSchema,
|
|
36
|
+
status: z.enum(["active", "paused"]).optional(),
|
|
37
|
+
policy: PolicySchema.optional(),
|
|
38
|
+
metadata: MetadataSchema.optional(),
|
|
39
|
+
}).passthrough();
|
|
40
|
+
const LegacyCreateScheduleSchema = z.object({
|
|
4
41
|
missionId: z.string().min(1),
|
|
5
42
|
expression: z.string().min(1),
|
|
6
43
|
recurring: z.boolean().optional(),
|
|
7
44
|
endDate: z.string().datetime().optional(),
|
|
8
|
-
});
|
|
9
|
-
const
|
|
45
|
+
}).strict();
|
|
46
|
+
const V2UpdateScheduleSchema = z.object({
|
|
47
|
+
name: z.string().min(1).nullable().optional(),
|
|
48
|
+
description: z.string().min(1).nullable().optional(),
|
|
49
|
+
timing: TimingSchema.optional(),
|
|
50
|
+
invocation: InvocationSchema.optional(),
|
|
51
|
+
status: z.enum(["active", "paused", "completed"]).optional(),
|
|
52
|
+
policy: PolicySchema.optional(),
|
|
53
|
+
metadata: MetadataSchema.optional(),
|
|
54
|
+
}).passthrough();
|
|
55
|
+
const LegacyUpdateScheduleSchema = z.object({
|
|
10
56
|
expression: z.string().min(1).optional(),
|
|
11
57
|
recurring: z.boolean().optional(),
|
|
12
58
|
enabled: z.boolean().optional(),
|
|
13
59
|
endDate: z.string().datetime().nullable().optional(),
|
|
60
|
+
}).strict().refine((value) => Object.keys(value).length > 0, {
|
|
61
|
+
message: "Schedule update must include at least one field",
|
|
62
|
+
});
|
|
63
|
+
const ScheduleIdParams = z.object({ scheduleId: z.string().min(1) });
|
|
64
|
+
const RevisionHeaders = z.object({ "if-match": z.string().optional() });
|
|
65
|
+
const SuccessSchema = z.object({ ok: z.literal(true), data: z.any() });
|
|
66
|
+
const ErrorSchema = z.object({
|
|
67
|
+
ok: z.literal(false),
|
|
68
|
+
error: z.string(),
|
|
69
|
+
code: z.string(),
|
|
70
|
+
retryable: z.boolean().optional(),
|
|
14
71
|
});
|
|
15
|
-
|
|
16
|
-
|
|
72
|
+
const commonErrors = {
|
|
73
|
+
400: {
|
|
74
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
75
|
+
description: "Invalid request",
|
|
76
|
+
},
|
|
77
|
+
404: {
|
|
78
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
79
|
+
description: "Schedule not found",
|
|
80
|
+
},
|
|
81
|
+
409: {
|
|
82
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
83
|
+
description: "Schedule conflict",
|
|
84
|
+
},
|
|
85
|
+
500: {
|
|
86
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
87
|
+
description: "Schedule operation failed",
|
|
88
|
+
},
|
|
89
|
+
503: {
|
|
90
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
91
|
+
description: "Schedule service unavailable",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const listRoute = createRoute({
|
|
17
95
|
method: "get",
|
|
18
96
|
path: "/",
|
|
19
97
|
tags: ["Schedules"],
|
|
20
98
|
summary: "List schedules",
|
|
99
|
+
request: {
|
|
100
|
+
query: z.object({
|
|
101
|
+
status: z.enum(["active", "paused", "completed", "deleted"]).optional(),
|
|
102
|
+
surface: z.enum([
|
|
103
|
+
"agent",
|
|
104
|
+
"task",
|
|
105
|
+
"channel",
|
|
106
|
+
"webhook",
|
|
107
|
+
"legacy_mission",
|
|
108
|
+
]).optional(),
|
|
109
|
+
includeDeleted: z.enum(["true", "false"]).optional(),
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
21
112
|
responses: {
|
|
22
113
|
200: {
|
|
23
|
-
content: { "application/json": { schema:
|
|
24
|
-
description: "
|
|
114
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
115
|
+
description: "Schedules",
|
|
25
116
|
},
|
|
117
|
+
...commonErrors,
|
|
26
118
|
},
|
|
27
119
|
});
|
|
28
|
-
const
|
|
120
|
+
const createRouteDefinition = createRoute({
|
|
29
121
|
method: "post",
|
|
30
122
|
path: "/",
|
|
31
123
|
tags: ["Schedules"],
|
|
32
|
-
summary: "Create schedule",
|
|
124
|
+
summary: "Create a schedule",
|
|
33
125
|
request: {
|
|
34
|
-
body: {
|
|
126
|
+
body: {
|
|
127
|
+
content: {
|
|
128
|
+
"application/json": {
|
|
129
|
+
schema: z.union([
|
|
130
|
+
V2CreateScheduleSchema,
|
|
131
|
+
LegacyCreateScheduleSchema,
|
|
132
|
+
]),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
35
136
|
},
|
|
36
137
|
responses: {
|
|
37
138
|
201: {
|
|
38
|
-
content: { "application/json": { schema:
|
|
139
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
39
140
|
description: "Schedule created",
|
|
40
141
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
142
|
+
...commonErrors,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const getRoute = createRoute({
|
|
146
|
+
method: "get",
|
|
147
|
+
path: "/{scheduleId}",
|
|
148
|
+
tags: ["Schedules"],
|
|
149
|
+
summary: "Get a schedule",
|
|
150
|
+
request: { params: ScheduleIdParams },
|
|
151
|
+
responses: {
|
|
152
|
+
200: {
|
|
153
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
154
|
+
description: "Schedule",
|
|
48
155
|
},
|
|
156
|
+
...commonErrors,
|
|
49
157
|
},
|
|
50
158
|
});
|
|
51
|
-
const
|
|
159
|
+
const updateRoute = createRoute({
|
|
52
160
|
method: "patch",
|
|
53
|
-
path: "/{
|
|
161
|
+
path: "/{scheduleId}",
|
|
54
162
|
tags: ["Schedules"],
|
|
55
|
-
summary: "Update schedule",
|
|
163
|
+
summary: "Update a schedule",
|
|
56
164
|
request: {
|
|
57
|
-
params:
|
|
58
|
-
|
|
165
|
+
params: ScheduleIdParams,
|
|
166
|
+
headers: RevisionHeaders,
|
|
167
|
+
body: {
|
|
168
|
+
content: {
|
|
169
|
+
"application/json": {
|
|
170
|
+
schema: z.union([
|
|
171
|
+
LegacyUpdateScheduleSchema,
|
|
172
|
+
V2UpdateScheduleSchema,
|
|
173
|
+
]),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
59
177
|
},
|
|
60
178
|
responses: {
|
|
61
179
|
200: {
|
|
62
|
-
content: { "application/json": { schema:
|
|
180
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
63
181
|
description: "Schedule updated",
|
|
64
182
|
},
|
|
65
|
-
|
|
66
|
-
content: { "application/json": { schema: z.object({ ok: z.boolean(), error: z.string(), code: z.string() }) } },
|
|
67
|
-
description: "Schedule not found",
|
|
68
|
-
},
|
|
183
|
+
...commonErrors,
|
|
69
184
|
},
|
|
70
185
|
});
|
|
71
|
-
const
|
|
186
|
+
const deleteRoute = createRoute({
|
|
72
187
|
method: "delete",
|
|
73
|
-
path: "/{
|
|
188
|
+
path: "/{scheduleId}",
|
|
189
|
+
tags: ["Schedules"],
|
|
190
|
+
summary: "Delete a schedule",
|
|
191
|
+
request: { params: ScheduleIdParams, headers: RevisionHeaders },
|
|
192
|
+
responses: {
|
|
193
|
+
200: {
|
|
194
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
195
|
+
description: "Schedule deleted",
|
|
196
|
+
},
|
|
197
|
+
...commonErrors,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const lifecycleRoute = (action) => createRoute({
|
|
201
|
+
method: "post",
|
|
202
|
+
path: `/{scheduleId}/${action}`,
|
|
203
|
+
tags: ["Schedules"],
|
|
204
|
+
summary: `${action === "pause" ? "Pause" : "Resume"} a schedule`,
|
|
205
|
+
request: { params: ScheduleIdParams, headers: RevisionHeaders },
|
|
206
|
+
responses: {
|
|
207
|
+
200: {
|
|
208
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
209
|
+
description: `Schedule ${action}d`,
|
|
210
|
+
},
|
|
211
|
+
...commonErrors,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const pauseRoute = lifecycleRoute("pause");
|
|
215
|
+
const resumeRoute = lifecycleRoute("resume");
|
|
216
|
+
const listRunsRoute = createRoute({
|
|
217
|
+
method: "get",
|
|
218
|
+
path: "/{scheduleId}/runs",
|
|
74
219
|
tags: ["Schedules"],
|
|
75
|
-
summary: "
|
|
220
|
+
summary: "List schedule runs",
|
|
76
221
|
request: {
|
|
77
|
-
params:
|
|
222
|
+
params: ScheduleIdParams,
|
|
223
|
+
query: z.object({
|
|
224
|
+
status: z.enum([
|
|
225
|
+
"pending",
|
|
226
|
+
"claimed",
|
|
227
|
+
"running",
|
|
228
|
+
"succeeded",
|
|
229
|
+
"failed",
|
|
230
|
+
"skipped",
|
|
231
|
+
"cancelled",
|
|
232
|
+
]).optional(),
|
|
233
|
+
limit: z.string().regex(/^\d+$/).refine((value) => Number(value) >= 1 && Number(value) <= 1_000, "Run history limit must be between 1 and 1000").optional(),
|
|
234
|
+
order: z.enum(["asc", "desc"]).optional(),
|
|
235
|
+
}),
|
|
78
236
|
},
|
|
79
237
|
responses: {
|
|
80
238
|
200: {
|
|
81
|
-
content: { "application/json": { schema:
|
|
82
|
-
description: "Schedule
|
|
239
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
240
|
+
description: "Schedule runs",
|
|
83
241
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
242
|
+
...commonErrors,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const triggerRoute = createRoute({
|
|
246
|
+
method: "post",
|
|
247
|
+
path: "/{scheduleId}/runs",
|
|
248
|
+
tags: ["Schedules"],
|
|
249
|
+
summary: "Create an idempotent manual schedule run",
|
|
250
|
+
request: {
|
|
251
|
+
params: ScheduleIdParams,
|
|
252
|
+
body: {
|
|
253
|
+
content: {
|
|
254
|
+
"application/json": {
|
|
255
|
+
schema: z.object({
|
|
256
|
+
idempotencyKey: z.string().min(1),
|
|
257
|
+
}).strict(),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
87
260
|
},
|
|
88
261
|
},
|
|
262
|
+
responses: {
|
|
263
|
+
202: {
|
|
264
|
+
content: { "application/json": { schema: SuccessSchema } },
|
|
265
|
+
description: "Run accepted",
|
|
266
|
+
},
|
|
267
|
+
...commonErrors,
|
|
268
|
+
},
|
|
89
269
|
});
|
|
90
|
-
// ── Route handlers ────────────────────────────────────────────────────
|
|
91
270
|
export function scheduleRoutes(getDeps) {
|
|
92
271
|
const app = new OpenAPIHono();
|
|
93
|
-
|
|
94
|
-
app.openapi(listSchedulesRoute, (c) => {
|
|
272
|
+
app.openapi(listRoute, async (c) => {
|
|
95
273
|
const deps = getDeps();
|
|
96
|
-
|
|
97
|
-
|
|
274
|
+
if (!deps.scheduleService)
|
|
275
|
+
return legacyList(c, deps);
|
|
276
|
+
try {
|
|
277
|
+
const query = c.req.valid("query");
|
|
278
|
+
const filter = {
|
|
279
|
+
...(query.status === undefined ? {} : { status: query.status }),
|
|
280
|
+
...(query.surface === undefined ? {} : { surface: query.surface }),
|
|
281
|
+
includeDeleted: query.includeDeleted === "true",
|
|
282
|
+
};
|
|
283
|
+
const schedules = await deps.scheduleService.list(filter);
|
|
284
|
+
return c.json({
|
|
285
|
+
ok: true,
|
|
286
|
+
data: schedules.map(withLegacyCompatibility),
|
|
287
|
+
}, 200);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return scheduleError(c, error);
|
|
291
|
+
}
|
|
98
292
|
});
|
|
99
|
-
|
|
100
|
-
app.openapi(createScheduleRoute, async (c) => {
|
|
293
|
+
app.openapi(createRouteDefinition, async (c) => {
|
|
101
294
|
const deps = getDeps();
|
|
102
|
-
const scheduler = deps.getScheduler();
|
|
103
|
-
if (!scheduler) {
|
|
104
|
-
return c.json({ ok: false, error: "Scheduler not available", code: "SCHEDULER_UNAVAILABLE" }, 400);
|
|
105
|
-
}
|
|
106
295
|
const body = c.req.valid("json");
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
296
|
+
if (isLegacyCreate(body)) {
|
|
297
|
+
if (!deps.scheduleService)
|
|
298
|
+
return legacyCreate(c, deps, body);
|
|
299
|
+
try {
|
|
300
|
+
const mission = await deps.getMission?.(body.missionId);
|
|
301
|
+
if (!mission)
|
|
302
|
+
throw new ScheduleNotFoundError("Schedule", body.missionId);
|
|
303
|
+
const translated = translateLegacyRequest(body);
|
|
304
|
+
const created = await deps.scheduleService.create({
|
|
305
|
+
...translated,
|
|
306
|
+
id: legacyMissionScheduleId(body.missionId),
|
|
307
|
+
});
|
|
308
|
+
return c.json({
|
|
309
|
+
ok: true,
|
|
310
|
+
data: withLegacyCompatibility(created),
|
|
311
|
+
}, 201);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return scheduleError(c, error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const service = deps.scheduleService;
|
|
318
|
+
if (!service)
|
|
319
|
+
return unavailable(c);
|
|
320
|
+
try {
|
|
321
|
+
const created = await service.create(body);
|
|
322
|
+
return c.json({ ok: true, data: created }, 201);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return scheduleError(c, error);
|
|
326
|
+
}
|
|
126
327
|
});
|
|
127
|
-
|
|
128
|
-
app.openapi(updateScheduleRoute, async (c) => {
|
|
328
|
+
app.openapi(getRoute, async (c) => {
|
|
129
329
|
const deps = getDeps();
|
|
130
|
-
const
|
|
131
|
-
if (!
|
|
132
|
-
return c
|
|
330
|
+
const service = deps.scheduleService;
|
|
331
|
+
if (!service)
|
|
332
|
+
return unavailable(c);
|
|
333
|
+
try {
|
|
334
|
+
const schedule = await resolveSchedule(service, c.req.valid("param").scheduleId);
|
|
335
|
+
return c.json({
|
|
336
|
+
ok: true,
|
|
337
|
+
data: withLegacyCompatibility(schedule),
|
|
338
|
+
}, 200);
|
|
133
339
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!existing) {
|
|
137
|
-
return c.json({ ok: false, error: `No schedule found for mission "${missionId}"`, code: "NOT_FOUND" }, 404);
|
|
340
|
+
catch (error) {
|
|
341
|
+
return scheduleError(c, error);
|
|
138
342
|
}
|
|
343
|
+
});
|
|
344
|
+
app.openapi(updateRoute, async (c) => {
|
|
345
|
+
const deps = getDeps();
|
|
139
346
|
const body = c.req.valid("json");
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
return c
|
|
347
|
+
const { scheduleId } = c.req.valid("param");
|
|
348
|
+
if (!deps.scheduleService) {
|
|
349
|
+
if (isLegacyUpdate(body))
|
|
350
|
+
return legacyUpdate(c, deps, scheduleId, body);
|
|
351
|
+
return unavailable(c);
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const expectedRevision = parseRevision(c.req.header("if-match"));
|
|
355
|
+
if (isLegacyUpdate(body)) {
|
|
356
|
+
const existing = await resolveSchedule(deps.scheduleService, scheduleId);
|
|
357
|
+
if (existing.invocation.surface !== "legacy_mission") {
|
|
358
|
+
throw new ScheduleServiceError("INVALID_REQUEST", `Schedule "${scheduleId}" is not a legacy mission schedule`, false);
|
|
359
|
+
}
|
|
360
|
+
const patch = legacyUpdatePatch(existing, body);
|
|
361
|
+
const updated = await deps.scheduleService.update(existing.id, patch, expectedRevision === undefined ? {} : { expectedRevision });
|
|
362
|
+
return c.json({
|
|
363
|
+
ok: true,
|
|
364
|
+
data: withLegacyCompatibility(updated),
|
|
365
|
+
}, 200);
|
|
144
366
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
367
|
+
const updated = await deps.scheduleService.update(scheduleId, body, expectedRevision === undefined ? {} : { expectedRevision });
|
|
368
|
+
return c.json({ ok: true, data: updated }, 200);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return scheduleError(c, error);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
app.openapi(deleteRoute, async (c) => {
|
|
375
|
+
const deps = getDeps();
|
|
376
|
+
const { scheduleId } = c.req.valid("param");
|
|
377
|
+
if (!deps.scheduleService)
|
|
378
|
+
return legacyDelete(c, deps, scheduleId);
|
|
379
|
+
try {
|
|
380
|
+
const expectedRevision = parseRevision(c.req.header("if-match"));
|
|
381
|
+
const existing = await resolveSchedule(deps.scheduleService, scheduleId);
|
|
382
|
+
const deleted = await deps.scheduleService.delete(existing.id, expectedRevision === undefined ? {} : { expectedRevision });
|
|
383
|
+
if (existing.invocation.surface === "legacy_mission") {
|
|
384
|
+
return c.json({
|
|
385
|
+
ok: true,
|
|
386
|
+
data: { deleted: true, schedule: withLegacyCompatibility(deleted) },
|
|
387
|
+
}, 200);
|
|
163
388
|
}
|
|
389
|
+
return c.json({ ok: true, data: deleted }, 200);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
return scheduleError(c, error);
|
|
164
393
|
}
|
|
165
|
-
|
|
166
|
-
|
|
394
|
+
});
|
|
395
|
+
app.openapi(pauseRoute, async (c) => {
|
|
396
|
+
const service = getDeps().scheduleService;
|
|
397
|
+
if (!service)
|
|
398
|
+
return unavailable(c);
|
|
399
|
+
try {
|
|
400
|
+
const expectedRevision = parseRevision(c.req.header("if-match"));
|
|
401
|
+
const paused = await service.pause(c.req.valid("param").scheduleId, expectedRevision === undefined ? {} : { expectedRevision });
|
|
402
|
+
return c.json({ ok: true, data: paused }, 200);
|
|
167
403
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
await deps.updateMission(missionId, { endDate });
|
|
404
|
+
catch (error) {
|
|
405
|
+
return scheduleError(c, error);
|
|
171
406
|
}
|
|
172
|
-
const updated = scheduler.getScheduleByMissionId(missionId);
|
|
173
|
-
return c.json({ ok: true, data: updated }, 200);
|
|
174
407
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return c
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
408
|
+
app.openapi(resumeRoute, async (c) => {
|
|
409
|
+
const service = getDeps().scheduleService;
|
|
410
|
+
if (!service)
|
|
411
|
+
return unavailable(c);
|
|
412
|
+
try {
|
|
413
|
+
const expectedRevision = parseRevision(c.req.header("if-match"));
|
|
414
|
+
const resumed = await service.resume(c.req.valid("param").scheduleId, expectedRevision === undefined ? {} : { expectedRevision });
|
|
415
|
+
return c.json({ ok: true, data: resumed }, 200);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
return scheduleError(c, error);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
app.openapi(listRunsRoute, async (c) => {
|
|
422
|
+
const service = getDeps().scheduleService;
|
|
423
|
+
if (!service)
|
|
424
|
+
return unavailable(c);
|
|
425
|
+
try {
|
|
426
|
+
const query = c.req.valid("query");
|
|
427
|
+
const filter = {
|
|
428
|
+
...(query.status === undefined ? {} : { status: query.status }),
|
|
429
|
+
...(query.limit === undefined ? {} : { limit: Number(query.limit) }),
|
|
430
|
+
...(query.order === undefined ? {} : { order: query.order }),
|
|
431
|
+
};
|
|
432
|
+
const runs = await service.listRuns(c.req.valid("param").scheduleId, filter);
|
|
433
|
+
return c.json({ ok: true, data: runs }, 200);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
return scheduleError(c, error);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
app.openapi(triggerRoute, async (c) => {
|
|
440
|
+
const service = getDeps().scheduleService;
|
|
441
|
+
if (!service)
|
|
442
|
+
return unavailable(c);
|
|
443
|
+
try {
|
|
444
|
+
const run = await service.trigger(c.req.valid("param").scheduleId, c.req.valid("json"));
|
|
445
|
+
return c.json({ ok: true, data: run }, 202);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
return scheduleError(c, error);
|
|
449
|
+
}
|
|
190
450
|
});
|
|
191
451
|
return app;
|
|
192
452
|
}
|
|
453
|
+
function isLegacyCreate(body) {
|
|
454
|
+
return "missionId" in body;
|
|
455
|
+
}
|
|
456
|
+
function isLegacyUpdate(body) {
|
|
457
|
+
return ["expression", "recurring", "enabled", "endDate"]
|
|
458
|
+
.some((key) => key in body);
|
|
459
|
+
}
|
|
460
|
+
async function resolveSchedule(service, idOrMissionId) {
|
|
461
|
+
try {
|
|
462
|
+
return await service.get(idOrMissionId);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (!(error instanceof ScheduleNotFoundError))
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
const legacy = (await service.list({ surface: "legacy_mission" }))
|
|
469
|
+
.find((schedule) => schedule.invocation.surface === "legacy_mission"
|
|
470
|
+
&& schedule.invocation.missionId === idOrMissionId);
|
|
471
|
+
if (!legacy)
|
|
472
|
+
throw new ScheduleNotFoundError("Schedule", idOrMissionId);
|
|
473
|
+
return legacy;
|
|
474
|
+
}
|
|
475
|
+
function withLegacyCompatibility(schedule) {
|
|
476
|
+
if (schedule.invocation.surface !== "legacy_mission")
|
|
477
|
+
return schedule;
|
|
478
|
+
const compatibility = compatibilityMetadata(schedule);
|
|
479
|
+
return {
|
|
480
|
+
...schedule,
|
|
481
|
+
missionId: schedule.invocation.missionId,
|
|
482
|
+
expression: timingExpression(schedule),
|
|
483
|
+
recurring: compatibility.recurring === true,
|
|
484
|
+
enabled: schedule.status === "active",
|
|
485
|
+
...(schedule.nextOccurrenceAt === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: { nextRunAt: schedule.nextOccurrenceAt }),
|
|
488
|
+
...(schedule.lastOccurrenceAt === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: { lastRunAt: schedule.lastOccurrenceAt }),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function legacyUpdatePatch(existing, body) {
|
|
494
|
+
if (existing.invocation.surface !== "legacy_mission") {
|
|
495
|
+
throw new ScheduleServiceError("INVALID_REQUEST", "Legacy update requires a legacy mission invocation", false);
|
|
496
|
+
}
|
|
497
|
+
const missionId = existing.invocation.missionId;
|
|
498
|
+
const currentCompatibility = compatibilityMetadata(existing);
|
|
499
|
+
const expression = body.expression ?? timingExpression(existing);
|
|
500
|
+
const recurring = body.recurring
|
|
501
|
+
?? (currentCompatibility.recurring === true);
|
|
502
|
+
const endDate = body.endDate === undefined
|
|
503
|
+
? stringValue(currentCompatibility.endDate)
|
|
504
|
+
: body.endDate ?? undefined;
|
|
505
|
+
const translated = translateLegacyRequest({
|
|
506
|
+
missionId,
|
|
507
|
+
expression,
|
|
508
|
+
recurring,
|
|
509
|
+
...(endDate === undefined ? {} : { endDate }),
|
|
510
|
+
}, { allowPastOnce: true });
|
|
511
|
+
return {
|
|
512
|
+
...(body.expression === undefined && body.recurring === undefined
|
|
513
|
+
? {}
|
|
514
|
+
: { timing: translated.timing }),
|
|
515
|
+
invocation: translated.invocation,
|
|
516
|
+
metadata: {
|
|
517
|
+
...existing.metadata,
|
|
518
|
+
...translated.metadata,
|
|
519
|
+
},
|
|
520
|
+
...(body.enabled === undefined
|
|
521
|
+
? {}
|
|
522
|
+
: { status: body.enabled ? "active" : "paused" }),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function compatibilityMetadata(schedule) {
|
|
526
|
+
const value = schedule.metadata.compatibility;
|
|
527
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
528
|
+
return {};
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function timingExpression(schedule) {
|
|
532
|
+
return schedule.timing.kind === "cron"
|
|
533
|
+
? schedule.timing.expression
|
|
534
|
+
: schedule.timing.at;
|
|
535
|
+
}
|
|
536
|
+
function stringValue(value) {
|
|
537
|
+
return typeof value === "string" ? value : undefined;
|
|
538
|
+
}
|
|
539
|
+
function parseRevision(value) {
|
|
540
|
+
if (value === undefined)
|
|
541
|
+
return undefined;
|
|
542
|
+
const normalized = value.trim().replace(/^W\//, "").replace(/^"|"$/g, "");
|
|
543
|
+
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
544
|
+
throw new ScheduleServiceError("INVALID_REQUEST", "If-Match must contain a positive schedule revision", false);
|
|
545
|
+
}
|
|
546
|
+
const revision = Number(normalized);
|
|
547
|
+
if (!Number.isSafeInteger(revision)) {
|
|
548
|
+
throw new ScheduleServiceError("INVALID_REQUEST", "If-Match must contain a safe positive schedule revision", false);
|
|
549
|
+
}
|
|
550
|
+
return revision;
|
|
551
|
+
}
|
|
552
|
+
function unavailable(c) {
|
|
553
|
+
return c.json({
|
|
554
|
+
ok: false,
|
|
555
|
+
error: "Schedule service is not enabled on this host",
|
|
556
|
+
code: "SCHEDULE_SERVICE_UNAVAILABLE",
|
|
557
|
+
}, 503);
|
|
558
|
+
}
|
|
559
|
+
function scheduleError(c, error) {
|
|
560
|
+
if (error instanceof ScheduleNotFoundError
|
|
561
|
+
|| (error instanceof ScheduleServiceError && error.code === "NOT_FOUND")) {
|
|
562
|
+
return c.json({
|
|
563
|
+
ok: false,
|
|
564
|
+
error: error.message,
|
|
565
|
+
code: "NOT_FOUND",
|
|
566
|
+
retryable: false,
|
|
567
|
+
}, 404);
|
|
568
|
+
}
|
|
569
|
+
if (error instanceof ScheduleConflictError) {
|
|
570
|
+
return c.json({
|
|
571
|
+
ok: false,
|
|
572
|
+
error: error.message,
|
|
573
|
+
code: "CONFLICT",
|
|
574
|
+
retryable: false,
|
|
575
|
+
}, 409);
|
|
576
|
+
}
|
|
577
|
+
if (error instanceof ScheduleInvalidStateError
|
|
578
|
+
|| (error instanceof ScheduleServiceError && error.code === "INVALID_STATE")) {
|
|
579
|
+
return c.json({
|
|
580
|
+
ok: false,
|
|
581
|
+
error: error.message,
|
|
582
|
+
code: "INVALID_STATE",
|
|
583
|
+
retryable: false,
|
|
584
|
+
}, 409);
|
|
585
|
+
}
|
|
586
|
+
if (error instanceof ScheduleServiceError) {
|
|
587
|
+
return c.json({
|
|
588
|
+
ok: false,
|
|
589
|
+
error: error.message,
|
|
590
|
+
code: error.code,
|
|
591
|
+
retryable: error.retryable,
|
|
592
|
+
}, 400);
|
|
593
|
+
}
|
|
594
|
+
return c.json({
|
|
595
|
+
ok: false,
|
|
596
|
+
error: "Schedule operation failed",
|
|
597
|
+
code: "SCHEDULE_OPERATION_FAILED",
|
|
598
|
+
}, 500);
|
|
599
|
+
}
|
|
600
|
+
function translateLegacyRequest(value, options = {}) {
|
|
601
|
+
try {
|
|
602
|
+
return translateLegacyMissionSchedule(value, options);
|
|
603
|
+
}
|
|
604
|
+
catch (cause) {
|
|
605
|
+
throw new ScheduleServiceError("INVALID_REQUEST", cause instanceof Error ? cause.message : "Invalid legacy schedule request", false, { cause });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function legacyList(c, deps) {
|
|
609
|
+
const schedules = deps.getScheduler?.()?.getAllSchedules() ?? [];
|
|
610
|
+
return c.json({ ok: true, data: schedules }, 200);
|
|
611
|
+
}
|
|
612
|
+
async function legacyCreate(c, deps, body) {
|
|
613
|
+
const scheduler = deps.getScheduler?.();
|
|
614
|
+
if (!scheduler)
|
|
615
|
+
return unavailable(c);
|
|
616
|
+
const mission = await deps.getMission?.(body.missionId);
|
|
617
|
+
if (!mission) {
|
|
618
|
+
return c.json({
|
|
619
|
+
ok: false,
|
|
620
|
+
error: `Mission "${body.missionId}" not found`,
|
|
621
|
+
code: "NOT_FOUND",
|
|
622
|
+
}, 404);
|
|
623
|
+
}
|
|
624
|
+
const updatedMission = await deps.updateMission?.(body.missionId, {
|
|
625
|
+
schedule: body.expression,
|
|
626
|
+
status: body.recurring ? "recurring" : "scheduled",
|
|
627
|
+
...(body.endDate === undefined ? {} : { endDate: body.endDate }),
|
|
628
|
+
});
|
|
629
|
+
const entry = scheduler.registerMission(updatedMission);
|
|
630
|
+
if (!entry) {
|
|
631
|
+
return c.json({
|
|
632
|
+
ok: false,
|
|
633
|
+
error: "Could not create schedule",
|
|
634
|
+
code: "INVALID_EXPRESSION",
|
|
635
|
+
}, 400);
|
|
636
|
+
}
|
|
637
|
+
return c.json({ ok: true, data: entry }, 201);
|
|
638
|
+
}
|
|
639
|
+
async function legacyUpdate(c, deps, missionId, body) {
|
|
640
|
+
const scheduler = deps.getScheduler?.();
|
|
641
|
+
if (!scheduler)
|
|
642
|
+
return unavailable(c);
|
|
643
|
+
const existing = scheduler.getScheduleByMissionId(missionId);
|
|
644
|
+
if (!existing) {
|
|
645
|
+
return c.json({
|
|
646
|
+
ok: false,
|
|
647
|
+
error: `No schedule found for mission "${missionId}"`,
|
|
648
|
+
code: "NOT_FOUND",
|
|
649
|
+
}, 404);
|
|
650
|
+
}
|
|
651
|
+
if (body.expression !== undefined || body.recurring !== undefined) {
|
|
652
|
+
const mission = await deps.getMission?.(missionId);
|
|
653
|
+
if (!mission) {
|
|
654
|
+
return c.json({
|
|
655
|
+
ok: false,
|
|
656
|
+
error: `Mission "${missionId}" not found`,
|
|
657
|
+
code: "NOT_FOUND",
|
|
658
|
+
}, 404);
|
|
659
|
+
}
|
|
660
|
+
const recurring = body.recurring ?? existing.recurring;
|
|
661
|
+
const updated = await deps.updateMission?.(missionId, {
|
|
662
|
+
schedule: body.expression ?? existing.expression,
|
|
663
|
+
status: recurring ? "recurring" : "scheduled",
|
|
664
|
+
});
|
|
665
|
+
scheduler.unregisterMission(missionId);
|
|
666
|
+
scheduler.registerMission(updated);
|
|
667
|
+
}
|
|
668
|
+
if (body.enabled !== undefined)
|
|
669
|
+
existing.enabled = body.enabled;
|
|
670
|
+
if (body.endDate !== undefined) {
|
|
671
|
+
await deps.updateMission?.(missionId, {
|
|
672
|
+
endDate: body.endDate ?? undefined,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return c.json({
|
|
676
|
+
ok: true,
|
|
677
|
+
data: scheduler.getScheduleByMissionId(missionId),
|
|
678
|
+
}, 200);
|
|
679
|
+
}
|
|
680
|
+
async function legacyDelete(c, deps, missionId) {
|
|
681
|
+
const scheduler = deps.getScheduler?.();
|
|
682
|
+
if (!scheduler)
|
|
683
|
+
return unavailable(c);
|
|
684
|
+
if (!scheduler.unregisterMission(missionId)) {
|
|
685
|
+
return c.json({
|
|
686
|
+
ok: false,
|
|
687
|
+
error: `No schedule found for mission "${missionId}"`,
|
|
688
|
+
code: "NOT_FOUND",
|
|
689
|
+
}, 404);
|
|
690
|
+
}
|
|
691
|
+
await deps.updateMission?.(missionId, {
|
|
692
|
+
schedule: undefined,
|
|
693
|
+
status: "draft",
|
|
694
|
+
});
|
|
695
|
+
return c.json({ ok: true, data: { deleted: true } }, 200);
|
|
696
|
+
}
|
|
193
697
|
//# sourceMappingURL=schedules.js.map
|