feat: modular domain driven boilerplate
This commit is contained in:
296
platform/relay/adapters/http.ts
Normal file
296
platform/relay/adapters/http.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import { encrypt } from "@platform/vault";
|
||||
|
||||
import {
|
||||
assertServerErrorResponse,
|
||||
RelayAdapter,
|
||||
RelayInput,
|
||||
RelayResponse,
|
||||
ServerErrorResponse,
|
||||
} from "../libraries/adapter.ts";
|
||||
import { ServerError, ServerErrorType } from "../libraries/errors.ts";
|
||||
|
||||
/**
|
||||
* HttpAdapter provides a unified transport layer for Relay.
|
||||
*
|
||||
* It supports sending JSON objects, nested structures, arrays, and file uploads
|
||||
* via FormData. The adapter automatically detects the payload type and formats
|
||||
* the request accordingly. Responses are normalized into `RelayResponse`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const adapter = new HttpAdapter({ url: "https://api.example.com" });
|
||||
*
|
||||
* // Sending JSON data
|
||||
* const jsonResponse = await adapter.send({
|
||||
* method: "POST",
|
||||
* endpoint: "/users",
|
||||
* body: { name: "Alice", age: 30 },
|
||||
* });
|
||||
*
|
||||
* // Sending files and nested objects
|
||||
* const formResponse = await adapter.send({
|
||||
* method: "POST",
|
||||
* endpoint: "/upload",
|
||||
* body: {
|
||||
* user: { name: "Bob", avatar: fileInput.files[0] },
|
||||
* documents: [fileInput.files[1], fileInput.files[2]],
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class HttpAdapter implements RelayAdapter {
|
||||
/**
|
||||
* Instantiate a new HttpAdapter instance.
|
||||
*
|
||||
* @param options - Adapter options.
|
||||
*/
|
||||
constructor(readonly options: HttpAdapterOptions) {}
|
||||
|
||||
/**
|
||||
* Override the initial url value set by instantiator.
|
||||
*/
|
||||
set url(value: string) {
|
||||
this.options.url = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URL value from options object.
|
||||
*/
|
||||
get url() {
|
||||
return this.options.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full URL from given endpoint.
|
||||
*
|
||||
* @param endpoint - Endpoint to get url for.
|
||||
*/
|
||||
getUrl(endpoint: string): string {
|
||||
return `${this.url}${endpoint}`;
|
||||
}
|
||||
|
||||
async send(
|
||||
{ method, endpoint, query, body, headers = new Headers() }: RelayInput,
|
||||
publicKey: string,
|
||||
): Promise<RelayResponse> {
|
||||
const init: RequestInit = { method, headers };
|
||||
|
||||
// ### Before Request
|
||||
// If any before request hooks has been defined, we run them here passing in the
|
||||
// request headers for further modification.
|
||||
|
||||
await this.#beforeRequest(headers);
|
||||
|
||||
// ### Body
|
||||
|
||||
if (body !== undefined) {
|
||||
const type = this.#getRequestFormat(body);
|
||||
if (type === "form-data") {
|
||||
headers.delete("content-type");
|
||||
init.body = this.#getFormData(body);
|
||||
}
|
||||
if (type === "json") {
|
||||
headers.set("content-type", "application/json");
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
}
|
||||
|
||||
// ### Internal
|
||||
// If public key is present we create a encrypted token on the header that
|
||||
// is verified by the server before allowing the request through.
|
||||
|
||||
if (publicKey !== undefined) {
|
||||
headers.set("x-internal", await encrypt("internal", publicKey));
|
||||
}
|
||||
|
||||
// ### Response
|
||||
|
||||
return this.request(`${endpoint}${query}`, init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a fetch request using the given fetch options and returns
|
||||
* a relay formatted response.
|
||||
*
|
||||
* @param endpoint - Which endpoint to submit request to.
|
||||
* @param init - Request init details to submit with the request.
|
||||
*/
|
||||
async request(endpoint: string, init?: RequestInit): Promise<RelayResponse> {
|
||||
return this.#toResponse(await fetch(this.getUrl(endpoint), init));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before request operations.
|
||||
*
|
||||
* @param headers - Headers to pass to hooks.
|
||||
*/
|
||||
async #beforeRequest(headers: Headers) {
|
||||
if (this.options.hooks?.beforeRequest !== undefined) {
|
||||
for (const hook of this.options.hooks.beforeRequest) {
|
||||
await hook(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the parser method required for the request.
|
||||
*
|
||||
* @param body - Request body.
|
||||
*/
|
||||
#getRequestFormat(body: unknown): "form-data" | "json" {
|
||||
if (containsFile(body) === true) {
|
||||
return "form-data";
|
||||
}
|
||||
return "json";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FormData instance for the given body.
|
||||
*
|
||||
* @param body - Request body.
|
||||
*/
|
||||
#getFormData(data: Record<string, unknown>, formData = new FormData(), parentKey?: string): FormData {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
const formKey = parentKey ? `${parentKey}[${key}]` : key;
|
||||
|
||||
if (value instanceof File) {
|
||||
formData.append(formKey, value, value.name);
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
if (item instanceof File) {
|
||||
formData.append(`${formKey}[${index}]`, item, item.name);
|
||||
} else if (typeof item === "object") {
|
||||
this.#getFormData(item as Record<string, unknown>, formData, `${formKey}[${index}]`);
|
||||
} else {
|
||||
formData.append(`${formKey}[${index}]`, String(item));
|
||||
}
|
||||
});
|
||||
} else if (typeof value === "object") {
|
||||
this.#getFormData(value as Record<string, unknown>, formData, formKey);
|
||||
} else {
|
||||
formData.append(formKey, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a fetch response to a compliant relay response.
|
||||
*
|
||||
* @param response - Fetch response to convert.
|
||||
*/
|
||||
async #toResponse(response: Response): Promise<RelayResponse> {
|
||||
const type = response.headers.get("content-type");
|
||||
|
||||
// ### Content Type
|
||||
// Ensure that the server responds with a 'content-type' definition. We should
|
||||
// always expect the server to respond with a type.
|
||||
|
||||
if (type === null) {
|
||||
return {
|
||||
result: "error",
|
||||
headers: response.headers,
|
||||
error: {
|
||||
code: "CONTENT_TYPE_MISSING",
|
||||
status: response.status,
|
||||
message: "Missing 'content-type' in header returned from server.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ### Empty Response
|
||||
// If the response comes back with empty response status 204 we simply return a
|
||||
// empty success.
|
||||
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
result: "success",
|
||||
headers: response.headers,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ### JSON
|
||||
// If the 'content-type' contains 'json' we treat it as a 'json' compliant response
|
||||
// and attempt to resolve it as such.
|
||||
|
||||
if (type.includes("json") === true) {
|
||||
const parsed = await response.json();
|
||||
if ("data" in parsed) {
|
||||
return {
|
||||
result: "success",
|
||||
headers: response.headers,
|
||||
data: parsed.data,
|
||||
};
|
||||
}
|
||||
if ("error" in parsed) {
|
||||
return {
|
||||
result: "error",
|
||||
headers: response.headers,
|
||||
error: this.#toError(parsed),
|
||||
};
|
||||
}
|
||||
return {
|
||||
result: "error",
|
||||
headers: response.headers,
|
||||
error: {
|
||||
code: "INVALID_SERVER_RESPONSE",
|
||||
status: response.status,
|
||||
message: "Unsupported 'json' body returned from server, missing 'data' or 'error' key.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
result: "error",
|
||||
headers: response.headers,
|
||||
error: {
|
||||
code: "UNSUPPORTED_CONTENT_TYPE",
|
||||
status: response.status,
|
||||
message: "Unsupported 'content-type' in header returned from server.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#toError(candidate: unknown, status: number = 500): ServerErrorType | ServerErrorResponse["error"] {
|
||||
if (assertServerErrorResponse(candidate)) {
|
||||
return ServerError.fromJSON(candidate.error);
|
||||
}
|
||||
if (typeof candidate === "string") {
|
||||
return {
|
||||
code: "ERROR",
|
||||
status,
|
||||
message: candidate,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: "UNSUPPORTED_SERVER_ERROR",
|
||||
status,
|
||||
message: "Unsupported 'error' returned from server.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function containsFile(value: unknown): boolean {
|
||||
if (value instanceof File) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(containsFile);
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return Object.values(value).some(containsFile);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type HttpAdapterOptions = {
|
||||
url: string;
|
||||
hooks?: {
|
||||
beforeRequest?: ((headers: Headers) => Promise<void>)[];
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import type { RouteMethod } from "./route.ts";
|
||||
|
||||
const ServerErrorResponseSchema = z.object({
|
||||
error: z.object({
|
||||
code: z.any(),
|
||||
status: z.number(),
|
||||
message: z.string(),
|
||||
data: z.any().optional(),
|
||||
@@ -51,9 +52,10 @@ export type RelayAdapter = {
|
||||
/**
|
||||
* Send a request to the configured relay url.
|
||||
*
|
||||
* @param input - Request input parameters.
|
||||
* @param input - Request input parameters.
|
||||
* @param publicKey - Key to encrypt the payload with.
|
||||
*/
|
||||
send(input: RelayInput): Promise<RelayResponse>;
|
||||
send(input: RelayInput, publicKey?: string): Promise<RelayResponse>;
|
||||
|
||||
/**
|
||||
* Sends a fetch request using the given options and returns a
|
||||
@@ -75,10 +77,12 @@ export type RelayInput = {
|
||||
export type RelayResponse<TData = unknown, TError = ServerErrorType | ServerErrorResponse["error"]> =
|
||||
| {
|
||||
result: "success";
|
||||
headers: Headers;
|
||||
data: TData;
|
||||
}
|
||||
| {
|
||||
result: "error";
|
||||
headers: Headers;
|
||||
error: TError;
|
||||
};
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ function getRouteFn(route: Route, { adapter }: Config) {
|
||||
|
||||
// ### Fetch
|
||||
|
||||
const response = await adapter.send(input);
|
||||
const response = await adapter.send(input, route.state.crypto?.publicKey);
|
||||
|
||||
if ("data" in response && route.state.response !== undefined) {
|
||||
response.data = route.state.response.parse(response.data);
|
||||
|
||||
4
platform/relay/libraries/context.ts
Normal file
4
platform/relay/libraries/context.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ServerContext {}
|
||||
|
||||
export const context: ServerContext = {} as any;
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { $ZodErrorTree } from "zod/v4/core";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
export abstract class ServerError<TData = unknown> extends Error {
|
||||
abstract readonly code: string;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
@@ -12,40 +14,40 @@ export abstract class ServerError<TData = unknown> extends Error {
|
||||
/**
|
||||
* Converts a server delivered JSON error to its native instance.
|
||||
*
|
||||
* @param value - Error JSON.
|
||||
* @param error - 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);
|
||||
static fromJSON(error: ServerErrorJSON): ServerErrorType {
|
||||
switch (error.code) {
|
||||
case "BAD_REQUEST":
|
||||
return new BadRequestError(error.message, error.data);
|
||||
case "UNAUTHORIZED":
|
||||
return new UnauthorizedError(error.message, error.data);
|
||||
case "FORBIDDEN":
|
||||
return new ForbiddenError(error.message, error.data);
|
||||
case "NOT_FOUND":
|
||||
return new NotFoundError(error.message, error.data);
|
||||
case "METHOD_NOT_ALLOWED":
|
||||
return new MethodNotAllowedError(error.message, error.data);
|
||||
case "NOT_ACCEPTABLE":
|
||||
return new NotAcceptableError(error.message, error.data);
|
||||
case "CONFLICT":
|
||||
return new ConflictError(error.message, error.data);
|
||||
case "GONE":
|
||||
return new GoneError(error.message, error.data);
|
||||
case "UNSUPPORTED_MEDIA_TYPE":
|
||||
return new UnsupportedMediaTypeError(error.message, error.data);
|
||||
case "UNPROCESSABLE_CONTENT":
|
||||
return new UnprocessableContentError(error.message, error.data);
|
||||
case "VALIDATION":
|
||||
return new ValidationError(error.message, error.data);
|
||||
case "INTERNAL_SERVER":
|
||||
return new InternalServerError(error.message, error.data);
|
||||
case "NOT_IMPLEMENTED":
|
||||
return new NotImplementedError(error.message, error.data);
|
||||
case "SERVICE_UNAVAILABLE":
|
||||
return new ServiceUnavailableError(error.message, error.data);
|
||||
default:
|
||||
return new InternalServerError(value.message, value.data);
|
||||
return new InternalServerError(error.message, error.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +56,7 @@ export abstract class ServerError<TData = unknown> extends Error {
|
||||
*/
|
||||
toJSON(): ServerErrorJSON {
|
||||
return {
|
||||
type: "relay",
|
||||
code: this.code as ServerErrorJSON["code"],
|
||||
status: this.status,
|
||||
message: this.message,
|
||||
data: this.data,
|
||||
@@ -63,6 +65,8 @@ export abstract class ServerError<TData = unknown> extends Error {
|
||||
}
|
||||
|
||||
export class BadRequestError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "BAD_REQUEST";
|
||||
|
||||
/**
|
||||
* Instantiate a new BadRequestError.
|
||||
*
|
||||
@@ -70,6 +74,7 @@ export class BadRequestError<TData = unknown> extends ServerError<TData> {
|
||||
* cannot or will not process the request due to something that is perceived to
|
||||
* be a client error.
|
||||
*
|
||||
* @param message - the message that describes the error. Default: "Bad Request".
|
||||
* @param data - Optional data to send with the error.
|
||||
*/
|
||||
constructor(message = "Bad Request", data?: TData) {
|
||||
@@ -78,6 +83,8 @@ export class BadRequestError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class UnauthorizedError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "UNAUTHORIZED";
|
||||
|
||||
/**
|
||||
* Instantiate a new UnauthorizedError.
|
||||
*
|
||||
@@ -104,6 +111,8 @@ export class UnauthorizedError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class ForbiddenError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "FORBIDDEN";
|
||||
|
||||
/**
|
||||
* Instantiate a new ForbiddenError.
|
||||
*
|
||||
@@ -125,6 +134,8 @@ export class ForbiddenError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class NotFoundError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "NOT_FOUND";
|
||||
|
||||
/**
|
||||
* Instantiate a new NotFoundError.
|
||||
*
|
||||
@@ -147,6 +158,8 @@ export class NotFoundError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class MethodNotAllowedError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "METHOD_NOT_ALLOWED";
|
||||
|
||||
/**
|
||||
* Instantiate a new MethodNotAllowedError.
|
||||
*
|
||||
@@ -164,6 +177,8 @@ export class MethodNotAllowedError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class NotAcceptableError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "NOT_ACCEPTABLE";
|
||||
|
||||
/**
|
||||
* Instantiate a new NotAcceptableError.
|
||||
*
|
||||
@@ -181,6 +196,8 @@ export class NotAcceptableError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class ConflictError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "CONFLICT";
|
||||
|
||||
/**
|
||||
* Instantiate a new ConflictError.
|
||||
*
|
||||
@@ -202,6 +219,8 @@ export class ConflictError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class GoneError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "GONE";
|
||||
|
||||
/**
|
||||
* Instantiate a new GoneError.
|
||||
*
|
||||
@@ -225,6 +244,8 @@ export class GoneError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class UnsupportedMediaTypeError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "UNSUPPORTED_MEDIA_TYPE";
|
||||
|
||||
/**
|
||||
* Instantiate a new UnsupportedMediaTypeError.
|
||||
*
|
||||
@@ -242,6 +263,8 @@ export class UnsupportedMediaTypeError<TData = unknown> extends ServerError<TDat
|
||||
}
|
||||
|
||||
export class UnprocessableContentError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "UNPROCESSABLE_CONTENT";
|
||||
|
||||
/**
|
||||
* Instantiate a new UnprocessableContentError.
|
||||
*
|
||||
@@ -263,22 +286,49 @@ export class UnprocessableContentError<TData = unknown> extends ServerError<TDat
|
||||
}
|
||||
}
|
||||
|
||||
export class ZodValidationError<TData extends $ZodErrorTree<any, any>> extends ServerError<TData> {
|
||||
export class ValidationError extends ServerError<ValidationErrorData> {
|
||||
readonly code = "VALIDATION";
|
||||
|
||||
/**
|
||||
* Instantiate a new ZodValidationError.
|
||||
* Instantiate a new ValidationError.
|
||||
*
|
||||
* This indicates that the server understood the request body, but the structure
|
||||
* failed validation against the expected schema.
|
||||
* This indicates that the server understood the request, but the content
|
||||
* failed semantic validation against the expected schema.
|
||||
*
|
||||
* @param message - Optional message to send with the error. Default: "Unprocessable Content".
|
||||
* @param data - ZodError instance to pass through.
|
||||
* @param message - Optional message to send with the error. Default: "Validation Failed".
|
||||
* @param data - Data with validation failure details.
|
||||
*/
|
||||
constructor(message: string, data: TData) {
|
||||
super(message, 432, data);
|
||||
constructor(message = "Validation Failed", data: ValidationErrorData) {
|
||||
super(message, 422, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new ValidationError.
|
||||
*
|
||||
* This indicates that the server understood the request, but the content
|
||||
* failed semantic validation against the expected schema.
|
||||
*
|
||||
* @param zodError - The original ZodError instance.
|
||||
* @param source - The source of the validation error.
|
||||
* @param message - Optional override for the main error message.
|
||||
*/
|
||||
static fromZod(zodError: ZodError, source: ErrorSource, message?: string) {
|
||||
return new ValidationError(message, {
|
||||
details: zodError.issues.map((issue) => {
|
||||
return {
|
||||
source: source,
|
||||
code: issue.code,
|
||||
field: issue.path.join("."),
|
||||
message: issue.message,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "INTERNAL_SERVER";
|
||||
|
||||
/**
|
||||
* Instantiate a new InternalServerError.
|
||||
*
|
||||
@@ -302,6 +352,8 @@ export class InternalServerError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class NotImplementedError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "NOT_IMPLEMENTED";
|
||||
|
||||
/**
|
||||
* Instantiate a new NotImplementedError.
|
||||
*
|
||||
@@ -319,6 +371,8 @@ export class NotImplementedError<TData = unknown> extends ServerError<TData> {
|
||||
}
|
||||
|
||||
export class ServiceUnavailableError<TData = unknown> extends ServerError<TData> {
|
||||
readonly code = "SERVICE_UNAVAILABLE";
|
||||
|
||||
/**
|
||||
* Instantiate a new ServiceUnavailableError.
|
||||
*
|
||||
@@ -338,15 +392,21 @@ export class ServiceUnavailableError<TData = unknown> extends ServerError<TData>
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerErrorClass<TData = unknown> = typeof ServerError<TData>;
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ServerErrorJSON = {
|
||||
type: "relay";
|
||||
code: ServerErrorType["code"];
|
||||
status: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
export type ServerErrorClass<TData = unknown> = typeof ServerError<TData>;
|
||||
|
||||
export type ServerErrorType =
|
||||
| BadRequestError
|
||||
| UnauthorizedError
|
||||
@@ -360,4 +420,18 @@ export type ServerErrorType =
|
||||
| UnprocessableContentError
|
||||
| NotImplementedError
|
||||
| ServiceUnavailableError
|
||||
| ValidationError
|
||||
| InternalServerError;
|
||||
|
||||
export type ErrorSource = "body" | "query" | "params" | "client";
|
||||
|
||||
type ValidationErrorData = {
|
||||
details: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
type ValidationErrorDetail = {
|
||||
source: ErrorSource;
|
||||
code: string;
|
||||
field: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import z, { ZodType } from "zod";
|
||||
|
||||
import { ServerContext } from "./context.ts";
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { RouteAccess, ServerContext } from "./route.ts";
|
||||
import { RouteAccess } from "./route.ts";
|
||||
|
||||
export class Procedure<const TState extends State = State> {
|
||||
readonly type = "procedure" as const;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { match, type MatchFunction } from "path-to-regexp";
|
||||
import z, { ZodObject, ZodRawShape, ZodType } from "zod";
|
||||
|
||||
import { ServerContext } from "./context.ts";
|
||||
import { ServerError, ServerErrorClass } from "./errors.ts";
|
||||
import { Hooks } from "./hooks.ts";
|
||||
|
||||
@@ -84,6 +85,23 @@ export class Route<const TState extends RouteState = RouteState> {
|
||||
return new Route({ ...this.state, meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cryptographic keys used to resolve cryptographic requests.
|
||||
*
|
||||
* @param crypto - Crypto configuration object.
|
||||
*
|
||||
* @examples
|
||||
*
|
||||
* ```ts
|
||||
* route.post("/foo").crypto({ publicKey: "..." });
|
||||
* ```
|
||||
*/
|
||||
crypto<TCrypto extends { publicKey: string }>(
|
||||
crypto: TCrypto,
|
||||
): Route<Prettify<Omit<TState, "crypto"> & { crypto: TCrypto }>> {
|
||||
return new Route({ ...this.state, crypto });
|
||||
}
|
||||
|
||||
/**
|
||||
* Access level of the route which acts as the first barrier of entry
|
||||
* to ensure that requests are valid.
|
||||
@@ -431,6 +449,9 @@ export type Routes = {
|
||||
type RouteState = {
|
||||
method: RouteMethod;
|
||||
path: string;
|
||||
crypto?: {
|
||||
publicKey: string;
|
||||
};
|
||||
meta?: RouteMeta;
|
||||
access?: RouteAccess;
|
||||
params?: ZodObject;
|
||||
@@ -451,10 +472,7 @@ export type RouteMeta = {
|
||||
|
||||
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 {}
|
||||
export type RouteAccess = "public" | "session" | ["internal:public", string] | ["internal:session", string];
|
||||
|
||||
type HandleFn<TArgs extends Array<any> = any[], TResponse = any> = (
|
||||
...args: TArgs
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from "./adapters/http.ts";
|
||||
export * from "./libraries/adapter.ts";
|
||||
export * from "./libraries/client.ts";
|
||||
export * from "./libraries/context.ts";
|
||||
export * from "./libraries/errors.ts";
|
||||
export * from "./libraries/hooks.ts";
|
||||
export * from "./libraries/procedure.ts";
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
".": "./mod.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@platform/auth": "workspace:*",
|
||||
"@platform/socket": "workspace:*",
|
||||
"@platform/vault": "workspace:*",
|
||||
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4",
|
||||
"path-to-regexp": "8",
|
||||
"zod": "4"
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user