Skip to content

Commit 2dd4d29

Browse files
committed
feat: Migrate from axios to ky
1 parent c5a6c97 commit 2dd4d29

File tree

7 files changed

+9654
-7723
lines changed

7 files changed

+9654
-7723
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"tsx": "^3.12.8",
3232
"typescript": "^5.2.2",
3333
"vitest": "^1.6.0"
34-
}
34+
},
35+
"packageManager": "[email protected]+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247"
3536
}

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"license": "MIT",
2929
"dependencies": {
3030
"@badgateway/oauth2-client": "^2.2.4",
31-
"axios": "^1.5.0",
31+
"ky": "^1.7.1",
3232
"uuid": "^9.0.0"
3333
},
3434
"devDependencies": {

packages/sdk/src/index.e2e.test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/**
22
* E2E tests
33
*/
4-
import { MermaidChart } from './index.js';
5-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
6-
4+
import { HTTPError } from 'ky';
75
import process from 'node:process';
8-
import { AxiosError } from 'axios';
6+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
7+
import { MermaidChart } from './index.js';
98
import type { MCDocument } from './types.js';
109

1110
let testProjectId = '316557b3-cb6f-47ed-acf7-fcfb7ce188d5';
@@ -190,16 +189,16 @@ describe('getDocument', () => {
190189
});
191190

192191
it('should throw 404 on unknown document', async () => {
193-
let error: AxiosError | undefined = undefined;
192+
let error: HTTPError | undefined = undefined;
194193
try {
195194
await client.getDocument({
196195
documentID: '00000000-0000-0000-0000-0000deaddead',
197196
});
198197
} catch (err) {
199-
error = err as AxiosError;
198+
error = err as HTTPError;
200199
}
201200

202-
expect(error).toBeInstanceOf(AxiosError);
201+
expect(error).toBeInstanceOf(HTTPError);
203202
expect(error?.response?.status).toBe(404);
204203
});
205204
});

packages/sdk/src/index.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
import { OAuth2Client } from '@badgateway/oauth2-client';
12
import { beforeEach, describe, expect, it, vi } from 'vitest';
23
import { MermaidChart } from './index.js';
34
import type { AuthorizationData } from './types.js';
45

