Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate SDK from axios to KY #27

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Migrate from axios to ky
  • Loading branch information
sidharthv96 committed Aug 23, 2024
commit 2dd4d29db7a6373e8e9bc79c1a93c874a545f03f
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -31,5 +31,6 @@
"tsx": "^3.12.8",
"typescript": "^5.2.2",
"vitest": "^1.6.0"
}
},
"packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247"
}
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -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": {
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';
@@ -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':
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 {
@@ -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;
@@ -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}`);
},
],
},
});
}

@@ -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`;
}

/**
@@ -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();
}

/**
@@ -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)}`,
);
}
}
@@ -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
@@ -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: (
@@ -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,
@@ -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',
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.