@critical-path/server 0.18.0 → 0.20.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 +60 -0
- package/README.md +2 -0
- package/dist/index.test.js +85 -1
- package/dist/index.test.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +5 -0
- package/dist/openapi.js.map +1 -1
- package/dist/openapi.test.js +2 -0
- package/dist/openapi.test.js.map +1 -1
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +583 -512
- package/dist/router.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +103 -1
- package/src/openapi.test.ts +3 -0
- package/src/openapi.ts +6 -0
- package/src/router.ts +560 -477
package/src/router.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CriticalPathEngine, type AuthorType, type CriticalPathConfig } from '@critical-path/core';
|
|
1
|
+
import { CriticalPathEngine, type AuthorType, type CriticalPathConfig, type PluginRoute } from '@critical-path/core';
|
|
2
2
|
import * as schemas from '@critical-path/core/schemas';
|
|
3
3
|
import { buildOpenApiDocument } from './openapi.js';
|
|
4
4
|
|
|
@@ -194,479 +194,159 @@ export class CriticalPathRouter {
|
|
|
194
194
|
);
|
|
195
195
|
|
|
196
196
|
try {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
// Plugin init (and seeding) must finish before any request is served.
|
|
198
|
+
await this.engine.ready;
|
|
199
|
+
const pluginContext = { engine, context: context ?? undefined, url };
|
|
200
|
+
const handle = async (): Promise<Response> => {
|
|
201
|
+
const matched = this.matchPluginRoute(method, segments);
|
|
202
|
+
if (matched) return matched.route.handler(request, { ...pluginContext, params: matched.params });
|
|
203
|
+
return this.builtInRoutes(request, { url, pathname, method, subpath, segments, engine, context });
|
|
204
|
+
};
|
|
205
|
+
// Plugin middleware wraps routing, in registration order (first registered runs outermost).
|
|
206
|
+
const chain = this.engine.plugins
|
|
207
|
+
.getMiddleware()
|
|
208
|
+
.reduceRight<() => Promise<Response>>(
|
|
209
|
+
(next, middleware) => async () => middleware(request, pluginContext, next),
|
|
210
|
+
handle
|
|
211
|
+
);
|
|
212
|
+
return await chain();
|
|
213
|
+
} catch (err: unknown) {
|
|
214
|
+
return this.errorResponse(err, request);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
} else {
|
|
216
|
-
if (method === 'GET') {
|
|
217
|
-
const workflow = await engine.getWorkflow(workflowId);
|
|
218
|
-
if (!workflow) return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
219
|
-
return this.jsonResponse({ workflow });
|
|
220
|
-
}
|
|
221
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
222
|
-
const body = await this.readBody(request, schemas.UpdateWorkflowSchema);
|
|
223
|
-
const updated = await engine.updateWorkflow(workflowId, body);
|
|
224
|
-
if (!updated) return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
225
|
-
return this.jsonResponse({ workflow: updated });
|
|
226
|
-
}
|
|
227
|
-
if (method === 'DELETE') {
|
|
228
|
-
const deleted = await engine.deleteWorkflow(workflowId);
|
|
229
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
230
|
-
return this.jsonResponse({ success: true });
|
|
231
|
-
}
|
|
218
|
+
private matchPluginRoute(
|
|
219
|
+
method: string,
|
|
220
|
+
segments: string[]
|
|
221
|
+
): { route: PluginRoute; params: Record<string, string> } | null {
|
|
222
|
+
for (const route of this.engine.plugins.getRoutes()) {
|
|
223
|
+
if (route.method !== method && !(route.method === 'PATCH' && method === 'PUT')) continue;
|
|
224
|
+
const pattern = route.path.split('/').filter(Boolean);
|
|
225
|
+
if (pattern.length !== segments.length) continue;
|
|
226
|
+
const params: Record<string, string> = {};
|
|
227
|
+
const matches = pattern.every((part, i) => {
|
|
228
|
+
if (part.startsWith(':')) {
|
|
229
|
+
params[part.slice(1)] = decodeURIComponent(segments[i]);
|
|
230
|
+
return true;
|
|
232
231
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!projectId) {
|
|
240
|
-
if (method === 'GET') {
|
|
241
|
-
const projects = await engine.getProjects();
|
|
242
|
-
return this.jsonResponse({ projects });
|
|
243
|
-
}
|
|
244
|
-
if (method === 'POST') {
|
|
245
|
-
const body = await this.readBody(request, schemas.CreateProjectSchema);
|
|
246
|
-
const project = await engine.createProject(body);
|
|
247
|
-
return this.jsonResponse({ project }, 201);
|
|
248
|
-
}
|
|
249
|
-
} else if (subResource === 'critical-path') {
|
|
250
|
-
if (method === 'GET') {
|
|
251
|
-
const analysis = await engine.calculateCriticalPath(projectId);
|
|
252
|
-
return this.jsonResponse({ analysis });
|
|
253
|
-
}
|
|
254
|
-
} else if (subResource === 'ladder' || subResource === 'timeline-ladder') {
|
|
255
|
-
if (method === 'GET') {
|
|
256
|
-
const level = (url.searchParams.get('level') || 'all') as any;
|
|
257
|
-
const containerId = url.searchParams.get('containerId') || undefined;
|
|
258
|
-
const iterationId = url.searchParams.get('iterationId') || undefined;
|
|
259
|
-
const ladder = await engine.getTimelineLadder(projectId, { level, containerId, iterationId });
|
|
260
|
-
return this.jsonResponse({ ladder });
|
|
261
|
-
}
|
|
262
|
-
} else if (subResource === 'workload' || subResource === 'workload-distribution') {
|
|
263
|
-
if (method === 'GET') {
|
|
264
|
-
const startDate = url.searchParams.get('startDate') || undefined;
|
|
265
|
-
const endDate = url.searchParams.get('endDate') || undefined;
|
|
266
|
-
const interval = (url.searchParams.get('interval') || undefined) as any;
|
|
267
|
-
const groupBy = (url.searchParams.get('groupBy') || undefined) as any;
|
|
268
|
-
const metric = (url.searchParams.get('metric') || undefined) as any;
|
|
269
|
-
const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
|
|
270
|
-
? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours')!)
|
|
271
|
-
: undefined;
|
|
272
|
-
|
|
273
|
-
const workload = await engine.getWorkloadDistribution(projectId, {
|
|
274
|
-
startDate,
|
|
275
|
-
endDate,
|
|
276
|
-
interval,
|
|
277
|
-
groupBy,
|
|
278
|
-
metric,
|
|
279
|
-
defaultWeeklyCapacityHours
|
|
280
|
-
});
|
|
281
|
-
return this.jsonResponse({ workload });
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
if (method === 'GET') {
|
|
285
|
-
const project = await engine.getProject(projectId);
|
|
286
|
-
if (!project) return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
287
|
-
return this.jsonResponse({ project });
|
|
288
|
-
}
|
|
289
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
290
|
-
const updates = await this.readBody(request, schemas.UpdateProjectSchema);
|
|
291
|
-
const project = await engine.updateProject(projectId, updates);
|
|
292
|
-
if (!project) return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
293
|
-
return this.jsonResponse({ project });
|
|
294
|
-
}
|
|
295
|
-
if (method === 'DELETE') {
|
|
296
|
-
const deleted = await engine.deleteProject(projectId);
|
|
297
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
298
|
-
return this.jsonResponse({ success: true });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
232
|
+
return part === segments[i];
|
|
233
|
+
});
|
|
234
|
+
if (matches) return { route, params };
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
302
238
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
239
|
+
private async builtInRoutes(
|
|
240
|
+
request: Request,
|
|
241
|
+
{ url, pathname, method, subpath, segments, engine, context }: {
|
|
242
|
+
url: URL;
|
|
243
|
+
pathname: string;
|
|
244
|
+
method: string;
|
|
245
|
+
subpath: string;
|
|
246
|
+
segments: string[];
|
|
247
|
+
engine: CriticalPathEngine;
|
|
248
|
+
context: RequestContext | null | undefined;
|
|
249
|
+
}
|
|
250
|
+
): Promise<Response> {
|
|
251
|
+
// OpenAPI contract (behind the same auth as every other route)
|
|
252
|
+
if (segments[0] === 'openapi.json' && segments.length === 1 && method === 'GET') {
|
|
253
|
+
return this.jsonResponse(buildOpenApiDocument({ serverUrl: this.openApiServerUrl(url, subpath) }));
|
|
254
|
+
}
|
|
307
255
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
if (method === 'POST') {
|
|
315
|
-
const body = await this.readBody(request, schemas.CreateTaskSchema);
|
|
316
|
-
const task = await engine.createTask(body);
|
|
317
|
-
return this.jsonResponse({ task }, 201);
|
|
318
|
-
}
|
|
319
|
-
} else if (subResource === 'comments') {
|
|
320
|
-
if (method === 'GET') {
|
|
321
|
-
const comments = await engine.getComments(taskId);
|
|
322
|
-
return this.jsonResponse({ comments });
|
|
323
|
-
}
|
|
324
|
-
if (method === 'POST') {
|
|
325
|
-
const body = await this.readBody(request, schemas.CreateCommentSchema, { taskId });
|
|
326
|
-
const comment = await engine.addComment(body);
|
|
327
|
-
return this.jsonResponse({ comment }, 201);
|
|
328
|
-
}
|
|
329
|
-
} else if (subResource === 'attachments') {
|
|
330
|
-
if (method === 'GET') {
|
|
331
|
-
const attachments = await engine.getAttachments({ taskId });
|
|
332
|
-
return this.jsonResponse({ attachments });
|
|
333
|
-
}
|
|
334
|
-
if (method === 'POST') {
|
|
335
|
-
const body = await this.readBody(request, schemas.CreateAttachmentSchema, { taskId });
|
|
336
|
-
const attachment = await engine.createAttachment(body);
|
|
337
|
-
return this.jsonResponse({ attachment }, 201);
|
|
338
|
-
}
|
|
339
|
-
} else if (subResource === 'dependencies') {
|
|
340
|
-
if (method === 'GET') {
|
|
341
|
-
const graph = await engine.getTaskDependencyGraph(taskId);
|
|
342
|
-
return this.jsonResponse({ graph });
|
|
343
|
-
}
|
|
344
|
-
if (method === 'POST') {
|
|
345
|
-
const body = await this.readBody(request, schemas.CreateDependencySchema);
|
|
346
|
-
const dep = await engine.addDependency({
|
|
347
|
-
taskId,
|
|
348
|
-
dependsOnTaskId: body.dependsOnTaskId,
|
|
349
|
-
type: body.type || 'blocking'
|
|
350
|
-
});
|
|
351
|
-
return this.jsonResponse({ dependency: dep }, 201);
|
|
352
|
-
}
|
|
353
|
-
} else if (subResource === 'state' || subResource === 'lifecycle') {
|
|
354
|
-
if (method === 'GET') {
|
|
355
|
-
const state = await engine.getTaskLifecycleState(taskId);
|
|
356
|
-
if (!state) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
357
|
-
return this.jsonResponse({ state });
|
|
358
|
-
}
|
|
359
|
-
} else if (subResource === 'transitions' || subResource === 'allowed-transitions') {
|
|
360
|
-
if (method === 'GET') {
|
|
361
|
-
const allowedNextStatuses = await engine.getAllowedTaskTransitions(taskId);
|
|
362
|
-
const allowedPreviousStatuses = await engine.getAllowedPreviousTaskTransitions(taskId);
|
|
363
|
-
return this.jsonResponse({ allowedNextStatuses, allowedPreviousStatuses });
|
|
364
|
-
}
|
|
365
|
-
} else if (subResource === 'ladder') {
|
|
366
|
-
if (method === 'GET') {
|
|
367
|
-
const taskLadder = await engine.getTaskLadder(taskId);
|
|
368
|
-
if (!taskLadder) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
369
|
-
return this.jsonResponse({ taskLadder });
|
|
370
|
-
}
|
|
371
|
-
} else if (subResource === 'metrics') {
|
|
372
|
-
if (method === 'GET') {
|
|
373
|
-
const metrics = await engine.getTaskMetrics(taskId);
|
|
374
|
-
if (!metrics) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
375
|
-
return this.jsonResponse({ metrics });
|
|
376
|
-
}
|
|
377
|
-
} else if (subResource === 'progress-history') {
|
|
378
|
-
if (method === 'GET') {
|
|
379
|
-
const progressHistory = await engine.getTaskProgressHistory(taskId);
|
|
380
|
-
if (!progressHistory) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
381
|
-
return this.jsonResponse({ progressHistory });
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
if (method === 'GET') {
|
|
385
|
-
const task = await engine.getTask(taskId);
|
|
386
|
-
if (!task) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
387
|
-
return this.jsonResponse({ task });
|
|
388
|
-
}
|
|
389
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
390
|
-
const body = await this.readBody(request, schemas.UpdateTaskSchema);
|
|
391
|
-
const updated = await engine.updateTask(taskId, body);
|
|
392
|
-
if (!updated) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
393
|
-
return this.jsonResponse({ task: updated });
|
|
394
|
-
}
|
|
395
|
-
if (method === 'DELETE') {
|
|
396
|
-
const deleted = await engine.deleteTask(taskId);
|
|
397
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
398
|
-
return this.jsonResponse({ success: true });
|
|
399
|
-
}
|
|
256
|
+
// Webhooks API
|
|
257
|
+
if (segments[0] === 'webhooks') {
|
|
258
|
+
const webhookId = segments[1];
|
|
259
|
+
if (!webhookId) {
|
|
260
|
+
if (method === 'GET') {
|
|
261
|
+
return this.jsonResponse({ webhooks: await engine.getWebhooks() });
|
|
400
262
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const teamId = segments[1];
|
|
406
|
-
if (!teamId) {
|
|
407
|
-
if (method === 'GET') {
|
|
408
|
-
const teams = await engine.getTeams();
|
|
409
|
-
return this.jsonResponse({ teams });
|
|
410
|
-
}
|
|
411
|
-
if (method === 'POST') {
|
|
412
|
-
const body = await this.readBody(request, schemas.CreateTeamSchema);
|
|
413
|
-
const team = await engine.createTeam(body);
|
|
414
|
-
return this.jsonResponse({ team }, 201);
|
|
415
|
-
}
|
|
416
|
-
} else {
|
|
417
|
-
if (method === 'GET') {
|
|
418
|
-
const team = await engine.getTeam(teamId);
|
|
419
|
-
if (!team) return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
420
|
-
return this.jsonResponse({ team });
|
|
421
|
-
}
|
|
422
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
423
|
-
const body = await this.readBody(request, schemas.UpdateTeamSchema);
|
|
424
|
-
const updated = await engine.updateTeam(teamId, body);
|
|
425
|
-
if (!updated) return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
426
|
-
return this.jsonResponse({ team: updated });
|
|
427
|
-
}
|
|
428
|
-
if (method === 'DELETE') {
|
|
429
|
-
const deleted = await engine.deleteTeam(teamId);
|
|
430
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
431
|
-
return this.jsonResponse({ success: true });
|
|
432
|
-
}
|
|
263
|
+
if (method === 'POST') {
|
|
264
|
+
const body = await this.readBody(request, schemas.CreateWebhookSchema);
|
|
265
|
+
const { webhook, secret } = await engine.createWebhook(body);
|
|
266
|
+
return this.jsonResponse({ webhook, secret }, 201);
|
|
433
267
|
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (!containerId) {
|
|
440
|
-
if (method === 'GET') {
|
|
441
|
-
const projectId = url.searchParams.get('projectId');
|
|
442
|
-
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
443
|
-
const containers = await engine.getContainers(projectId);
|
|
444
|
-
return this.jsonResponse({ containers });
|
|
445
|
-
}
|
|
446
|
-
if (method === 'POST') {
|
|
447
|
-
const body = await this.readBody(request, schemas.CreateContainerSchema);
|
|
448
|
-
const container = await engine.createContainer(body);
|
|
449
|
-
return this.jsonResponse({ container }, 201);
|
|
450
|
-
}
|
|
451
|
-
} else {
|
|
452
|
-
if (method === 'GET') {
|
|
453
|
-
const container = await engine.getContainer(containerId);
|
|
454
|
-
if (!container) return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
455
|
-
return this.jsonResponse({ container });
|
|
456
|
-
}
|
|
457
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
458
|
-
const body = await this.readBody(request, schemas.UpdateContainerSchema);
|
|
459
|
-
const updated = await engine.updateContainer(containerId, body);
|
|
460
|
-
if (!updated) return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
461
|
-
return this.jsonResponse({ container: updated });
|
|
462
|
-
}
|
|
463
|
-
if (method === 'DELETE') {
|
|
464
|
-
const deleted = await engine.deleteContainer(containerId);
|
|
465
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
466
|
-
return this.jsonResponse({ success: true });
|
|
467
|
-
}
|
|
268
|
+
} else {
|
|
269
|
+
if (method === 'GET') {
|
|
270
|
+
const webhook = await engine.getWebhook(webhookId);
|
|
271
|
+
if (!webhook) return this.jsonResponse({ error: 'Webhook not found' }, 404);
|
|
272
|
+
return this.jsonResponse({ webhook });
|
|
468
273
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (!deliverableId) {
|
|
475
|
-
if (method === 'GET') {
|
|
476
|
-
const projectId = url.searchParams.get('projectId');
|
|
477
|
-
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
478
|
-
const deliverables = await engine.getDeliverables(projectId);
|
|
479
|
-
return this.jsonResponse({ deliverables });
|
|
480
|
-
}
|
|
481
|
-
if (method === 'POST') {
|
|
482
|
-
const body = await this.readBody(request, schemas.CreateDeliverableSchema);
|
|
483
|
-
const deliverable = await engine.createDeliverable(body);
|
|
484
|
-
return this.jsonResponse({ deliverable }, 201);
|
|
485
|
-
}
|
|
486
|
-
} else if (segments[2] === 'summary') {
|
|
487
|
-
if (method === 'GET') {
|
|
488
|
-
const summary = await engine.getDeliverableSummary(deliverableId);
|
|
489
|
-
if (!summary) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
490
|
-
return this.jsonResponse({ summary });
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
if (method === 'GET') {
|
|
494
|
-
const deliverable = await engine.getDeliverable(deliverableId);
|
|
495
|
-
if (!deliverable) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
496
|
-
return this.jsonResponse({ deliverable });
|
|
497
|
-
}
|
|
498
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
499
|
-
const body = await this.readBody(request, schemas.UpdateDeliverableSchema);
|
|
500
|
-
const updated = await engine.updateDeliverable(deliverableId, body);
|
|
501
|
-
if (!updated) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
502
|
-
return this.jsonResponse({ deliverable: updated });
|
|
503
|
-
}
|
|
504
|
-
if (method === 'DELETE') {
|
|
505
|
-
const deleted = await engine.deleteDeliverable(deliverableId);
|
|
506
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
507
|
-
return this.jsonResponse({ success: true });
|
|
508
|
-
}
|
|
274
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
275
|
+
const body = await this.readBody(request, schemas.UpdateWebhookSchema);
|
|
276
|
+
const webhook = await engine.updateWebhook(webhookId, body);
|
|
277
|
+
if (!webhook) return this.jsonResponse({ error: 'Webhook not found' }, 404);
|
|
278
|
+
return this.jsonResponse({ webhook });
|
|
509
279
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const iterationId = segments[1];
|
|
515
|
-
if (!iterationId) {
|
|
516
|
-
if (method === 'GET') {
|
|
517
|
-
const projectId = url.searchParams.get('projectId');
|
|
518
|
-
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
519
|
-
const iterations = await engine.getIterations(projectId);
|
|
520
|
-
return this.jsonResponse({ iterations });
|
|
521
|
-
}
|
|
522
|
-
if (method === 'POST') {
|
|
523
|
-
const body = await this.readBody(request, schemas.CreateIterationSchema);
|
|
524
|
-
const iteration = await engine.createIteration(body);
|
|
525
|
-
return this.jsonResponse({ iteration }, 201);
|
|
526
|
-
}
|
|
527
|
-
} else {
|
|
528
|
-
if (method === 'GET') {
|
|
529
|
-
const iteration = await engine.getIteration(iterationId);
|
|
530
|
-
if (!iteration) return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
531
|
-
return this.jsonResponse({ iteration });
|
|
532
|
-
}
|
|
533
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
534
|
-
const body = await this.readBody(request, schemas.UpdateIterationSchema);
|
|
535
|
-
const updated = await engine.updateIteration(iterationId, body);
|
|
536
|
-
if (!updated) return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
537
|
-
return this.jsonResponse({ iteration: updated });
|
|
538
|
-
}
|
|
539
|
-
if (method === 'DELETE') {
|
|
540
|
-
const deleted = await engine.deleteIteration(iterationId);
|
|
541
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
542
|
-
return this.jsonResponse({ success: true });
|
|
543
|
-
}
|
|
280
|
+
if (method === 'DELETE') {
|
|
281
|
+
const deleted = await engine.deleteWebhook(webhookId);
|
|
282
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
283
|
+
return this.jsonResponse({ success: true });
|
|
544
284
|
}
|
|
545
285
|
}
|
|
286
|
+
}
|
|
546
287
|
|
|
547
|
-
|
|
548
|
-
|
|
288
|
+
// Workflows API
|
|
289
|
+
if (segments[0] === 'workflows') {
|
|
290
|
+
const workflowId = segments[1];
|
|
291
|
+
if (!workflowId) {
|
|
549
292
|
if (method === 'GET') {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
const activities = await engine.getActivities({ projectId, taskId });
|
|
553
|
-
return this.jsonResponse({ activities });
|
|
293
|
+
const workflows = await engine.getWorkflows();
|
|
294
|
+
return this.jsonResponse({ workflows });
|
|
554
295
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const commentId = segments[1];
|
|
560
|
-
const subResource = segments[2];
|
|
561
|
-
if (commentId && subResource === 'reactions') {
|
|
562
|
-
if (method === 'POST') {
|
|
563
|
-
const body = await this.readBody(request, schemas.CommentReactionSchema);
|
|
564
|
-
const comment = await engine.addCommentReaction(commentId, body);
|
|
565
|
-
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
566
|
-
return this.jsonResponse({ comment }, 200);
|
|
567
|
-
}
|
|
568
|
-
if (method === 'DELETE') {
|
|
569
|
-
const emoji = url.searchParams.get('emoji');
|
|
570
|
-
if (!emoji) {
|
|
571
|
-
return this.jsonResponse({ error: 'emoji query parameter is required' }, 400);
|
|
572
|
-
}
|
|
573
|
-
const comment = await engine.removeCommentReaction(commentId, { emoji });
|
|
574
|
-
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
575
|
-
return this.jsonResponse({ comment }, 200);
|
|
576
|
-
}
|
|
577
|
-
} else if (!commentId) {
|
|
578
|
-
if (method === 'GET') {
|
|
579
|
-
const taskId = url.searchParams.get('taskId');
|
|
580
|
-
if (!taskId) return this.jsonResponse({ error: 'taskId parameter required' }, 400);
|
|
581
|
-
const comments = await engine.getComments(taskId);
|
|
582
|
-
return this.jsonResponse({ comments });
|
|
583
|
-
}
|
|
584
|
-
if (method === 'POST') {
|
|
585
|
-
const body = await this.readBody(request, schemas.CreateCommentSchema);
|
|
586
|
-
const comment = await engine.addComment(body);
|
|
587
|
-
return this.jsonResponse({ comment }, 201);
|
|
588
|
-
}
|
|
589
|
-
} else {
|
|
590
|
-
if (method === 'GET') {
|
|
591
|
-
const comment = await engine.getComment(commentId);
|
|
592
|
-
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
593
|
-
return this.jsonResponse({ comment });
|
|
594
|
-
}
|
|
595
|
-
if (method === 'PATCH' || method === 'PUT') {
|
|
596
|
-
const body = await this.readBody(request, schemas.UpdateCommentSchema);
|
|
597
|
-
const updated = await engine.updateComment(commentId, body);
|
|
598
|
-
if (!updated) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
599
|
-
return this.jsonResponse({ comment: updated });
|
|
600
|
-
}
|
|
601
|
-
if (method === 'DELETE') {
|
|
602
|
-
const deleted = await engine.deleteComment(commentId);
|
|
603
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
604
|
-
return this.jsonResponse({ success: true });
|
|
605
|
-
}
|
|
296
|
+
if (method === 'POST') {
|
|
297
|
+
const body = await this.readBody(request, schemas.CreateWorkflowSchema);
|
|
298
|
+
const workflow = await engine.createWorkflow(body);
|
|
299
|
+
return this.jsonResponse({ workflow }, 201);
|
|
606
300
|
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
} else if (!attachmentId) {
|
|
625
|
-
if (method === 'GET') {
|
|
626
|
-
const taskId = url.searchParams.get('taskId') || undefined;
|
|
627
|
-
const projectId = url.searchParams.get('projectId') || undefined;
|
|
628
|
-
const commentId = url.searchParams.get('commentId') || undefined;
|
|
629
|
-
const attachments = await engine.getAttachments({ taskId, projectId, commentId });
|
|
630
|
-
return this.jsonResponse({ attachments });
|
|
631
|
-
}
|
|
632
|
-
if (method === 'POST') {
|
|
633
|
-
const body = await this.readBody(request, schemas.CreateAttachmentSchema);
|
|
634
|
-
const attachment = await engine.createAttachment(body);
|
|
635
|
-
return this.jsonResponse({ attachment }, 201);
|
|
636
|
-
}
|
|
637
|
-
} else {
|
|
638
|
-
if (method === 'GET') {
|
|
639
|
-
const attachment = await engine.getAttachment(attachmentId);
|
|
640
|
-
if (!attachment) return this.jsonResponse({ error: 'Attachment not found' }, 404);
|
|
641
|
-
return this.jsonResponse({ attachment });
|
|
642
|
-
}
|
|
643
|
-
if (method === 'DELETE') {
|
|
644
|
-
const deleted = await engine.deleteAttachment(attachmentId);
|
|
645
|
-
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
646
|
-
return this.jsonResponse({ success: true });
|
|
647
|
-
}
|
|
301
|
+
} else {
|
|
302
|
+
if (method === 'GET') {
|
|
303
|
+
const workflow = await engine.getWorkflow(workflowId);
|
|
304
|
+
if (!workflow) return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
305
|
+
return this.jsonResponse({ workflow });
|
|
306
|
+
}
|
|
307
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
308
|
+
const body = await this.readBody(request, schemas.UpdateWorkflowSchema);
|
|
309
|
+
const updated = await engine.updateWorkflow(workflowId, body);
|
|
310
|
+
if (!updated) return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
311
|
+
return this.jsonResponse({ workflow: updated });
|
|
312
|
+
}
|
|
313
|
+
if (method === 'DELETE') {
|
|
314
|
+
const deleted = await engine.deleteWorkflow(workflowId);
|
|
315
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
316
|
+
return this.jsonResponse({ success: true });
|
|
648
317
|
}
|
|
649
318
|
}
|
|
319
|
+
}
|
|
650
320
|
|
|
651
|
-
|
|
652
|
-
|
|
321
|
+
// Projects API
|
|
322
|
+
if (segments[0] === 'projects') {
|
|
323
|
+
const projectId = segments[1];
|
|
324
|
+
const subResource = segments[2];
|
|
325
|
+
if (!projectId) {
|
|
653
326
|
if (method === 'GET') {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
const entries = await engine.getTimeEntries(taskId);
|
|
657
|
-
return this.jsonResponse({ timeEntries: entries });
|
|
327
|
+
const projects = await engine.getProjects();
|
|
328
|
+
return this.jsonResponse({ projects });
|
|
658
329
|
}
|
|
659
330
|
if (method === 'POST') {
|
|
660
|
-
const body = await this.readBody(request, schemas.
|
|
661
|
-
const
|
|
662
|
-
return this.jsonResponse({
|
|
331
|
+
const body = await this.readBody(request, schemas.CreateProjectSchema);
|
|
332
|
+
const project = await engine.createProject(body);
|
|
333
|
+
return this.jsonResponse({ project }, 201);
|
|
663
334
|
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
335
|
+
} else if (subResource === 'critical-path') {
|
|
336
|
+
if (method === 'GET') {
|
|
337
|
+
const analysis = await engine.calculateCriticalPath(projectId);
|
|
338
|
+
return this.jsonResponse({ analysis });
|
|
339
|
+
}
|
|
340
|
+
} else if (subResource === 'ladder' || subResource === 'timeline-ladder') {
|
|
341
|
+
if (method === 'GET') {
|
|
342
|
+
const level = (url.searchParams.get('level') || 'all') as any;
|
|
343
|
+
const containerId = url.searchParams.get('containerId') || undefined;
|
|
344
|
+
const iterationId = url.searchParams.get('iterationId') || undefined;
|
|
345
|
+
const ladder = await engine.getTimelineLadder(projectId, { level, containerId, iterationId });
|
|
346
|
+
return this.jsonResponse({ ladder });
|
|
347
|
+
}
|
|
348
|
+
} else if (subResource === 'workload' || subResource === 'workload-distribution') {
|
|
668
349
|
if (method === 'GET') {
|
|
669
|
-
const projectId = url.searchParams.get('projectId') || undefined;
|
|
670
350
|
const startDate = url.searchParams.get('startDate') || undefined;
|
|
671
351
|
const endDate = url.searchParams.get('endDate') || undefined;
|
|
672
352
|
const interval = (url.searchParams.get('interval') || undefined) as any;
|
|
@@ -686,39 +366,442 @@ export class CriticalPathRouter {
|
|
|
686
366
|
});
|
|
687
367
|
return this.jsonResponse({ workload });
|
|
688
368
|
}
|
|
369
|
+
} else {
|
|
370
|
+
if (method === 'GET') {
|
|
371
|
+
const project = await engine.getProject(projectId);
|
|
372
|
+
if (!project) return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
373
|
+
return this.jsonResponse({ project });
|
|
374
|
+
}
|
|
375
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
376
|
+
const updates = await this.readBody(request, schemas.UpdateProjectSchema);
|
|
377
|
+
const project = await engine.updateProject(projectId, updates);
|
|
378
|
+
if (!project) return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
379
|
+
return this.jsonResponse({ project });
|
|
380
|
+
}
|
|
381
|
+
if (method === 'DELETE') {
|
|
382
|
+
const deleted = await engine.deleteProject(projectId);
|
|
383
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
384
|
+
return this.jsonResponse({ success: true });
|
|
385
|
+
}
|
|
689
386
|
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Tasks API & Task Sub-resources (Dependencies, Lifecycle State, Transitions)
|
|
390
|
+
if (segments[0] === 'tasks') {
|
|
391
|
+
const taskId = segments[1];
|
|
392
|
+
const subResource = segments[2];
|
|
690
393
|
|
|
691
|
-
|
|
692
|
-
|
|
394
|
+
if (!taskId) {
|
|
395
|
+
if (method === 'GET') {
|
|
396
|
+
const projectId = url.searchParams.get('projectId') || undefined;
|
|
397
|
+
const tasks = await engine.getTasks(projectId);
|
|
398
|
+
return this.jsonResponse({ tasks });
|
|
399
|
+
}
|
|
693
400
|
if (method === 'POST') {
|
|
694
|
-
const body = await this.
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
401
|
+
const body = await this.readBody(request, schemas.CreateTaskSchema);
|
|
402
|
+
const task = await engine.createTask(body);
|
|
403
|
+
return this.jsonResponse({ task }, 201);
|
|
404
|
+
}
|
|
405
|
+
} else if (subResource === 'comments') {
|
|
406
|
+
if (method === 'GET') {
|
|
407
|
+
const comments = await engine.getComments(taskId);
|
|
408
|
+
return this.jsonResponse({ comments });
|
|
409
|
+
}
|
|
410
|
+
if (method === 'POST') {
|
|
411
|
+
const body = await this.readBody(request, schemas.CreateCommentSchema, { taskId });
|
|
412
|
+
const comment = await engine.addComment(body);
|
|
413
|
+
return this.jsonResponse({ comment }, 201);
|
|
414
|
+
}
|
|
415
|
+
} else if (subResource === 'attachments') {
|
|
416
|
+
if (method === 'GET') {
|
|
417
|
+
const attachments = await engine.getAttachments({ taskId });
|
|
418
|
+
return this.jsonResponse({ attachments });
|
|
419
|
+
}
|
|
420
|
+
if (method === 'POST') {
|
|
421
|
+
const body = await this.readBody(request, schemas.CreateAttachmentSchema, { taskId });
|
|
422
|
+
const attachment = await engine.createAttachment(body);
|
|
423
|
+
return this.jsonResponse({ attachment }, 201);
|
|
424
|
+
}
|
|
425
|
+
} else if (subResource === 'dependencies') {
|
|
426
|
+
if (method === 'GET') {
|
|
427
|
+
const graph = await engine.getTaskDependencyGraph(taskId);
|
|
428
|
+
return this.jsonResponse({ graph });
|
|
429
|
+
}
|
|
430
|
+
if (method === 'POST') {
|
|
431
|
+
const body = await this.readBody(request, schemas.CreateDependencySchema);
|
|
432
|
+
const dep = await engine.addDependency({
|
|
433
|
+
taskId,
|
|
434
|
+
dependsOnTaskId: body.dependsOnTaskId,
|
|
435
|
+
type: body.type || 'blocking'
|
|
714
436
|
});
|
|
437
|
+
return this.jsonResponse({ dependency: dep }, 201);
|
|
438
|
+
}
|
|
439
|
+
} else if (subResource === 'state' || subResource === 'lifecycle') {
|
|
440
|
+
if (method === 'GET') {
|
|
441
|
+
const state = await engine.getTaskLifecycleState(taskId);
|
|
442
|
+
if (!state) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
443
|
+
return this.jsonResponse({ state });
|
|
444
|
+
}
|
|
445
|
+
} else if (subResource === 'transitions' || subResource === 'allowed-transitions') {
|
|
446
|
+
if (method === 'GET') {
|
|
447
|
+
const allowedNextStatuses = await engine.getAllowedTaskTransitions(taskId);
|
|
448
|
+
const allowedPreviousStatuses = await engine.getAllowedPreviousTaskTransitions(taskId);
|
|
449
|
+
return this.jsonResponse({ allowedNextStatuses, allowedPreviousStatuses });
|
|
450
|
+
}
|
|
451
|
+
} else if (subResource === 'ladder') {
|
|
452
|
+
if (method === 'GET') {
|
|
453
|
+
const taskLadder = await engine.getTaskLadder(taskId);
|
|
454
|
+
if (!taskLadder) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
455
|
+
return this.jsonResponse({ taskLadder });
|
|
456
|
+
}
|
|
457
|
+
} else if (subResource === 'metrics') {
|
|
458
|
+
if (method === 'GET') {
|
|
459
|
+
const metrics = await engine.getTaskMetrics(taskId);
|
|
460
|
+
if (!metrics) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
461
|
+
return this.jsonResponse({ metrics });
|
|
462
|
+
}
|
|
463
|
+
} else if (subResource === 'progress-history') {
|
|
464
|
+
if (method === 'GET') {
|
|
465
|
+
const progressHistory = await engine.getTaskProgressHistory(taskId);
|
|
466
|
+
if (!progressHistory) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
467
|
+
return this.jsonResponse({ progressHistory });
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
if (method === 'GET') {
|
|
471
|
+
const task = await engine.getTask(taskId);
|
|
472
|
+
if (!task) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
473
|
+
return this.jsonResponse({ task });
|
|
474
|
+
}
|
|
475
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
476
|
+
const body = await this.readBody(request, schemas.UpdateTaskSchema);
|
|
477
|
+
const updated = await engine.updateTask(taskId, body);
|
|
478
|
+
if (!updated) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
479
|
+
return this.jsonResponse({ task: updated });
|
|
480
|
+
}
|
|
481
|
+
if (method === 'DELETE') {
|
|
482
|
+
const deleted = await engine.deleteTask(taskId);
|
|
483
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
484
|
+
return this.jsonResponse({ success: true });
|
|
715
485
|
}
|
|
716
486
|
}
|
|
487
|
+
}
|
|
717
488
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
489
|
+
// Teams API
|
|
490
|
+
if (segments[0] === 'teams') {
|
|
491
|
+
const teamId = segments[1];
|
|
492
|
+
if (!teamId) {
|
|
493
|
+
if (method === 'GET') {
|
|
494
|
+
const teams = await engine.getTeams();
|
|
495
|
+
return this.jsonResponse({ teams });
|
|
496
|
+
}
|
|
497
|
+
if (method === 'POST') {
|
|
498
|
+
const body = await this.readBody(request, schemas.CreateTeamSchema);
|
|
499
|
+
const team = await engine.createTeam(body);
|
|
500
|
+
return this.jsonResponse({ team }, 201);
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
if (method === 'GET') {
|
|
504
|
+
const team = await engine.getTeam(teamId);
|
|
505
|
+
if (!team) return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
506
|
+
return this.jsonResponse({ team });
|
|
507
|
+
}
|
|
508
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
509
|
+
const body = await this.readBody(request, schemas.UpdateTeamSchema);
|
|
510
|
+
const updated = await engine.updateTeam(teamId, body);
|
|
511
|
+
if (!updated) return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
512
|
+
return this.jsonResponse({ team: updated });
|
|
513
|
+
}
|
|
514
|
+
if (method === 'DELETE') {
|
|
515
|
+
const deleted = await engine.deleteTeam(teamId);
|
|
516
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
517
|
+
return this.jsonResponse({ success: true });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Task Containers API
|
|
523
|
+
if (segments[0] === 'containers') {
|
|
524
|
+
const containerId = segments[1];
|
|
525
|
+
if (!containerId) {
|
|
526
|
+
if (method === 'GET') {
|
|
527
|
+
const projectId = url.searchParams.get('projectId');
|
|
528
|
+
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
529
|
+
const containers = await engine.getContainers(projectId);
|
|
530
|
+
return this.jsonResponse({ containers });
|
|
531
|
+
}
|
|
532
|
+
if (method === 'POST') {
|
|
533
|
+
const body = await this.readBody(request, schemas.CreateContainerSchema);
|
|
534
|
+
const container = await engine.createContainer(body);
|
|
535
|
+
return this.jsonResponse({ container }, 201);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
if (method === 'GET') {
|
|
539
|
+
const container = await engine.getContainer(containerId);
|
|
540
|
+
if (!container) return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
541
|
+
return this.jsonResponse({ container });
|
|
542
|
+
}
|
|
543
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
544
|
+
const body = await this.readBody(request, schemas.UpdateContainerSchema);
|
|
545
|
+
const updated = await engine.updateContainer(containerId, body);
|
|
546
|
+
if (!updated) return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
547
|
+
return this.jsonResponse({ container: updated });
|
|
548
|
+
}
|
|
549
|
+
if (method === 'DELETE') {
|
|
550
|
+
const deleted = await engine.deleteContainer(containerId);
|
|
551
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
552
|
+
return this.jsonResponse({ success: true });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
721
555
|
}
|
|
556
|
+
|
|
557
|
+
// Deliverables API
|
|
558
|
+
if (segments[0] === 'deliverables') {
|
|
559
|
+
const deliverableId = segments[1];
|
|
560
|
+
if (!deliverableId) {
|
|
561
|
+
if (method === 'GET') {
|
|
562
|
+
const projectId = url.searchParams.get('projectId');
|
|
563
|
+
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
564
|
+
const deliverables = await engine.getDeliverables(projectId);
|
|
565
|
+
return this.jsonResponse({ deliverables });
|
|
566
|
+
}
|
|
567
|
+
if (method === 'POST') {
|
|
568
|
+
const body = await this.readBody(request, schemas.CreateDeliverableSchema);
|
|
569
|
+
const deliverable = await engine.createDeliverable(body);
|
|
570
|
+
return this.jsonResponse({ deliverable }, 201);
|
|
571
|
+
}
|
|
572
|
+
} else if (segments[2] === 'summary') {
|
|
573
|
+
if (method === 'GET') {
|
|
574
|
+
const summary = await engine.getDeliverableSummary(deliverableId);
|
|
575
|
+
if (!summary) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
576
|
+
return this.jsonResponse({ summary });
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
if (method === 'GET') {
|
|
580
|
+
const deliverable = await engine.getDeliverable(deliverableId);
|
|
581
|
+
if (!deliverable) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
582
|
+
return this.jsonResponse({ deliverable });
|
|
583
|
+
}
|
|
584
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
585
|
+
const body = await this.readBody(request, schemas.UpdateDeliverableSchema);
|
|
586
|
+
const updated = await engine.updateDeliverable(deliverableId, body);
|
|
587
|
+
if (!updated) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
588
|
+
return this.jsonResponse({ deliverable: updated });
|
|
589
|
+
}
|
|
590
|
+
if (method === 'DELETE') {
|
|
591
|
+
const deleted = await engine.deleteDeliverable(deliverableId);
|
|
592
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
593
|
+
return this.jsonResponse({ success: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Iterations API
|
|
599
|
+
if (segments[0] === 'iterations') {
|
|
600
|
+
const iterationId = segments[1];
|
|
601
|
+
if (!iterationId) {
|
|
602
|
+
if (method === 'GET') {
|
|
603
|
+
const projectId = url.searchParams.get('projectId');
|
|
604
|
+
if (!projectId) return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
605
|
+
const iterations = await engine.getIterations(projectId);
|
|
606
|
+
return this.jsonResponse({ iterations });
|
|
607
|
+
}
|
|
608
|
+
if (method === 'POST') {
|
|
609
|
+
const body = await this.readBody(request, schemas.CreateIterationSchema);
|
|
610
|
+
const iteration = await engine.createIteration(body);
|
|
611
|
+
return this.jsonResponse({ iteration }, 201);
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
if (method === 'GET') {
|
|
615
|
+
const iteration = await engine.getIteration(iterationId);
|
|
616
|
+
if (!iteration) return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
617
|
+
return this.jsonResponse({ iteration });
|
|
618
|
+
}
|
|
619
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
620
|
+
const body = await this.readBody(request, schemas.UpdateIterationSchema);
|
|
621
|
+
const updated = await engine.updateIteration(iterationId, body);
|
|
622
|
+
if (!updated) return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
623
|
+
return this.jsonResponse({ iteration: updated });
|
|
624
|
+
}
|
|
625
|
+
if (method === 'DELETE') {
|
|
626
|
+
const deleted = await engine.deleteIteration(iterationId);
|
|
627
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
628
|
+
return this.jsonResponse({ success: true });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Activities API
|
|
634
|
+
if (segments[0] === 'activities') {
|
|
635
|
+
if (method === 'GET') {
|
|
636
|
+
const projectId = url.searchParams.get('projectId') || undefined;
|
|
637
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
638
|
+
const activities = await engine.getActivities({ projectId, taskId });
|
|
639
|
+
return this.jsonResponse({ activities });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Comments API
|
|
644
|
+
if (segments[0] === 'comments') {
|
|
645
|
+
const commentId = segments[1];
|
|
646
|
+
const subResource = segments[2];
|
|
647
|
+
if (commentId && subResource === 'reactions') {
|
|
648
|
+
if (method === 'POST') {
|
|
649
|
+
const body = await this.readBody(request, schemas.CommentReactionSchema);
|
|
650
|
+
const comment = await engine.addCommentReaction(commentId, body);
|
|
651
|
+
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
652
|
+
return this.jsonResponse({ comment }, 200);
|
|
653
|
+
}
|
|
654
|
+
if (method === 'DELETE') {
|
|
655
|
+
const emoji = url.searchParams.get('emoji');
|
|
656
|
+
if (!emoji) {
|
|
657
|
+
return this.jsonResponse({ error: 'emoji query parameter is required' }, 400);
|
|
658
|
+
}
|
|
659
|
+
const comment = await engine.removeCommentReaction(commentId, { emoji });
|
|
660
|
+
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
661
|
+
return this.jsonResponse({ comment }, 200);
|
|
662
|
+
}
|
|
663
|
+
} else if (!commentId) {
|
|
664
|
+
if (method === 'GET') {
|
|
665
|
+
const taskId = url.searchParams.get('taskId');
|
|
666
|
+
if (!taskId) return this.jsonResponse({ error: 'taskId parameter required' }, 400);
|
|
667
|
+
const comments = await engine.getComments(taskId);
|
|
668
|
+
return this.jsonResponse({ comments });
|
|
669
|
+
}
|
|
670
|
+
if (method === 'POST') {
|
|
671
|
+
const body = await this.readBody(request, schemas.CreateCommentSchema);
|
|
672
|
+
const comment = await engine.addComment(body);
|
|
673
|
+
return this.jsonResponse({ comment }, 201);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
if (method === 'GET') {
|
|
677
|
+
const comment = await engine.getComment(commentId);
|
|
678
|
+
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
679
|
+
return this.jsonResponse({ comment });
|
|
680
|
+
}
|
|
681
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
682
|
+
const body = await this.readBody(request, schemas.UpdateCommentSchema);
|
|
683
|
+
const updated = await engine.updateComment(commentId, body);
|
|
684
|
+
if (!updated) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
685
|
+
return this.jsonResponse({ comment: updated });
|
|
686
|
+
}
|
|
687
|
+
if (method === 'DELETE') {
|
|
688
|
+
const deleted = await engine.deleteComment(commentId);
|
|
689
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
690
|
+
return this.jsonResponse({ success: true });
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Attachments API
|
|
696
|
+
if (segments[0] === 'attachments') {
|
|
697
|
+
const attachmentId = segments[1];
|
|
698
|
+
if (attachmentId === 'presign') {
|
|
699
|
+
if (method === 'POST') {
|
|
700
|
+
const body = await this.readBody(request, schemas.PresignAttachmentSchema);
|
|
701
|
+
const presigned = await engine.getPresignedAttachmentUploadUrl(body);
|
|
702
|
+
return this.jsonResponse({ presigned });
|
|
703
|
+
}
|
|
704
|
+
} else if (attachmentId === 'upload') {
|
|
705
|
+
if (method === 'POST') {
|
|
706
|
+
const body = await this.readBody(request, schemas.UploadAttachmentSchema);
|
|
707
|
+
const attachment = await engine.uploadAttachmentFile(body);
|
|
708
|
+
return this.jsonResponse({ attachment }, 201);
|
|
709
|
+
}
|
|
710
|
+
} else if (!attachmentId) {
|
|
711
|
+
if (method === 'GET') {
|
|
712
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
713
|
+
const projectId = url.searchParams.get('projectId') || undefined;
|
|
714
|
+
const commentId = url.searchParams.get('commentId') || undefined;
|
|
715
|
+
const attachments = await engine.getAttachments({ taskId, projectId, commentId });
|
|
716
|
+
return this.jsonResponse({ attachments });
|
|
717
|
+
}
|
|
718
|
+
if (method === 'POST') {
|
|
719
|
+
const body = await this.readBody(request, schemas.CreateAttachmentSchema);
|
|
720
|
+
const attachment = await engine.createAttachment(body);
|
|
721
|
+
return this.jsonResponse({ attachment }, 201);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
if (method === 'GET') {
|
|
725
|
+
const attachment = await engine.getAttachment(attachmentId);
|
|
726
|
+
if (!attachment) return this.jsonResponse({ error: 'Attachment not found' }, 404);
|
|
727
|
+
return this.jsonResponse({ attachment });
|
|
728
|
+
}
|
|
729
|
+
if (method === 'DELETE') {
|
|
730
|
+
const deleted = await engine.deleteAttachment(attachmentId);
|
|
731
|
+
if (!deleted) return this.jsonResponse({ error: 'Not found' }, 404);
|
|
732
|
+
return this.jsonResponse({ success: true });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Time Tracking API
|
|
738
|
+
if (segments[0] === 'time-entries') {
|
|
739
|
+
if (method === 'GET') {
|
|
740
|
+
const taskId = url.searchParams.get('taskId');
|
|
741
|
+
if (!taskId) return this.jsonResponse({ error: 'taskId parameter required' }, 400);
|
|
742
|
+
const entries = await engine.getTimeEntries(taskId);
|
|
743
|
+
return this.jsonResponse({ timeEntries: entries });
|
|
744
|
+
}
|
|
745
|
+
if (method === 'POST') {
|
|
746
|
+
const body = await this.readBody(request, schemas.LogTimeSchema);
|
|
747
|
+
const entry = await engine.logTime(body);
|
|
748
|
+
return this.jsonResponse({ timeEntry: entry }, 201);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Workload & Capacity API
|
|
753
|
+
if (segments[0] === 'workload') {
|
|
754
|
+
if (method === 'GET') {
|
|
755
|
+
const projectId = url.searchParams.get('projectId') || undefined;
|
|
756
|
+
const startDate = url.searchParams.get('startDate') || undefined;
|
|
757
|
+
const endDate = url.searchParams.get('endDate') || undefined;
|
|
758
|
+
const interval = (url.searchParams.get('interval') || undefined) as any;
|
|
759
|
+
const groupBy = (url.searchParams.get('groupBy') || undefined) as any;
|
|
760
|
+
const metric = (url.searchParams.get('metric') || undefined) as any;
|
|
761
|
+
const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
|
|
762
|
+
? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours')!)
|
|
763
|
+
: undefined;
|
|
764
|
+
|
|
765
|
+
const workload = await engine.getWorkloadDistribution(projectId, {
|
|
766
|
+
startDate,
|
|
767
|
+
endDate,
|
|
768
|
+
interval,
|
|
769
|
+
groupBy,
|
|
770
|
+
metric,
|
|
771
|
+
defaultWeeklyCapacityHours
|
|
772
|
+
});
|
|
773
|
+
return this.jsonResponse({ workload });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Agent Status / Telemetry API
|
|
778
|
+
if (segments[0] === 'status') {
|
|
779
|
+
if (method === 'POST') {
|
|
780
|
+
const body = await this.readJson(request);
|
|
781
|
+
const status = typeof body.status === 'string' ? body.status : 'active';
|
|
782
|
+
if (engine.events) {
|
|
783
|
+
engine.events.publish({
|
|
784
|
+
type: 'agent.status_updated' as any,
|
|
785
|
+
aggregateId: body.taskId || body.projectId || 'system',
|
|
786
|
+
payload: {
|
|
787
|
+
status,
|
|
788
|
+
taskId: body.taskId,
|
|
789
|
+
projectId: body.projectId,
|
|
790
|
+
details: body.details,
|
|
791
|
+
isEngaged: body.isEngaged ?? true,
|
|
792
|
+
timestamp: Date.now()
|
|
793
|
+
}
|
|
794
|
+
} as any);
|
|
795
|
+
}
|
|
796
|
+
return this.jsonResponse({
|
|
797
|
+
success: true,
|
|
798
|
+
status,
|
|
799
|
+
timestamp: Date.now()
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return this.jsonResponse({ error: `Route not found: ${method} ${pathname}` }, 404);
|
|
722
805
|
}
|
|
723
806
|
|
|
724
807
|
/**
|