feat: spec to platform
This commit is contained in:
85
platform/relay/libraries/adapter.ts
Normal file
85
platform/relay/libraries/adapter.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import z from "zod";
|
||||
|
||||
import type { ServerErrorType } from "./errors.ts";
|
||||
import type { RouteMethod } from "./route.ts";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const ServerErrorResponseSchema = z.object({
|
||||
error: z.object({
|
||||
status: z.number(),
|
||||
message: z.string(),
|
||||
data: z.any().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Utilities
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if the given candidate is a valid relay error response.
|
||||
*
|
||||
* @param candidate - Candidate to check.
|
||||
*/
|
||||
export function assertServerErrorResponse(candidate: unknown): candidate is ServerErrorResponse {
|
||||
return ServerErrorResponseSchema.safeParse(candidate).success;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type RelayAdapter = {
|
||||
readonly url: string;
|
||||
|
||||
/**
|
||||
* Return the full URL from given endpoint.
|
||||
*
|
||||
* @param endpoint - Endpoint to get url for.
|
||||
*/
|
||||
getUrl(endpoint: string): string;
|
||||
|
||||
/**
|
||||
* Send a request to the configured relay url.
|
||||
*
|
||||
* @param input - Request input parameters.
|
||||
*/
|
||||
send(input: RelayInput): Promise<RelayResponse>;
|
||||
|
||||
/**
|
||||
* Sends a fetch request using the given options and returns a
|
||||
* raw response.
|
||||
*
|
||||
* @param options - Relay request options.
|
||||
*/
|
||||
request(input: RequestInfo | URL, init?: RequestInit): Promise<RelayResponse>;
|
||||
};
|
||||
|
||||
export type RelayInput = {
|
||||
method: RouteMethod;
|
||||
endpoint: string;
|
||||
query?: string;
|
||||
body?: Record<string, unknown>;
|
||||
headers?: Headers;
|
||||
};
|
||||
|
||||
export type RelayResponse<TData = unknown, TError = ServerErrorType | ServerErrorResponse["error"]> =
|
||||
| {
|
||||
result: "success";
|
||||
data: TData;
|
||||
}
|
||||
| {
|
||||
result: "error";
|
||||
error: TError;
|
||||
};
|
||||
|
||||
export type ServerErrorResponse = z.infer<typeof ServerErrorResponseSchema>;
|
||||
190
platform/relay/libraries/client.ts
Normal file
190
platform/relay/libraries/client.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
|
||||
import type { ZodObject, ZodType } from "zod";
|
||||
|
||||
import type { RelayAdapter, RelayInput, RelayResponse } from "./adapter.ts";
|
||||
import { Route, type Routes } from "./route.ts";
|
||||
|
||||
/**
|
||||
* Factory method for generating a new relay client instance.
|
||||
*
|
||||
* @param config - Client configuration.
|
||||
* @param procedures - Map of routes to make available to the client.
|
||||
*/
|
||||
export function makeClient<TRoutes extends Routes>(config: Config, routes: TRoutes): RelayClient<TRoutes> {
|
||||
const client: any = {
|
||||
getUrl: config.adapter.getUrl.bind(config.adapter),
|
||||
request: config.adapter.request.bind(config.adapter),
|
||||
};
|
||||
for (const key in routes) {
|
||||
const route = routes[key];
|
||||
if (route instanceof Route) {
|
||||
client[key] = getRouteFn(route, config);
|
||||
} else {
|
||||
client[key] = getNestedRoute(config, route);
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function getNestedRoute<TRoutes extends Routes>(config: Config, routes: TRoutes): RelayClient<TRoutes> {
|
||||
const nested: any = {};
|
||||
for (const key in routes) {
|
||||
const route = routes[key];
|
||||
if (route instanceof Route) {
|
||||
nested[key] = getRouteFn(route, config);
|
||||
} else {
|
||||
nested[key] = getNestedRoute(config, route);
|
||||
}
|
||||
}
|
||||
return nested;
|
||||
}
|
||||
|
||||
function getRouteFn(route: Route, { adapter }: Config) {
|
||||
return async (options: any = {}) => {
|
||||
const input: RelayInput = {
|
||||
method: route.state.method,
|
||||
endpoint: route.state.path,
|
||||
query: "",
|
||||
};
|
||||
|
||||
// ### Params
|
||||
// Prepare request parameters by replacing :param notations with the
|
||||
// parameter argument provided.
|
||||
|
||||
if (route.state.params !== undefined) {
|
||||
const params = await toParsedArgs(
|
||||
route.state.params,
|
||||
options.params,
|
||||
`Invalid 'params' passed to ${route.state.path} handler.`,
|
||||
);
|
||||
for (const key in params) {
|
||||
input.endpoint = input.endpoint.replace(`:${key}`, encodeURIComponent(params[key]));
|
||||
}
|
||||
}
|
||||
|
||||
// ### Query
|
||||
// Prepare request query by looping through the query argument and
|
||||
// creating a query string to pass onto the fetch request.
|
||||
|
||||
if (route.state.query !== undefined) {
|
||||
const query = await toParsedArgs(
|
||||
route.state.query,
|
||||
options.query,
|
||||
`Invalid 'query' passed to ${route.state.path} handler.`,
|
||||
);
|
||||
const pieces: string[] = [];
|
||||
for (const key in query) {
|
||||
pieces.push(`${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`);
|
||||
}
|
||||
if (pieces.length > 0) {
|
||||
input.query = `?${pieces.join("&")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ### Body
|
||||
// Attach the body to the input which is handled internally based on the
|
||||
// type of fetch body is submitted.
|
||||
|
||||
if (route.state.body !== undefined) {
|
||||
input.body = await toParsedArgs(
|
||||
route.state.body,
|
||||
options.body,
|
||||
`Invalid 'body' passed to ${route.state.path} handler.`,
|
||||
);
|
||||
}
|
||||
|
||||
// ### Request Init
|
||||
// List of request init options that we can extract and forward to the
|
||||
// request adapter.
|
||||
|
||||
if (options.headers !== undefined) {
|
||||
input.headers = new Headers(options.headers);
|
||||
}
|
||||
|
||||
// ### Fetch
|
||||
|
||||
const response = await adapter.send(input);
|
||||
|
||||
if ("data" in response && route.state.response !== undefined) {
|
||||
response.data = route.state.response.parse(response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
async function toParsedArgs(
|
||||
zod: ZodType,
|
||||
args: unknown,
|
||||
msg: string,
|
||||
): Promise<Record<string, string | number | boolean>> {
|
||||
const result = await zod.safeParseAsync(args);
|
||||
if (result.success === false) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
return result.data as Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type RelayClient<TRoutes extends Routes> = RelayRequest & RelayRoutes<TRoutes>;
|
||||
|
||||
type RelayRequest = {
|
||||
url: string;
|
||||
getUrl: (endpoint: string) => string;
|
||||
request: <TData = unknown>(input: RequestInfo | URL, init?: RequestInit) => Promise<RelayResponse<TData>>;
|
||||
};
|
||||
|
||||
type RelayRoutes<TRoutes extends Routes> = {
|
||||
[TKey in keyof TRoutes]: TRoutes[TKey] extends Route
|
||||
? HasPayload<TRoutes[TKey]> extends true
|
||||
? (
|
||||
payload: Prettify<
|
||||
(TRoutes[TKey]["state"]["params"] extends ZodObject ? { params: TRoutes[TKey]["$params"] } : {}) &
|
||||
(TRoutes[TKey]["state"]["query"] extends ZodObject ? { query: TRoutes[TKey]["$query"] } : {}) &
|
||||
(TRoutes[TKey]["state"]["body"] extends ZodType ? { body: TRoutes[TKey]["$body"] } : {}) & {
|
||||
headers?: HeadersInit;
|
||||
}
|
||||
>,
|
||||
) => RouteResponse<TRoutes[TKey]>
|
||||
: (payload?: { headers: HeadersInit }) => RouteResponse<TRoutes[TKey]>
|
||||
: TRoutes[TKey] extends Routes
|
||||
? RelayRoutes<TRoutes[TKey]>
|
||||
: never;
|
||||
};
|
||||
|
||||
type HasPayload<TRoute extends Route> = TRoute["state"]["params"] extends ZodObject
|
||||
? true
|
||||
: TRoute["state"]["query"] extends ZodObject
|
||||
? true
|
||||
: TRoute["state"]["body"] extends ZodType
|
||||
? true
|
||||
: false;
|
||||
|
||||
type RouteResponse<TRoute extends Route> = Promise<RelayResponse<RouteOutput<TRoute>, RouteErrors<TRoute>>> & {
|
||||
$params: TRoute["$params"];
|
||||
$query: TRoute["$query"];
|
||||
$body: TRoute["$body"];
|
||||
$response: TRoute["$response"];
|
||||
};
|
||||
|
||||
type RouteOutput<TRoute extends Route> = TRoute["state"]["response"] extends ZodType ? TRoute["$response"] : null;
|
||||
|
||||
type RouteErrors<TRoute extends Route> = InstanceType<TRoute["state"]["errors"][number]>;
|
||||
|
||||
type Config = {
|
||||
adapter: RelayAdapter;
|
||||
};
|
||||
|
||||
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
||||
363
platform/relay/libraries/errors.ts
Normal file
363
platform/relay/libraries/errors.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import type { $ZodErrorTree } from "zod/v4/core";
|
||||
|
||||
export abstract class ServerError<TData = unknown> extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
readonly data?: TData,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a server delivered JSON error to its native instance.
|
||||
*
|
||||
* @param value - Error JSON.
|
||||
*/
|
||||
static fromJSON(value: ServerErrorJSON): ServerErrorType {
|
||||
switch (value.status) {
|
||||
case 400:
|
||||
return new BadRequestError(value.message, value.data);
|
||||
case 401:
|
||||
return new UnauthorizedError(value.message, value.data);
|
||||
case 403:
|
||||
return new ForbiddenError(value.message, value.data);
|
||||
case 404:
|
||||
return new NotFoundError(value.message, value.data);
|
||||
case 405:
|
||||
return new MethodNotAllowedError(value.message, value.data);
|
||||
case 406:
|
||||
return new NotAcceptableError(value.message, value.data);
|
||||
case 409:
|
||||
return new ConflictError(value.message, value.data);
|
||||
case 410:
|
||||
return new GoneError(value.message, value.data);
|
||||
case 415:
|
||||
return new UnsupportedMediaTypeError(value.message, value.data);
|
||||
case 422:
|
||||
return new UnprocessableContentError(value.message, value.data);
|
||||
case 432:
|
||||
return new ZodValidationError(value.message, value.data);
|
||||
case 500:
|
||||
return new InternalServerError(value.message, value.data);
|
||||
case 501:
|
||||
return new NotImplementedError(value.message, value.data);
|
||||
case 503:
|
||||
return new ServiceUnavailableError(value.message, value.data);
|
||||
default:
|
||||
return new InternalServerError(value.message, value.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error instance to a JSON object.
|
||||
*/
|
||||
toJSON(): ServerErrorJSON {
|
||||
return {
|
||||
type: "relay",
|
||||
status: this.status,
|
||||
message: this.message,
|
||||
data: this.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new BadRequestError.
|
||||
*
|
||||
* The **HTTP 400 Bad Request** response status code indicates that the server
|
||||
* cannot or will not process the request due to something that is perceived to
|
||||
* be a client error.
|
||||
*
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Bad Request", data?: TData) {
|
||||
super(message, 400, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new UnauthorizedError.
|
||||
*
|
||||
* The **HTTP 401 Unauthorized** response status code indicates that the client
|
||||
* request has not been completed because it lacks valid authentication
|
||||
* credentials for the requested resource.
|
||||
*
|
||||
* This status code is sent with an HTTP WWW-Authenticate response header that
|
||||
* contains information on how the client can request for the resource again after
|
||||
* prompting the user for authentication credentials.
|
||||
*
|
||||
* This status code is similar to the **403 Forbidden** status code, except that
|
||||
* in situations resulting in this status code, user authentication can allow
|
||||
* access to the resource.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Unauthorized".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Unauthorized", data?: TData) {
|
||||
super(message, 401, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new ForbiddenError.
|
||||
*
|
||||
* The **HTTP 403 Forbidden** response status code indicates that the server
|
||||
* understands the request but refuses to authorize it.
|
||||
*
|
||||
* This status is similar to **401**, but for the **403 Forbidden** status code
|
||||
* re-authenticating makes no difference. The access is permanently forbidden and
|
||||
* tied to the application logic, such as insufficient rights to a resource.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Forbidden".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Forbidden", data?: TData) {
|
||||
super(message, 403, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new NotFoundError.
|
||||
*
|
||||
* The **HTTP 404 Not Found** response status code indicates that the server
|
||||
* cannot find the requested resource. Links that lead to a 404 page are often
|
||||
* called broken or dead links and can be subject to link rot.
|
||||
*
|
||||
* A 404 status code only indicates that the resource is missing: not whether the
|
||||
* absence is temporary or permanent. If a resource is permanently removed,
|
||||
* use the **410 _(Gone)_** status instead.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Not Found".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Not Found", data?: TData) {
|
||||
super(message, 404, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class MethodNotAllowedError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new MethodNotAllowedError.
|
||||
*
|
||||
* The **HTTP 405 Method Not Allowed** response code indicates that the
|
||||
* request method is known by the server but is not supported by the target resource.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Method Not Allowed".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Method Not Allowed", data?: TData) {
|
||||
super(message, 405, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotAcceptableError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new NotAcceptableError.
|
||||
*
|
||||
* The **HTTP 406 Not Acceptable** client error response code indicates that the
|
||||
* server cannot produce a response matching the list of acceptable values
|
||||
* defined in the request, and that the server is unwilling to supply a default
|
||||
* representation.
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Not Acceptable".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Not Acceptable", data?: TData) {
|
||||
super(message, 406, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new ConflictError.
|
||||
*
|
||||
* The **HTTP 409 Conflict** response status code indicates a request conflict
|
||||
* with the current state of the target resource.
|
||||
*
|
||||
* Conflicts are most likely to occur in response to a PUT request. For example,
|
||||
* you may get a 409 response when uploading a file that is older than the
|
||||
* existing one on the server, resulting in a version control conflict.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Conflict".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Conflict", data?: TData) {
|
||||
super(message, 409, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class GoneError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new GoneError.
|
||||
*
|
||||
* The **HTTP 410 Gone** indicates that the target resource is no longer
|
||||
* available at the origin server and that this condition is likely to be
|
||||
* permanent. A 410 response is cacheable by default.
|
||||
*
|
||||
* Clients should not repeat requests for resources that return a 410 response,
|
||||
* and website owners should remove or replace links that return this code. If
|
||||
* server owners don't know whether this condition is temporary or permanent,
|
||||
* a 404 status code should be used instead.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Gone".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Gone", data?: TData) {
|
||||
super(message, 410, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedMediaTypeError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new UnsupportedMediaTypeError.
|
||||
*
|
||||
* The **HTTP 415 Unsupported Media Type** response code indicates that the
|
||||
* server refuses to accept the request because the payload format is in an unsupported format.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Unsupported Media Type".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Unsupported Media Type", data?: TData) {
|
||||
super(message, 415, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnprocessableContentError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new UnprocessableContentError.
|
||||
*
|
||||
* The **HTTP 422 Unprocessable Content** client error response status code
|
||||
* indicates that the server understood the content type of the request entity,
|
||||
* and the syntax of the request entity was correct, but it was unable to
|
||||
* process the contained instructions.
|
||||
*
|
||||
* Clients that receive a 422 response should expect that repeating the request
|
||||
* without modification will fail with the same error.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Unprocessable Content".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Unprocessable Content", data?: TData) {
|
||||
super(message, 422, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ZodValidationError<TData extends $ZodErrorTree<any, any>> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new ZodValidationError.
|
||||
*
|
||||
* This indicates that the server understood the request body, but the structure
|
||||
* failed validation against the expected schema.
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Unprocessable Content".
|
||||
* @param data - ZodError instance to pass through.
|
||||
*/
|
||||
constructor(message: string, data: TData) {
|
||||
super(message, 432, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new InternalServerError.
|
||||
*
|
||||
* The **HTTP 500 Internal Server Error** server error response code indicates that
|
||||
* the server encountered an unexpected condition that prevented it from fulfilling
|
||||
* the request.
|
||||
*
|
||||
* This error response is a generic "catch-all" response. Usually, this indicates
|
||||
* the server cannot find a better 5xx error code to response. Sometimes, server
|
||||
* administrators log error responses like the 500 status code with more details
|
||||
* about the request to prevent the error from happening again in the future.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Internal Server Error".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Internal Server Error", data?: TData) {
|
||||
super(message, 500, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotImplementedError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new NotImplementedError.
|
||||
*
|
||||
* The **HTTP 501 Not Implemented** server error response status code means that
|
||||
* the server does not support the functionality required to fulfill the request.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Service Unavailable".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Not Implemented", data?: TData) {
|
||||
super(message, 501, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServiceUnavailableError<TData = unknown> extends ServerError<TData> {
|
||||
/**
|
||||
* Instantiate a new ServiceUnavailableError.
|
||||
*
|
||||
* The **HTTP 503 Service Unavailable** server error response status code indicates
|
||||
* that the server is not ready to handle the request.
|
||||
*
|
||||
* This response should be used for temporary conditions and the Retry-After HTTP header
|
||||
* should contain the estimated time for the recovery of the service, if possible.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Service Unavailable".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Service Unavailable", data?: TData) {
|
||||
super(message, 503, data);
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerErrorClass<TData = unknown> = typeof ServerError<TData>;
|
||||
|
||||
export type ServerErrorJSON = {
|
||||
type: "relay";
|
||||
status: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
export type ServerErrorType =
|
||||
| BadRequestError
|
||||
| UnauthorizedError
|
||||
| ForbiddenError
|
||||
| NotFoundError
|
||||
| MethodNotAllowedError
|
||||
| NotAcceptableError
|
||||
| ConflictError
|
||||
| GoneError
|
||||
| UnsupportedMediaTypeError
|
||||
| UnprocessableContentError
|
||||
| NotImplementedError
|
||||
| ServiceUnavailableError
|
||||
| InternalServerError;
|
||||
10
platform/relay/libraries/hooks.ts
Normal file
10
platform/relay/libraries/hooks.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Hooks = {
|
||||
/**
|
||||
* Executes when any error is thrown before or during the lifetime
|
||||
* of the route. This allows for custom handling of errors if the
|
||||
* route has unique requirements to error handling.
|
||||
*
|
||||
* @param error - Error which has been thrown.
|
||||
*/
|
||||
onError?: (error: unknown) => Response;
|
||||
};
|
||||
239
platform/relay/libraries/procedure.ts
Normal file
239
platform/relay/libraries/procedure.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import z, { ZodType } from "zod";
|
||||
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { RouteAccess, ServerContext } from "./route.ts";
|
||||
|
||||
export class Procedure<const TState extends State = State> {
|
||||
readonly type = "procedure" as const;
|
||||
|
||||
declare readonly $params: TState["params"] extends ZodType ? z.input<TState["params"]> : never;
|
||||
declare readonly $response: TState["response"] extends ZodType ? z.output<TState["response"]> : never;
|
||||
|
||||
/**
|
||||
* Instantiate a new Procedure instance.
|
||||
*
|
||||
* @param state - Procedure state.
|
||||
*/
|
||||
constructor(readonly state: TState) {}
|
||||
|
||||
/**
|
||||
* Procedure method value.
|
||||
*/
|
||||
get method(): State["method"] {
|
||||
return this.state.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access level of the procedure which acts as the first barrier of entry
|
||||
* to ensure that requests are valid.
|
||||
*
|
||||
* By default on the server the lack of access definition will result
|
||||
* in an error as all procedures needs an access definition.
|
||||
*
|
||||
* @param access - Access level of the procedure.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .method("users:create")
|
||||
* .access("public")
|
||||
* .handle(async () => {
|
||||
* // ...
|
||||
* });
|
||||
*
|
||||
* procedure
|
||||
* .method("users:get-by-id")
|
||||
* .access("session")
|
||||
* .params(z.string())
|
||||
* .handle(async (userId, context) => {
|
||||
* if (userId !== context.session.userId) {
|
||||
* return new ForbiddenError("Cannot read other users details.");
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* procedure
|
||||
* .method("users:update")
|
||||
* .access([resource("users", "update")])
|
||||
* .params(z.array(z.string(), z.object({ name: z.string() })))
|
||||
* .handle(async ([userId, payload], context) => {
|
||||
* if (userId !== context.session.userId) {
|
||||
* return new ForbiddenError("Cannot update other users details.");
|
||||
* }
|
||||
* console.log(userId, payload); // => string, { name: string }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
access<TAccess extends RouteAccess>(access: TAccess): Procedure<Omit<TState, "access"> & { access: TAccess }> {
|
||||
return new Procedure({ ...this.state, access: access as TAccess });
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the payload forwarded to the handler.
|
||||
*
|
||||
* @param params - Method payload.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .method("users:create")
|
||||
* .access([resource("users", "create")])
|
||||
* .params(z.object({
|
||||
* name: z.string(),
|
||||
* email: z.email(),
|
||||
* }))
|
||||
* .handle(async ({ name, email }, context) => {
|
||||
* return { name, email, createdBy: context.session.userId };
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
params<TParams extends ZodType>(params: TParams): Procedure<Omit<TState, "params"> & { params: TParams }> {
|
||||
return new Procedure({ ...this.state, params });
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of the possible error responses this procedure produces.
|
||||
*
|
||||
* @param errors - Error shapes of the procedure.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .method("users:list")
|
||||
* .errors([
|
||||
* BadRequestError
|
||||
* ])
|
||||
* .handle(async () => {
|
||||
* return new BadRequestError();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
errors<TErrors extends ServerErrorClass[]>(errors: TErrors): Procedure<Omit<TState, "errors"> & { errors: TErrors }> {
|
||||
return new Procedure({ ...this.state, errors });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the success response this procedure produces. This is used by the transform
|
||||
* tools to ensure the client receives parsed data.
|
||||
*
|
||||
* @param response - Response shape of the procedure.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .post("users:list")
|
||||
* .response(
|
||||
* z.array(
|
||||
* z.object({
|
||||
* name: z.string()
|
||||
* }),
|
||||
* )
|
||||
* )
|
||||
* .handle(async () => {
|
||||
* return [{ name: "John Doe" }];
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
response<TResponse extends ZodType>(
|
||||
response: TResponse,
|
||||
): Procedure<Omit<TState, "response"> & { response: TResponse }> {
|
||||
return new Procedure({ ...this.state, response });
|
||||
}
|
||||
|
||||
/**
|
||||
* Server handler callback method.
|
||||
*
|
||||
* Handler receives the params, query, body, actions in order of definition.
|
||||
* So if your route has params, and body the route handle method will
|
||||
* receive (params, body) as arguments.
|
||||
*
|
||||
* @param handle - Handle function to trigger when the route is executed.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .method("users:list")
|
||||
* .response(
|
||||
* z.array(
|
||||
* z.object({
|
||||
* name: z.string()
|
||||
* }),
|
||||
* )
|
||||
* )
|
||||
* .handle(async () => {
|
||||
* return [{ name: "John Doe" }];
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
handle<THandleFn extends HandleFn<ServerArgs<TState>, TState["response"]>>(
|
||||
handle: THandleFn,
|
||||
): Procedure<Omit<TState, "handle"> & { handle: THandleFn }> {
|
||||
return new Procedure({ ...this.state, handle });
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Factories
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Route factories allowing for easy generation of relay compliant routes.
|
||||
*/
|
||||
export const procedure: {
|
||||
/**
|
||||
* Create a new procedure with given method name.
|
||||
*
|
||||
* @param method Name of the procedure used to match requests against.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* procedure
|
||||
* .method("users:get-by-id")
|
||||
* .params(
|
||||
* z.string().describe("Users unique identifier")
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
method<TMethod extends string>(method: TMethod): Procedure<{ method: TMethod }>;
|
||||
} = {
|
||||
method<TMethod extends string>(method: TMethod) {
|
||||
return new Procedure({ method });
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type Procedures = {
|
||||
[key: string]: Procedures | Procedure;
|
||||
};
|
||||
|
||||
type State = {
|
||||
method: string;
|
||||
access?: RouteAccess;
|
||||
params?: ZodType;
|
||||
errors?: ServerErrorClass[];
|
||||
response?: ZodType;
|
||||
handle?: HandleFn;
|
||||
};
|
||||
|
||||
type HandleFn<TArgs extends Array<any> = any[], TResponse = any> = (
|
||||
...args: TArgs
|
||||
) => TResponse extends ZodType
|
||||
? Promise<z.infer<TResponse> | Response | ServerError>
|
||||
: Promise<Response | ServerError | void>;
|
||||
|
||||
type ServerArgs<TState extends State> =
|
||||
HasInputArgs<TState> extends true ? [z.output<TState["params"]>, ServerContext] : [ServerContext];
|
||||
|
||||
type HasInputArgs<TState extends State> = TState["params"] extends ZodType ? true : false;
|
||||
485
platform/relay/libraries/route.ts
Normal file
485
platform/relay/libraries/route.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import { match, type MatchFunction } from "path-to-regexp";
|
||||
import z, { ZodObject, ZodRawShape, ZodType } from "zod";
|
||||
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { Hooks } from "./hooks.ts";
|
||||
|
||||
export class Route<const TState extends RouteState = RouteState> {
|
||||
readonly type = "route" as const;
|
||||
|
||||
declare readonly $params: TState["params"] extends ZodObject ? z.input<TState["params"]> : never;
|
||||
declare readonly $query: TState["query"] extends ZodObject ? z.input<TState["query"]> : never;
|
||||
declare readonly $body: TState["body"] extends ZodType ? z.input<TState["body"]> : never;
|
||||
declare readonly $response: TState["response"] extends ZodType ? z.output<TState["response"]> : never;
|
||||
|
||||
#matchFn?: MatchFunction<any>;
|
||||
|
||||
/**
|
||||
* Instantiate a new Route instance.
|
||||
*
|
||||
* @param state - Route state.
|
||||
*/
|
||||
constructor(readonly state: TState) {}
|
||||
|
||||
/**
|
||||
* HTTP Method
|
||||
*/
|
||||
get method(): RouteMethod {
|
||||
return this.state.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL pattern of the route.
|
||||
*/
|
||||
get matchFn(): MatchFunction<any> {
|
||||
if (this.#matchFn === undefined) {
|
||||
this.#matchFn = match(this.path);
|
||||
}
|
||||
return this.#matchFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL path
|
||||
*/
|
||||
get path(): string {
|
||||
return this.state.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided URL matches the route pattern.
|
||||
*
|
||||
* @param url - HTTP request.url
|
||||
*/
|
||||
match(url: string): boolean {
|
||||
return this.matchFn(url) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameters from the provided URL based on the route pattern.
|
||||
*
|
||||
* @param url - HTTP request.url
|
||||
*/
|
||||
getParsedParams<TParams = TState["params"] extends ZodObject ? z.infer<TState["params"]> : object>(
|
||||
url: string,
|
||||
): TParams {
|
||||
const result = match(this.path)(url);
|
||||
if (result === false) {
|
||||
return {} as TParams;
|
||||
}
|
||||
return result.params as TParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the meta data for this route which can be used in e.g. OpenAPI generation
|
||||
*
|
||||
* @param meta - Meta object
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route.post("/foo").meta({ description: "Super route" });
|
||||
* ```
|
||||
*/
|
||||
meta<TRouteMeta extends RouteMeta>(meta: TRouteMeta): Route<Prettify<Omit<TState, "meta"> & { meta: TRouteMeta }>> {
|
||||
return new Route({ ...this.state, meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* Access level of the route which acts as the first barrier of entry
|
||||
* to ensure that requests are valid.
|
||||
*
|
||||
* By default on the server the lack of access definition will result
|
||||
* in an error as all routes needs an access definition.
|
||||
*
|
||||
* @param access - Access level of the route.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* const hasFooBar = action
|
||||
* .make("hasFooBar")
|
||||
* .response(z.object({ foobar: z.number() }))
|
||||
* .handle(async () => {
|
||||
* return {
|
||||
* foobar: 1,
|
||||
* };
|
||||
* });
|
||||
*
|
||||
* // ### Public Endpoint
|
||||
*
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .access("public")
|
||||
* .handle(async ({ foobar }) => {
|
||||
* console.log(typeof foobar); // => number
|
||||
* });
|
||||
*
|
||||
* // ### Require Session
|
||||
*
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .access("session")
|
||||
* .handle(async ({ foobar }) => {
|
||||
* console.log(typeof foobar); // => number
|
||||
* });
|
||||
*
|
||||
* // ### Require Session & Resource Assignment
|
||||
*
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .access([resource("foo", "create")])
|
||||
* .handle(async ({ foobar }) => {
|
||||
* console.log(typeof foobar); // => number
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
access<TAccess extends RouteAccess>(access: TAccess): Route<Prettify<Omit<TState, "access"> & { access: TAccess }>> {
|
||||
return new Route({ ...this.state, access: access as TAccess });
|
||||
}
|
||||
|
||||
/**
|
||||
* Params allows for custom casting of URL parameters. If a parameter does not
|
||||
* have a corresponding zod schema the default param type is "string".
|
||||
*
|
||||
* @param params - URL params.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo/:bar")
|
||||
* .params({
|
||||
* bar: z.coerce.number()
|
||||
* })
|
||||
* .handle(async ({ bar }) => {
|
||||
* console.log(typeof bar); // => number
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
params<TParams extends ZodRawShape>(
|
||||
params: TParams,
|
||||
): Route<Prettify<Omit<TState, "params"> & { params: ZodObject<TParams> }>> {
|
||||
return new Route({ ...this.state, params: z.object(params) as any });
|
||||
}
|
||||
|
||||
/**
|
||||
* Search allows for custom casting of URL query parameters. If a parameter does
|
||||
* not have a corresponding zod schema the default param type is "string".
|
||||
*
|
||||
* @param query - URL query arguments.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .query({
|
||||
* bar: z.number({ coerce: true })
|
||||
* })
|
||||
* .handle(async ({ bar }) => {
|
||||
* console.log(typeof bar); // => number
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
query<TQuery extends ZodRawShape>(
|
||||
query: TQuery,
|
||||
): Route<Prettify<Omit<TState, "search"> & { query: ZodObject<TQuery> }>> {
|
||||
return new Route({ ...this.state, query: z.object(query) as any });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the body this route expects to receive. This is used by all
|
||||
* mutator routes and has no effect when defined on "GET" methods.
|
||||
*
|
||||
* @param body - Body the route expects.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .body(
|
||||
* z.object({
|
||||
* bar: z.number()
|
||||
* })
|
||||
* )
|
||||
* .handle(async ({ body: { bar } }) => {
|
||||
* console.log(typeof bar); // => number
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
body<TBody extends ZodType>(body: TBody): Route<Prettify<Omit<TState, "body"> & { body: TBody }>> {
|
||||
return new Route({ ...this.state, body });
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of the possible error responses this route produces.
|
||||
*
|
||||
* @param errors - Error shapes of the route.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .errors([
|
||||
* BadRequestError
|
||||
* ])
|
||||
* .handle(async () => {
|
||||
* return new BadRequestError();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
errors<TErrors extends ServerErrorClass[]>(
|
||||
errors: TErrors,
|
||||
): Route<Prettify<Omit<TState, "errors"> & { errors: TErrors }>> {
|
||||
return new Route({ ...this.state, errors });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the success response this route produces. This is used by the transform
|
||||
* tools to ensure the client receives parsed data.
|
||||
*
|
||||
* @param response - Response shape of the route.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .response(
|
||||
* z.object({
|
||||
* bar: z.number()
|
||||
* })
|
||||
* )
|
||||
* .handle(async () => {
|
||||
* return {
|
||||
* bar: 1
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
response<TResponse extends ZodType>(
|
||||
response: TResponse,
|
||||
): Route<Prettify<Omit<TState, "response"> & { response: TResponse }>> {
|
||||
return new Route({ ...this.state, response });
|
||||
}
|
||||
|
||||
/**
|
||||
* Server handler callback method.
|
||||
*
|
||||
* Handler receives the params, query, body, actions in order of definition.
|
||||
* So if your route has params, and body the route handle method will
|
||||
* receive (params, body) as arguments.
|
||||
*
|
||||
* @param handle - Handle function to trigger when the route is executed.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* relay
|
||||
* .post("/api/v1/foo/:bar")
|
||||
* .params({ bar: z.string() })
|
||||
* .body(z.tuple([z.string(), z.number()]))
|
||||
* .handle(async ({ bar }, [ "string", number ]) => {});
|
||||
* ```
|
||||
*/
|
||||
handle<THandleFn extends HandleFn<ServerArgs<TState>, TState["response"]>>(
|
||||
handle: THandleFn,
|
||||
): Route<Omit<TState, "handle"> & { handle: THandleFn }> {
|
||||
return new Route({ ...this.state, handle });
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign lifetime hooks to a route allowing for custom handling of
|
||||
* events that can occur during a request or response.
|
||||
*
|
||||
* Can be used on both server and client with the appropriate
|
||||
* implementation.
|
||||
*
|
||||
* @param hooks - Hooks to register with the route.
|
||||
*/
|
||||
hooks<THooks extends Hooks>(hooks: THooks): Route<Prettify<Omit<TState, "hooks"> & { hooks: THooks }>> {
|
||||
return new Route({ ...this.state, hooks });
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Factories
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Route factories allowing for easy generation of relay compliant routes.
|
||||
*/
|
||||
export const route: {
|
||||
post<TPath extends string>(
|
||||
path: TPath,
|
||||
): Route<{ method: "POST"; path: TPath; content: "json"; errors: [ServerErrorClass] }>;
|
||||
get<TPath extends string>(
|
||||
path: TPath,
|
||||
): Route<{ method: "GET"; path: TPath; content: "json"; errors: [ServerErrorClass] }>;
|
||||
put<TPath extends string>(
|
||||
path: TPath,
|
||||
): Route<{ method: "PUT"; path: TPath; content: "json"; errors: [ServerErrorClass] }>;
|
||||
patch<TPath extends string>(
|
||||
path: TPath,
|
||||
): Route<{ method: "PATCH"; path: TPath; content: "json"; errors: [ServerErrorClass] }>;
|
||||
delete<TPath extends string>(
|
||||
path: TPath,
|
||||
): Route<{ method: "DELETE"; path: TPath; content: "json"; errors: [ServerErrorClass] }>;
|
||||
} = {
|
||||
/**
|
||||
* Create a new "POST" route for the given path.
|
||||
*
|
||||
* @param path - Path to generate route for.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .post("/foo")
|
||||
* .body(
|
||||
* z.object({ bar: z.string() })
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
post<TPath extends string>(path: TPath) {
|
||||
return new Route({ method: "POST", path, content: "json", errors: [ServerError] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new "GET" route for the given path.
|
||||
*
|
||||
* @param path - Path to generate route for.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route.get("/foo");
|
||||
* ```
|
||||
*/
|
||||
get<TPath extends string>(path: TPath) {
|
||||
return new Route({ method: "GET", path, content: "json", errors: [ServerError] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new "PUT" route for the given path.
|
||||
*
|
||||
* @param path - Path to generate route for.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .put("/foo")
|
||||
* .body(
|
||||
* z.object({ bar: z.string() })
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
put<TPath extends string>(path: TPath) {
|
||||
return new Route({ method: "PUT", path, content: "json", errors: [ServerError] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new "PATCH" route for the given path.
|
||||
*
|
||||
* @param path - Path to generate route for.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route
|
||||
* .patch("/foo")
|
||||
* .body(
|
||||
* z.object({ bar: z.string() })
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
patch<TPath extends string>(path: TPath) {
|
||||
return new Route({ method: "PATCH", path, content: "json", errors: [ServerError] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new "DELETE" route for the given path.
|
||||
*
|
||||
* @param path - Path to generate route for.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route.delete("/foo");
|
||||
* ```
|
||||
*/
|
||||
delete<TPath extends string>(path: TPath) {
|
||||
return new Route({ method: "DELETE", path, content: "json", errors: [ServerError] });
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type Routes = {
|
||||
[key: string]: Routes | Route;
|
||||
};
|
||||
|
||||
type RouteState = {
|
||||
method: RouteMethod;
|
||||
path: string;
|
||||
meta?: RouteMeta;
|
||||
access?: RouteAccess;
|
||||
params?: ZodObject;
|
||||
query?: ZodObject;
|
||||
body?: ZodType;
|
||||
errors: ServerErrorClass[];
|
||||
response?: ZodType;
|
||||
handle?: HandleFn;
|
||||
hooks?: Hooks;
|
||||
};
|
||||
|
||||
export type RouteMeta = {
|
||||
openapi?: "internal" | "external";
|
||||
description?: string;
|
||||
summary?: string;
|
||||
tags?: string[];
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export type RouteMethod = "POST" | "GET" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
export type RouteAccess = "public" | "authenticated";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ServerContext {}
|
||||
|
||||
type HandleFn<TArgs extends Array<any> = any[], TResponse = any> = (
|
||||
...args: TArgs
|
||||
) => TResponse extends ZodType
|
||||
? Promise<z.infer<TResponse> | Response | ServerError>
|
||||
: Promise<Response | ServerError | void>;
|
||||
|
||||
type ServerArgs<TState extends RouteState> =
|
||||
HasInputArgs<TState> extends true
|
||||
? [
|
||||
(TState["params"] extends ZodObject ? { params: z.output<TState["params"]> } : unknown) &
|
||||
(TState["query"] extends ZodObject ? { query: z.output<TState["query"]> } : unknown) &
|
||||
(TState["body"] extends ZodType ? { body: z.output<TState["body"]> } : unknown),
|
||||
ServerContext,
|
||||
]
|
||||
: [ServerContext];
|
||||
|
||||
type HasInputArgs<TState extends RouteState> = TState["params"] extends ZodObject
|
||||
? true
|
||||
: TState["query"] extends ZodObject
|
||||
? true
|
||||
: TState["body"] extends ZodType
|
||||
? true
|
||||
: false;
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
Reference in New Issue
Block a user