mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
reworked api to return a custom promise to extract request info
This commit is contained in:
96
app/__test__/api/ModuleApi.spec.ts
Normal file
96
app/__test__/api/ModuleApi.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { secureRandomString } from "../../src/core/utils";
|
||||
import { ModuleApi } from "../../src/modules";
|
||||
|
||||
class Api extends ModuleApi {
|
||||
_getUrl(path: string) {
|
||||
return this.getUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
const host = "http://localhost";
|
||||
|
||||
describe("ModuleApi", () => {
|
||||
it("resolves options correctly", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.options).toEqual({ host });
|
||||
});
|
||||
|
||||
it("returns correct url from path", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api._getUrl("/test")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("test")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("test/")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("//test?foo=1")).toEqual("http://localhost/test?foo=1");
|
||||
});
|
||||
|
||||
it("fetches endpoint", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||
const api = new Api({ host });
|
||||
api.fetcher = app.request as typeof fetch;
|
||||
|
||||
const res = await api.get("/endpoint");
|
||||
expect(res.res.ok).toEqual(true);
|
||||
expect(res.res.status).toEqual(200);
|
||||
expect(res.data).toEqual({ foo: "bar" });
|
||||
expect(res.body).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("has accessible request", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||
const api = new Api({ host });
|
||||
api.fetcher = app.request as typeof fetch;
|
||||
|
||||
const promise = api.get("/endpoint");
|
||||
expect(promise.request).toBeDefined();
|
||||
expect(promise.request.url).toEqual("http://localhost/endpoint");
|
||||
|
||||
expect((await promise).body).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("adds token to headers when given in options", () => {
|
||||
const token = secureRandomString(20);
|
||||
const api = new Api({ host, token, token_transport: "header" });
|
||||
|
||||
expect(api.get("/").request.headers.get("Authorization")).toEqual(`Bearer ${token}`);
|
||||
});
|
||||
|
||||
it("sets header to accept json", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.get("/").request.headers.get("Accept")).toEqual("application/json");
|
||||
});
|
||||
|
||||
it("adds additional headers from options", () => {
|
||||
const headers = new Headers({
|
||||
"X-Test": "123"
|
||||
});
|
||||
const api = new Api({ host, headers });
|
||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||
});
|
||||
|
||||
it("uses basepath & removes trailing slash", () => {
|
||||
const api = new Api({ host, basepath: "/api" });
|
||||
expect(api.get("/").request.url).toEqual("http://localhost/api");
|
||||
});
|
||||
|
||||
it("uses search params", () => {
|
||||
const api = new Api({ host });
|
||||
const search = new URLSearchParams({
|
||||
foo: "bar"
|
||||
});
|
||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||
});
|
||||
|
||||
it("resolves method shortcut fns correctly", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.get("/").request.method).toEqual("GET");
|
||||
expect(api.post("/").request.method).toEqual("POST");
|
||||
expect(api.put("/").request.method).toEqual("PUT");
|
||||
expect(api.patch("/").request.method).toEqual("PATCH");
|
||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||
});
|
||||
|
||||
// @todo: test error response
|
||||
// @todo: test method shortcut functions
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import { DataApi } from "../../src/modules/data/api/DataApi";
|
||||
|
||||
describe("Api", async () => {
|
||||
test("...", async () => {
|
||||
/*const dataApi = new DataApi({
|
||||
host: "https://dev-config-soma.bknd.run"
|
||||
});
|
||||
|
||||
const one = await dataApi.readOne("users", 1);
|
||||
const many = await dataApi.readMany("users", { limit: 2 });
|
||||
console.log("one", one);
|
||||
console.log("many", many);*/
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,11 @@ export type ApiResponse<Data = any> = {
|
||||
res: Response;
|
||||
};
|
||||
|
||||
export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
||||
export type TInput = string | (string | number | PrimaryFieldType)[];
|
||||
|
||||
export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModuleApiOptions> {
|
||||
fetcher = fetch;
|
||||
|
||||
constructor(protected readonly _options: Partial<Options> = {}) {}
|
||||
|
||||
protected getDefaultOptions(): Partial<Options> {
|
||||
@@ -35,14 +39,15 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
||||
}
|
||||
|
||||
protected getUrl(path: string) {
|
||||
return this.options.host + (this.options.basepath + "/" + path).replace(/\/\//g, "/");
|
||||
const basepath = this.options.basepath ?? "";
|
||||
return this.options.host + (basepath + "/" + path).replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
protected async request<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
protected request<Data = any>(
|
||||
_input: TInput,
|
||||
_query?: Record<string, any> | URLSearchParams,
|
||||
_init?: RequestInit
|
||||
): Promise<ApiResponse<Data>> {
|
||||
): FetchPromise<ApiResponse<Data>> {
|
||||
const method = _init?.method ?? "GET";
|
||||
const input = Array.isArray(_input) ? _input.join("/") : _input;
|
||||
let url = this.getUrl(input);
|
||||
@@ -78,14 +83,70 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("url", url);
|
||||
const res = await fetch(url, {
|
||||
const request = new Request(url, {
|
||||
..._init,
|
||||
method,
|
||||
body,
|
||||
headers
|
||||
});
|
||||
|
||||
return new FetchPromise(request, this.fetcher);
|
||||
}
|
||||
|
||||
get<Data = any>(
|
||||
_input: TInput,
|
||||
_query?: Record<string, any> | URLSearchParams,
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, _query, {
|
||||
..._init,
|
||||
method: "GET"
|
||||
});
|
||||
}
|
||||
|
||||
post<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
patch<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "PATCH"
|
||||
});
|
||||
}
|
||||
|
||||
put<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "PUT"
|
||||
});
|
||||
}
|
||||
|
||||
delete<Data = any>(_input: TInput, _init?: RequestInit) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
method: "DELETE"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FetchPromise<T> implements Promise<T> {
|
||||
// @ts-ignore
|
||||
[Symbol.toStringTag]: "FetchPromise";
|
||||
|
||||
constructor(
|
||||
public request: Request,
|
||||
protected fetcher = fetch
|
||||
) {}
|
||||
|
||||
async execute() {
|
||||
const res = await this.fetcher(this.request);
|
||||
let resBody: any;
|
||||
let resData: any;
|
||||
|
||||
@@ -108,60 +169,30 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions> {
|
||||
};
|
||||
}
|
||||
|
||||
protected async get<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
_query?: Record<string, any> | URLSearchParams,
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, _query, {
|
||||
..._init,
|
||||
method: "GET"
|
||||
});
|
||||
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
||||
then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.execute().then(onfulfilled as any, onrejected);
|
||||
}
|
||||
|
||||
protected async post<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
body?: any,
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "POST"
|
||||
});
|
||||
catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
|
||||
): Promise<T | TResult> {
|
||||
return this.then(undefined, onrejected);
|
||||
}
|
||||
|
||||
protected async patch<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
body?: any,
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "PATCH"
|
||||
});
|
||||
}
|
||||
|
||||
protected async put<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
body?: any,
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
body,
|
||||
method: "PUT"
|
||||
});
|
||||
}
|
||||
|
||||
protected async delete<Data = any>(
|
||||
_input: string | (string | number | PrimaryFieldType)[],
|
||||
_init?: RequestInit
|
||||
) {
|
||||
return this.request<Data>(_input, undefined, {
|
||||
..._init,
|
||||
method: "DELETE"
|
||||
});
|
||||
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
||||
return this.then(
|
||||
(value) => {
|
||||
onfinally?.();
|
||||
return value;
|
||||
},
|
||||
(reason) => {
|
||||
onfinally?.();
|
||||
throw reason;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user