Skip to content

Commit

Permalink
feat: Migrate from axios to ky
Browse files Browse the repository at this point in the history
  • Loading branch information
sidharthv96 committed Aug 23, 2024
1 parent c5a6c97 commit 2dd4d29
Show file tree
Hide file tree
Showing 7 changed files with 9,654 additions and 7,723 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
"tsx": "^3.12.8",
"typescript": "^5.2.2",
"vitest": "^1.6.0"
}
},
"packageManager": "[email protected]+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247"
}
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"license": "MIT",
"dependencies": {
"@badgateway/oauth2-client": "^2.2.4",
"axios": "^1.5.0",
"ky": "^1.7.1",
"uuid": "^9.0.0"
},
"devDependencies": {
Expand Down
13 changes: 6 additions & 7 deletions packages/sdk/src/index.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/**
* E2E tests
*/
import { MermaidChart } from './index.js';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { HTTPError } from 'ky';
import process from 'node:process';
import { AxiosError } from 'axios';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { MermaidChart } from './index.js';
import type { MCDocument } from './types.js';

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

it('should throw 404 on unknown document', async () => {
let error: AxiosError | undefined = undefined;
let error: HTTPError | undefined = undefined;
try {
await client.getDocument({
documentID: '00000000-0000-0000-0000-0000deaddead',
});
} catch (err) {
error = err as AxiosError;
error = err as HTTPError;
}

expect(error).toBeInstanceOf(AxiosError);
expect(error).toBeInstanceOf(HTTPError);
expect(error?.response?.status).toBe(404);
});
});
3 changes: 1 addition & 2 deletions packages/sdk/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { OAuth2Client } from '@badgateway/oauth2-client';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MermaidChart } from './index.js';
import type { AuthorizationData } from './types.js';

import { OAuth2Client } from '@badgateway/oauth2-client';

