@critical-path/server 0.13.5 → 0.15.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 +63 -0
- package/README.md +68 -0
- package/dist/adapters/next.d.ts +18 -8
- package/dist/adapters/next.d.ts.map +1 -1
- package/dist/adapters/next.js +11 -4
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/sveltekit.d.ts +18 -20
- package/dist/adapters/sveltekit.d.ts.map +1 -1
- package/dist/adapters/sveltekit.js +5 -4
- package/dist/adapters/sveltekit.js.map +1 -1
- package/dist/adapters/universal.d.ts +8 -0
- package/dist/adapters/universal.d.ts.map +1 -0
- package/dist/adapters/universal.js +12 -0
- package/dist/adapters/universal.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +284 -0
- package/dist/index.test.js.map +1 -1
- package/dist/router.d.ts +72 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +274 -124
- package/dist/router.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/next.ts +27 -6
- package/src/adapters/sveltekit.ts +22 -5
- package/src/adapters/universal.ts +16 -0
- package/src/index.test.ts +342 -0
- package/src/index.ts +1 -0
- package/src/router.ts +326 -125
- package/tsconfig.tsbuildinfo +1 -1
package/dist/router.js
CHANGED
|
@@ -1,53 +1,147 @@
|
|
|
1
1
|
import { CriticalPathEngine } from '@critical-path/core';
|
|
2
|
+
const CORS_ALLOW_METHODS = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
|
|
3
|
+
const DEFAULT_CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization'];
|
|
4
|
+
class BadRequestError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'BadRequestError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function isDevelopment() {
|
|
11
|
+
return globalThis.process?.env?.NODE_ENV === 'development';
|
|
12
|
+
}
|
|
2
13
|
export class CriticalPathRouter {
|
|
3
14
|
engine;
|
|
4
|
-
|
|
15
|
+
onError;
|
|
16
|
+
exposeErrors;
|
|
17
|
+
getContext;
|
|
18
|
+
requireAuth;
|
|
19
|
+
basePath;
|
|
20
|
+
cors;
|
|
21
|
+
constructor(configOrEngine, options = {}) {
|
|
5
22
|
if (configOrEngine instanceof CriticalPathEngine) {
|
|
6
23
|
this.engine = configOrEngine;
|
|
7
24
|
}
|
|
8
25
|
else {
|
|
9
26
|
this.engine = new CriticalPathEngine(configOrEngine);
|
|
10
27
|
}
|
|
28
|
+
this.onError = options.onError;
|
|
29
|
+
this.exposeErrors = options.exposeErrors ?? isDevelopment();
|
|
30
|
+
this.getContext = options.getContext;
|
|
31
|
+
this.requireAuth = options.requireAuth ?? false;
|
|
32
|
+
this.basePath = options.basePath ? '/' + options.basePath.replace(/^\/+|\/+$/g, '') : undefined;
|
|
33
|
+
this.cors = options.cors === undefined ? { origins: '*' } : options.cors;
|
|
34
|
+
if (this.cors && this.cors.origins === '*' && this.cors.credentials) {
|
|
35
|
+
throw new Error('CORS credentials cannot be combined with origins "*"; list the allowed origins instead.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Handles a request. Adapters that resolve identity from something other than the `Request`
|
|
40
|
+
* (e.g. SvelteKit `locals`) pass `init.getContext`, which replaces the router's `getContext`
|
|
41
|
+
* for this request.
|
|
42
|
+
*/
|
|
43
|
+
async handleRequest(request, init = {}) {
|
|
44
|
+
const response = await this.dispatch(request, init);
|
|
45
|
+
return this.applyCors(request, response);
|
|
46
|
+
}
|
|
47
|
+
/** Returns the path after the mount point, or `null` when the request is outside `basePath`. */
|
|
48
|
+
resolveSubpath(pathname) {
|
|
49
|
+
if (this.basePath) {
|
|
50
|
+
if (pathname !== this.basePath && !pathname.startsWith(this.basePath + '/'))
|
|
51
|
+
return null;
|
|
52
|
+
return pathname.slice(this.basePath.length).replace(/^\/+/, '');
|
|
53
|
+
}
|
|
54
|
+
// Extract subpath after /critical-path/ or /api/critical-path/
|
|
55
|
+
return pathname.replace(/^.*?\/critical-path\/?/, '').replace(/^\/+/, '');
|
|
56
|
+
}
|
|
57
|
+
applyCors(request, response) {
|
|
58
|
+
if (!this.cors)
|
|
59
|
+
return response;
|
|
60
|
+
const origin = request.headers.get('Origin');
|
|
61
|
+
let allowOrigin;
|
|
62
|
+
if (this.cors.origins === '*') {
|
|
63
|
+
allowOrigin = '*';
|
|
64
|
+
}
|
|
65
|
+
else if (origin && this.cors.origins.includes(origin)) {
|
|
66
|
+
allowOrigin = origin;
|
|
67
|
+
}
|
|
68
|
+
// Copy so headers are mutable even if a hook returned a Response with immutable headers.
|
|
69
|
+
const result = new Response(response.body, response);
|
|
70
|
+
if (this.cors.origins !== '*')
|
|
71
|
+
result.headers.append('Vary', 'Origin');
|
|
72
|
+
if (!allowOrigin)
|
|
73
|
+
return result;
|
|
74
|
+
result.headers.set('Access-Control-Allow-Origin', allowOrigin);
|
|
75
|
+
result.headers.set('Access-Control-Allow-Methods', CORS_ALLOW_METHODS);
|
|
76
|
+
result.headers.set('Access-Control-Allow-Headers', (this.cors.allowHeaders ?? DEFAULT_CORS_ALLOW_HEADERS).join(', '));
|
|
77
|
+
if (this.cors.credentials)
|
|
78
|
+
result.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
79
|
+
if (request.method.toUpperCase() === 'OPTIONS' && this.cors.maxAge !== undefined) {
|
|
80
|
+
result.headers.set('Access-Control-Max-Age', String(this.cors.maxAge));
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
11
83
|
}
|
|
12
|
-
async
|
|
84
|
+
async dispatch(request, init) {
|
|
13
85
|
const url = new URL(request.url);
|
|
14
86
|
const pathname = url.pathname;
|
|
15
87
|
const method = request.method.toUpperCase();
|
|
16
|
-
|
|
17
|
-
|
|
88
|
+
const subpath = this.resolveSubpath(pathname);
|
|
89
|
+
if (subpath === null) {
|
|
90
|
+
return this.jsonResponse({ error: `Route not found: ${method} ${pathname}` }, 404);
|
|
91
|
+
}
|
|
18
92
|
const segments = subpath.split('/').filter(Boolean);
|
|
93
|
+
// CORS preflight (browsers send it without credentials, so it is answered before auth)
|
|
94
|
+
if (method === 'OPTIONS') {
|
|
95
|
+
return new Response(null, { status: 204 });
|
|
96
|
+
}
|
|
97
|
+
let context;
|
|
98
|
+
try {
|
|
99
|
+
const resolve = init.getContext ?? this.getContext;
|
|
100
|
+
context = resolve ? await resolve(request) : undefined;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return this.errorResponse(err, request);
|
|
104
|
+
}
|
|
105
|
+
if (this.requireAuth && !context?.userId) {
|
|
106
|
+
return this.jsonResponse({ error: 'Authentication required' }, 401);
|
|
107
|
+
}
|
|
108
|
+
const engine = context?.userId
|
|
109
|
+
? this.engine.withActor({ userId: context.userId, username: context.userName, actorType: context.actorType })
|
|
110
|
+
: this.engine;
|
|
19
111
|
try {
|
|
20
112
|
// Workflows API
|
|
21
113
|
if (segments[0] === 'workflows') {
|
|
22
114
|
const workflowId = segments[1];
|
|
23
115
|
if (!workflowId) {
|
|
24
116
|
if (method === 'GET') {
|
|
25
|
-
const workflows = await
|
|
117
|
+
const workflows = await engine.getWorkflows();
|
|
26
118
|
return this.jsonResponse({ workflows });
|
|
27
119
|
}
|
|
28
120
|
if (method === 'POST') {
|
|
29
|
-
const body = await
|
|
30
|
-
const workflow = await
|
|
121
|
+
const body = await this.readJson(request);
|
|
122
|
+
const workflow = await engine.createWorkflow(body);
|
|
31
123
|
return this.jsonResponse({ workflow }, 201);
|
|
32
124
|
}
|
|
33
125
|
}
|
|
34
126
|
else {
|
|
35
127
|
if (method === 'GET') {
|
|
36
|
-
const workflow = await
|
|
128
|
+
const workflow = await engine.getWorkflow(workflowId);
|
|
37
129
|
if (!workflow)
|
|
38
130
|
return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
39
131
|
return this.jsonResponse({ workflow });
|
|
40
132
|
}
|
|
41
133
|
if (method === 'PATCH' || method === 'PUT') {
|
|
42
|
-
const body = await
|
|
43
|
-
const updated = await
|
|
134
|
+
const body = await this.readJson(request);
|
|
135
|
+
const updated = await engine.updateWorkflow(workflowId, body);
|
|
44
136
|
if (!updated)
|
|
45
137
|
return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
46
138
|
return this.jsonResponse({ workflow: updated });
|
|
47
139
|
}
|
|
48
140
|
if (method === 'DELETE') {
|
|
49
|
-
const deleted = await
|
|
50
|
-
|
|
141
|
+
const deleted = await engine.deleteWorkflow(workflowId);
|
|
142
|
+
if (!deleted)
|
|
143
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
144
|
+
return this.jsonResponse({ success: true });
|
|
51
145
|
}
|
|
52
146
|
}
|
|
53
147
|
}
|
|
@@ -57,18 +151,18 @@ export class CriticalPathRouter {
|
|
|
57
151
|
const subResource = segments[2];
|
|
58
152
|
if (!projectId) {
|
|
59
153
|
if (method === 'GET') {
|
|
60
|
-
const projects = await
|
|
154
|
+
const projects = await engine.getProjects();
|
|
61
155
|
return this.jsonResponse({ projects });
|
|
62
156
|
}
|
|
63
157
|
if (method === 'POST') {
|
|
64
|
-
const body = await
|
|
65
|
-
const project = await
|
|
158
|
+
const body = await this.readJson(request);
|
|
159
|
+
const project = await engine.createProject(body);
|
|
66
160
|
return this.jsonResponse({ project }, 201);
|
|
67
161
|
}
|
|
68
162
|
}
|
|
69
163
|
else if (subResource === 'critical-path') {
|
|
70
164
|
if (method === 'GET') {
|
|
71
|
-
const analysis = await
|
|
165
|
+
const analysis = await engine.calculateCriticalPath(projectId);
|
|
72
166
|
return this.jsonResponse({ analysis });
|
|
73
167
|
}
|
|
74
168
|
}
|
|
@@ -77,7 +171,7 @@ export class CriticalPathRouter {
|
|
|
77
171
|
const level = (url.searchParams.get('level') || 'all');
|
|
78
172
|
const containerId = url.searchParams.get('containerId') || undefined;
|
|
79
173
|
const iterationId = url.searchParams.get('iterationId') || undefined;
|
|
80
|
-
const ladder = await
|
|
174
|
+
const ladder = await engine.getTimelineLadder(projectId, { level, containerId, iterationId });
|
|
81
175
|
return this.jsonResponse({ ladder });
|
|
82
176
|
}
|
|
83
177
|
}
|
|
@@ -91,7 +185,7 @@ export class CriticalPathRouter {
|
|
|
91
185
|
const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
|
|
92
186
|
? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours'))
|
|
93
187
|
: undefined;
|
|
94
|
-
const workload = await
|
|
188
|
+
const workload = await engine.getWorkloadDistribution(projectId, {
|
|
95
189
|
startDate,
|
|
96
190
|
endDate,
|
|
97
191
|
interval,
|
|
@@ -104,14 +198,23 @@ export class CriticalPathRouter {
|
|
|
104
198
|
}
|
|
105
199
|
else {
|
|
106
200
|
if (method === 'GET') {
|
|
107
|
-
const project = await
|
|
201
|
+
const project = await engine.getProject(projectId);
|
|
202
|
+
if (!project)
|
|
203
|
+
return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
204
|
+
return this.jsonResponse({ project });
|
|
205
|
+
}
|
|
206
|
+
if (method === 'PATCH' || method === 'PUT') {
|
|
207
|
+
const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, ...updates } = await this.readJson(request);
|
|
208
|
+
const project = await engine.updateProject(projectId, updates);
|
|
108
209
|
if (!project)
|
|
109
210
|
return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
110
211
|
return this.jsonResponse({ project });
|
|
111
212
|
}
|
|
112
213
|
if (method === 'DELETE') {
|
|
113
|
-
const deleted = await
|
|
114
|
-
|
|
214
|
+
const deleted = await engine.deleteProject(projectId);
|
|
215
|
+
if (!deleted)
|
|
216
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
217
|
+
return this.jsonResponse({ success: true });
|
|
115
218
|
}
|
|
116
219
|
}
|
|
117
220
|
}
|
|
@@ -122,45 +225,48 @@ export class CriticalPathRouter {
|
|
|
122
225
|
if (!taskId) {
|
|
123
226
|
if (method === 'GET') {
|
|
124
227
|
const projectId = url.searchParams.get('projectId') || undefined;
|
|
125
|
-
const tasks = await
|
|
228
|
+
const tasks = await engine.getTasks(projectId);
|
|
126
229
|
return this.jsonResponse({ tasks });
|
|
127
230
|
}
|
|
128
231
|
if (method === 'POST') {
|
|
129
|
-
const body = await
|
|
130
|
-
const task = await
|
|
232
|
+
const body = await this.readJson(request);
|
|
233
|
+
const task = await engine.createTask(body);
|
|
131
234
|
return this.jsonResponse({ task }, 201);
|
|
132
235
|
}
|
|
133
236
|
}
|
|
134
237
|
else if (subResource === 'comments') {
|
|
135
238
|
if (method === 'GET') {
|
|
136
|
-
const comments = await
|
|
239
|
+
const comments = await engine.getComments(taskId);
|
|
137
240
|
return this.jsonResponse({ comments });
|
|
138
241
|
}
|
|
139
242
|
if (method === 'POST') {
|
|
140
|
-
const body = await
|
|
141
|
-
const comment = await
|
|
243
|
+
const body = await this.readJson(request);
|
|
244
|
+
const comment = await engine.addComment({ ...body, taskId });
|
|
142
245
|
return this.jsonResponse({ comment }, 201);
|
|
143
246
|
}
|
|
144
247
|
}
|
|
145
248
|
else if (subResource === 'attachments') {
|
|
146
249
|
if (method === 'GET') {
|
|
147
|
-
const attachments = await
|
|
250
|
+
const attachments = await engine.getAttachments({ taskId });
|
|
148
251
|
return this.jsonResponse({ attachments });
|
|
149
252
|
}
|
|
150
253
|
if (method === 'POST') {
|
|
151
|
-
const body = await
|
|
152
|
-
const attachment = await
|
|
254
|
+
const body = await this.readJson(request);
|
|
255
|
+
const attachment = await engine.createAttachment({ ...body, taskId });
|
|
153
256
|
return this.jsonResponse({ attachment }, 201);
|
|
154
257
|
}
|
|
155
258
|
}
|
|
156
259
|
else if (subResource === 'dependencies') {
|
|
157
260
|
if (method === 'GET') {
|
|
158
|
-
const graph = await
|
|
261
|
+
const graph = await engine.getTaskDependencyGraph(taskId);
|
|
159
262
|
return this.jsonResponse({ graph });
|
|
160
263
|
}
|
|
161
264
|
if (method === 'POST') {
|
|
162
|
-
const body = await
|
|
163
|
-
|
|
265
|
+
const body = await this.readJson(request);
|
|
266
|
+
if (typeof body.dependsOnTaskId !== 'string' || !body.dependsOnTaskId) {
|
|
267
|
+
return this.jsonResponse({ error: 'dependsOnTaskId is required' }, 400);
|
|
268
|
+
}
|
|
269
|
+
const dep = await engine.addDependency({
|
|
164
270
|
taskId,
|
|
165
271
|
dependsOnTaskId: body.dependsOnTaskId,
|
|
166
272
|
type: body.type || 'blocking'
|
|
@@ -170,7 +276,7 @@ export class CriticalPathRouter {
|
|
|
170
276
|
}
|
|
171
277
|
else if (subResource === 'state' || subResource === 'lifecycle') {
|
|
172
278
|
if (method === 'GET') {
|
|
173
|
-
const state = await
|
|
279
|
+
const state = await engine.getTaskLifecycleState(taskId);
|
|
174
280
|
if (!state)
|
|
175
281
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
176
282
|
return this.jsonResponse({ state });
|
|
@@ -178,14 +284,14 @@ export class CriticalPathRouter {
|
|
|
178
284
|
}
|
|
179
285
|
else if (subResource === 'transitions' || subResource === 'allowed-transitions') {
|
|
180
286
|
if (method === 'GET') {
|
|
181
|
-
const allowedNextStatuses = await
|
|
182
|
-
const allowedPreviousStatuses = await
|
|
287
|
+
const allowedNextStatuses = await engine.getAllowedTaskTransitions(taskId);
|
|
288
|
+
const allowedPreviousStatuses = await engine.getAllowedPreviousTaskTransitions(taskId);
|
|
183
289
|
return this.jsonResponse({ allowedNextStatuses, allowedPreviousStatuses });
|
|
184
290
|
}
|
|
185
291
|
}
|
|
186
292
|
else if (subResource === 'ladder') {
|
|
187
293
|
if (method === 'GET') {
|
|
188
|
-
const taskLadder = await
|
|
294
|
+
const taskLadder = await engine.getTaskLadder(taskId);
|
|
189
295
|
if (!taskLadder)
|
|
190
296
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
191
297
|
return this.jsonResponse({ taskLadder });
|
|
@@ -193,7 +299,7 @@ export class CriticalPathRouter {
|
|
|
193
299
|
}
|
|
194
300
|
else if (subResource === 'metrics') {
|
|
195
301
|
if (method === 'GET') {
|
|
196
|
-
const metrics = await
|
|
302
|
+
const metrics = await engine.getTaskMetrics(taskId);
|
|
197
303
|
if (!metrics)
|
|
198
304
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
199
305
|
return this.jsonResponse({ metrics });
|
|
@@ -201,7 +307,7 @@ export class CriticalPathRouter {
|
|
|
201
307
|
}
|
|
202
308
|
else if (subResource === 'progress-history') {
|
|
203
309
|
if (method === 'GET') {
|
|
204
|
-
const progressHistory = await
|
|
310
|
+
const progressHistory = await engine.getTaskProgressHistory(taskId);
|
|
205
311
|
if (!progressHistory)
|
|
206
312
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
207
313
|
return this.jsonResponse({ progressHistory });
|
|
@@ -209,21 +315,23 @@ export class CriticalPathRouter {
|
|
|
209
315
|
}
|
|
210
316
|
else {
|
|
211
317
|
if (method === 'GET') {
|
|
212
|
-
const task = await
|
|
318
|
+
const task = await engine.getTask(taskId);
|
|
213
319
|
if (!task)
|
|
214
320
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
215
321
|
return this.jsonResponse({ task });
|
|
216
322
|
}
|
|
217
323
|
if (method === 'PATCH' || method === 'PUT') {
|
|
218
|
-
const body = await
|
|
219
|
-
const updated = await
|
|
324
|
+
const body = await this.readJson(request);
|
|
325
|
+
const updated = await engine.updateTask(taskId, body);
|
|
220
326
|
if (!updated)
|
|
221
327
|
return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
222
328
|
return this.jsonResponse({ task: updated });
|
|
223
329
|
}
|
|
224
330
|
if (method === 'DELETE') {
|
|
225
|
-
const deleted = await
|
|
226
|
-
|
|
331
|
+
const deleted = await engine.deleteTask(taskId);
|
|
332
|
+
if (!deleted)
|
|
333
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
334
|
+
return this.jsonResponse({ success: true });
|
|
227
335
|
}
|
|
228
336
|
}
|
|
229
337
|
}
|
|
@@ -232,32 +340,34 @@ export class CriticalPathRouter {
|
|
|
232
340
|
const teamId = segments[1];
|
|
233
341
|
if (!teamId) {
|
|
234
342
|
if (method === 'GET') {
|
|
235
|
-
const teams = await
|
|
343
|
+
const teams = await engine.getTeams();
|
|
236
344
|
return this.jsonResponse({ teams });
|
|
237
345
|
}
|
|
238
346
|
if (method === 'POST') {
|
|
239
|
-
const body = await
|
|
240
|
-
const team = await
|
|
347
|
+
const body = await this.readJson(request);
|
|
348
|
+
const team = await engine.createTeam(body);
|
|
241
349
|
return this.jsonResponse({ team }, 201);
|
|
242
350
|
}
|
|
243
351
|
}
|
|
244
352
|
else {
|
|
245
353
|
if (method === 'GET') {
|
|
246
|
-
const team = await
|
|
354
|
+
const team = await engine.getTeam(teamId);
|
|
247
355
|
if (!team)
|
|
248
356
|
return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
249
357
|
return this.jsonResponse({ team });
|
|
250
358
|
}
|
|
251
359
|
if (method === 'PATCH' || method === 'PUT') {
|
|
252
|
-
const body = await
|
|
253
|
-
const updated = await
|
|
360
|
+
const body = await this.readJson(request);
|
|
361
|
+
const updated = await engine.updateTeam(teamId, body);
|
|
254
362
|
if (!updated)
|
|
255
363
|
return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
256
364
|
return this.jsonResponse({ team: updated });
|
|
257
365
|
}
|
|
258
366
|
if (method === 'DELETE') {
|
|
259
|
-
const deleted = await
|
|
260
|
-
|
|
367
|
+
const deleted = await engine.deleteTeam(teamId);
|
|
368
|
+
if (!deleted)
|
|
369
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
370
|
+
return this.jsonResponse({ success: true });
|
|
261
371
|
}
|
|
262
372
|
}
|
|
263
373
|
}
|
|
@@ -269,32 +379,34 @@ export class CriticalPathRouter {
|
|
|
269
379
|
const projectId = url.searchParams.get('projectId');
|
|
270
380
|
if (!projectId)
|
|
271
381
|
return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
272
|
-
const containers = await
|
|
382
|
+
const containers = await engine.getContainers(projectId);
|
|
273
383
|
return this.jsonResponse({ containers });
|
|
274
384
|
}
|
|
275
385
|
if (method === 'POST') {
|
|
276
|
-
const body = await
|
|
277
|
-
const container = await
|
|
386
|
+
const body = await this.readJson(request);
|
|
387
|
+
const container = await engine.createContainer(body);
|
|
278
388
|
return this.jsonResponse({ container }, 201);
|
|
279
389
|
}
|
|
280
390
|
}
|
|
281
391
|
else {
|
|
282
392
|
if (method === 'GET') {
|
|
283
|
-
const container = await
|
|
393
|
+
const container = await engine.getContainer(containerId);
|
|
284
394
|
if (!container)
|
|
285
395
|
return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
286
396
|
return this.jsonResponse({ container });
|
|
287
397
|
}
|
|
288
398
|
if (method === 'PATCH' || method === 'PUT') {
|
|
289
|
-
const body = await
|
|
290
|
-
const updated = await
|
|
399
|
+
const body = await this.readJson(request);
|
|
400
|
+
const updated = await engine.updateContainer(containerId, body);
|
|
291
401
|
if (!updated)
|
|
292
402
|
return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
293
403
|
return this.jsonResponse({ container: updated });
|
|
294
404
|
}
|
|
295
405
|
if (method === 'DELETE') {
|
|
296
|
-
const deleted = await
|
|
297
|
-
|
|
406
|
+
const deleted = await engine.deleteContainer(containerId);
|
|
407
|
+
if (!deleted)
|
|
408
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
409
|
+
return this.jsonResponse({ success: true });
|
|
298
410
|
}
|
|
299
411
|
}
|
|
300
412
|
}
|
|
@@ -306,18 +418,18 @@ export class CriticalPathRouter {
|
|
|
306
418
|
const projectId = url.searchParams.get('projectId');
|
|
307
419
|
if (!projectId)
|
|
308
420
|
return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
309
|
-
const deliverables = await
|
|
421
|
+
const deliverables = await engine.getDeliverables(projectId);
|
|
310
422
|
return this.jsonResponse({ deliverables });
|
|
311
423
|
}
|
|
312
424
|
if (method === 'POST') {
|
|
313
|
-
const body = await
|
|
314
|
-
const deliverable = await
|
|
425
|
+
const body = await this.readJson(request);
|
|
426
|
+
const deliverable = await engine.createDeliverable(body);
|
|
315
427
|
return this.jsonResponse({ deliverable }, 201);
|
|
316
428
|
}
|
|
317
429
|
}
|
|
318
430
|
else if (segments[2] === 'summary') {
|
|
319
431
|
if (method === 'GET') {
|
|
320
|
-
const summary = await
|
|
432
|
+
const summary = await engine.getDeliverableSummary(deliverableId);
|
|
321
433
|
if (!summary)
|
|
322
434
|
return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
323
435
|
return this.jsonResponse({ summary });
|
|
@@ -325,21 +437,23 @@ export class CriticalPathRouter {
|
|
|
325
437
|
}
|
|
326
438
|
else {
|
|
327
439
|
if (method === 'GET') {
|
|
328
|
-
const deliverable = await
|
|
440
|
+
const deliverable = await engine.getDeliverable(deliverableId);
|
|
329
441
|
if (!deliverable)
|
|
330
442
|
return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
331
443
|
return this.jsonResponse({ deliverable });
|
|
332
444
|
}
|
|
333
445
|
if (method === 'PATCH' || method === 'PUT') {
|
|
334
|
-
const body = await
|
|
335
|
-
const updated = await
|
|
446
|
+
const body = await this.readJson(request);
|
|
447
|
+
const updated = await engine.updateDeliverable(deliverableId, body);
|
|
336
448
|
if (!updated)
|
|
337
449
|
return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
338
450
|
return this.jsonResponse({ deliverable: updated });
|
|
339
451
|
}
|
|
340
452
|
if (method === 'DELETE') {
|
|
341
|
-
const deleted = await
|
|
342
|
-
|
|
453
|
+
const deleted = await engine.deleteDeliverable(deliverableId);
|
|
454
|
+
if (!deleted)
|
|
455
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
456
|
+
return this.jsonResponse({ success: true });
|
|
343
457
|
}
|
|
344
458
|
}
|
|
345
459
|
}
|
|
@@ -351,32 +465,34 @@ export class CriticalPathRouter {
|
|
|
351
465
|
const projectId = url.searchParams.get('projectId');
|
|
352
466
|
if (!projectId)
|
|
353
467
|
return this.jsonResponse({ error: 'projectId parameter required' }, 400);
|
|
354
|
-
const iterations = await
|
|
468
|
+
const iterations = await engine.getIterations(projectId);
|
|
355
469
|
return this.jsonResponse({ iterations });
|
|
356
470
|
}
|
|
357
471
|
if (method === 'POST') {
|
|
358
|
-
const body = await
|
|
359
|
-
const iteration = await
|
|
472
|
+
const body = await this.readJson(request);
|
|
473
|
+
const iteration = await engine.createIteration(body);
|
|
360
474
|
return this.jsonResponse({ iteration }, 201);
|
|
361
475
|
}
|
|
362
476
|
}
|
|
363
477
|
else {
|
|
364
478
|
if (method === 'GET') {
|
|
365
|
-
const iteration = await
|
|
479
|
+
const iteration = await engine.getIteration(iterationId);
|
|
366
480
|
if (!iteration)
|
|
367
481
|
return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
368
482
|
return this.jsonResponse({ iteration });
|
|
369
483
|
}
|
|
370
484
|
if (method === 'PATCH' || method === 'PUT') {
|
|
371
|
-
const body = await
|
|
372
|
-
const updated = await
|
|
485
|
+
const body = await this.readJson(request);
|
|
486
|
+
const updated = await engine.updateIteration(iterationId, body);
|
|
373
487
|
if (!updated)
|
|
374
488
|
return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
375
489
|
return this.jsonResponse({ iteration: updated });
|
|
376
490
|
}
|
|
377
491
|
if (method === 'DELETE') {
|
|
378
|
-
const deleted = await
|
|
379
|
-
|
|
492
|
+
const deleted = await engine.deleteIteration(iterationId);
|
|
493
|
+
if (!deleted)
|
|
494
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
495
|
+
return this.jsonResponse({ success: true });
|
|
380
496
|
}
|
|
381
497
|
}
|
|
382
498
|
}
|
|
@@ -385,7 +501,7 @@ export class CriticalPathRouter {
|
|
|
385
501
|
if (method === 'GET') {
|
|
386
502
|
const projectId = url.searchParams.get('projectId') || undefined;
|
|
387
503
|
const taskId = url.searchParams.get('taskId') || undefined;
|
|
388
|
-
const activities = await
|
|
504
|
+
const activities = await engine.store.getActivities({ projectId, taskId });
|
|
389
505
|
return this.jsonResponse({ activities });
|
|
390
506
|
}
|
|
391
507
|
}
|
|
@@ -395,11 +511,12 @@ export class CriticalPathRouter {
|
|
|
395
511
|
const subResource = segments[2];
|
|
396
512
|
if (commentId && subResource === 'reactions') {
|
|
397
513
|
if (method === 'POST') {
|
|
398
|
-
const body = await
|
|
399
|
-
|
|
514
|
+
const body = await this.readJson(request);
|
|
515
|
+
const userId = context?.userId ?? body.userId;
|
|
516
|
+
if (!body.emoji || !userId) {
|
|
400
517
|
return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
|
|
401
518
|
}
|
|
402
|
-
const comment = await
|
|
519
|
+
const comment = await engine.addCommentReaction(commentId, { emoji: body.emoji, userId });
|
|
403
520
|
if (!comment)
|
|
404
521
|
return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
405
522
|
return this.jsonResponse({ comment }, 200);
|
|
@@ -407,17 +524,17 @@ export class CriticalPathRouter {
|
|
|
407
524
|
if (method === 'DELETE') {
|
|
408
525
|
let body = {};
|
|
409
526
|
try {
|
|
410
|
-
body = await
|
|
527
|
+
body = await this.readJson(request);
|
|
411
528
|
}
|
|
412
529
|
catch {
|
|
413
530
|
// Body may be empty on DELETE, fallback to searchParams
|
|
414
531
|
}
|
|
415
532
|
const emoji = body.emoji || url.searchParams.get('emoji');
|
|
416
|
-
const userId = body.userId || url.searchParams.get('userId');
|
|
533
|
+
const userId = context?.userId ?? (body.userId || url.searchParams.get('userId'));
|
|
417
534
|
if (!emoji || !userId) {
|
|
418
535
|
return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
|
|
419
536
|
}
|
|
420
|
-
const comment = await
|
|
537
|
+
const comment = await engine.removeCommentReaction(commentId, { emoji, userId });
|
|
421
538
|
if (!comment)
|
|
422
539
|
return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
423
540
|
return this.jsonResponse({ comment }, 200);
|
|
@@ -428,32 +545,34 @@ export class CriticalPathRouter {
|
|
|
428
545
|
const taskId = url.searchParams.get('taskId');
|
|
429
546
|
if (!taskId)
|
|
430
547
|
return this.jsonResponse({ error: 'taskId parameter required' }, 400);
|
|
431
|
-
const comments = await
|
|
548
|
+
const comments = await engine.getComments(taskId);
|
|
432
549
|
return this.jsonResponse({ comments });
|
|
433
550
|
}
|
|
434
551
|
if (method === 'POST') {
|
|
435
|
-
const body = await
|
|
436
|
-
const comment = await
|
|
552
|
+
const body = await this.readJson(request);
|
|
553
|
+
const comment = await engine.addComment(body);
|
|
437
554
|
return this.jsonResponse({ comment }, 201);
|
|
438
555
|
}
|
|
439
556
|
}
|
|
440
557
|
else {
|
|
441
558
|
if (method === 'GET') {
|
|
442
|
-
const comment = await
|
|
559
|
+
const comment = await engine.getComment(commentId);
|
|
443
560
|
if (!comment)
|
|
444
561
|
return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
445
562
|
return this.jsonResponse({ comment });
|
|
446
563
|
}
|
|
447
564
|
if (method === 'PATCH' || method === 'PUT') {
|
|
448
|
-
const body = await
|
|
449
|
-
const updated = await
|
|
565
|
+
const body = await this.readJson(request);
|
|
566
|
+
const updated = await engine.updateComment(commentId, body);
|
|
450
567
|
if (!updated)
|
|
451
568
|
return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
452
569
|
return this.jsonResponse({ comment: updated });
|
|
453
570
|
}
|
|
454
571
|
if (method === 'DELETE') {
|
|
455
|
-
const deleted = await
|
|
456
|
-
|
|
572
|
+
const deleted = await engine.deleteComment(commentId);
|
|
573
|
+
if (!deleted)
|
|
574
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
575
|
+
return this.jsonResponse({ success: true });
|
|
457
576
|
}
|
|
458
577
|
}
|
|
459
578
|
}
|
|
@@ -462,15 +581,15 @@ export class CriticalPathRouter {
|
|
|
462
581
|
const attachmentId = segments[1];
|
|
463
582
|
if (attachmentId === 'presign') {
|
|
464
583
|
if (method === 'POST') {
|
|
465
|
-
const body = await
|
|
466
|
-
const presigned = await
|
|
584
|
+
const body = await this.readJson(request);
|
|
585
|
+
const presigned = await engine.getPresignedAttachmentUploadUrl(body);
|
|
467
586
|
return this.jsonResponse({ presigned });
|
|
468
587
|
}
|
|
469
588
|
}
|
|
470
589
|
else if (attachmentId === 'upload') {
|
|
471
590
|
if (method === 'POST') {
|
|
472
|
-
const body = await
|
|
473
|
-
const attachment = await
|
|
591
|
+
const body = await this.readJson(request);
|
|
592
|
+
const attachment = await engine.uploadAttachmentFile(body);
|
|
474
593
|
return this.jsonResponse({ attachment }, 201);
|
|
475
594
|
}
|
|
476
595
|
}
|
|
@@ -479,25 +598,27 @@ export class CriticalPathRouter {
|
|
|
479
598
|
const taskId = url.searchParams.get('taskId') || undefined;
|
|
480
599
|
const projectId = url.searchParams.get('projectId') || undefined;
|
|
481
600
|
const commentId = url.searchParams.get('commentId') || undefined;
|
|
482
|
-
const attachments = await
|
|
601
|
+
const attachments = await engine.getAttachments({ taskId, projectId, commentId });
|
|
483
602
|
return this.jsonResponse({ attachments });
|
|
484
603
|
}
|
|
485
604
|
if (method === 'POST') {
|
|
486
|
-
const body = await
|
|
487
|
-
const attachment = await
|
|
605
|
+
const body = await this.readJson(request);
|
|
606
|
+
const attachment = await engine.createAttachment(body);
|
|
488
607
|
return this.jsonResponse({ attachment }, 201);
|
|
489
608
|
}
|
|
490
609
|
}
|
|
491
610
|
else {
|
|
492
611
|
if (method === 'GET') {
|
|
493
|
-
const attachment = await
|
|
612
|
+
const attachment = await engine.getAttachment(attachmentId);
|
|
494
613
|
if (!attachment)
|
|
495
614
|
return this.jsonResponse({ error: 'Attachment not found' }, 404);
|
|
496
615
|
return this.jsonResponse({ attachment });
|
|
497
616
|
}
|
|
498
617
|
if (method === 'DELETE') {
|
|
499
|
-
const deleted = await
|
|
500
|
-
|
|
618
|
+
const deleted = await engine.deleteAttachment(attachmentId);
|
|
619
|
+
if (!deleted)
|
|
620
|
+
return this.jsonResponse({ error: 'Not found' }, 404);
|
|
621
|
+
return this.jsonResponse({ success: true });
|
|
501
622
|
}
|
|
502
623
|
}
|
|
503
624
|
}
|
|
@@ -507,12 +628,12 @@ export class CriticalPathRouter {
|
|
|
507
628
|
const taskId = url.searchParams.get('taskId');
|
|
508
629
|
if (!taskId)
|
|
509
630
|
return this.jsonResponse({ error: 'taskId parameter required' }, 400);
|
|
510
|
-
const entries = await
|
|
631
|
+
const entries = await engine.store.getTimeEntries(taskId);
|
|
511
632
|
return this.jsonResponse({ timeEntries: entries });
|
|
512
633
|
}
|
|
513
634
|
if (method === 'POST') {
|
|
514
|
-
const body = await
|
|
515
|
-
const entry = await
|
|
635
|
+
const body = await this.readJson(request);
|
|
636
|
+
const entry = await engine.logTime(body);
|
|
516
637
|
return this.jsonResponse({ timeEntry: entry }, 201);
|
|
517
638
|
}
|
|
518
639
|
}
|
|
@@ -528,7 +649,7 @@ export class CriticalPathRouter {
|
|
|
528
649
|
const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
|
|
529
650
|
? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours'))
|
|
530
651
|
: undefined;
|
|
531
|
-
const workload = await
|
|
652
|
+
const workload = await engine.getWorkloadDistribution(projectId, {
|
|
532
653
|
startDate,
|
|
533
654
|
endDate,
|
|
534
655
|
interval,
|
|
@@ -542,10 +663,10 @@ export class CriticalPathRouter {
|
|
|
542
663
|
// Agent Status / Telemetry API
|
|
543
664
|
if (segments[0] === 'status') {
|
|
544
665
|
if (method === 'POST') {
|
|
545
|
-
const body = await
|
|
666
|
+
const body = await this.readJson(request);
|
|
546
667
|
const status = typeof body.status === 'string' ? body.status : 'active';
|
|
547
|
-
if (
|
|
548
|
-
|
|
668
|
+
if (engine.events) {
|
|
669
|
+
engine.events.publish({
|
|
549
670
|
type: 'agent.status_updated',
|
|
550
671
|
aggregateId: body.taskId || body.projectId || 'system',
|
|
551
672
|
payload: {
|
|
@@ -568,28 +689,57 @@ export class CriticalPathRouter {
|
|
|
568
689
|
return this.jsonResponse({ error: `Route not found: ${method} ${pathname}` }, 404);
|
|
569
690
|
}
|
|
570
691
|
catch (err) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
692
|
+
return this.errorResponse(err, request);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async readJson(request) {
|
|
696
|
+
try {
|
|
697
|
+
return await request.json();
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
throw new BadRequestError('Request body must be valid JSON.');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async errorResponse(err, request) {
|
|
704
|
+
const name = err && typeof err === 'object' && 'name' in err ? err.name : undefined;
|
|
705
|
+
const e = err;
|
|
706
|
+
switch (name) {
|
|
707
|
+
case 'BadRequestError':
|
|
708
|
+
case 'ValidationError':
|
|
709
|
+
case 'AttachmentValidationError':
|
|
710
|
+
return this.jsonResponse({ error: e.message }, 400);
|
|
711
|
+
case 'WorkflowValidationError':
|
|
712
|
+
return this.jsonResponse({ error: e.message, fromStatus: e.fromStatus, toStatus: e.toStatus }, 400);
|
|
713
|
+
case 'CustomFieldValidationError':
|
|
714
|
+
return this.jsonResponse({ error: e.message, fieldKey: e.fieldKey }, 400);
|
|
715
|
+
case 'CircularDependencyError':
|
|
716
|
+
return this.jsonResponse({ error: e.message, cyclePath: e.cyclePath }, 409);
|
|
717
|
+
case 'NotFoundError':
|
|
718
|
+
return this.jsonResponse({ error: e.message }, 404);
|
|
719
|
+
}
|
|
720
|
+
// Unexpected errors may carry internals (SQL, URLs, config details), so they are only
|
|
721
|
+
// exposed to callers when explicitly enabled.
|
|
722
|
+
if (this.onError) {
|
|
723
|
+
try {
|
|
724
|
+
const custom = await this.onError(err, request);
|
|
725
|
+
if (custom instanceof Response)
|
|
726
|
+
return custom;
|
|
727
|
+
}
|
|
728
|
+
catch (hookErr) {
|
|
729
|
+
console.error('[CriticalPathRouter] onError hook threw:', hookErr);
|
|
580
730
|
}
|
|
581
|
-
const message = err instanceof Error ? err.message : 'Internal Server Error';
|
|
582
|
-
return this.jsonResponse({ error: message }, 500);
|
|
583
731
|
}
|
|
732
|
+
else {
|
|
733
|
+
console.error('[CriticalPathRouter] Unhandled error:', err);
|
|
734
|
+
}
|
|
735
|
+
const message = this.exposeErrors && err instanceof Error ? err.message : 'Internal Server Error';
|
|
736
|
+
return this.jsonResponse({ error: message }, 500);
|
|
584
737
|
}
|
|
585
738
|
jsonResponse(data, status = 200) {
|
|
586
739
|
return new Response(JSON.stringify(data), {
|
|
587
740
|
status,
|
|
588
741
|
headers: {
|
|
589
|
-
'Content-Type': 'application/json'
|
|
590
|
-
'Access-Control-Allow-Origin': '*',
|
|
591
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
592
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
742
|
+
'Content-Type': 'application/json'
|
|
593
743
|
}
|
|
594
744
|
});
|
|
595
745
|
}
|