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