diff --git a/README.md b/README.md index 16ae234..f09c5b7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Bare metal TypeScript and JavaScript client for web API implementing [JSON:API v - Type-safe - Isomorphic +Documentations +- [Typescript documentation](https://masterT.github.io/jsonapi-metal-client/typescript/) + --- ## Table of Contents @@ -18,7 +21,8 @@ Bare metal TypeScript and JavaScript client for web API implementing [JSON:API v * [Usage](#usage) * [Documentation](#documentation) + [HTTP adapter](#http-adapter) - - [Using `fetch`](#using--fetch-) + - [Using `fetch`](#using-fetch) + - [Using `axios`](#using-axios) + [Client](#client) - [Configure custom HTTP headers](#configure-custom-http-headers) - [Result](#result) @@ -162,6 +166,30 @@ client.deleteRelationshipToMany( ### HTTP adapter +#### Using `axios` + +HTTP Adapter using the [axios](://github.com/axios/axios). + +```js +import axios from 'axios' + +const httpAdapter = new HttpAdapters.AxiosHttpAdapter( + axios +); +``` + +Using custom instance: + +```js +import axios from 'axios' + +const instance = axios.create({ /* ... */ }); + +const httpAdapter = new HttpAdapters.AxiosHttpAdapter( + instance +); +``` + #### Using `fetch` HTTP Adapter using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). diff --git a/__tests__/HttpAdapters/AxiosHttpAdapter.test.ts b/__tests__/HttpAdapters/AxiosHttpAdapter.test.ts new file mode 100644 index 0000000..386be9a --- /dev/null +++ b/__tests__/HttpAdapters/AxiosHttpAdapter.test.ts @@ -0,0 +1,67 @@ +import axios, { AxiosInstance } from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { HttpAdapters, HttpAdapter } from '../../src'; + +describe.only('HttpAdapters.AxiosHttpAdapter', () => { + let axiosInstance: AxiosInstance; + let axiosMock: MockAdapter; + let request: HttpAdapter.AdapterRequest; + let httpAdapter: HttpAdapters.AxiosHttpAdapter; + + beforeAll(() => { + axiosInstance = axios.create(); + axiosMock = new MockAdapter(axiosInstance); + httpAdapter = new HttpAdapters.AxiosHttpAdapter(axiosInstance); + }); + + beforeEach(() => { + request = { + url: 'https://examples.com/', + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: '{"foo":"bar"}' + }; + }); + + afterEach(() => { + axiosMock.reset(); + }); + + describe('request', () => { + beforeEach(() => { + axiosMock + .onPost('https://examples.com/') + .reply(200, '{"message":"ok"}', { 'Content-Type': 'application/json' }); + }); + + test('calls request on the axios instance', async () => { + await httpAdapter.request(request); + + expect(axiosMock.history.post.length).toBe(1); + expect(axiosMock.history.post[0]).toMatchObject({ + url: 'https://examples.com/', + method: expect.stringMatching(/post/i), + headers: { + 'Content-Type': 'application/json' + }, + data: '{"foo":"bar"}', + transitional: { + silentJSONParsing: false, + forcedJSONParsing: false + } + }); + }); + + test('resolves with response', async () => { + await expect(httpAdapter.request(request)).resolves.toEqual({ + body: '{"message":"ok"}', + headers: { + 'Content-Type': 'application/json' + }, + status: 200 + }); + }); + }); +}); diff --git a/package.json b/package.json index 351688c..747003e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/jest": "^27.4.0", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", + "axios-mock-adapter": "^1.20.0", "cross-fetch": "^3.1.5", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", @@ -33,5 +34,8 @@ "ts-jest": "^27.1.2", "typedoc": "^0.22.10", "typescript": "^4.5.4" + }, + "dependencies": { + "axios": "^0.26.1" } } diff --git a/src/HttpAdapter.ts b/src/HttpAdapter.ts index 7c23805..32e0f76 100644 --- a/src/HttpAdapter.ts +++ b/src/HttpAdapter.ts @@ -7,10 +7,22 @@ export interface AdapterResponse { body?: string; } +export type AdapterRequestMethod = + | 'GET' + | 'DELETE' + | 'HEAD' + | 'OPTIONS' + | 'POST' + | 'PUT' + | 'PATCH' + | 'PURGE' + | 'LINK' + | 'UNLINK'; + /** @see {@link isAdapterRequest} ts-auto-guard:type-guard */ export interface AdapterRequest { url: string; - method: string; + method: AdapterRequestMethod; headers?: { [key: string]: string }; body: string | null; } diff --git a/src/HttpAdapters/AxiosHttpAdapter.ts b/src/HttpAdapters/AxiosHttpAdapter.ts new file mode 100644 index 0000000..5bd775e --- /dev/null +++ b/src/HttpAdapters/AxiosHttpAdapter.ts @@ -0,0 +1,47 @@ +import type { AxiosInstance } from 'axios'; +import * as HttpAdapter from '../HttpAdapter'; + +/** + * {@link HttpAdapter.Adapter} using {@link https://github.com/axios/axios|axios} for make HTTP requests. + */ +export class AxiosHttpAdapter implements HttpAdapter.Adapter { + /** + * The Axios instance. + */ + instance: AxiosInstance; + + /** + * Create an axios HTTP adapter. + * @param instance - The Axios instance. + */ + constructor(instance: AxiosInstance) { + this.instance = instance; + } + + /** + * @see {@link HttpAdapter.Adapter.request} + * @param options + * @returns + */ + async request( + options: HttpAdapter.AdapterRequest + ): Promise { + const { url, method, headers, body } = options; + const response = await this.instance.request({ + headers, + method, + url, + data: body, + // Specify not to parse JSON as the adapter should return the raw response body. + transitional: { + silentJSONParsing: false, + forcedJSONParsing: false + } + }); + return { + status: response.status, + body: response.data, + headers: response.headers + }; + } +} diff --git a/src/HttpAdapters/index.ts b/src/HttpAdapters/index.ts index fc684ea..4c1bbff 100644 --- a/src/HttpAdapters/index.ts +++ b/src/HttpAdapters/index.ts @@ -1 +1,2 @@ export { FetchHttpAdapter } from './FetchHttpAdapter'; +export { AxiosHttpAdapter } from './AxiosHttpAdapter'; diff --git a/src/JsonApi/Client.ts b/src/JsonApi/Client.ts index bbf1181..3c4e6ec 100644 --- a/src/JsonApi/Client.ts +++ b/src/JsonApi/Client.ts @@ -541,7 +541,7 @@ export class Client { private async requestUpdateRelationshipToMany( url: string, - method: string, + method: HttpAdapter.AdapterRequestMethod, document: any ): Promise> { try { diff --git a/yarn.lock b/yarn.lock index 94ee5c4..799f1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -921,6 +921,22 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +axios-mock-adapter@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz#21f5b4b625306f43e8c05673616719da86e20dcb" + integrity sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w== + dependencies: + fast-deep-equal "^3.1.3" + is-blob "^2.1.0" + is-buffer "^2.0.5" + +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-jest@^27.5.0: version "27.5.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.0.tgz#c653985241af3c76f59d70d65a570860c2594a50" @@ -1579,6 +1595,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -1793,6 +1814,16 @@ is-absolute@^1.0.0: is-relative "^1.0.0" is-windows "^1.0.1" +is-blob@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-blob/-/is-blob-2.1.0.tgz#e36cd82c90653f1e1b930f11baf9c64216a05385" + integrity sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw== + +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"