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