feat: add application productivity foundations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.28",
|
||||
"version": "0.8.29",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export interface ApiRequest<Input = unknown> {
|
||||
method?: string;
|
||||
params?: Record<string, string | number>;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
body?: Input;
|
||||
}
|
||||
|
||||
export interface ApiClientOptions {
|
||||
baseUrl?: string;
|
||||
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
}
|
||||
|
||||
/** Runtime used by generated API clients; throws a typed response error on non-2xx results. */
|
||||
export function createApiClient(options: ApiClientOptions = {}) {
|
||||
const fetcher = options.fetch ?? globalThis.fetch;
|
||||
return async function call<Output, Input = unknown>(
|
||||
template: string,
|
||||
request: ApiRequest<Input> = {},
|
||||
): Promise<Output> {
|
||||
let path = template;
|
||||
for (const [name, value] of Object.entries(request.params ?? {})) {
|
||||
path = path.replace(`[${name}]`, encodeURIComponent(String(value)));
|
||||
}
|
||||
const url = new URL(path, options.baseUrl ?? globalThis.location?.origin ?? "http://localhost");
|
||||
for (const [name, value] of Object.entries(request.query ?? {}))
|
||||
if (value !== undefined) url.searchParams.set(name, String(value));
|
||||
const response = await fetcher(url, {
|
||||
method: request.method ?? (request.body === undefined ? "GET" : "POST"),
|
||||
credentials: "same-origin",
|
||||
headers: request.body === undefined ? undefined : { "content-type": "application/json" },
|
||||
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
||||
});
|
||||
const data = response.status === 204 ? undefined : await response.json();
|
||||
if (!response.ok)
|
||||
throw Object.assign(
|
||||
new Error((data as { error?: string })?.error ?? `HTTP ${response.status}`),
|
||||
{ status: response.status, data },
|
||||
);
|
||||
return data as Output;
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export { REACTIVE_RUNTIME } from "./reactive-runtime.ts";
|
||||
export { NAV_RUNTIME } from "./nav-runtime.ts";
|
||||
export { REALTIME_RUNTIME } from "./realtime-runtime.ts";
|
||||
export { ACTION_RUNTIME } from "./action-runtime.ts";
|
||||
export { createApiClient } from "./api-client.ts";
|
||||
export type { ApiClientOptions, ApiRequest } from "./api-client.ts";
|
||||
|
||||
const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createApiClient } from "../src/index.ts";
|
||||
|
||||
test("typed API client expands params, query and JSON input", async () => {
|
||||
let request: Request | undefined;
|
||||
const call = createApiClient({
|
||||
baseUrl: "https://app.test",
|
||||
fetch: async (input, init) => {
|
||||
request = new Request(input, init);
|
||||
return Response.json({ ok: true });
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await call<{ ok: boolean }, { name: string }>("/api/users/[id]", {
|
||||
method: "PUT",
|
||||
params: { id: 7 },
|
||||
query: { audit: true },
|
||||
body: { name: "Ada" },
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
expect(request?.url).toBe("https://app.test/api/users/7?audit=true");
|
||||
expect(await request?.json()).toEqual({ name: "Ada" });
|
||||
});
|
||||
Reference in New Issue
Block a user