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