@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.
- package/CHANGELOG.md +38 -0
- package/README.md +50 -0
- package/dist/index.d.ts +53 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -43
- package/dist/index.js.map +1 -1
- package/dist/routes.test.d.ts +2 -0
- package/dist/routes.test.d.ts.map +1 -0
- package/dist/routes.test.js +95 -0
- package/dist/routes.test.js.map +1 -0
- package/dist/transport.test.d.ts +2 -0
- package/dist/transport.test.d.ts.map +1 -0
- package/dist/transport.test.js +89 -0
- package/dist/transport.test.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +151 -48
- package/src/routes.test.ts +96 -0
- package/src/transport.test.ts +111 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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 @@
|
|
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@critical-path/client",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
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.
|
|
156
|
-
|
|
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
|
|
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,
|
|
239
|
+
private async request<T>(endpoint: string, init: RequestInit = {}): Promise<T> {
|
|
174
240
|
const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
if (errJson.error) errorMsg = errJson.error;
|
|
279
|
+
body = await response.json();
|
|
189
280
|
} catch {
|
|
190
|
-
//
|
|
281
|
+
// Non-JSON error body
|
|
191
282
|
}
|
|
192
|
-
|
|
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
|