const mockOAuth2ClientRequest = (async (endpoint, _body) => {
switch (endpoint) {
case 'tokenEndpoint':
Expand Down
82 changes: 43 additions & 39 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client';
import type { AxiosInstance, AxiosResponse } from 'axios';
import defaultAxios from 'axios';
import ky, { KyInstance } from 'ky';
import { v4 as uuid } from 'uuid';
import { OAuthError, RequiredParameterMissingError } from './errors.js';
import type {
Expand All @@ -20,7 +19,7 @@ const authorizationURLTimeout = 60_000;
export class MermaidChart {
private clientID: string;
#baseURL!: string;
private axios!: AxiosInstance;
private api!: KyInstance;
private oauth!: OAuth2Client;
private pendingStates: Record<string, AuthState> = {};
private redirectURI!: string;
Expand Down Expand Up @@ -54,17 +53,26 @@ export class MermaidChart {
tokenEndpoint: URLS.oauth.token,
authorizationEndpoint: URLS.oauth.authorize,
});
this.axios = defaultAxios.create({
baseURL: this.#baseURL,
timeout: this.requestTimeout,
});

this.axios.interceptors.response.use((res: AxiosResponse) => {
// Reset token if a 401 is thrown
if (res.status === 401) {
this.resetAccessToken();
}
return res;
this.api = ky.create({
prefixUrl: this.#baseURL + '/',
timeout: this.requestTimeout,
hooks: {
beforeError: [
(error) => {
// Reset token if a 401 is thrown
if (error.response.status === 401) {
this.resetAccessToken();
}
return error;
},
],
beforeRequest: [
(request) => {
request.headers.set('Authorization', `Bearer ${this.accessToken}`);
},
],
},
});
}

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

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

/**
Expand All @@ -175,42 +181,40 @@ export class MermaidChart {
}

public async getUser(): Promise<MCUser> {
const user = await this.axios.get<MCUser>(URLS.rest.users.self);
return user.data;
const user = await this.api.get<MCUser>(URLS.rest.users.self);
return user.json();
}

public async getProjects(): Promise<MCProject[]> {
const projects = await this.axios.get<MCProject[]>(URLS.rest.projects.list);
return projects.data;
const projects = await this.api.get<MCProject[]>(URLS.rest.projects.list);
return projects.json();
}

public async getDocuments(projectID: string): Promise<MCDocument[]> {
const projects = await this.axios.get<MCDocument[]>(
URLS.rest.projects.get(projectID).documents,
);
return projects.data;
const documents = await this.api.get<MCDocument[]>(URLS.rest.projects.get(projectID).documents);
return documents.json();
}

public async createDocument(projectID: string) {
const newDocument = await this.axios.post<MCDocument>(
const newDocument = await this.api.post<MCDocument>(
URLS.rest.projects.get(projectID).documents,
{}, // force sending empty JSON to avoid triggering CSRF check
{ json: {} }, // force sending empty JSON to avoid triggering CSRF check
);
return newDocument.data;
return newDocument.json();
}

public async getEditURL(
document: Pick<MCDocument, 'documentID' | 'major' | 'minor' | 'projectID'>,
) {
const url = `${this.#baseURL}${URLS.diagram(document).edit}`;
const url = `${this.#baseURL}/${URLS.diagram(document).edit}`;
return url;
}

public async getDocument(
document: Pick<MCDocument, 'documentID'> | Pick<MCDocument, 'documentID' | 'major' | 'minor'>,
) {
const { data } = await this.axios.get<MCDocument>(URLS.rest.documents.pick(document).self);
return data;
const res = await this.api.get<MCDocument>(URLS.rest.documents.pick(document).self);
return res.json();
}

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

if (data.result === 'failed') {
if (!res.ok) {
throw new Error(
`setDocument(${JSON.stringify({
documentID: document.documentID,
})} failed due to ${JSON.stringify(data.error)}`,
})} failed due to ${JSON.stringify(res.statusText)}`,
);
}
}
Expand All @@ -241,18 +245,18 @@ export class MermaidChart {
* @returns Metadata about the deleted document.
*/
public async deleteDocument(documentID: MCDocument['documentID']) {
const deletedDocument = await this.axios.delete<Document>(
const deletedDocument = await this.api.delete<Document>(
URLS.rest.documents.pick({ documentID }).self,
{}, // force sending empty JSON to avoid triggering CSRF check
{ json: {} }, // force sending empty JSON to avoid triggering CSRF check
);
return deletedDocument.data;
return deletedDocument.json();
}

public async getRawDocument(
document: Pick<MCDocument, 'documentID' | 'major' | 'minor'>,
theme: 'light' | 'dark',
) {
const raw = await this.axios.get<string>(URLS.raw(document, theme).svg);
return raw.data;
const raw = await this.api.get<string>(URLS.raw(document, theme).svg);
return raw.text();
}
}
13 changes: 7 additions & 6 deletions packages/sdk/src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const URLS = {
authorize: `/oauth/authorize`,
token: `/oauth/token`,
},
// KY does not allow / at the beginning of URLs, when using prefixURL option.
rest: {
documents: {
pick: (
Expand All @@ -19,7 +20,7 @@ export const URLS = {
queryParams = `v${major ?? 0}.${minor ?? 1}`;
}

const baseURL = `/rest-api/documents/${documentID}`;
const baseURL = `rest-api/documents/${documentID}`;
return {
presentations: `${baseURL}/presentations`,
self: baseURL,
Expand All @@ -28,26 +29,26 @@ export const URLS = {
},
},
users: {
self: `/rest-api/users/me`,
self: `rest-api/users/me`,
},
projects: {
list: `/rest-api/projects`,
list: `rest-api/projects`,
get: (projectID: string) => {
return {
documents: `/rest-api/projects/${projectID}/documents`,
documents: `rest-api/projects/${projectID}/documents`,
};
},
},
},
raw: (document: Pick<MCDocument, 'documentID' | 'major' | 'minor'>, theme: 'light' | 'dark') => {
const base = `/raw/${document.documentID}?version=v${document.major}.${document.minor}&theme=${theme}&format=`;
const base = `raw/${document.documentID}?version=v${document.major}.${document.minor}&theme=${theme}&format=`;
return {
html: base + 'html',
svg: base + 'svg',
};
},
diagram: (d: Pick<MCDocument, 'projectID' | 'documentID' | 'major' | 'minor'>) => {
const base = `/app/projects/${d.projectID}/diagrams/${d.documentID}/version/v${d.major}.${d.minor}`;
const base = `app/projects/${d.projectID}/diagrams/${d.documentID}/version/v${d.major}.${d.minor}`;
return {
self: base,
edit: base + '/edit',
Expand Down
Loading

0 comments on commit 2dd4d29

Please sign in to comment.