Template
1
0

feat: add additional processing tools

This commit is contained in:
2025-04-21 23:02:37 +00:00
parent 22325285be
commit 425439e493
8 changed files with 142 additions and 45 deletions

View File

@@ -1,4 +1,5 @@
import type { RelayAdapter, RelayRequestInput, RelayResponse } from "../libraries/adapter.ts"; import type { RelayAdapter, RelayRequestInput, RelayResponse } from "../libraries/adapter.ts";
import { RelayError, UnprocessableContentError } from "../libraries/errors.ts";
export class HttpAdapter implements RelayAdapter { export class HttpAdapter implements RelayAdapter {
#id: number = 0; #id: number = 0;
@@ -6,10 +7,28 @@ export class HttpAdapter implements RelayAdapter {
constructor(readonly url: string) {} constructor(readonly url: string) {}
async send({ method, params }: RelayRequestInput): Promise<RelayResponse> { async send({ method, params }: RelayRequestInput): Promise<RelayResponse> {
const res = await fetch(this.url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params, id: this.#id++ }) }); const id = this.#id++;
if (res.headers.get("content-type")?.includes("application/json") === false) { const res = await fetch(this.url, {
throw new Error("Unexpected return type"); method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ relay: "1.0", method, params, id }),
});
const contentType = res.headers.get("content-type");
if (contentType !== "application/json") {
return {
relay: "1.0",
error: new UnprocessableContentError(`Invalid 'content-type' in header header, expected 'application/json', received '${contentType}'`),
id,
};
} }
return res.json(); const json = await res.json();
if ("error" in json) {
return {
relay: "1.0",
error: RelayError.fromJSON(json.error),
id,
};
}
return json;
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@valkyr/relay", "name": "@valkyr/relay",
"version": "0.3.0", "version": "0.3.1",
"exports": { "exports": {
".": "./mod.ts", ".": "./mod.ts",
"./http": "./adapters/http.ts" "./http": "./adapters/http.ts"

View File

@@ -1,3 +1,5 @@
import { RelayError } from "./errors.ts";
export type RelayAdapter = { export type RelayAdapter = {
send(input: RelayRequestInput): Promise<RelayResponse>; send(input: RelayRequestInput): Promise<RelayResponse>;
}; };
@@ -9,12 +11,12 @@ export type RelayRequestInput = {
export type RelayResponse = export type RelayResponse =
| { | {
relay: "1.0";
result: unknown; result: unknown;
id: string; id: string | number;
} }
| { | {
error: { relay: "1.0";
message: string; error: RelayError;
}; id: string | number;
id: string;
}; };

View File

@@ -2,6 +2,7 @@ import z from "zod";
import { BadRequestError, InternalServerError, NotFoundError, RelayError } from "./errors.ts"; import { BadRequestError, InternalServerError, NotFoundError, RelayError } from "./errors.ts";
import { Procedure } from "./procedure.ts"; import { Procedure } from "./procedure.ts";
import { RelayRequest, request } from "./request.ts";
export class RelayApi<TProcedures extends Procedure[]> { export class RelayApi<TProcedures extends Procedure[]> {
/** /**
@@ -21,6 +22,15 @@ export class RelayApi<TProcedures extends Procedure[]> {
} }
} }
/**
* Takes a request candidate and parses its json body.
*
* @param candidate - Request candidate to parse.
*/
async parse(candidate: Request): Promise<RelayRequest> {
return request.parseAsync(await candidate.json());
}
/** /**
* Handle a incoming fetch request. * Handle a incoming fetch request.
* *
@@ -28,7 +38,7 @@ export class RelayApi<TProcedures extends Procedure[]> {
* @param params - Parameters provided with the method request. * @param params - Parameters provided with the method request.
* @param id - Request id used for response identification. * @param id - Request id used for response identification.
*/ */
async call(method: string, params: unknown, id: string): Promise<Response> { async call({ method, params, id }: RelayRequest): Promise<Response> {
const procedure = this.#index.get(method); const procedure = this.#index.get(method);
if (procedure === undefined) { if (procedure === undefined) {
return toResponse(new NotFoundError(`Method '' does not exist`), id); return toResponse(new NotFoundError(`Method '' does not exist`), id);
@@ -112,10 +122,11 @@ export class RelayApi<TProcedures extends Procedure[]> {
* @param result - Result to send back as a Response. * @param result - Result to send back as a Response.
* @param id - Request id which can be used to identify the response. * @param id - Request id which can be used to identify the response.
*/ */
function toResponse(result: object | RelayError | Response | void, id: string): Response { export function toResponse(result: object | RelayError | Response | void, id: string | number): Response {
if (result === undefined) { if (result === undefined) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
relay: "1.0",
result: null, result: null,
id, id,
}), }),
@@ -133,6 +144,7 @@ function toResponse(result: object | RelayError | Response | void, id: string):
if (result instanceof RelayError) { if (result instanceof RelayError) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
relay: "1.0",
error: result, error: result,
id, id,
}), }),
@@ -146,6 +158,7 @@ function toResponse(result: object | RelayError | Response | void, id: string):
} }
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
relay: "1.0",
result, result,
id, id,
}), }),

View File

@@ -1,17 +1,50 @@
export abstract class RelayError<D = unknown> extends Error { export abstract class RelayError<TData = unknown> extends Error {
constructor( constructor(
message: string, message: string,
readonly status: number, readonly status: number,
readonly data?: D, readonly data?: TData,
) { ) {
super(message); super(message);
} }
toJSON(): { /**
status: number; * Converts a server delivered JSON error to its native instance.
message: string; *
data: any; * @param value - Error JSON.
} { */
static fromJSON(value: RelayErrorJSON): RelayErrorType {
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 503:
return new ServiceUnavailableError(value.message, value.data);
default:
return new InternalServerError(value.message, value.data);
}
}
/**
* Convert error instance to a JSON object.
*/
toJSON(): RelayErrorJSON {
return { return {
status: this.status, status: this.status,
message: this.message, message: this.message,
@@ -20,7 +53,7 @@ export abstract class RelayError<D = unknown> extends Error {
} }
} }
export class BadRequestError<D = unknown> extends RelayError<D> { export class BadRequestError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new BadRequestError. * Instantiate a new BadRequestError.
* *
@@ -30,12 +63,12 @@ export class BadRequestError<D = unknown> extends RelayError<D> {
* *
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Bad Request", data?: D) { constructor(message = "Bad Request", data?: TData) {
super(message, 400, data); super(message, 400, data);
} }
} }
export class UnauthorizedError<D = unknown> extends RelayError<D> { export class UnauthorizedError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new UnauthorizedError. * Instantiate a new UnauthorizedError.
* *
@@ -56,12 +89,12 @@ export class UnauthorizedError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Unauthorized". * @param message - Optional message to send with the error. Default: "Unauthorized".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Unauthorized", data?: D) { constructor(message = "Unauthorized", data?: TData) {
super(message, 401, data); super(message, 401, data);
} }
} }
export class ForbiddenError<D = unknown> extends RelayError<D> { export class ForbiddenError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new ForbiddenError. * Instantiate a new ForbiddenError.
* *
@@ -77,12 +110,12 @@ export class ForbiddenError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Forbidden". * @param message - Optional message to send with the error. Default: "Forbidden".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Forbidden", data?: D) { constructor(message = "Forbidden", data?: TData) {
super(message, 403, data); super(message, 403, data);
} }
} }
export class NotFoundError<D = unknown> extends RelayError<D> { export class NotFoundError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new NotFoundError. * Instantiate a new NotFoundError.
* *
@@ -99,12 +132,12 @@ export class NotFoundError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Not Found". * @param message - Optional message to send with the error. Default: "Not Found".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Not Found", data?: D) { constructor(message = "Not Found", data?: TData) {
super(message, 404, data); super(message, 404, data);
} }
} }
export class MethodNotAllowedError<D = unknown> extends RelayError<D> { export class MethodNotAllowedError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new MethodNotAllowedError. * Instantiate a new MethodNotAllowedError.
* *
@@ -116,12 +149,12 @@ export class MethodNotAllowedError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Method Not Allowed". * @param message - Optional message to send with the error. Default: "Method Not Allowed".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Method Not Allowed", data?: D) { constructor(message = "Method Not Allowed", data?: TData) {
super(message, 405, data); super(message, 405, data);
} }
} }
export class NotAcceptableError<D = unknown> extends RelayError<D> { export class NotAcceptableError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new NotAcceptableError. * Instantiate a new NotAcceptableError.
* *
@@ -133,12 +166,12 @@ export class NotAcceptableError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Not Acceptable". * @param message - Optional message to send with the error. Default: "Not Acceptable".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Not Acceptable", data?: D) { constructor(message = "Not Acceptable", data?: TData) {
super(message, 406, data); super(message, 406, data);
} }
} }
export class ConflictError<D = unknown> extends RelayError<D> { export class ConflictError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new ConflictError. * Instantiate a new ConflictError.
* *
@@ -154,12 +187,12 @@ export class ConflictError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Conflict". * @param message - Optional message to send with the error. Default: "Conflict".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Conflict", data?: D) { constructor(message = "Conflict", data?: TData) {
super(message, 409, data); super(message, 409, data);
} }
} }
export class GoneError<D = unknown> extends RelayError<D> { export class GoneError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new GoneError. * Instantiate a new GoneError.
* *
@@ -177,12 +210,12 @@ export class GoneError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Gone". * @param message - Optional message to send with the error. Default: "Gone".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Gone", data?: D) { constructor(message = "Gone", data?: TData) {
super(message, 410, data); super(message, 410, data);
} }
} }
export class UnsupportedMediaTypeError<D = unknown> extends RelayError<D> { export class UnsupportedMediaTypeError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new UnsupportedMediaTypeError. * Instantiate a new UnsupportedMediaTypeError.
* *
@@ -194,12 +227,12 @@ export class UnsupportedMediaTypeError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Unsupported Media Type". * @param message - Optional message to send with the error. Default: "Unsupported Media Type".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Unsupported Media Type", data?: D) { constructor(message = "Unsupported Media Type", data?: TData) {
super(message, 415, data); super(message, 415, data);
} }
} }
export class UnprocessableContentError<D = unknown> extends RelayError<D> { export class UnprocessableContentError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new UnprocessableContentError. * Instantiate a new UnprocessableContentError.
* *
@@ -216,12 +249,12 @@ export class UnprocessableContentError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Unprocessable Content". * @param message - Optional message to send with the error. Default: "Unprocessable Content".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Unprocessable Content", data?: D) { constructor(message = "Unprocessable Content", data?: TData) {
super(message, 422, data); super(message, 422, data);
} }
} }
export class InternalServerError<D = unknown> extends RelayError<D> { export class InternalServerError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new InternalServerError. * Instantiate a new InternalServerError.
* *
@@ -239,12 +272,12 @@ export class InternalServerError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Internal Server Error". * @param message - Optional message to send with the error. Default: "Internal Server Error".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Internal Server Error", data?: D) { constructor(message = "Internal Server Error", data?: TData) {
super(message, 500, data); super(message, 500, data);
} }
} }
export class ServiceUnavailableError<D = unknown> extends RelayError<D> { export class ServiceUnavailableError<TData = unknown> extends RelayError<TData> {
/** /**
* Instantiate a new ServiceUnavailableError. * Instantiate a new ServiceUnavailableError.
* *
@@ -259,7 +292,27 @@ export class ServiceUnavailableError<D = unknown> extends RelayError<D> {
* @param message - Optional message to send with the error. Default: "Service Unavailable". * @param message - Optional message to send with the error. Default: "Service Unavailable".
* @param data - Optional data to send with the error. * @param data - Optional data to send with the error.
*/ */
constructor(message = "Service Unavailable", data?: D) { constructor(message = "Service Unavailable", data?: TData) {
super(message, 503, data); super(message, 503, data);
} }
} }
export type RelayErrorJSON = {
status: number;
message: string;
data: any;
};
export type RelayErrorType =
| BadRequestError
| UnauthorizedError
| ForbiddenError
| NotFoundError
| MethodNotAllowedError
| NotAcceptableError
| ConflictError
| GoneError
| UnsupportedMediaTypeError
| UnprocessableContentError
| ServiceUnavailableError
| InternalServerError;

10
libraries/request.ts Normal file
View File

@@ -0,0 +1,10 @@
import z from "zod";
export const request = z.object({
relay: z.literal("1.0"),
method: z.string(),
params: z.unknown(),
id: z.string().or(z.number()),
});
export type RelayRequest = z.infer<typeof request>;

1
mod.ts
View File

@@ -4,3 +4,4 @@ export * from "./libraries/api.ts";
export * from "./libraries/errors.ts"; export * from "./libraries/errors.ts";
export * from "./libraries/procedure.ts"; export * from "./libraries/procedure.ts";
export * from "./libraries/relay.ts"; export * from "./libraries/relay.ts";
export * from "./libraries/request.ts";

View File

@@ -19,8 +19,7 @@ describe("Procedure", () => {
}, },
}, },
async (request) => { async (request) => {
const { method, params, id } = await request.json(); return api.call(await api.parse(request));
return api.call(method, params, id);
}, },
); );
client = relay.client({ client = relay.client({