diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c05514..820ff84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release Notes +## [3.0.0 (2024-XX-XX)](https://github.com/axe-api/axe-api/compare/3.0.0...x.x.x) + +- Added navite `RequestInit` type instead of `IRequest` +- Removed `DEFINED_STATUS_CODES` constant. +- Removed `response.json()` calls internally. Request functions return `Response` object now. +- Added `searchParams()` method to add additional URLSearchParams. +- Fixed `addResponse()` interceptor issues. + ## [2.1.0 (2024-09-28)](https://github.com/axe-api/axe-api/compare/2.1.0...2.0.1) - Added missing requests to the Resource object diff --git a/package-lock.json b/package-lock.json index bd94a36..f7e807d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "axe-api-client", - "version": "2.1.0", + "version": "3.0.0-rc-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "axe-api-client", - "version": "2.1.0", + "version": "3.0.0-rc-3", "license": "MIT", "devDependencies": { "@babel/preset-env": "^7.25.4", diff --git a/package.json b/package.json index b605726..ca3859c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "axe-api-client", - "version": "2.1.0", + "version": "3.0.0-rc-3", "description": "axe-api-client is a native JavaScript client for Axe API servers.", "type": "module", "main": "dist/index.cjs", diff --git a/readme.md b/readme.md index b7cddc8..34baed4 100644 --- a/readme.md +++ b/readme.md @@ -30,7 +30,7 @@ You can send insert, update, delete, and fetch data from Axe API servers without ## ⚙️ Config ```ts -import { api, IRequest } from "axe-api-client"; +import { api } from "axe-api-client"; api.setConfig({ baseURL: "https://bookstore.axe-api.com/api/v1", @@ -38,7 +38,7 @@ api.setConfig({ params: {}, }); -api.interceptors.addRequest((request: IRequest) => { +api.interceptors.addRequest((request: RequestInit) => { return request; }); diff --git a/src/Constants.ts b/src/Constants.ts index dc3040e..f6a36d6 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -16,5 +16,3 @@ export const SUFFIX_MAP: Record = { NULL: "", "NOT NULL": "$not", }; - -export const DEFINED_STATUS_CODES = [200, 204, 400, 404]; diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 617db12..05b4dc8 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -1,12 +1,10 @@ import { Resource } from "./Resource"; -import { ConditionTypes, MethodType, QueryFunctionType } from "./Types"; - -export interface IRequest { - url: string; - method: MethodType; - headers: Record; - body: string | undefined; -} +import { + ConditionTypes, + QueryFunctionType, + RequestInterceptorType, + ResponseInterceptorType, +} from "./Types"; export interface IConfig { baseURL?: string; @@ -20,8 +18,8 @@ export interface IInternalConfig extends IConfig { } export interface IInterceptors { - requests: any[]; - responses: any[]; + requests: RequestInterceptorType[]; + responses: ResponseInterceptorType[]; } export interface IPaginate { diff --git a/src/Resource.ts b/src/Resource.ts index 8568d50..c563e1d 100644 --- a/src/Resource.ts +++ b/src/Resource.ts @@ -1,12 +1,6 @@ import { getConfig } from "./Config"; -import { DEFINED_STATUS_CODES, SUFFIX_MAP } from "./Constants"; -import { - IInternalConfig, - IPaginate, - IPagination, - IQueryable, - IRequest, -} from "./Interfaces"; +import { SUFFIX_MAP } from "./Constants"; +import { IInternalConfig, IPaginate, IQueryable } from "./Interfaces"; import { ConditionTypes, FormBody, @@ -41,6 +35,20 @@ export class Resource implements IQueryable { this.withPath = undefined; } + /** + * Add additonal URLSearchParams to the request URL + * + * @param searchParams Record + * @returns IQueryable + */ + searchParams(searchParams: Record) { + for (const key in searchParams) { + this.params.append(key, searchParams[key]); + } + + return this; + } + /** * Select the fields that will be fetched. * @@ -489,7 +497,7 @@ export class Resource implements IQueryable { * @param query IPaginate * @returns object */ - async paginate(query?: IPaginate): Promise { + async paginate(query?: IPaginate): Promise { this.params.append("page", query?.page?.toString() ?? "1"); this.params.append("per_page", query?.perPage?.toString() ?? "10"); return this.sendRequest("GET"); @@ -543,9 +551,11 @@ export class Resource implements IQueryable { return this.sendRequest("DELETE"); } - private async sendRequest(method: MethodType, data?: FormBody) { - let request: IRequest = { - url: this.toURL(), + private async sendRequest( + method: MethodType, + data?: FormBody, + ): Promise { + let request: RequestInit = { method, headers: { "Content-Type": "application/json", @@ -553,18 +563,17 @@ export class Resource implements IQueryable { body: data ? JSON.stringify(data) : undefined, }; + // Calls the request interceptors for (const interceptor of this.config.interceptors.requests) { request = interceptor(request); } - const response = await fetch(request.url, request); + // Send the request + let response = await fetch(this.toURL(), request); + // Calls the response interceptors for (const interceptor of this.config.interceptors.responses) { - interceptor(response); - } - - if (DEFINED_STATUS_CODES.includes(response.status)) { - return response.json(); + response = interceptor(response); } return response; diff --git a/src/Types.ts b/src/Types.ts index 61ad1d9..616b647 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -1,4 +1,4 @@ -import { IQueryable, IRequest } from "./Interfaces"; +import { IQueryable } from "./Interfaces"; export type FormBody = object | undefined; @@ -6,9 +6,9 @@ export type QueryArray = object[]; export type QueryFunctionType = (query: IQueryable) => IQueryable; -export type RequestInterceptorType = (request: IRequest) => IRequest; +export type RequestInterceptorType = (request: RequestInit) => RequestInit; -export type ResponseInterceptorType = (response: Response) => void; +export type ResponseInterceptorType = (response: Response) => Response; export type LogicType = "$or" | "$and"; diff --git a/tests/index.spec.js b/tests/index.spec.js index a413d24..33449e1 100644 --- a/tests/index.spec.js +++ b/tests/index.spec.js @@ -11,9 +11,12 @@ const BASE_CONFIG = { }; const mock = (status, data) => { - return jest.fn(() => { + return jest.fn((url) => { return Promise.resolve({ status, + mock: { + url, + }, json: () => { return data; }, @@ -173,12 +176,15 @@ describe("axe-api-client", () => { }); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users"); // NOSONAR + expect(url).toBe("https://axe-api.com/api/v1/users"); // NOSONAR expect(request.method).toBe("POST"); expect(request.headers["Content-Type"]).toBe("application/json"); expect(request.body).toBe(`{"name":"Karl"}`); - expect(response.id).toBe(100); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data.id).toBe(100); }); test(`update()`, async () => { @@ -191,11 +197,13 @@ describe("axe-api-client", () => { }); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users/1"); + expect(url).toBe("https://axe-api.com/api/v1/users/1"); expect(request.method).toBe("PUT"); expect(request.body).toBe(`{"name":"Karl"}`); - expect(response.id).toBe(100); + const data = await response.json(); + expect(data.id).toBe(100); }); test(`patch()`, async () => { @@ -208,11 +216,13 @@ describe("axe-api-client", () => { }); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users/1"); + expect(url).toBe("https://axe-api.com/api/v1/users/1"); expect(request.method).toBe("PATCH"); expect(request.body).toBe(`{"name":"Karl"}`); - expect(response.id).toBe(100); + const data = await response.json(); + expect(data.id).toBe(100); }); test(`delete()`, async () => { @@ -221,11 +231,13 @@ describe("axe-api-client", () => { const response = await api.resource("users/1").delete(); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users/1"); + expect(url).toBe("https://axe-api.com/api/v1/users/1"); expect(request.method).toBe("DELETE"); expect(request.body).toBe(undefined); - expect(response).toBe(undefined); + const json = await response.json(); + expect(json).toBe(undefined); }); test(`paginate()`, async () => { @@ -237,12 +249,12 @@ describe("axe-api-client", () => { .paginate({ page: 10, perPage: 5 }); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe( - "https://axe-api.com/api/v1/users?page=10&per_page=5", - ); + expect(url).toBe("https://axe-api.com/api/v1/users?page=10&per_page=5"); expect(request.method).toBe("GET"); - expect(response).toBe(mockResponse); + const json = await response.json(); + expect(json).toBe(mockResponse); }); test(`URL Tests`, async () => { @@ -257,12 +269,14 @@ describe("axe-api-client", () => { .paginate(); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe( + expect(url).toBe( `https://axe-api.com/api/v1/users?page=1&per_page=10&fields=id&sort=id&q=%5B%7B%22id%22%3A1%7D%5D`, ); expect(request.method).toBe("GET"); - expect(response).toBe(mockResponse); + const json = await response.json(); + expect(json).toBe(mockResponse); }); test(`post()`, async () => { @@ -273,11 +287,13 @@ describe("axe-api-client", () => { const response = await api.resource("users").post(data); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users"); + expect(url).toBe("https://axe-api.com/api/v1/users"); expect(request.method).toBe("POST"); expect(JSON.parse(request.body).name).toBe(data.name); - expect(response).toBe("RESULT"); + const json = await response.json(); + expect(json).toBe("RESULT"); }); test(`put()`, async () => { @@ -288,11 +304,13 @@ describe("axe-api-client", () => { const response = await api.resource("users").put(data); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users"); + expect(url).toBe("https://axe-api.com/api/v1/users"); expect(request.method).toBe("PUT"); expect(JSON.parse(request.body).name).toBe(data.name); - expect(response).toBe("RESULT"); + const json = await response.json(); + expect(json).toBe("RESULT"); }); test(`patch()`, async () => { @@ -303,10 +321,24 @@ describe("axe-api-client", () => { const response = await api.resource("users").patch(data); expect(global.fetch.mock.calls.length).toBe(1); + const url = global.fetch.mock.calls[0][0]; const request = global.fetch.mock.calls[0][1]; - expect(request.url).toBe("https://axe-api.com/api/v1/users"); + expect(url).toBe("https://axe-api.com/api/v1/users"); expect(request.method).toBe("PATCH"); expect(JSON.parse(request.body).name).toBe(data.name); - expect(response).toBe("RESULT"); + const json = await response.json(); + expect(json).toBe("RESULT"); + }); + + test(`searchParams()`, async () => { + global.fetch = mock(200, "RESULT"); + + const response = await api + .resource("users") + .searchParams({ myFlag: "true", addUsers: 1 }) + .paginate(); + + expect(response.mock.url.includes("myFlag=true")).toBe(true); + expect(response.mock.url.includes("addUsers=1")).toBe(true); }); }); diff --git a/tests/interceptors.spec.js b/tests/interceptors.spec.js new file mode 100644 index 0000000..c1d9f1f --- /dev/null +++ b/tests/interceptors.spec.js @@ -0,0 +1,45 @@ +import { api } from "../index"; + +const BASE_CONFIG = { + baseURL: "https://axe-api.com/api/v1", // NOSONAR + headers: { + "x-api-request": 100, + }, + params: { + requestTime: "20231021", + }, +}; + +describe("Interceptors", () => { + beforeAll(() => { + api.setConfig(BASE_CONFIG); + }); + + test("addRequet()", async () => { + global.fetch = (url, request) => { + return { url, request, status: 200, data: { id: 1 } }; + }; + + api.interceptors.addRequest((request) => { + request.headers["x-my-header"] = "MyValue"; + return request; + }); + + const { request } = await api.resource("users").get(); + expect(request.headers["x-my-header"]).toBe("MyValue"); + }); + + test("addResponse()", async () => { + global.fetch = (url, request) => { + return { url, request, status: 200, data: { id: 1 } }; + }; + + api.interceptors.addResponse((response) => { + response.status = 404; + return response; + }); + + const { status } = await api.resource("users").get(); + expect(status).toBe(404); + }); +});