5-
import { OAuth2Client } from '@badgateway/oauth2-client';
6-
76
const mockOAuth2ClientRequest = (async (endpoint, _body) => {
87
switch (endpoint) {
98
case 'tokenEndpoint':

packages/sdk/src/index.ts

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client';
2-
import type { AxiosInstance, AxiosResponse } from 'axios';
3-
import defaultAxios from 'axios';
2+
import ky, { KyInstance } from 'ky';
43
import { v4 as uuid } from 'uuid';
54
import { OAuthError, RequiredParameterMissingError } from './errors.js';
65
import type {
@@ -20,7 +19,7 @@ const authorizationURLTimeout = 60_000;
2019
export class MermaidChart {
2120
private clientID: string;
2221
#baseURL!: string;
23-
private axios!: AxiosInstance;
22+
private api!: KyInstance;
2423
private oauth!: OAuth2Client;
2524
private pendingStates: Record<string, AuthState> = {};
2625
private redirectURI!: string;
@@ -54,17 +53,26 @@ export class MermaidChart {
5453
tokenEndpoint: URLS.oauth.token,
5554
authorizationEndpoint: URLS.oauth.authorize,
5655
});
57-
this.axios = defaultAxios.create({
58-
baseURL: this.#baseURL,
59-
timeout: this.requestTimeout,
60-
});
6156

62-
this.axios.interceptors.response.use((res: AxiosResponse) => {
63-
// Reset token if a 401 is thrown
64-
if (res.status === 401) {
65-
this.resetAccessToken();
66-
}
67-
return res;
57+
this.api = ky.create({
58+
prefixUrl: this.#baseURL + '/',
59+
timeout: this.requestTimeout,
60+
hooks: {
61+
beforeError: [
62+
(error) => {
63+
// Reset token if a 401 is thrown
64+
if (error.response.status === 401) {
65+
this.resetAccessToken();
66+
}
67+
return error;
68+
},
69+
],
70+
beforeRequest: [
71+
(request) => {
72+
request.headers.set('Authorization', `Bearer ${this.accessToken}`);
73+
},
74+
],
75+
},
6876
});
6977
}
7078

@@ -151,15 +159,13 @@ export class MermaidChart {
151159
* @param accessToken - access token to use for requests
152160
*/
153161
public async setAccessToken(accessToken: string): Promise<void> {
154-
this.axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
162+
this.accessToken = accessToken;
155163
// This is to verify that the token is valid
156164
await this.getUser();
157-
this.accessToken = accessToken;
158165
}
159166

160167
public resetAccessToken(): void {
161168
this.accessToken = undefined;
162-
this.axios.defaults.headers.common['Authorization'] = `Bearer none`;
163169
}
164170

165171
/**
@@ -175,42 +181,40 @@ export class MermaidChart {
175181
}
176182

177183
public async getUser(): Promise<MCUser> {
178-
const user = await this.axios.get<MCUser>(URLS.rest.users.self);
179-
return user.data;
184+
const user = await this.api.get<MCUser>(URLS.rest.users.self);
185+
return user.json();
180186
}
181187

182188
public async getProjects(): Promise<MCProject[]> {
183-
const projects = await this.axios.get<MCProject[]>(URLS.rest.projects.list);
184-
return projects.data;
189+
const projects = await this.api.get<MCProject[]>(URLS.rest.projects.list);
190+
return projects.json();
185191
}
186192

187193
public async getDocuments(projectID: string): Promise<MCDocument[]> {
188-
const projects = await this.axios.get<MCDocument[]>(
189-
URLS.rest.projects.get(projectID).documents,
190-
);
191-
return projects.data;
194+
const documents = await this.api.get<MCDocument[]>(URLS.rest.projects.get(projectID).documents);
195+
return documents.json();
192196
}
193197

194198
public async createDocument(projectID: string) {
195-
const newDocument = await this.axios.post<MCDocument>(
199+
const newDocument = await this.api.post<MCDocument>(
196200
URLS.rest.projects.get(projectID).documents,
197-
{}, // force sending empty JSON to avoid triggering CSRF check
201+
{ json: {} }, // force sending empty JSON to avoid triggering CSRF check
198202
);
199-
return newDocument.data;
203+
return newDocument.json();
200204
}
201205

202206
public async getEditURL(
203207
document: Pick<MCDocument, 'documentID' | 'major' | 'minor' | 'projectID'>,
204208
) {
205-
const url = `${this.#baseURL}${URLS.diagram(document).edit}`;
209+
const url = `${this.#baseURL}/${URLS.diagram(document).edit}`;
206210
return url;
207211
}
208212

209213
public async getDocument(
210214
document: Pick<MCDocument, 'documentID'> | Pick<MCDocument, 'documentID' | 'major' | 'minor'>,
211215
) {
212-
const { data } = await this.axios.get<MCDocument>(URLS.rest.documents.pick(document).self);
213-
return data;
216+
const res = await this.api.get<MCDocument>(URLS.rest.documents.pick(document).self);
217+
return res.json();
214218
}
215219

216220
/**
@@ -221,16 +225,16 @@ export class MermaidChart {
221225
public async setDocument(
222226
document: Pick<MCDocument, 'documentID' | 'projectID'> & Partial<MCDocument>,
223227
) {
224-
const { data } = await this.axios.put<{ result: 'ok' } | { result: 'failed'; error: unknown }>(
228+
const res = await this.api.put<{ result: 'ok' } | { result: 'failed'; error: unknown }>(
225229
URLS.rest.documents.pick(document).self,
226-
document,
230+
{ json: document },
227231
);
228232

229-
if (data.result === 'failed') {
233+
if (!res.ok) {
230234
throw new Error(
231235
`setDocument(${JSON.stringify({
232236
documentID: document.documentID,
233-
})} failed due to ${JSON.stringify(data.error)}`,
237+
})} failed due to ${JSON.stringify(res.statusText)}`,
234238
);
235239
}
236240
}
@@ -241,18 +245,18 @@ export class MermaidChart {
241245
* @returns Metadata about the deleted document.
242246
*/
243247
public async deleteDocument(documentID: MCDocument['documentID']) {
244-
const deletedDocument = await this.axios.delete<Document>(
248+
const deletedDocument = await this.api.delete<Document>(
245249
URLS.rest.documents.pick({ documentID }).self,
246-
{}, // force sending empty JSON to avoid triggering CSRF check
250+
{ json: {} }, // force sending empty JSON to avoid triggering CSRF check
247251
);
248-
return deletedDocument.data;
252+
return deletedDocument.json();
249253
}
250254

251255
public async getRawDocument(
252256
document: Pick<MCDocument, 'documentID' | 'major' | 'minor'>,
253257
theme: 'light' | 'dark',
254258
) {
255-
const raw = await this.axios.get<string>(URLS.raw(document, theme).svg);
256-
return raw.data;
259+
const raw = await this.api.get<string>(URLS.raw(document, theme).svg);
260+
return raw.text();
257261
}
258262
}

packages/sdk/src/urls.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const URLS = {
55
authorize: `/oauth/authorize`,
66
token: `/oauth/token`,
77
},
8+
// KY does not allow / at the beginning of URLs, when using prefixURL option.
89
rest: {
910
documents: {
1011
pick: (
@@ -19,7 +20,7 @@ export const URLS = {
1920
queryParams = `v${major ?? 0}.${minor ?? 1}`;
2021
}
2122

22-
const baseURL = `/rest-api/documents/${documentID}`;
23+
const baseURL = `rest-api/documents/${documentID}`;
2324
return {
2425
presentations: `${baseURL}/presentations`,
2526
self: baseURL,
@@ -28,26 +29,26 @@ export const URLS = {
2829
},
2930
},
3031
users: {
31-
self: `/rest-api/users/me`,
32+
self: `rest-api/users/me`,
3233
},
3334
projects: {
34-
list: `/rest-api/projects`,
35+
list: `rest-api/projects`,
3536
get: (projectID: string) => {
3637
return {
37-
documents: `/rest-api/projects/${projectID}/documents`,
38+
documents: `rest-api/projects/${projectID}/documents`,
3839
};
3940
},
4041
},
4142
},
4243
raw: (document: Pick<MCDocument, 'documentID' | 'major' | 'minor'>, theme: 'light' | 'dark') => {
43-
const base = `/raw/${document.documentID}?version=v${document.major}.${document.minor}&theme=${theme}&format=`;
44+
const base = `raw/${document.documentID}?version=v${document.major}.${document.minor}&theme=${theme}&format=`;
4445
return {
4546
html: base + 'html',
4647
svg: base + 'svg',
4748
};
4849
},
4950
diagram: (d: Pick<MCDocument, 'projectID' | 'documentID' | 'major' | 'minor'>) => {
50-
const base = `/app/projects/${d.projectID}/diagrams/${d.documentID}/version/v${d.major}.${d.minor}`;
51+
const base = `app/projects/${d.projectID}/diagrams/${d.documentID}/version/v${d.major}.${d.minor}`;
5152
return {
5253
self: base,
5354
edit: base + '/edit',

0 commit comments

Comments
 (0)