@critical-path/client 0.17.0 → 0.19.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 +41 -0
- package/README.md +54 -1
- package/dist/contract.test.js +5 -0
- package/dist/contract.test.js.map +1 -1
- package/dist/index.d.ts +64 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +155 -53
- 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/contract.test.ts +6 -0
- package/src/index.ts +195 -56
- package/src/routes.test.ts +96 -0
- package/src/transport.test.ts +111 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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.19.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.30.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.22.0"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=24.0.0"
|
package/src/contract.test.ts
CHANGED
|
@@ -68,6 +68,12 @@ describe('client ↔ server contract', () => {
|
|
|
68
68
|
expect((await client.getWebhooks()).map((w) => w.id)).toContain(webhook.id);
|
|
69
69
|
expect(await client.deleteWebhook(webhook.id)).toBe(true);
|
|
70
70
|
|
|
71
|
+
const page = await client.queryTasks({ projectId: project.id, limit: 1, parentId: null });
|
|
72
|
+
expect(page.items).toHaveLength(1);
|
|
73
|
+
expect(page.nextCursor).toBeTruthy();
|
|
74
|
+
expect((await client.getTasks(project.id)).length).toBeGreaterThan(1);
|
|
75
|
+
expect((await client.getActivities({ projectId: project.id })).length).toBeGreaterThan(0);
|
|
76
|
+
|
|
71
77
|
expect(await client.deleteProject(project.id)).toBe(true);
|
|
72
78
|
expect(await client.deleteProject(project.id)).toBe(false);
|
|
73
79
|
});
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,9 @@ import type {
|
|
|
25
25
|
UpdateWebhookBody
|
|
26
26
|
} from '@critical-path/core/schemas';
|
|
27
27
|
import type {
|
|
28
|
+
ActivityQuery,
|
|
29
|
+
Page,
|
|
30
|
+
TaskQuery,
|
|
28
31
|
PublicWebhook,
|
|
29
32
|
TaskDependency,
|
|
30
33
|
Project,
|
|
@@ -120,12 +123,68 @@ export interface UpdateStatusResult {
|
|
|
120
123
|
timestamp: number;
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
type HeaderMap = Record<string, string>;
|
|
127
|
+
|
|
123
128
|
export interface ClientOptions {
|
|
124
|
-
|
|
125
|
-
|
|
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>);
|
|
126
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 };
|
|
144
|
+
}
|
|
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;
|
|
127
151
|
}
|
|
128
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
|
+
|
|
129
188
|
function toBase64(data: Uint8Array | ArrayBuffer): string {
|
|
130
189
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
131
190
|
let binary = '';
|
|
@@ -135,22 +194,32 @@ function toBase64(data: Uint8Array | ArrayBuffer): string {
|
|
|
135
194
|
return btoa(binary);
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
class NotFoundResponseError extends Error {
|
|
139
|
-
constructor(message: string) {
|
|
140
|
-
super(message);
|
|
141
|
-
this.name = 'NotFoundResponseError';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
197
|
export class CriticalPathClient {
|
|
146
|
-
private baseUrl: string;
|
|
147
|
-
private
|
|
148
|
-
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;
|
|
149
203
|
|
|
150
204
|
constructor(options: ClientOptions) {
|
|
205
|
+
this.options = options;
|
|
151
206
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
152
|
-
this.
|
|
153
|
-
|
|
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;
|
|
154
223
|
}
|
|
155
224
|
|
|
156
225
|
/**
|
|
@@ -162,36 +231,61 @@ export class CriticalPathClient {
|
|
|
162
231
|
const res = await this.request<{ success: boolean }>(endpoint, { method: 'DELETE' });
|
|
163
232
|
return res.success;
|
|
164
233
|
} catch (err) {
|
|
165
|
-
if (err instanceof
|
|
234
|
+
if (err instanceof CriticalPathError && err.isNotFound) return false;
|
|
166
235
|
throw err;
|
|
167
236
|
}
|
|
168
237
|
}
|
|
169
238
|
|
|
170
|
-
private async request<T>(endpoint: string,
|
|
239
|
+
private async request<T>(endpoint: string, init: RequestInit = {}): Promise<T> {
|
|
171
240
|
const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
178
267
|
}
|
|
179
|
-
});
|
|
180
268
|
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
183
278
|
try {
|
|
184
|
-
|
|
185
|
-
if (errJson.error) errorMsg = errJson.error;
|
|
279
|
+
body = await response.json();
|
|
186
280
|
} catch {
|
|
187
|
-
//
|
|
281
|
+
// Non-JSON error body
|
|
188
282
|
}
|
|
189
|
-
|
|
283
|
+
const message = typeof body?.error === 'string' ? body.error : `HTTP Error ${response.status}: ${response.statusText}`;
|
|
284
|
+
throw new CriticalPathError(message, response.status, body);
|
|
190
285
|
}
|
|
191
|
-
|
|
192
|
-
return response.json();
|
|
193
286
|
}
|
|
194
287
|
|
|
288
|
+
|
|
195
289
|
// Workflows
|
|
196
290
|
async getWorkflows(): Promise<Workflow[]> {
|
|
197
291
|
const res = await this.request<{ workflows: Workflow[] }>('/workflows');
|
|
@@ -199,7 +293,7 @@ export class CriticalPathClient {
|
|
|
199
293
|
}
|
|
200
294
|
|
|
201
295
|
async getWorkflow(id: string): Promise<Workflow> {
|
|
202
|
-
const res = await this.request<{ workflow: Workflow }>(`/workflows/${id}`);
|
|
296
|
+
const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`);
|
|
203
297
|
return res.workflow;
|
|
204
298
|
}
|
|
205
299
|
|
|
@@ -212,7 +306,7 @@ export class CriticalPathClient {
|
|
|
212
306
|
}
|
|
213
307
|
|
|
214
308
|
async updateWorkflow(id: string, updates: UpdateWorkflowBody): Promise<Workflow> {
|
|
215
|
-
const res = await this.request<{ workflow: Workflow }>(`/workflows/${id}`, {
|
|
309
|
+
const res = await this.request<{ workflow: Workflow }>(`/workflows/${encodeURIComponent(id)}`, {
|
|
216
310
|
method: 'PATCH',
|
|
217
311
|
body: JSON.stringify(updates)
|
|
218
312
|
});
|
|
@@ -220,7 +314,7 @@ export class CriticalPathClient {
|
|
|
220
314
|
}
|
|
221
315
|
|
|
222
316
|
async deleteWorkflow(id: string): Promise<boolean> {
|
|
223
|
-
return this.deleteRequest(`/workflows/${id}`);
|
|
317
|
+
return this.deleteRequest(`/workflows/${encodeURIComponent(id)}`);
|
|
224
318
|
}
|
|
225
319
|
|
|
226
320
|
// Projects
|
|
@@ -230,7 +324,7 @@ export class CriticalPathClient {
|
|
|
230
324
|
}
|
|
231
325
|
|
|
232
326
|
async getProject(id: string): Promise<Project> {
|
|
233
|
-
const res = await this.request<{ project: Project }>(`/projects/${id}`);
|
|
327
|
+
const res = await this.request<{ project: Project }>(`/projects/${encodeURIComponent(id)}`);
|
|
234
328
|
return res.project;
|
|
235
329
|
}
|
|
236
330
|
|
|
@@ -303,14 +397,40 @@ export class CriticalPathClient {
|
|
|
303
397
|
}
|
|
304
398
|
|
|
305
399
|
// Tasks
|
|
400
|
+
/** All tasks (optionally in one project), following pagination. Prefer `queryTasks` for large lists. */
|
|
306
401
|
async getTasks(projectId?: string): Promise<Task[]> {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
402
|
+
return this.collectPages((cursor) => this.queryTasks({ projectId, cursor, limit: 500 }));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* One page of tasks, oldest first. Filters: `status` and `priority` (arrays), `assigneeId`,
|
|
407
|
+
* `iterationId`, `deliverableId`, `containerId`, `parentId` (`null` for top-level tasks).
|
|
408
|
+
*/
|
|
409
|
+
async queryTasks(query: TaskQuery = {}): Promise<Page<Task>> {
|
|
410
|
+
const params = new URLSearchParams();
|
|
411
|
+
for (const [key, value] of Object.entries(query)) {
|
|
412
|
+
if (value === undefined || key === 'projectIds') continue;
|
|
413
|
+
if (key === 'parentId' && value === null) params.set('parentId', 'none');
|
|
414
|
+
else params.set(key, Array.isArray(value) ? value.join(',') : String(value));
|
|
415
|
+
}
|
|
416
|
+
const qs = params.size ? `?${params}` : '';
|
|
417
|
+
const res = await this.request<{ tasks: Task[]; nextCursor?: string }>(`/tasks${qs}`);
|
|
418
|
+
return { items: res.tasks, nextCursor: res.nextCursor };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private async collectPages<T>(fetchPage: (cursor?: string) => Promise<Page<T>>): Promise<T[]> {
|
|
422
|
+
const items: T[] = [];
|
|
423
|
+
let cursor: string | undefined;
|
|
424
|
+
do {
|
|
425
|
+
const page = await fetchPage(cursor);
|
|
426
|
+
items.push(...page.items);
|
|
427
|
+
cursor = page.nextCursor;
|
|
428
|
+
} while (cursor);
|
|
429
|
+
return items;
|
|
310
430
|
}
|
|
311
431
|
|
|
312
432
|
async getTask(id: string): Promise<Task> {
|
|
313
|
-
const res = await this.request<{ task: Task }>(`/tasks/${id}`);
|
|
433
|
+
const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`);
|
|
314
434
|
return res.task;
|
|
315
435
|
}
|
|
316
436
|
|
|
@@ -344,7 +464,7 @@ export class CriticalPathClient {
|
|
|
344
464
|
}
|
|
345
465
|
|
|
346
466
|
async updateTask(id: string, updates: UpdateTaskBody): Promise<Task> {
|
|
347
|
-
const res = await this.request<{ task: Task }>(`/tasks/${id}`, {
|
|
467
|
+
const res = await this.request<{ task: Task }>(`/tasks/${encodeURIComponent(id)}`, {
|
|
348
468
|
method: 'PATCH',
|
|
349
469
|
body: JSON.stringify(updates)
|
|
350
470
|
});
|
|
@@ -421,6 +541,10 @@ export class CriticalPathClient {
|
|
|
421
541
|
return (await this.request<{ webhooks: PublicWebhook[] }>('/webhooks')).webhooks;
|
|
422
542
|
}
|
|
423
543
|
|
|
544
|
+
async getWebhook(id: string): Promise<PublicWebhook> {
|
|
545
|
+
return (await this.request<{ webhook: PublicWebhook }>(`/webhooks/${encodeURIComponent(id)}`)).webhook;
|
|
546
|
+
}
|
|
547
|
+
|
|
424
548
|
/** Registers a webhook. The returned `secret` is shown only once; store it to verify deliveries. */
|
|
425
549
|
async createWebhook(data: CreateWebhookBody): Promise<{ webhook: PublicWebhook; secret: string }> {
|
|
426
550
|
return this.request<{ webhook: PublicWebhook; secret: string }>('/webhooks', {
|
|
@@ -490,7 +614,7 @@ export class CriticalPathClient {
|
|
|
490
614
|
}
|
|
491
615
|
|
|
492
616
|
async getTeam(id: string): Promise<Team> {
|
|
493
|
-
const res = await this.request<{ team: Team }>(`/teams/${id}`);
|
|
617
|
+
const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`);
|
|
494
618
|
return res.team;
|
|
495
619
|
}
|
|
496
620
|
|
|
@@ -503,7 +627,7 @@ export class CriticalPathClient {
|
|
|
503
627
|
}
|
|
504
628
|
|
|
505
629
|
async updateTeam(id: string, updates: UpdateTeamBody): Promise<Team> {
|
|
506
|
-
const res = await this.request<{ team: Team }>(`/teams/${id}`, {
|
|
630
|
+
const res = await this.request<{ team: Team }>(`/teams/${encodeURIComponent(id)}`, {
|
|
507
631
|
method: 'PATCH',
|
|
508
632
|
body: JSON.stringify(updates)
|
|
509
633
|
});
|
|
@@ -511,7 +635,7 @@ export class CriticalPathClient {
|
|
|
511
635
|
}
|
|
512
636
|
|
|
513
637
|
async deleteTeam(id: string): Promise<boolean> {
|
|
514
|
-
return this.deleteRequest(`/teams/${id}`);
|
|
638
|
+
return this.deleteRequest(`/teams/${encodeURIComponent(id)}`);
|
|
515
639
|
}
|
|
516
640
|
|
|
517
641
|
// Containers
|
|
@@ -520,6 +644,10 @@ export class CriticalPathClient {
|
|
|
520
644
|
return res.containers;
|
|
521
645
|
}
|
|
522
646
|
|
|
647
|
+
async getContainer(id: string): Promise<TaskContainer> {
|
|
648
|
+
return (await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`)).container;
|
|
649
|
+
}
|
|
650
|
+
|
|
523
651
|
async createContainer(data: CreateContainerBody): Promise<TaskContainer> {
|
|
524
652
|
const res = await this.request<{ container: TaskContainer }>('/containers', {
|
|
525
653
|
method: 'POST',
|
|
@@ -529,7 +657,7 @@ export class CriticalPathClient {
|
|
|
529
657
|
}
|
|
530
658
|
|
|
531
659
|
async updateContainer(id: string, updates: UpdateContainerBody): Promise<TaskContainer> {
|
|
532
|
-
const res = await this.request<{ container: TaskContainer }>(`/containers/${id}`, {
|
|
660
|
+
const res = await this.request<{ container: TaskContainer }>(`/containers/${encodeURIComponent(id)}`, {
|
|
533
661
|
method: 'PATCH',
|
|
534
662
|
body: JSON.stringify(updates)
|
|
535
663
|
});
|
|
@@ -537,7 +665,7 @@ export class CriticalPathClient {
|
|
|
537
665
|
}
|
|
538
666
|
|
|
539
667
|
async deleteContainer(id: string): Promise<boolean> {
|
|
540
|
-
return this.deleteRequest(`/containers/${id}`);
|
|
668
|
+
return this.deleteRequest(`/containers/${encodeURIComponent(id)}`);
|
|
541
669
|
}
|
|
542
670
|
|
|
543
671
|
// Deliverables
|
|
@@ -547,12 +675,12 @@ export class CriticalPathClient {
|
|
|
547
675
|
}
|
|
548
676
|
|
|
549
677
|
async getDeliverable(id: string): Promise<Deliverable> {
|
|
550
|
-
const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${id}`);
|
|
678
|
+
const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`);
|
|
551
679
|
return res.deliverable;
|
|
552
680
|
}
|
|
553
681
|
|
|
554
682
|
async getDeliverableSummary(id: string): Promise<DeliverableSummary> {
|
|
555
|
-
const res = await this.request<{ summary: DeliverableSummary }>(`/deliverables/${id}/summary`);
|
|
683
|
+
const res = await this.request<{ summary: DeliverableSummary }>(`/deliverables/${encodeURIComponent(id)}/summary`);
|
|
556
684
|
return res.summary;
|
|
557
685
|
}
|
|
558
686
|
|
|
@@ -565,7 +693,7 @@ export class CriticalPathClient {
|
|
|
565
693
|
}
|
|
566
694
|
|
|
567
695
|
async updateDeliverable(id: string, updates: UpdateDeliverableBody): Promise<Deliverable> {
|
|
568
|
-
const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${id}`, {
|
|
696
|
+
const res = await this.request<{ deliverable: Deliverable }>(`/deliverables/${encodeURIComponent(id)}`, {
|
|
569
697
|
method: 'PATCH',
|
|
570
698
|
body: JSON.stringify(updates)
|
|
571
699
|
});
|
|
@@ -573,7 +701,7 @@ export class CriticalPathClient {
|
|
|
573
701
|
}
|
|
574
702
|
|
|
575
703
|
async deleteDeliverable(id: string): Promise<boolean> {
|
|
576
|
-
return this.deleteRequest(`/deliverables/${id}`);
|
|
704
|
+
return this.deleteRequest(`/deliverables/${encodeURIComponent(id)}`);
|
|
577
705
|
}
|
|
578
706
|
|
|
579
707
|
// Iterations
|
|
@@ -582,6 +710,10 @@ export class CriticalPathClient {
|
|
|
582
710
|
return res.iterations;
|
|
583
711
|
}
|
|
584
712
|
|
|
713
|
+
async getIteration(id: string): Promise<Iteration> {
|
|
714
|
+
return (await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`)).iteration;
|
|
715
|
+
}
|
|
716
|
+
|
|
585
717
|
async createIteration(data: CreateIterationBody): Promise<Iteration> {
|
|
586
718
|
const res = await this.request<{ iteration: Iteration }>('/iterations', {
|
|
587
719
|
method: 'POST',
|
|
@@ -591,7 +723,7 @@ export class CriticalPathClient {
|
|
|
591
723
|
}
|
|
592
724
|
|
|
593
725
|
async updateIteration(id: string, updates: UpdateIterationBody): Promise<Iteration> {
|
|
594
|
-
const res = await this.request<{ iteration: Iteration }>(`/iterations/${id}`, {
|
|
726
|
+
const res = await this.request<{ iteration: Iteration }>(`/iterations/${encodeURIComponent(id)}`, {
|
|
595
727
|
method: 'PATCH',
|
|
596
728
|
body: JSON.stringify(updates)
|
|
597
729
|
});
|
|
@@ -599,17 +731,24 @@ export class CriticalPathClient {
|
|
|
599
731
|
}
|
|
600
732
|
|
|
601
733
|
async deleteIteration(id: string): Promise<boolean> {
|
|
602
|
-
return this.deleteRequest(`/iterations/${id}`);
|
|
734
|
+
return this.deleteRequest(`/iterations/${encodeURIComponent(id)}`);
|
|
603
735
|
}
|
|
604
736
|
|
|
605
737
|
// Activity Stream
|
|
738
|
+
/** The whole activity feed (newest first), following pagination. Prefer `queryActivities` for feeds. */
|
|
606
739
|
async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
|
|
740
|
+
return this.collectPages((cursor) => this.queryActivities({ ...filter, cursor, limit: 500 }));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** One page of the activity feed, newest first. */
|
|
744
|
+
async queryActivities(query: ActivityQuery = {}): Promise<Page<Activity>> {
|
|
607
745
|
const params = new URLSearchParams();
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
|
|
746
|
+
for (const [key, value] of Object.entries(query)) {
|
|
747
|
+
if (value !== undefined && key !== 'projectIds') params.set(key, String(value));
|
|
748
|
+
}
|
|
749
|
+
const qs = params.size ? `?${params}` : '';
|
|
750
|
+
const res = await this.request<{ activities: Activity[]; nextCursor?: string }>(`/activities${qs}`);
|
|
751
|
+
return { items: res.activities, nextCursor: res.nextCursor };
|
|
613
752
|
}
|
|
614
753
|
|
|
615
754
|
// Comments
|