@critical-path/client 0.18.0 → 0.20.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.
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ROUTES } from '@critical-path/server';
3
+ import { CriticalPathClient } from './index.js';
4
+ /**
5
+ * Every server route must be reachable through the SDK. When a route is added to the router (and
6
+ * its OpenAPI `ROUTES` table), add the client method that calls it here.
7
+ */
8
+ const CLIENT_METHOD_FOR_ROUTE = {
9
+ 'GET /webhooks': 'getWebhooks',
10
+ 'POST /webhooks': 'createWebhook',
11
+ 'GET /webhooks/{webhookId}': 'getWebhook',
12
+ 'PATCH /webhooks/{webhookId}': 'updateWebhook',
13
+ 'DELETE /webhooks/{webhookId}': 'deleteWebhook',
14
+ 'GET /workflows': 'getWorkflows',
15
+ 'POST /workflows': 'createWorkflow',
16
+ 'GET /workflows/{workflowId}': 'getWorkflow',
17
+ 'PATCH /workflows/{workflowId}': 'updateWorkflow',
18
+ 'DELETE /workflows/{workflowId}': 'deleteWorkflow',
19
+ 'GET /projects': 'getProjects',
20
+ 'POST /projects': 'createProject',
21
+ 'GET /projects/{projectId}': 'getProject',
22
+ 'PATCH /projects/{projectId}': 'updateProject',
23
+ 'DELETE /projects/{projectId}': 'deleteProject',
24
+ 'GET /projects/{projectId}/critical-path': 'calculateCriticalPath',
25
+ 'GET /projects/{projectId}/ladder': 'getTimelineLadder',
26
+ 'GET /projects/{projectId}/workload': 'getWorkloadDistribution',
27
+ 'GET /tasks': 'queryTasks',
28
+ 'POST /tasks': 'createTask',
29
+ 'GET /tasks/{taskId}': 'getTask',
30
+ 'PATCH /tasks/{taskId}': 'updateTask',
31
+ 'DELETE /tasks/{taskId}': 'deleteTask',
32
+ // Task-scoped aliases of the comment and attachment collections
33
+ 'GET /tasks/{taskId}/comments': 'getComments',
34
+ 'POST /tasks/{taskId}/comments': 'addComment',
35
+ 'GET /tasks/{taskId}/attachments': 'getAttachments',
36
+ 'POST /tasks/{taskId}/attachments': 'createAttachment',
37
+ 'GET /tasks/{taskId}/dependencies': 'getTaskDependencies',
38
+ 'POST /tasks/{taskId}/dependencies': 'addDependency',
39
+ 'DELETE /tasks/{taskId}/dependencies/{dependencyId}': 'removeDependency',
40
+ 'GET /tasks/{taskId}/lifecycle': 'getTaskLifecycleState',
41
+ 'GET /tasks/{taskId}/transitions': 'getAllowedTaskTransitions',
42
+ 'GET /tasks/{taskId}/ladder': 'getTaskLadder',
43
+ 'GET /tasks/{taskId}/metrics': 'getTaskMetrics',
44
+ 'GET /tasks/{taskId}/progress-history': 'getTaskProgressHistory',
45
+ 'GET /teams': 'getTeams',
46
+ 'POST /teams': 'createTeam',
47
+ 'GET /teams/{teamId}': 'getTeam',
48
+ 'PATCH /teams/{teamId}': 'updateTeam',
49
+ 'DELETE /teams/{teamId}': 'deleteTeam',
50
+ 'GET /containers': 'getContainers',
51
+ 'POST /containers': 'createContainer',
52
+ 'GET /containers/{containerId}': 'getContainer',
53
+ 'PATCH /containers/{containerId}': 'updateContainer',
54
+ 'DELETE /containers/{containerId}': 'deleteContainer',
55
+ 'GET /deliverables': 'getDeliverables',
56
+ 'POST /deliverables': 'createDeliverable',
57
+ 'GET /deliverables/{deliverableId}': 'getDeliverable',
58
+ 'GET /deliverables/{deliverableId}/summary': 'getDeliverableSummary',
59
+ 'PATCH /deliverables/{deliverableId}': 'updateDeliverable',
60
+ 'DELETE /deliverables/{deliverableId}': 'deleteDeliverable',
61
+ 'GET /iterations': 'getIterations',
62
+ 'POST /iterations': 'createIteration',
63
+ 'GET /iterations/{iterationId}': 'getIteration',
64
+ 'PATCH /iterations/{iterationId}': 'updateIteration',
65
+ 'DELETE /iterations/{iterationId}': 'deleteIteration',
66
+ 'GET /activities': 'queryActivities',
67
+ 'GET /comments': 'getComments',
68
+ 'POST /comments': 'addComment',
69
+ 'GET /comments/{commentId}': 'getComment',
70
+ 'PATCH /comments/{commentId}': 'updateComment',
71
+ 'DELETE /comments/{commentId}': 'deleteComment',
72
+ 'POST /comments/{commentId}/reactions': 'addCommentReaction',
73
+ 'DELETE /comments/{commentId}/reactions': 'removeCommentReaction',
74
+ 'GET /attachments': 'getAttachments',
75
+ 'POST /attachments': 'createAttachment',
76
+ 'POST /attachments/upload': 'uploadAttachmentFile',
77
+ 'POST /attachments/presign': 'getPresignedAttachmentUploadUrl',
78
+ 'GET /attachments/{attachmentId}': 'getAttachment',
79
+ 'DELETE /attachments/{attachmentId}': 'deleteAttachment',
80
+ 'GET /time-entries': 'getTimeEntries',
81
+ 'POST /time-entries': 'logTime',
82
+ 'GET /workload': 'getWorkloadDistribution',
83
+ 'POST /status': 'updateStatus'
84
+ };
85
+ describe('client route coverage', () => {
86
+ it('has a client method for every server route', () => {
87
+ const client = new CriticalPathClient({ baseUrl: 'http://localhost' });
88
+ const missing = ROUTES.map((r) => `${r.method.toUpperCase()} ${r.path}`).filter((key) => {
89
+ const method = CLIENT_METHOD_FOR_ROUTE[key];
90
+ return !method || typeof client[method] !== 'function';
91
+ });
92
+ expect(missing).toEqual([]);
93
+ });
94
+ });
95
+ //# sourceMappingURL=routes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.test.js","sourceRoot":"","sources":["../src/routes.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=transport.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.test.d.ts","sourceRoot":"","sources":["../src/transport.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { CriticalPathRouter } from '@critical-path/server';
3
+ import { CriticalPathClient, CriticalPathError } from './index.js';
4
+ function recordingFetch(respond) {
5
+ const calls = [];
6
+ const fetchImpl = (async (url, init) => {
7
+ const call = { url, method: init.method ?? 'GET', headers: init.headers };
8
+ calls.push(call);
9
+ return respond(call, calls.length);
10
+ });
11
+ return { calls, fetchImpl };
12
+ }
13
+ const ok = (body) => Response.json(body);
14
+ describe('client transport', () => {
15
+ it('throws CriticalPathError with status and server details', async () => {
16
+ const router = new CriticalPathRouter();
17
+ const client = new CriticalPathClient({
18
+ baseUrl: 'http://localhost/api/critical-path',
19
+ fetch: ((input, init) => router.handleRequest(new Request(input, init)))
20
+ });
21
+ const project = await client.createProject({ name: 'Errors' });
22
+ const a = await client.createTask({ projectId: project.id, title: 'A' });
23
+ const b = await client.createTask({ projectId: project.id, title: 'B' });
24
+ await client.addDependency(a.id, { dependsOnTaskId: b.id });
25
+ const cycle = await client.addDependency(b.id, { dependsOnTaskId: a.id }).catch((e) => e);
26
+ expect(cycle).toBeInstanceOf(CriticalPathError);
27
+ expect(cycle.status).toBe(409);
28
+ expect(cycle.body.cyclePath).toBeDefined();
29
+ const invalid = await client.createTask({ projectId: project.id, title: '' }).catch((e) => e);
30
+ expect(invalid.status).toBe(400);
31
+ expect(invalid.issues?.[0].path).toBe('title');
32
+ const missing = await client.getTask('nope').catch((e) => e);
33
+ expect(missing.isNotFound).toBe(true);
34
+ });
35
+ it('resolves the headers provider for every request and sends Content-Type only with a body', async () => {
36
+ let token = 0;
37
+ const { calls, fetchImpl } = recordingFetch(() => ok({ projects: [], project: { id: 'p' } }));
38
+ const client = new CriticalPathClient({
39
+ baseUrl: 'http://api.test',
40
+ fetch: fetchImpl,
41
+ headers: async () => ({ Authorization: `Bearer t${++token}` })
42
+ });
43
+ await client.getProjects();
44
+ await client.createProject({ name: 'x' });
45
+ expect(calls.map((c) => c.headers.Authorization)).toEqual(['Bearer t1', 'Bearer t2']);
46
+ expect(calls[0].headers['Content-Type']).toBeUndefined();
47
+ expect(calls[1].headers['Content-Type']).toBe('application/json');
48
+ });
49
+ it('encodes ids in paths', async () => {
50
+ const { calls, fetchImpl } = recordingFetch(() => ok({ task: {} }));
51
+ await new CriticalPathClient({ baseUrl: 'http://api.test', fetch: fetchImpl }).getTask('../projects/p1');
52
+ expect(calls[0].url).toBe('http://api.test/tasks/..%2Fprojects%2Fp1');
53
+ });
54
+ it('times out hung requests', async () => {
55
+ const hanging = ((_url, init) => new Promise((_resolve, reject) => init.signal.addEventListener('abort', () => reject(init.signal.reason))));
56
+ const client = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: hanging, timeoutMs: 10 });
57
+ await expect(client.getProjects()).rejects.toMatchObject({ name: 'TimeoutError' });
58
+ });
59
+ it('scopes signals and headers with with(), leaving the original client untouched', async () => {
60
+ const { calls, fetchImpl } = recordingFetch(() => ok({ projects: [] }));
61
+ const client = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: fetchImpl, headers: { 'X-App': 'web' } });
62
+ const controller = new AbortController();
63
+ controller.abort();
64
+ const aborting = ((_url, init) => Promise.reject(init.signal?.reason));
65
+ const cancelled = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: aborting }).with({ signal: controller.signal });
66
+ await expect(cancelled.getProjects()).rejects.toMatchObject({ name: 'AbortError' });
67
+ await client.with({ headers: { 'X-Trace': 'abc' } }).getProjects();
68
+ await client.getProjects();
69
+ expect(calls[0].headers).toMatchObject({ 'X-App': 'web', 'X-Trace': 'abc' });
70
+ expect(calls[1].headers['X-Trace']).toBeUndefined();
71
+ });
72
+ it('retries GET requests on retryable failures but never writes', async () => {
73
+ const { calls, fetchImpl } = recordingFetch((_call, n) => (n < 3 ? new Response(null, { status: 503 }) : ok({ projects: [] })));
74
+ const client = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: fetchImpl, retry: { retries: 2, baseDelayMs: 1 } });
75
+ await expect(client.getProjects()).resolves.toEqual([]);
76
+ expect(calls).toHaveLength(3);
77
+ const { calls: writes, fetchImpl: failingWrites } = recordingFetch(() => new Response(null, { status: 503 }));
78
+ const writer = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: failingWrites, retry: { retries: 2, baseDelayMs: 1 } });
79
+ await expect(writer.createProject({ name: 'x' })).rejects.toBeInstanceOf(CriticalPathError);
80
+ expect(writes).toHaveLength(1);
81
+ });
82
+ it('does not retry client errors', async () => {
83
+ const fetchSpy = vi.fn(async () => Response.json({ error: 'nope' }, { status: 400 }));
84
+ const client = new CriticalPathClient({ baseUrl: 'http://api.test', fetch: fetchSpy, retry: { retries: 3 } });
85
+ await expect(client.getProjects()).rejects.toMatchObject({ status: 400 });
86
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
87
+ });
88
+ });
89
+ //# sourceMappingURL=transport.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.test.js","sourceRoot":"","sources":["../src/transport.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAQnE,SAAS,cAAc,CAAC,OAAgE;IACtF,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,GAAW,EAAE,IAAiB,EAAE,EAAE;QAC1D,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAiC,EAAE,CAAC;QACpG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC,CAA4B,CAAC;IAC9B,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,EAAE,GAAG,CAAC,IAAa,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAElD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC;YACpC,OAAO,EAAE,oCAAoC;YAC7C,KAAK,EAAE,CAAC,CAAC,KAAwB,EAAE,IAAkB,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAiB;SAC1H,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACzE,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACzE,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAE5D,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1F,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;QAE3C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9F,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAE/C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yFAAyF,EAAE,KAAK,IAAI,EAAE;QACvG,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QAC9F,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC;YACpC,OAAO,EAAE,iBAAiB;YAC1B,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC;SAC/D,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;QAC3B,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;QACtF,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACpE,MAAM,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACzG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,KAAK,IAAI,EAAE;QACvC,MAAM,OAAO,GAAG,CAAC,CAAC,IAAY,EAAE,IAAiB,EAAE,EAAE,CACnD,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,MAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAA4B,CAAC;QAC3I,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;QACrG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAErH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAY,EAAE,IAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAA4B,CAAC;QACvH,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9H,MAAM,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QAEpF,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACnE,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAChI,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/H,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxD,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAE9B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QAC9G,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACnI,MAAM,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;QAC5F,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACtF,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAmC,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACzI,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,MAAM,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/client",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Type-safe JavaScript/TypeScript client SDK for consuming Critical Path API endpoints",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@critical-path/core": "0.30.0"
24
+ "@critical-path/core": "0.31.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^24.0.0",
28
28
  "typescript": "^5.7.0",
