@critical-path/server 0.15.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 +26 -0
- package/README.md +19 -1
- 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 +64 -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 +12 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +62 -37
- package/dist/router.js.map +1 -1
- package/package.json +3 -2
- package/src/index.test.ts +78 -0
- package/src/index.ts +1 -0
- package/src/openapi.test.ts +95 -0
- package/src/openapi.ts +178 -0
- package/src/router.ts +69 -37
- package/tsconfig.tsbuildinfo +1 -1
package/src/router.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CriticalPathEngine, type AuthorType, type CriticalPathConfig } from '@critical-path/core';
|
|
2
|
+
import * as schemas from '@critical-path/core/schemas';
|
|
3
|
+
import { buildOpenApiDocument } from './openapi.js';
|
|
2
4
|
|
|
3
5
|
const CORS_ALLOW_METHODS = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
|
|
4
6
|
const DEFAULT_CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization'];
|
|
@@ -173,6 +175,11 @@ export class CriticalPathRouter {
|
|
|
173
175
|
: this.engine;
|
|
174
176
|
|
|
175
177
|
try {
|
|
178
|
+
// OpenAPI contract (behind the same auth as every other route)
|
|
179
|
+
if (segments[0] === 'openapi.json' && segments.length === 1 && method === 'GET') {
|
|
180
|
+
return this.jsonResponse(buildOpenApiDocument({ serverUrl: this.openApiServerUrl(url, subpath) }));
|
|
181
|
+
}
|
|
182
|
+
|
|
176
183
|
// Workflows API
|
|
177
184
|
if (segments[0] === 'workflows') {
|
|
178
185
|
const workflowId = segments[1];
|
|
@@ -182,7 +189,7 @@ export class CriticalPathRouter {
|
|
|
182
189
|
return this.jsonResponse({ workflows });
|
|
183
190
|
}
|
|
184
191
|
if (method === 'POST') {
|
|
185
|
-
const body = await this.
|
|
192
|
+
const body = await this.readBody(request, schemas.CreateWorkflowSchema);
|
|
186
193
|
const workflow = await engine.createWorkflow(body);
|
|
187
194
|
return this.jsonResponse({ workflow }, 201);
|
|
188
195
|
}
|
|
@@ -193,7 +200,7 @@ export class CriticalPathRouter {
|
|
|
193
200
|
return this.jsonResponse({ workflow });
|
|
194
201
|
}
|
|
195
202
|
if (method === 'PATCH' || method === 'PUT') {
|
|
196
|
-
const body = await this.
|
|
203
|
+
const body = await this.readBody(request, schemas.UpdateWorkflowSchema);
|
|
197
204
|
const updated = await engine.updateWorkflow(workflowId, body);
|
|
198
205
|
if (!updated) return this.jsonResponse({ error: 'Workflow not found' }, 404);
|
|
199
206
|
return this.jsonResponse({ workflow: updated });
|
|
@@ -216,7 +223,7 @@ export class CriticalPathRouter {
|
|
|
216
223
|
return this.jsonResponse({ projects });
|
|
217
224
|
}
|
|
218
225
|
if (method === 'POST') {
|
|
219
|
-
const body = await this.
|
|
226
|
+
const body = await this.readBody(request, schemas.CreateProjectSchema);
|
|
220
227
|
const project = await engine.createProject(body);
|
|
221
228
|
return this.jsonResponse({ project }, 201);
|
|
222
229
|
}
|
|
@@ -261,7 +268,7 @@ export class CriticalPathRouter {
|
|
|
261
268
|
return this.jsonResponse({ project });
|
|
262
269
|
}
|
|
263
270
|
if (method === 'PATCH' || method === 'PUT') {
|
|
264
|
-
const
|
|
271
|
+
const updates = await this.readBody(request, schemas.UpdateProjectSchema);
|
|
265
272
|
const project = await engine.updateProject(projectId, updates);
|
|
266
273
|
if (!project) return this.jsonResponse({ error: 'Project not found' }, 404);
|
|
267
274
|
return this.jsonResponse({ project });
|
|
@@ -286,7 +293,7 @@ export class CriticalPathRouter {
|
|
|
286
293
|
return this.jsonResponse({ tasks });
|
|
287
294
|
}
|
|
288
295
|
if (method === 'POST') {
|
|
289
|
-
const body = await this.
|
|
296
|
+
const body = await this.readBody(request, schemas.CreateTaskSchema);
|
|
290
297
|
const task = await engine.createTask(body);
|
|
291
298
|
return this.jsonResponse({ task }, 201);
|
|
292
299
|
}
|
|
@@ -296,8 +303,8 @@ export class CriticalPathRouter {
|
|
|
296
303
|
return this.jsonResponse({ comments });
|
|
297
304
|
}
|
|
298
305
|
if (method === 'POST') {
|
|
299
|
-
const body = await this.
|
|
300
|
-
const comment = await engine.addComment({ ...body,
|
|
306
|
+
const body = await this.readBody(request, schemas.CreateCommentSchema, { taskId });
|
|
307
|
+
const comment = await engine.addComment({ ...body, authorId: this.requireIdentity(context, body.authorId, 'authorId') });
|
|
301
308
|
return this.jsonResponse({ comment }, 201);
|
|
302
309
|
}
|
|
303
310
|
} else if (subResource === 'attachments') {
|
|
@@ -306,8 +313,8 @@ export class CriticalPathRouter {
|
|
|
306
313
|
return this.jsonResponse({ attachments });
|
|
307
314
|
}
|
|
308
315
|
if (method === 'POST') {
|
|
309
|
-
const body = await this.
|
|
310
|
-
const attachment = await engine.createAttachment({ ...body,
|
|
316
|
+
const body = await this.readBody(request, schemas.CreateAttachmentSchema, { taskId });
|
|
317
|
+
const attachment = await engine.createAttachment({ ...body, uploaderId: this.requireIdentity(context, body.uploaderId, 'uploaderId') });
|
|
311
318
|
return this.jsonResponse({ attachment }, 201);
|
|
312
319
|
}
|
|
313
320
|
} else if (subResource === 'dependencies') {
|
|
@@ -316,10 +323,7 @@ export class CriticalPathRouter {
|
|
|
316
323
|
return this.jsonResponse({ graph });
|
|
317
324
|
}
|
|
318
325
|
if (method === 'POST') {
|
|
319
|
-
const body = await this.
|
|
320
|
-
if (typeof body.dependsOnTaskId !== 'string' || !body.dependsOnTaskId) {
|
|
321
|
-
return this.jsonResponse({ error: 'dependsOnTaskId is required' }, 400);
|
|
322
|
-
}
|
|
326
|
+
const body = await this.readBody(request, schemas.CreateDependencySchema);
|
|
323
327
|
const dep = await engine.addDependency({
|
|
324
328
|
taskId,
|
|
325
329
|
dependsOnTaskId: body.dependsOnTaskId,
|
|
@@ -364,7 +368,7 @@ export class CriticalPathRouter {
|
|
|
364
368
|
return this.jsonResponse({ task });
|
|
365
369
|
}
|
|
366
370
|
if (method === 'PATCH' || method === 'PUT') {
|
|
367
|
-
const body = await this.
|
|
371
|
+
const body = await this.readBody(request, schemas.UpdateTaskSchema);
|
|
368
372
|
const updated = await engine.updateTask(taskId, body);
|
|
369
373
|
if (!updated) return this.jsonResponse({ error: 'Task not found' }, 404);
|
|
370
374
|
return this.jsonResponse({ task: updated });
|
|
@@ -386,7 +390,7 @@ export class CriticalPathRouter {
|
|
|
386
390
|
return this.jsonResponse({ teams });
|
|
387
391
|
}
|
|
388
392
|
if (method === 'POST') {
|
|
389
|
-
const body = await this.
|
|
393
|
+
const body = await this.readBody(request, schemas.CreateTeamSchema);
|
|
390
394
|
const team = await engine.createTeam(body);
|
|
391
395
|
return this.jsonResponse({ team }, 201);
|
|
392
396
|
}
|
|
@@ -397,7 +401,7 @@ export class CriticalPathRouter {
|
|
|
397
401
|
return this.jsonResponse({ team });
|
|
398
402
|
}
|
|
399
403
|
if (method === 'PATCH' || method === 'PUT') {
|
|
400
|
-
const body = await this.
|
|
404
|
+
const body = await this.readBody(request, schemas.UpdateTeamSchema);
|
|
401
405
|
const updated = await engine.updateTeam(teamId, body);
|
|
402
406
|
if (!updated) return this.jsonResponse({ error: 'Team not found' }, 404);
|
|
403
407
|
return this.jsonResponse({ team: updated });
|
|
@@ -421,7 +425,7 @@ export class CriticalPathRouter {
|
|
|
421
425
|
return this.jsonResponse({ containers });
|
|
422
426
|
}
|
|
423
427
|
if (method === 'POST') {
|
|
424
|
-
const body = await this.
|
|
428
|
+
const body = await this.readBody(request, schemas.CreateContainerSchema);
|
|
425
429
|
const container = await engine.createContainer(body);
|
|
426
430
|
return this.jsonResponse({ container }, 201);
|
|
427
431
|
}
|
|
@@ -432,7 +436,7 @@ export class CriticalPathRouter {
|
|
|
432
436
|
return this.jsonResponse({ container });
|
|
433
437
|
}
|
|
434
438
|
if (method === 'PATCH' || method === 'PUT') {
|
|
435
|
-
const body = await this.
|
|
439
|
+
const body = await this.readBody(request, schemas.UpdateContainerSchema);
|
|
436
440
|
const updated = await engine.updateContainer(containerId, body);
|
|
437
441
|
if (!updated) return this.jsonResponse({ error: 'Container not found' }, 404);
|
|
438
442
|
return this.jsonResponse({ container: updated });
|
|
@@ -456,7 +460,7 @@ export class CriticalPathRouter {
|
|
|
456
460
|
return this.jsonResponse({ deliverables });
|
|
457
461
|
}
|
|
458
462
|
if (method === 'POST') {
|
|
459
|
-
const body = await this.
|
|
463
|
+
const body = await this.readBody(request, schemas.CreateDeliverableSchema);
|
|
460
464
|
const deliverable = await engine.createDeliverable(body);
|
|
461
465
|
return this.jsonResponse({ deliverable }, 201);
|
|
462
466
|
}
|
|
@@ -473,7 +477,7 @@ export class CriticalPathRouter {
|
|
|
473
477
|
return this.jsonResponse({ deliverable });
|
|
474
478
|
}
|
|
475
479
|
if (method === 'PATCH' || method === 'PUT') {
|
|
476
|
-
const body = await this.
|
|
480
|
+
const body = await this.readBody(request, schemas.UpdateDeliverableSchema);
|
|
477
481
|
const updated = await engine.updateDeliverable(deliverableId, body);
|
|
478
482
|
if (!updated) return this.jsonResponse({ error: 'Deliverable not found' }, 404);
|
|
479
483
|
return this.jsonResponse({ deliverable: updated });
|
|
@@ -497,7 +501,7 @@ export class CriticalPathRouter {
|
|
|
497
501
|
return this.jsonResponse({ iterations });
|
|
498
502
|
}
|
|
499
503
|
if (method === 'POST') {
|
|
500
|
-
const body = await this.
|
|
504
|
+
const body = await this.readBody(request, schemas.CreateIterationSchema);
|
|
501
505
|
const iteration = await engine.createIteration(body);
|
|
502
506
|
return this.jsonResponse({ iteration }, 201);
|
|
503
507
|
}
|
|
@@ -508,7 +512,7 @@ export class CriticalPathRouter {
|
|
|
508
512
|
return this.jsonResponse({ iteration });
|
|
509
513
|
}
|
|
510
514
|
if (method === 'PATCH' || method === 'PUT') {
|
|
511
|
-
const body = await this.
|
|
515
|
+
const body = await this.readBody(request, schemas.UpdateIterationSchema);
|
|
512
516
|
const updated = await engine.updateIteration(iterationId, body);
|
|
513
517
|
if (!updated) return this.jsonResponse({ error: 'Iteration not found' }, 404);
|
|
514
518
|
return this.jsonResponse({ iteration: updated });
|
|
@@ -537,11 +541,8 @@ export class CriticalPathRouter {
|
|
|
537
541
|
const subResource = segments[2];
|
|
538
542
|
if (commentId && subResource === 'reactions') {
|
|
539
543
|
if (method === 'POST') {
|
|
540
|
-
const body = await this.
|
|
541
|
-
const userId = context
|
|
542
|
-
if (!body.emoji || !userId) {
|
|
543
|
-
return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
|
|
544
|
-
}
|
|
544
|
+
const body = await this.readBody(request, schemas.CommentReactionSchema);
|
|
545
|
+
const userId = this.requireIdentity(context, body.userId, 'userId');
|
|
545
546
|
const comment = await engine.addCommentReaction(commentId, { emoji: body.emoji, userId });
|
|
546
547
|
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
547
548
|
return this.jsonResponse({ comment }, 200);
|
|
@@ -570,8 +571,8 @@ export class CriticalPathRouter {
|
|
|
570
571
|
return this.jsonResponse({ comments });
|
|
571
572
|
}
|
|
572
573
|
if (method === 'POST') {
|
|
573
|
-
const body = await this.
|
|
574
|
-
const comment = await engine.addComment(body);
|
|
574
|
+
const body = await this.readBody(request, schemas.CreateCommentSchema);
|
|
575
|
+
const comment = await engine.addComment({ ...body, authorId: this.requireIdentity(context, body.authorId, 'authorId') });
|
|
575
576
|
return this.jsonResponse({ comment }, 201);
|
|
576
577
|
}
|
|
577
578
|
} else {
|
|
@@ -581,7 +582,7 @@ export class CriticalPathRouter {
|
|
|
581
582
|
return this.jsonResponse({ comment });
|
|
582
583
|
}
|
|
583
584
|
if (method === 'PATCH' || method === 'PUT') {
|
|
584
|
-
const body = await this.
|
|
585
|
+
const body = await this.readBody(request, schemas.UpdateCommentSchema);
|
|
585
586
|
const updated = await engine.updateComment(commentId, body);
|
|
586
587
|
if (!updated) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
587
588
|
return this.jsonResponse({ comment: updated });
|
|
@@ -599,14 +600,14 @@ export class CriticalPathRouter {
|
|
|
599
600
|
const attachmentId = segments[1];
|
|
600
601
|
if (attachmentId === 'presign') {
|
|
601
602
|
if (method === 'POST') {
|
|
602
|
-
const body = await this.
|
|
603
|
+
const body = await this.readBody(request, schemas.PresignAttachmentSchema);
|
|
603
604
|
const presigned = await engine.getPresignedAttachmentUploadUrl(body);
|
|
604
605
|
return this.jsonResponse({ presigned });
|
|
605
606
|
}
|
|
606
607
|
} else if (attachmentId === 'upload') {
|
|
607
608
|
if (method === 'POST') {
|
|
608
|
-
const body = await this.
|
|
609
|
-
const attachment = await engine.uploadAttachmentFile(body);
|
|
609
|
+
const body = await this.readBody(request, schemas.UploadAttachmentSchema);
|
|
610
|
+
const attachment = await engine.uploadAttachmentFile({ ...body, uploaderId: this.requireIdentity(context, body.uploaderId, 'uploaderId') });
|
|
610
611
|
return this.jsonResponse({ attachment }, 201);
|
|
611
612
|
}
|
|
612
613
|
} else if (!attachmentId) {
|
|
@@ -618,8 +619,8 @@ export class CriticalPathRouter {
|
|
|
618
619
|
return this.jsonResponse({ attachments });
|
|
619
620
|
}
|
|
620
621
|
if (method === 'POST') {
|
|
621
|
-
const body = await this.
|
|
622
|
-
const attachment = await engine.createAttachment(body);
|
|
622
|
+
const body = await this.readBody(request, schemas.CreateAttachmentSchema);
|
|
623
|
+
const attachment = await engine.createAttachment({ ...body, uploaderId: this.requireIdentity(context, body.uploaderId, 'uploaderId') });
|
|
623
624
|
return this.jsonResponse({ attachment }, 201);
|
|
624
625
|
}
|
|
625
626
|
} else {
|
|
@@ -645,7 +646,7 @@ export class CriticalPathRouter {
|
|
|
645
646
|
return this.jsonResponse({ timeEntries: entries });
|
|
646
647
|
}
|
|
647
648
|
if (method === 'POST') {
|
|
648
|
-
const body = await this.
|
|
649
|
+
const body = await this.readBody(request, schemas.LogTimeSchema);
|
|
649
650
|
const entry = await engine.logTime(body);
|
|
650
651
|
return this.jsonResponse({ timeEntry: entry }, 201);
|
|
651
652
|
}
|
|
@@ -709,6 +710,36 @@ export class CriticalPathRouter {
|
|
|
709
710
|
}
|
|
710
711
|
}
|
|
711
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Reads and validates a JSON body. `overrides` (e.g. a `taskId` from the URL) are applied before
|
|
715
|
+
* validation so path parameters always win over body fields.
|
|
716
|
+
*/
|
|
717
|
+
private async readBody<S extends { safeParse(data: unknown): unknown }>(
|
|
718
|
+
request: Request,
|
|
719
|
+
schema: S,
|
|
720
|
+
overrides: Record<string, unknown> = {}
|
|
721
|
+
): Promise<S extends { _output: infer O } ? O : never> {
|
|
722
|
+
const raw = await this.readJson(request);
|
|
723
|
+
const body = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
724
|
+
return schemas.parsePayload(schema as any, { ...body, ...overrides });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Returns the identity to record for a write: the resolved caller when there is one, otherwise
|
|
729
|
+
* the value supplied in the body. Throws a 400 when neither is present.
|
|
730
|
+
*/
|
|
731
|
+
private requireIdentity(context: RequestContext | null | undefined, claimed: string | undefined, field: string): string {
|
|
732
|
+
const identity = context?.userId ?? claimed;
|
|
733
|
+
if (!identity) throw new BadRequestError(`${field} is required`);
|
|
734
|
+
return identity;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** The API root as seen by this request, e.g. `https://host/api/critical-path`. */
|
|
738
|
+
private openApiServerUrl(url: URL, subpath: string): string {
|
|
739
|
+
const root = url.pathname.slice(0, url.pathname.length - subpath.length).replace(/\/+$/, '');
|
|
740
|
+
return `${url.origin}${root}`;
|
|
741
|
+
}
|
|
742
|
+
|
|
712
743
|
private async readJson(request: Request): Promise<any> {
|
|
713
744
|
try {
|
|
714
745
|
return await request.json();
|
|
@@ -723,9 +754,10 @@ export class CriticalPathRouter {
|
|
|
723
754
|
|
|
724
755
|
switch (name) {
|
|
725
756
|
case 'BadRequestError':
|
|
726
|
-
case 'ValidationError':
|
|
727
757
|
case 'AttachmentValidationError':
|
|
728
758
|
return this.jsonResponse({ error: e.message }, 400);
|
|
759
|
+
case 'ValidationError':
|
|
760
|
+
return this.jsonResponse(e.issues ? { error: e.message, issues: e.issues } : { error: e.message }, 400);
|
|
729
761
|
case 'WorkflowValidationError':
|
|
730
762
|
return this.jsonResponse({ error: e.message, fromStatus: e.fromStatus, toStatus: e.toStatus }, 400);
|
|
731
763
|
case 'CustomFieldValidationError':
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.test.ts","./src/index.ts","./src/router.ts","./src/adapters/next.ts","./src/adapters/sveltekit.ts","./src/adapters/universal.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/index.test.ts","./src/index.ts","./src/openapi.test.ts","./src/openapi.ts","./src/router.ts","./src/adapters/next.ts","./src/adapters/sveltekit.ts","./src/adapters/universal.ts"],"version":"5.9.3"}
|