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 all commits
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
4 changes: 1 addition & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -17,9 +17,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- uses: pnpm/action-setup@v2
with:
version: 8
- uses: pnpm/action-setup@v4

- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
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"
}
1 change: 1 addition & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changes

- Uses KY instead of Axios for HTTP requests.
- Set a 30 second default timeout for all requests
- `MermaidChart#resetAccessToken()` no longer returns a `Promise`.

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, { type KyInstance } from 'ky';
import { v4 as uuid } from 'uuid';
import { OAuthError, RequiredParameterMissingError } from './errors.js';
import type {
@@ -20,7 +19,7 @@
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 @@
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 @@
* @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 @@
}

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>(

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, macos-latest)

src/index.e2e.test.ts > createDocument > should create document in project

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:104:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, macos-latest)

src/index.e2e.test.ts > setDocument > should set document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:119:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, macos-latest)

src/index.e2e.test.ts > setDocument > should throw an error on invalid data

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:142:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, macos-latest)

src/index.e2e.test.ts > deleteDocument > should delete document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:157:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, macos-latest)

src/index.e2e.test.ts > getDocument > should get diagram

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:171:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > createDocument > should create document in project

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:104:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > setDocument > should set document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:119:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > setDocument > should throw an error on invalid data

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:142:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > deleteDocument > should delete document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:157:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > getDocument > should get diagram

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:171:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > createDocument > should create document in project

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:104:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > setDocument > should set document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:119:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > setDocument > should throw an error on invalid data

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:142:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > deleteDocument > should delete document

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:157:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }

Check failure on line 199 in packages/sdk/src/index.ts

GitHub Actions / test (18.18.x, sdk, ubuntu-latest)

src/index.e2e.test.ts > getDocument > should get diagram

TypeError: fetch failed ❯ function_ ../../node_modules/.pnpm/ky@1.7.1/node_modules/ky/source/core/Ky.ts:38:19 ❯ MermaidChart.createDocument src/index.ts:199:25 ❯ src/index.e2e.test.ts:171:25 Caused by: RequestContentLengthMismatchError: Request body length does not match content-length header ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }
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 @@
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 @@
* @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.