release: WRNexusJS 0.3.0
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import type { Context } from "./context.ts";
|
||||
|
||||
export interface SchemaLike<T> {
|
||||
parse(input: unknown): T;
|
||||
}
|
||||
|
||||
export interface EndpointErrorBody {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
export class EndpointError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string,
|
||||
message: string,
|
||||
readonly details?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "EndpointError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface EndpointDefinition<I, O> {
|
||||
input?: SchemaLike<I>;
|
||||
output?: SchemaLike<O>;
|
||||
auth?: "optional" | "required";
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
handler(input: I, ctx: Context): O | Promise<O>;
|
||||
}
|
||||
|
||||
export interface DefinedEndpoint<I, O> {
|
||||
readonly definition: EndpointDefinition<I, O>;
|
||||
(ctx: Context, input?: unknown): Promise<Response>;
|
||||
}
|
||||
|
||||
function json(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
/** Define a validated, typed endpoint that can also drive SDK/OpenAPI generation. */
|
||||
export function defineEndpoint<I = unknown, O = unknown>(
|
||||
definition: EndpointDefinition<I, O>,
|
||||
): DefinedEndpoint<I, O> {
|
||||
const endpoint = async (ctx: Context, rawInput?: unknown): Promise<Response> => {
|
||||
try {
|
||||
if (definition.auth === "required" && !ctx.user) {
|
||||
throw new EndpointError(401, "UNAUTHENTICATED", "Authentication is required.");
|
||||
}
|
||||
const input = definition.input ? definition.input.parse(rawInput) : (rawInput as I);
|
||||
const rawOutput = await definition.handler(input, ctx);
|
||||
const output = definition.output ? definition.output.parse(rawOutput) : rawOutput;
|
||||
return output instanceof Response ? output : json({ data: output });
|
||||
} catch (error) {
|
||||
if (error instanceof EndpointError) {
|
||||
return json(
|
||||
{ error: { code: error.code, message: error.message, details: error.details } },
|
||||
error.status,
|
||||
);
|
||||
}
|
||||
return json(
|
||||
{
|
||||
error: {
|
||||
code: "INTERNAL_ERROR",
|
||||
message: "The endpoint failed unexpectedly.",
|
||||
} satisfies EndpointErrorBody,
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
};
|
||||
return Object.assign(endpoint, { definition });
|
||||
}
|
||||
|
||||
export interface RpcClientOptions {
|
||||
baseUrl?: string;
|
||||
fetch?: typeof globalThis.fetch;
|
||||
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
||||
}
|
||||
|
||||
/** Create a tiny typed RPC caller for endpoints exposed by a WrNexus app. */
|
||||
export function createRpcClient(options: RpcClientOptions = {}) {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
return async function call<I, O>(path: string, input: I): Promise<O> {
|
||||
const headers =
|
||||
typeof options.headers === "function" ? await options.headers() : (options.headers ?? {});
|
||||
const response = await request(new URL(path, options.baseUrl ?? globalThis.location?.origin), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const body = (await response.json()) as { data?: O; error?: EndpointErrorBody };
|
||||
if (!response.ok || body.error) {
|
||||
throw new EndpointError(
|
||||
response.status,
|
||||
body.error?.code ?? "RPC_ERROR",
|
||||
body.error?.message ?? `RPC request failed with ${response.status}`,
|
||||
body.error?.details,
|
||||
);
|
||||
}
|
||||
return body.data as O;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user