working on api
This commit is contained in:
11
server/api.ts
Normal file
11
server/api.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Api } from "@src/lib/apiValidator.ts";
|
||||
import { z } from "@shared/utils/validator.ts";
|
||||
|
||||
const schema = {
|
||||
req: z.obj({
|
||||
password: z.string(),
|
||||
}),
|
||||
res: z.result(z.string(), z.any()),
|
||||
};
|
||||
|
||||
export const loginApi = new Api("/login", "POST", schema);
|
||||
@ -7,11 +7,12 @@ await esbuild.build({
|
||||
plugins: [
|
||||
...denoPlugins(),
|
||||
],
|
||||
entryPoints: ["../shared/utils/index.ts"],
|
||||
entryPoints: ["./src/js/shared.bundle.ts"],
|
||||
outfile: "./public/js/shared.bundle.js",
|
||||
bundle: true,
|
||||
minify: true,
|
||||
format: "esm",
|
||||
treeShaking: true,
|
||||
});
|
||||
esbuild.stop();
|
||||
|
||||
|
||||
103
server/main.ts
103
server/main.ts
@ -3,11 +3,19 @@ import { Eta } from "@eta-dev/eta";
|
||||
import { serveFile } from "jsr:@std/http/file-server";
|
||||
import rateLimitMiddleware from "@src/middleware/rateLimiter.ts";
|
||||
import authMiddleware from "@src/middleware/auth.ts";
|
||||
import { ok, ResultFromJSON } from "@shared/utils/result.ts";
|
||||
import { ResultResponseFromJSON } from "@src/lib/context.ts";
|
||||
import admin from "@src/lib/admin.ts";
|
||||
import UsbipManager from "@shared/utils/usbip.ts";
|
||||
import loggerMiddleware from "@src/middleware/logger.ts";
|
||||
import { SchemaValidationError, z } from "@shared/utils/validator.ts";
|
||||
import { Api } from "@src/lib/apiValidator.ts";
|
||||
import { loginApi } from "./api.ts";
|
||||
import { err, getMessageFromError, ok } from "@shared/utils/result.ts";
|
||||
import admin from "@src/lib/admin.ts";
|
||||
import {
|
||||
FailedToParseRequestAsJSON,
|
||||
QueryExecutionError,
|
||||
} from "@src/lib/errors.ts";
|
||||
import { Context } from "@src/lib/context.ts";
|
||||
|
||||
const AUTH_COOKIE_NAME = "token";
|
||||
|
||||
const router = new HttpRouter();
|
||||
|
||||
@ -23,15 +31,15 @@ const cache: Map<string, Response> = new Map();
|
||||
router.get("/public/*", async (c) => {
|
||||
const filePath = "." + c.path;
|
||||
|
||||
const cached = cache.get(filePath);
|
||||
|
||||
if (cached) {
|
||||
return cached.clone();
|
||||
}
|
||||
//const cached = cache.get(filePath);
|
||||
//
|
||||
//if (cached) {
|
||||
// return cached.clone();
|
||||
//}
|
||||
|
||||
const res = await serveFile(c.req, filePath);
|
||||
|
||||
cache.set(filePath, res.clone());
|
||||
//cache.set(filePath, res.clone());
|
||||
|
||||
return res;
|
||||
});
|
||||
@ -42,26 +50,69 @@ router
|
||||
})
|
||||
.get(["/login", "/login.html"], (c) => {
|
||||
return c.html(eta.render("./login.html", {}));
|
||||
})
|
||||
.post("/login", async (c) => {
|
||||
const r = await ResultFromJSON<{ password: string }>(
|
||||
await c.req.text(),
|
||||
);
|
||||
});
|
||||
|
||||
router
|
||||
.get("/user/:id/:name/*", (c) => {
|
||||
return c.html(
|
||||
`id = ${c.params.id}, name = ${c.params.name}, rest = ${c.params.restOfThePath}`,
|
||||
);
|
||||
});
|
||||
const schema = {
|
||||
req: z.obj({
|
||||
password: z.string().max(1024),
|
||||
}),
|
||||
res: z.result(z.void(), z.string()),
|
||||
};
|
||||
|
||||
router
|
||||
.get("/user/:idButDifferent", (c) => {
|
||||
return c.html(
|
||||
`idButDifferent = ${c.params.idButDifferent}`,
|
||||
router.api(loginApi, async (c) => {
|
||||
const r = await c
|
||||
.parseBody()
|
||||
.andThenAsync(
|
||||
({ password }) => admin.verifyPassword(password),
|
||||
);
|
||||
});
|
||||
|
||||
if (r.isErr()) {
|
||||
if (r.error.type === "AdminPasswordNotSetError") {
|
||||
return c.json(
|
||||
err({
|
||||
type: r.error.type,
|
||||
msg: r.error.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return handleCommonErrors(c, r.error);
|
||||
}
|
||||
|
||||
return admin.sessions.create()
|
||||
.map(({ value, expires }) => {
|
||||
c.cookies.set({
|
||||
name: AUTH_COOKIE_NAME,
|
||||
value,
|
||||
expires,
|
||||
});
|
||||
return ok();
|
||||
}).match(
|
||||
() => c.json(ok()),
|
||||
(e) => handleCommonErrors(c, e),
|
||||
);
|
||||
});
|
||||
|
||||
function handleCommonErrors(
|
||||
c: Context<any, any, any>,
|
||||
error:
|
||||
| QueryExecutionError
|
||||
| FailedToParseRequestAsJSON
|
||||
| SchemaValidationError,
|
||||
): Response {
|
||||
switch (error.type) {
|
||||
case "QueryExecutionError":
|
||||
return c.json(
|
||||
err(new QueryExecutionError("Server failed to execute query")),
|
||||
{ status: 500 },
|
||||
);
|
||||
case "FailedToParseRequestAsJSON":
|
||||
case "SchemaValiationError":
|
||||
return c.json(
|
||||
err(error),
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(req, connInfo) {
|
||||
|
||||
@ -1 +1 @@
|
||||
import{ok as n}from"./shared.bundle.js";const s=document.getElementById("loginForm"),a=document.getElementById("passwordInput");s.addEventListener("submit",async t=>{t.preventDefault();const o=a.value,e=JSON.stringify(n({password:o}).toJSON()),r=await(await fetch("/login",{method:"POST",headers:{accept:"application/json"},body:e})).json(),c=8});
|
||||
import{loginApi as o}from"./shared.bundle.js";const s=document.getElementById("loginForm"),m=document.getElementById("passwordInput");s.addEventListener("submit",async e=>{e.preventDefault();const t=m.value,n=await o.makeRequest({password:t},{});console.log(n)});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
/// <reference lib="dom" />
|
||||
|
||||
import { ok } from "./shared.bundle.ts";
|
||||
import { loginApi } from "./shared.bundle.ts";
|
||||
|
||||
const form = document.getElementById("loginForm") as HTMLFormElement;
|
||||
const passwordInput = document.getElementById(
|
||||
@ -12,19 +12,7 @@ form.addEventListener("submit", async (e) => {
|
||||
|
||||
const password = passwordInput.value;
|
||||
|
||||
const bodyReq = JSON.stringify(
|
||||
ok({
|
||||
password: password,
|
||||
}).toJSON(),
|
||||
);
|
||||
const res = await loginApi.makeRequest({ password }, {});
|
||||
|
||||
const response = await fetch("/login", {
|
||||
method: "POST",
|
||||
headers: { accept: "application/json" },
|
||||
body: bodyReq,
|
||||
});
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
const a = 8;
|
||||
console.log(res);
|
||||
});
|
||||
|
||||
@ -1 +0,0 @@
|
||||
../../../shared/utils/index.ts
|
||||
5
server/src/js/shared.bundle.ts
Normal file
5
server/src/js/shared.bundle.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "@shared/utils/option.ts";
|
||||
export * from "@shared/utils/result.ts";
|
||||
export * from "@shared/utils/resultasync.ts";
|
||||
export * from "@shared/utils/validator.ts";
|
||||
export * from "../../api.ts";
|
||||
@ -131,13 +131,20 @@ class AdminSessions {
|
||||
}, EXPIRED_TOKENS_DELETION_INTERVAL);
|
||||
}
|
||||
|
||||
public create(expiresAt?: Date): Result<string, QueryExecutionError> {
|
||||
public create(
|
||||
expiresAt?: Date,
|
||||
): Result<{ value: string; expires: Date }, QueryExecutionError> {
|
||||
const token = generateRandomString(TOKEN_LENGTH);
|
||||
|
||||
if (expiresAt) {
|
||||
return this.statements
|
||||
.insertSessionTokenWithExpiry(token, expiresAt.toISOString())
|
||||
.map(() => token);
|
||||
.map(() => {
|
||||
return {
|
||||
value: token,
|
||||
expires: expiresAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
@ -148,7 +155,12 @@ class AdminSessions {
|
||||
|
||||
return this.statements
|
||||
.insertSessionTokenWithExpiry(token, expiresAtDefault.toISOString())
|
||||
.map(() => token);
|
||||
.map(() => {
|
||||
return {
|
||||
value: token,
|
||||
expires: expiresAtDefault,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public verifyToken(token: string): Result<boolean, QueryExecutionError> {
|
||||
|
||||
@ -1,10 +1,88 @@
|
||||
import { Result } from "@shared/utils/result.ts";
|
||||
import {
|
||||
InferSchemaType,
|
||||
ResultSchema,
|
||||
Schema,
|
||||
z,
|
||||
} from "@shared/utils/validator.ts";
|
||||
import {
|
||||
RequestValidationError,
|
||||
ResponseValidationError,
|
||||
} from "@src/lib/errors.ts";
|
||||
import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
|
||||
class Api<Req extends object, Res extends object> {
|
||||
client = {
|
||||
validate(res: Response): Result<Req, any>,
|
||||
};
|
||||
server = {
|
||||
validate(req: Request): Result<Res, any>,
|
||||
};
|
||||
export type ExtractRouteParams<T extends string> = T extends string
|
||||
? T extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
? Param | ExtractRouteParams<Rest>
|
||||
: T extends `${infer _Start}:${infer Param}` ? Param
|
||||
: never
|
||||
: never;
|
||||
|
||||
type ApiError =
|
||||
| RequestValidationError
|
||||
| ResponseValidationError;
|
||||
|
||||
export class Api<
|
||||
Path extends string,
|
||||
ReqSchema extends Schema<any>,
|
||||
ResSchema extends Schema<any>,
|
||||
> {
|
||||
private readonly pathSplitted: string[];
|
||||
private readonly paramIndexes: Record<string, number>;
|
||||
|
||||
constructor(
|
||||
public readonly path: Path,
|
||||
public readonly method: string,
|
||||
public readonly schema: {
|
||||
req: ReqSchema;
|
||||
res: ResSchema;
|
||||
},
|
||||
) {
|
||||
this.pathSplitted = path.split("/");
|
||||
this.paramIndexes = this.pathSplitted.reduce<Record<string, number>>(
|
||||
(acc, segment, index) => {
|
||||
if (segment.startsWith(":")) {
|
||||
acc[segment.slice(1)] = index;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
makeRequest(
|
||||
reqBody: InferSchemaType<ReqSchema>,
|
||||
params: { [K in ExtractRouteParams<Path>]: string },
|
||||
): ResultAsync<InferSchemaType<ResSchema>, ApiError> {
|
||||
return this.schema.req
|
||||
.parse(reqBody)
|
||||
.toAsync()
|
||||
.mapErr((e) => new RequestValidationError(e.input, e.detail))
|
||||
.andThenAsync(async (data) => {
|
||||
const pathSplitted = this.pathSplitted;
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
pathSplitted[this.paramIndexes[key]] = value as string;
|
||||
}
|
||||
const path = pathSplitted.join("/");
|
||||
|
||||
const response = await fetch(
|
||||
path,
|
||||
{
|
||||
method: this.method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
|
||||
const resBody = await response.json();
|
||||
|
||||
return this.schema.res.parse(resBody).toAsync()
|
||||
.map((v) => v as InferSchemaType<ResSchema>)
|
||||
.mapErr((e) =>
|
||||
new ResponseValidationError(e.input, e.detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,21 @@ import { type ExtractRouteParams } from "@lib/router.ts";
|
||||
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
||||
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
||||
import { type Cookie } from "@std/http/cookie";
|
||||
import { Err, Ok, type Result, ResultFromJSON } from "@shared/utils/result.ts";
|
||||
import {
|
||||
Err,
|
||||
getMessageFromError,
|
||||
Ok,
|
||||
type Result,
|
||||
ResultFromJSON,
|
||||
} from "@shared/utils/result.ts";
|
||||
import {
|
||||
InferSchemaType,
|
||||
Schema,
|
||||
SchemaValidationError,
|
||||
} from "@shared/utils/validator.ts";
|
||||
import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import log from "@shared/utils/logger.ts";
|
||||
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
|
||||
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
|
||||
const SECURITY_HEADERS: Headers = new Headers({
|
||||
@ -36,7 +50,11 @@ function mergeHeaders(...headers: Headers[]): Headers {
|
||||
return mergedHeaders;
|
||||
}
|
||||
|
||||
export class Context<S extends string = string> {
|
||||
export class Context<
|
||||
S extends string = string,
|
||||
ReqSchema extends Schema<any> | never = never,
|
||||
ResSchema extends Schema<any> | never = never,
|
||||
> {
|
||||
private _url?: URL;
|
||||
private _hostname?: string;
|
||||
private _port?: number;
|
||||
@ -48,8 +66,30 @@ export class Context<S extends string = string> {
|
||||
public readonly req: Request,
|
||||
public readonly info: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||
public readonly params: Params<ExtractRouteParams<S>>,
|
||||
public schema: [ReqSchema, ResSchema] extends [never, never] ? never
|
||||
: {
|
||||
req: ReqSchema;
|
||||
res: ResSchema;
|
||||
},
|
||||
) {}
|
||||
|
||||
public parseBody(): ResultAsync<
|
||||
InferSchemaType<ReqSchema>,
|
||||
SchemaValidationError | FailedToParseRequestAsJSON
|
||||
> {
|
||||
if (!this.schema || !this.schema.req) {
|
||||
log.error("No schema provided!");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
return ResultAsync
|
||||
.fromPromise(
|
||||
this.req.json(),
|
||||
(e) => new FailedToParseRequestAsJSON(getMessageFromError(e)),
|
||||
)
|
||||
.andThen((data: unknown) => this.schema.req.parse(data));
|
||||
}
|
||||
|
||||
get url(): URL {
|
||||
return this._url ?? (this._url = new URL(this.req.url));
|
||||
}
|
||||
@ -60,7 +100,6 @@ export class Context<S extends string = string> {
|
||||
|
||||
get preferredType(): Option<"json" | "html"> {
|
||||
const headers = new Headers(this.req.headers);
|
||||
|
||||
return fromNullableVal(headers.get("accept")).andThen(
|
||||
(types_header) => {
|
||||
const types = types_header.split(";")[0].trim().split(",");
|
||||
@ -132,6 +171,16 @@ export class Context<S extends string = string> {
|
||||
});
|
||||
}
|
||||
|
||||
public json400(body?: object | string, init: ResponseInit = {}) {
|
||||
init.status = 400;
|
||||
return this.json(body, init);
|
||||
}
|
||||
|
||||
public json500(body?: object | string, init: ResponseInit = {}) {
|
||||
init.status = 400;
|
||||
return this.json(body, init);
|
||||
}
|
||||
|
||||
public html(body?: BodyInit | null, init: ResponseInit = {}): Response {
|
||||
const headers = mergeHeaders(
|
||||
SECURITY_HEADERS,
|
||||
@ -192,6 +241,7 @@ export class Context<S extends string = string> {
|
||||
newCtx._port = ctx._port;
|
||||
newCtx._cookies = ctx._cookies;
|
||||
newCtx._responseHeaders = ctx._responseHeaders;
|
||||
newCtx.schema = ctx.schema;
|
||||
|
||||
return newCtx;
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import log from "@shared/utils/logger.ts";
|
||||
import {
|
||||
SchemaValidationError,
|
||||
ValidationErrorDetail,
|
||||
} from "@shared/utils/validator.ts";
|
||||
|
||||
export class ErrorBase extends Error {
|
||||
constructor(message: string = "An unknown error has occurred") {
|
||||
@ -8,42 +11,69 @@ export class ErrorBase extends Error {
|
||||
}
|
||||
|
||||
export class QueryExecutionError extends ErrorBase {
|
||||
public readonly code = "QueryExecutionError";
|
||||
public readonly type = "QueryExecutionError";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoAdminEntryError extends ErrorBase {
|
||||
public readonly code = "NoAdminEntry";
|
||||
public readonly type = "NoAdminEntry";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class FailedToReadFileError extends ErrorBase {
|
||||
public readonly code = "FailedToReadFileError";
|
||||
public readonly type = "FailedToReadFileError";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidSyntaxError extends ErrorBase {
|
||||
public readonly code = "InvalidSyntax";
|
||||
public readonly type = "InvalidSyntax";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidPathError extends ErrorBase {
|
||||
public readonly code = "InvalidPath";
|
||||
public readonly type = "InvalidPath";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminPasswordNotSetError extends ErrorBase {
|
||||
public readonly code = "AdminPasswordNotSetError";
|
||||
public readonly type = "AdminPasswordNotSetError";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestValidationError extends SchemaValidationError {
|
||||
public readonly type = "RequestValidationError";
|
||||
constructor(
|
||||
input: unknown,
|
||||
detail: ValidationErrorDetail,
|
||||
) {
|
||||
super(input, detail);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseValidationError extends SchemaValidationError {
|
||||
public readonly type = "ResponseValidationError";
|
||||
constructor(
|
||||
input: unknown,
|
||||
detail: ValidationErrorDetail,
|
||||
) {
|
||||
super(input, detail);
|
||||
}
|
||||
}
|
||||
|
||||
export class FailedToParseRequestAsJSON extends ErrorBase {
|
||||
public readonly type = "FailedToParseRequestAsJSON";
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@ -1,25 +1,44 @@
|
||||
import { RouterTree } from "@lib/routerTree.ts";
|
||||
import { none, Option, some } from "@shared/utils/option.ts";
|
||||
import { Context } from "@lib/context.ts";
|
||||
import { Schema } from "@shared/utils/validator.ts";
|
||||
import { Api } from "@src/lib/apiValidator.ts";
|
||||
|
||||
type RequestHandler<S extends string> = (
|
||||
c: Context<S>,
|
||||
type RequestHandler<
|
||||
S extends string,
|
||||
ReqSchema extends Schema<any> = never,
|
||||
ResSchema extends Schema<any> = never,
|
||||
> = (
|
||||
c: Context<S, ReqSchema, ResSchema>,
|
||||
) => Promise<Response> | Response;
|
||||
type RequestHandlerWithSchema<S extends string> = {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
};
|
||||
};
|
||||
export type Middleware = (
|
||||
c: Context<string>,
|
||||
next: () => Promise<void>,
|
||||
) => Promise<Response | void> | Response | void;
|
||||
|
||||
type MethodHandlers<S extends string> = Partial<
|
||||
Record<string, RequestHandler<S>>
|
||||
Record<string, {
|
||||
handler: RequestHandler<S>;
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
};
|
||||
}>
|
||||
>;
|
||||
|
||||
const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");
|
||||
|
||||
class HttpRouter {
|
||||
routerTree = new RouterTree<MethodHandlers<any>>();
|
||||
public readonly routerTree = new RouterTree<MethodHandlers<any>>();
|
||||
pathPreprocessor?: (path: string) => string;
|
||||
middlewares: Middleware[] = [];
|
||||
private middlewares: Middleware[] = [];
|
||||
defaultNotFoundHandler: RequestHandler<string> = DEFAULT_NOT_FOUND_HANDLER;
|
||||
|
||||
setPathProcessor(processor: (path: string) => string) {
|
||||
@ -35,27 +54,39 @@ class HttpRouter {
|
||||
path: S,
|
||||
method: string,
|
||||
handler: RequestHandler<S>,
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
},
|
||||
): HttpRouter;
|
||||
add<S extends string>(
|
||||
path: S[],
|
||||
method: string,
|
||||
handler: RequestHandler<string>,
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
},
|
||||
): HttpRouter;
|
||||
add(
|
||||
path: string | string[],
|
||||
method: string,
|
||||
handler: RequestHandler<string>,
|
||||
schema?: {
|
||||
res: Schema<any>;
|
||||
req: Schema<any>;
|
||||
},
|
||||
): HttpRouter {
|
||||
const paths = Array.isArray(path) ? path : [path];
|
||||
|
||||
for (const p of paths) {
|
||||
this.routerTree.getHandler(p).match(
|
||||
(mth) => {
|
||||
mth[method] = handler;
|
||||
mth[method] = { handler, schema };
|
||||
},
|
||||
() => {
|
||||
const mth: MethodHandlers<string> = {};
|
||||
mth[method] = handler;
|
||||
mth[method] = { handler, schema };
|
||||
this.routerTree.add(p, mth);
|
||||
},
|
||||
);
|
||||
@ -64,14 +95,11 @@ class HttpRouter {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Overload signatures for 'get'
|
||||
get<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
|
||||
get<S extends string>(
|
||||
path: S[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter;
|
||||
|
||||
// Non-generic implementation for 'get'
|
||||
get(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
||||
if (Array.isArray(path)) {
|
||||
return this.add(path, "GET", handler);
|
||||
@ -84,7 +112,6 @@ class HttpRouter {
|
||||
path: string[],
|
||||
handler: RequestHandler<string>,
|
||||
): HttpRouter;
|
||||
|
||||
post(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
||||
if (Array.isArray(path)) {
|
||||
return this.add(path, "POST", handler);
|
||||
@ -92,6 +119,17 @@ class HttpRouter {
|
||||
return this.add(path, "POST", handler);
|
||||
}
|
||||
|
||||
api<
|
||||
Path extends string,
|
||||
ReqSchema extends Schema<any>,
|
||||
ResSchema extends Schema<any>,
|
||||
>(
|
||||
api: Api<Path, ReqSchema, ResSchema>,
|
||||
handler: RequestHandler<Path, ReqSchema, ResSchema>,
|
||||
): HttpRouter {
|
||||
return this.add(api.path, api.method, handler, api.schema);
|
||||
}
|
||||
|
||||
async handleRequest(
|
||||
req: Request,
|
||||
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||
@ -102,25 +140,37 @@ class HttpRouter {
|
||||
? this.pathPreprocessor(c.path)
|
||||
: c.path;
|
||||
|
||||
let params: string[] = [];
|
||||
let params: Record<string, string> = {};
|
||||
|
||||
const handler = this.routerTree
|
||||
.find(path)
|
||||
.andThen((routeMatch) => {
|
||||
const { value: handlers, params: paramsMatched } = routeMatch;
|
||||
const { value: methodToHandler, params: paramsMatched } =
|
||||
routeMatch;
|
||||
|
||||
params = paramsMatched;
|
||||
const handler = handlers[req.method];
|
||||
return handler ? some(handler) : none;
|
||||
|
||||
const handlerAndSchema = methodToHandler[req.method];
|
||||
|
||||
if (!handlerAndSchema) {
|
||||
return none;
|
||||
}
|
||||
|
||||
const handler = handlerAndSchema.handler;
|
||||
|
||||
c.schema = handlerAndSchema.schema;
|
||||
|
||||
return some(handler);
|
||||
})
|
||||
.unwrapOrElse(() => this.defaultNotFoundHandler);
|
||||
|
||||
const cf = await this.executeMiddlewareChain(
|
||||
const res = (await this.executeMiddlewareChain(
|
||||
this.middlewares,
|
||||
handler,
|
||||
Context.setParams(c, params),
|
||||
);
|
||||
)).res;
|
||||
|
||||
return cf.res;
|
||||
return res;
|
||||
}
|
||||
|
||||
private async executeMiddlewareChain<S extends string>(
|
||||
|
||||
@ -4,6 +4,7 @@ const loggerMiddleware: Middleware = async (c, next) => {
|
||||
console.log("", c.req.method, c.path);
|
||||
await next();
|
||||
console.log("", c.res.status, "\n");
|
||||
console.log(await c.res.json());
|
||||
};
|
||||
|
||||
export default loggerMiddleware;
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { type Result } from "@shared/utils/result.ts";
|
||||
import { type InferSchema, Schema } from "@shared/utils/validator.ts";
|
||||
import {
|
||||
type InferSchema,
|
||||
InferSchemaType,
|
||||
Schema,
|
||||
} from "@shared/utils/validator.ts";
|
||||
|
||||
export type ExtractRouteParams<T extends string> = T extends string
|
||||
? T extends `${infer _Start}:${infer Param}/${infer Rest}`
|
||||
@ -13,22 +17,35 @@ class ClientApi<
|
||||
ReqSchema extends Schema<any>,
|
||||
ResSchema extends Schema<any>,
|
||||
> {
|
||||
private readonly path: string[];
|
||||
private readonly paramsIndexes: Record<string, number>;
|
||||
|
||||
constructor(
|
||||
public readonly path: Path,
|
||||
path: Path,
|
||||
public readonly reqSchema: ReqSchema,
|
||||
public readonly resSchema: ResSchema,
|
||||
) {
|
||||
this.path = path.split("/");
|
||||
this.paramsIndexes = this.path.reduce<Record<number, string>>(
|
||||
(acc, segment, index) => {
|
||||
if (segment.startsWith(":")) {
|
||||
acc[index] = segment.slice(1);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
makeRequest(
|
||||
reqBody: InferSchemaType<ReqSchema>,
|
||||
params?: ExtractRouteParams<Path>,
|
||||
params?: { [K in ExtractRouteParams<Path>]: string },
|
||||
) {
|
||||
const pathWithParams = this.path.split("/").map((segment) => {
|
||||
if (segment.startsWith(":")) {
|
||||
return params[segment.slice(1)];
|
||||
const path = this.path.slice().reduce<string>((acc, cur) => {});
|
||||
if (params) {
|
||||
for (const param of Object.keys(params)) {
|
||||
pathSplitted[this.paramsIndexes[param]] = param;
|
||||
}
|
||||
return segment;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "@shared/utils/option.ts";
|
||||
export * from "@shared/utils/result.ts";
|
||||
export * from "@shared/utils/resultasync.ts";
|
||||
//export * from "@shared/utils/validator.ts";
|
||||
export * from "@shared/utils/validator.ts";
|
||||
|
||||
@ -150,7 +150,7 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
|
||||
}
|
||||
|
||||
andThenAsync<U, F>(
|
||||
fn: (value: T) => ResultAsync<U, F>,
|
||||
fn: (value: T) => ResultAsync<U, E | F> | Promise<Result<U, E | F>>,
|
||||
): ResultAsync<U, E | F> {
|
||||
return new ResultAsync(
|
||||
this._promise.then(
|
||||
|
||||
@ -2,7 +2,7 @@ import { err, Result } from "@shared/utils/result.ts";
|
||||
import { ok } from "@shared/utils/index.ts";
|
||||
import { None, none, Option, some } from "@shared/utils/option.ts";
|
||||
// ── Error Types ─────────────────────────────────────────────────────
|
||||
type ValidationErrorDetail =
|
||||
export type ValidationErrorDetail =
|
||||
| {
|
||||
kind: "typeMismatch";
|
||||
expected: string;
|
||||
@ -33,14 +33,18 @@ type ValidationErrorDetail =
|
||||
}
|
||||
| { kind: "general"; mark?: string; msg: string };
|
||||
|
||||
class SchemaValidationError extends Error {
|
||||
public readonly type = "SchemaValidationError";
|
||||
export class SchemaValidationError extends Error {
|
||||
public readonly type = "SchemaValiationError";
|
||||
|
||||
constructor(
|
||||
public readonly input: unknown,
|
||||
public readonly detail: ValidationErrorDetail,
|
||||
) {
|
||||
super(detail.msg || "Schema validation error");
|
||||
super(
|
||||
SchemaValidationError.getBestMsg(detail) ||
|
||||
"Schema validation error",
|
||||
);
|
||||
this.name = "SchemaValidationError";
|
||||
}
|
||||
|
||||
public format(): Record<string, unknown> {
|
||||
@ -971,7 +975,7 @@ class NullishSchema<S extends Schema<any>>
|
||||
}
|
||||
}
|
||||
|
||||
class ResultSchema<T extends Schema<any>, E extends Schema<any>>
|
||||
export class ResultSchema<T extends Schema<any>, E extends Schema<any>>
|
||||
extends BaseSchema<Result<InferSchemaType<T>, InferSchemaType<E>>> {
|
||||
constructor(
|
||||
private readonly okSchema: T,
|
||||
@ -1078,7 +1082,7 @@ class ResultSchema<T extends Schema<any>, E extends Schema<any>>
|
||||
}
|
||||
}
|
||||
|
||||
class OptionSchema<T extends Schema<any>>
|
||||
export class OptionSchema<T extends Schema<any>>
|
||||
extends BaseSchema<Option<InferSchemaType<T>>> {
|
||||
constructor(
|
||||
private readonly schema: T,
|
||||
|
||||
Reference in New Issue
Block a user