29
29
  "vitest": "^3.0.0",
30
- "@critical-path/server": "0.22.0"
30
+ "@critical-path/server": "0.23.0"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=24.0.0"
package/src/index.ts CHANGED
@@ -123,12 +123,68 @@ export interface UpdateStatusResult {
123
123
  timestamp: number;
124
124
  }
125
125
 
126
+ type HeaderMap = Record<string, string>;
127
+
126
128
  export interface ClientOptions {
127
- baseUrl: string; // e.g. "http://localhost:3000/api/critical-path"
128
- headers?: Record<string, string>;
129
+ /** API base URL, e.g. `http://localhost:3000/api/critical-path`. */
130
+ baseUrl: string;
131
+ /**
132
+ * Headers sent with every request. Pass a function to compute them per request, e.g. to attach
133
+ * a freshly refreshed access token: `headers: async () => ({ Authorization: `Bearer ${await getToken()}` })`.
134
+ */
135
+ headers?: HeaderMap | (() => HeaderMap | Promise<HeaderMap>);
129
136
  fetch?: typeof fetch;
137
+ /** Abort requests that take longer than this. Default: no timeout. */
138
+ timeoutMs?: number;
139
+ /**
140
+ * Retry GET requests that fail with a network error, 429, 502, 503 or 504. Default: no retries.
141
+ * Delays double from `baseDelayMs` (default 250 ms) and honour `Retry-After` on 429/503.
142
+ */
143
+ retry?: { retries: number; baseDelayMs?: number };
130
144
  }
131
145
 
146
+ /** Per-call options for a scoped client (see `CriticalPathClient.with`). */
147
+ export interface RequestOptions {
148
+ signal?: AbortSignal;
149
+ headers?: HeaderMap;
150
+ timeoutMs?: number;
151
+ }
152
+
153
+ /**
154
+ * Error thrown for non-2xx responses. Carries the HTTP status and the server's error details
155
+ * (`issues` for validation errors, `cyclePath` for dependency cycles, and so on).
156
+ */
157
+ export class CriticalPathError extends Error {
158
+ /** HTTP status code. */
159
+ readonly status: number;
160
+ /** Field-level validation problems for 400 responses. */
161
+ readonly issues?: Array<{ path: string; message: string }>;
162
+ /** The parsed JSON error body, if any (e.g. `fromStatus`, `toStatus`, `cyclePath`, `fieldKey`). */
163
+ readonly body?: Record<string, unknown>;
164
+
165
+ constructor(message: string, status: number, body?: Record<string, unknown>) {
166
+ super(message);
167
+ this.name = 'CriticalPathError';
168
+ this.status = status;
169
+ this.body = body;
170
+ this.issues = Array.isArray(body?.issues) ? (body!.issues as CriticalPathError['issues']) : undefined;
171
+ }
172
+
173
+ get isNotFound(): boolean {
174
+ return this.status === 404;
175
+ }
176
+ }
177
+
178
+ const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
179
+ const sleep = (ms: number, signal?: AbortSignal | null) =>
180
+ new Promise<void>((resolve, reject) => {
181
+ const timer = setTimeout(resolve, ms);
182
+ signal?.addEventListener('abort', () => {
183
+ clearTimeout(timer);
184
+ reject(signal.reason);
185
+ }, { once: true });
186
+ });
187
+
132
188
  function toBase64(data: Uint8Array | ArrayBuffer): string {
133
189
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
134
190
  let binary = '';
@@ -138,22 +194,32 @@ function toBase64(data: Uint8Array | ArrayBuffer): string {
138
194
  return btoa(binary);
139
195
  }
140
196
 
141
- class NotFoundResponseError extends Error {
142
- constructor(message: string) {
143
- super(message);
144
- this.name = 'NotFoundResponseError';
145
- }
146
- }
147
-
148
197
  export class CriticalPathClient {
149
- private baseUrl: string;
150
- private headers: Record<string, string>;
151
- private customFetch: typeof fetch;
198
+ private readonly baseUrl: string;
199
+ private readonly options: ClientOptions;
200
+ private readonly customFetch: typeof fetch;
201
+ /** Set on scoped clients created by `with()`. */
202
+ private readonly requestOptions?: RequestOptions;
152
203
 
153
204
  constructor(options: ClientOptions) {
205
+ this.options = options;
154
206
  this.baseUrl = options.baseUrl.replace(/\/+$/, '');
155
- this.headers = options.headers || {};
156
- this.customFetch = options.fetch || globalThis.fetch;
207
+ this.customFetch = options.fetch || ((...args) => globalThis.fetch(...args));
208
+ }
209
+
210
+ /**
211
+ * Returns a client that applies `options` to every call, e.g. to cancel requests when a
212
+ * component unmounts: `client.with({ signal: controller.signal }).getTasks(projectId)`.
213
+ */
214
+ with(options: RequestOptions): CriticalPathClient {
215
+ const scoped = Object.create(this) as CriticalPathClient;
216
+ const merged: RequestOptions = {
217
+ ...this.requestOptions,
218
+ ...options,
219
+ headers: { ...this.requestOptions?.headers, ...options.headers }
220
+ };
221
+ Object.defineProperty(scoped, 'requestOptions', { value: merged });
222
+ return scoped;
157
223
  }
158
224
 
159
225
  /**
@@ -165,36 +231,61 @@ export class CriticalPathClient {
165
231
  const res = await this.request<{ success: boolean }>(endpoint, { method: 'DELETE' });
166
232
  return res.success;
167
233
  } catch (err) {
168
- if (err instanceof NotFoundResponseError) return false;
234
+ if (err instanceof CriticalPathError && err.isNotFound) return false;
169
235
  throw err;
170
236
  }
171
237
  }
172
238
 
173
- private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
239
+ private async request<T>(endpoint: string, init: RequestInit = {}): Promise<T> {
174
240
  const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
175
- const response = await this.customFetch(url, {
176
- ...options,
177
- headers: {
178
- 'Content-Type': 'application/json',
179
- ...this.headers,
180
- ...options.headers
241
+ const method = (init.method ?? 'GET').toUpperCase();
242
+ const configured = typeof this.options.headers === 'function' ? await this.options.headers() : this.options.headers;
243
+ const headers: HeaderMap = {
244
+ ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
245
+ ...configured,
246
+ ...this.requestOptions?.headers
247
+ };
248
+ const timeoutMs = this.requestOptions?.timeoutMs ?? this.options.timeoutMs;
249
+ const signals = [this.requestOptions?.signal, timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined].filter(
250
+ (s): s is AbortSignal => !!s
251
+ );
252
+ const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
253
+ const retries = method === 'GET' ? this.options.retry?.retries ?? 0 : 0;
254
+ const baseDelay = this.options.retry?.baseDelayMs ?? 250;
255
+
256
+ for (let attempt = 0; ; attempt++) {
257
+ let response: Response;
258
+ try {
259
+ response = await this.customFetch(url, { ...init, method, headers, signal });
260
+ } catch (err) {
261
+ // Network failure; never retry after an abort or timeout.
262
+ if (attempt < retries && !signal?.aborted) {
263
+ await sleep(baseDelay * 2 ** attempt, signal);
264
+ continue;
265
+ }
266
+ throw err;
181
267
  }
182
- });
183
268
 
184
- if (!response.ok) {
185
- let errorMsg = `HTTP Error ${response.status}: ${response.statusText}`;
269
+ if (response.ok) return response.json() as Promise<T>;
270
+
271
+ if (attempt < retries && RETRYABLE_STATUSES.has(response.status)) {
272
+ const retryAfter = Number(response.headers.get('Retry-After'));
273
+ await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : baseDelay * 2 ** attempt, signal);
274
+ continue;
275
+ }
276
+
277
+ let body: Record<string, unknown> | undefined;
186
278
  try {
187
- const errJson = await response.json();
188
- if (errJson.error) errorMsg = errJson.error;
279
+ body = await response.json();
189
280
  } catch {
190
- // Fallback to HTTP error
281
+ // Non-JSON error body
191
282
  }
192
- throw response.status === 404 ? new NotFoundResponseError(errorMsg) : new Error(errorMsg);
283
+ const message = typeof body?.error === 'string' ? body.error : `HTTP Error ${response.status}: ${response.statusText}`;
284
+ throw new CriticalPathError(message, response.status, body);
193
285
  }
194
-
195
- return response.json();
196
286
  }
197
287
 
288
+
198
289
  // Workflows
199
290
  async getWorkflows(): Promise<Workflow[]> {
200
291
  const res = await this.request<{ workflows: Workflow[] }>('/workflows');
@@ -202,7 +293,7 @@ export class CriticalPathClient {
202
293
  }
203
294
 
204
295
  async getWorkflow(id: string): Promise<Workflow> {
205
- const res = await this.request<{ workflow: Workflow }>(`/workflows/${id}`);
296
+ const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`);
206
297
  return res.workflow;
207
298
  }
208
299
 
@@ -215,7 +306,7 @@ export class CriticalPathClient {
215
306
  }
216
307
 
217
308
  async updateWorkflow(id: string, updates: UpdateWorkflowBody): Promise<Workflow> {
218
- const res = await this.request<{ workflow: Workflow }>(`/workflows/${id}`, {
309
+ const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`, {
219
310
  method: 'PATCH',
220
311
  body: JSON.stringify(updates)
221
312
  });
@@ -223,7 +314,7 @@ export class CriticalPathClient {
223
314
  }
224
315
 
225
316
  async deleteWorkflow(id: string): Promise<boolean> {
226
- return this.deleteRequest(`/workflows/${id}`);
317
+ return this.deleteRequest(`/workflows/${encodeURIComponent(id)}`);
227
318
  }
228
319
 
229
320
  // Projects
@@ -233,7 +324,7 @@ export class CriticalPathClient {
233
324
  }
234
325
 
235
326
  async getProject(id: string): Promise<Project> {
236
- const res = await this.request<{ project: Project }>(`/projects/${id}`);
327
+ const res = await this.request<{ project: Project }>(`/projects/${encodeURIComponent(id)}`);
237
328
  return res.project;
238
329
  }
239
330
 
@@ -339,7 +430,7 @@ export class CriticalPathClient {
339
430
  }
340
431
 
341
432
  async getTask(id: string): Promise<Task> {
342
- const res = await this.request<{ task: Task }>(`/tasks/${id}`);
433
+ const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`);
343
434
  return res.task;
344
435
  }
345
436
 
@@ -373,7 +464,7 @@ export class CriticalPathClient {
373
464
  }
374
465
 
375
466
  async updateTask(id: string, updates: UpdateTaskBody): Promise<Task> {
376
- const res = await this.request<{ task: Task }>(`/tasks/${id}`, {
467
+ const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`, {
377
468
  method: 'PATCH',
378
469
  body: JSON.stringify(updates)
379
470
  });
@@ -450,6 +541,10 @@ export class CriticalPathClient {
450
541
  return (await this.request<{ webhooks: PublicWebhook[] }>('/webhooks')).webhooks;
451
542
  }
452
543
 
544
+ async getWebhook(id: string): Promise<PublicWebhook> {
545
+ return (await this.request<{ webhook: PublicWebhook }>(`/webhooks/${encodeURIComponent(id)}`)).webhook;
546
+ }
547
+
453
548
  /** Registers a webhook. The returned `secret` is shown only once; store it to verify deliveries. */
454
549
  async createWebhook(data: CreateWebhookBody): Promise<{ webhook: PublicWebhook; secret: string }> {
455
550
  return this.request<{ webhook: PublicWebhook; secret: string }>('/webhooks', {
@@ -519,7 +614,7 @@ export class CriticalPathClient {
519
614
  }
520
615
 
521
616
  async getTeam(id: string): Promise<Team> {
522
- const res = await this.request<{ team: Team }>(`/teams/${id}`);
617
+ const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`);
523
618
  return res.team;
524
619
  }
525
620
 
@@ -532,7 +627,7 @@ export class CriticalPathClient {
532
627
  }
533
628
 
534
629
  async updateTeam(id: string, updates: UpdateTeamBody): Promise<Team> {
535
- const res = await this.request<{ team: Team }>(`/teams/${id}`, {
630
+ const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`, {
536
631
  method: 'PATCH',
537
632
  body: JSON.stringify(updates)
538
633
  });
@@ -540,7 +635,7 @@ export class CriticalPathClient {
540
635
  }
541
636
 
542
637
  async deleteTeam(id: string): Promise<boolean> {
543
- return this.deleteRequest(`/teams/${id}`);
638
+ return this.deleteRequest(`/teams/${encodeURIComponent(id)}`);
544
639
  }
545
640
 
546
641
  // Containers
@@ -549,6 +644,10 @@ export class CriticalPathClient {
549
644
  return res.containers;
550
645
  }
551
646
 
647
+ async getContainer(id: string): Promise<TaskContainer> {
648
+ return (await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`)).container;
649
+ }
650
+
552
651
  async createContainer(data: CreateContainerBody): Promise<TaskContainer> {
553
652
  const res = await this.request<{ container: TaskContainer }>('/containers', {
554
653
  method: 'POST',
@@ -558,7 +657,7 @@ export class CriticalPathClient {
558
657
  }
559
658
 
560
659
  async updateContainer(id: string, updates: UpdateContainerBody): Promise<TaskContainer> {
561
- const res = await this.request<{ container: TaskContainer }>(`/containers/${id}`, {
660
+ const res = await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`, {
562
661
  method: 'PATCH',
563
662
  body: JSON.stringify(updates)
564
663
  });
@@ -566,7 +665,7 @@ export class CriticalPathClient {
566
665
  }
567
666
 
568
667
  async deleteContainer(id: string): Promise<boolean> {
569
- return this.deleteRequest(`/containers/${id}`);
668
+ return this.deleteRequest(`/containers/${encodeURIComponent(id)}`);
570
669
  }
571
670
 
572
671
  // Deliverables
@@ -576,12 +675,12 @@ export class CriticalPathClient {
576
675
  }
577
676
 
578
677
  async getDeliverable(id: string): Promise<Deliverable> {
579
- const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${id}`);
678
+ const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`);
580
679
  return res.deliverable;
581
680
  }
582
681
 
583
682
  async getDeliverableSummary(id: string): Promise<DeliverableSummary> {
584
- const res = await this.request<{ summary: DeliverableSummary }>(`/deliverables/${id}/summary`);
683
+ const res = await this.request<{ summary: DeliverableSummary }>(`/deliverables/${encodeURIComponent(id)}/summary`);
585
684
  return res.summary;
586
685
  }
587
686
 
@@ -594,7 +693,7 @@ export class CriticalPathClient {
594
693
  }
595
694
 
596
695
  async updateDeliverable(id: string, updates: UpdateDeliverableBody): Promise<Deliverable> {
597
- const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${id}`, {
696
+ const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`, {
598
697
  method: 'PATCH',
599
698
  body: JSON.stringify(updates)
600
699
  });
@@ -602,7 +701,7 @@ export class CriticalPathClient {
602
701
  }
603
702
 
604
703
  async deleteDeliverable(id: string): Promise<boolean> {
605
- return this.deleteRequest(`/deliverables/${id}`);
704
+ return this.deleteRequest(`/deliverables/${encodeURIComponent(id)}`);
606
705
  }
607
706
 
608
707
  // Iterations
@@ -611,6 +710,10 @@ export class CriticalPathClient {
611
710
  return res.iterations;
612
711
  }
613
712
 
713
+ async getIteration(id: string): Promise<Iteration> {
714
+ return (await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`)).iteration;
715
+ }
716
+
614
717
  async createIteration(data: CreateIterationBody): Promise<Iteration> {
615
718
  const res = await this.request<{ iteration: Iteration }>('/iterations', {
616
719
  method: 'POST',
@@ -620,7 +723,7 @@ export class CriticalPathClient {
620
723
  }
621
724
 
622
725
  async updateIteration(id: string, updates: UpdateIterationBody): Promise<Iteration> {
623
- const res = await this.request<{ iteration: Iteration }>(`/iterations/${id}`, {
726
+ const res = await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`, {
624
727
  method: 'PATCH',
625
728
  body: JSON.stringify(updates)
626
729
  });
@@ -628,7 +731,7 @@ export class CriticalPathClient {
628
731
  }
629
732
 
630
733
  async deleteIteration(id: string): Promise<boolean> {
631
- return this.deleteRequest(`/iterations/${id}`);
734
+ return this.deleteRequest(`/iterations/${encodeURIComponent(id)}`);
632
735
  }
633
736
 
634
737
  // Activity Stream