diff --git a/package.json b/package.json deleted file mode 100644 index 188c262..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "clonegur", - "version": "1.0.0", - "main": "index.ts", - "license": "MIT", - "type": "commonjs", - "dependencies": { - "mongodb": "^5.2.0", - "reflect-metadata": "^0.1.13" - }, - "devDependencies": { - "@types/express": "^4.17.14", - "@types/node": "^16.11.10", - "ts-node": "^10.9.1", - "typescript": "^5.1.3" - }, - "scripts": { - "start": "ts-node src/index.ts", - "typeorm": "typeorm-ts-node-commonjs" - } -} diff --git a/src/index.ts b/src/index.ts index 7161f14..d9bf42e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import { Collection, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; import UserRouter from "./routers/UserRouter.ts"; -import { Router, rest, route } from "./server/Server.ts"; import User from "./models/User.ts"; -import Response from "./server/Response.ts"; +import Response from "./server/RestResponse.ts"; +import Router, { rest, route } from "./server/Router.ts"; class RootRouter extends Router { @route('users/*') users: UserRouter; @@ -27,4 +27,4 @@ export default async function clonegur() { await new RootRouter(salt, db.collection('users')).attach(Deno.listen({ port: 4000, hostname: 'localhost' })); } -clonegur(); +await clonegur(); diff --git a/src/routers/AppRouter.ts b/src/routers/AppRouter.ts index d9c1ff8..235144b 100644 --- a/src/routers/AppRouter.ts +++ b/src/routers/AppRouter.ts @@ -1,4 +1,7 @@ -import { HttpError, RestRequest, RestResponse, Router } from "../server/Server.ts"; +import { HttpError } from "../server/HttpError.ts"; +import RestRequest from "../server/RestRequest.ts"; +import RestResponse from "../server/RestResponse.ts"; +import Router from "../server/Router.ts"; export default class AppRouter extends Router { public onError(_req: RestRequest, error: unknown): RestResponse | HttpError | Promise { diff --git a/src/routers/UserRouter.ts b/src/routers/UserRouter.ts index 947a5ff..f3ac353 100644 --- a/src/routers/UserRouter.ts +++ b/src/routers/UserRouter.ts @@ -1,7 +1,8 @@ import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; -import { HttpError, rest } from "../server/Server.ts"; +import HttpError from "../server/HttpError.ts"; import User from "../models/User.ts"; -import { body, schema } from "../server/Router.ts"; +import { rest } from "../server/Router.ts"; +import { body, schema } from "../server/decorators.ts"; import AppRouter from "./AppRouter.ts"; import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; diff --git a/src/server/HttpError.ts b/src/server/HttpError.ts new file mode 100644 index 0000000..8dd1549 --- /dev/null +++ b/src/server/HttpError.ts @@ -0,0 +1,8 @@ +import serialize from "./serialize.ts"; + +export default class HttpError extends Error { + constructor(public readonly body: unknown, public readonly status = 400) { + super(); + serialize(body).then(v => this.message = v.toString()); + } +} diff --git a/src/server/Request.ts b/src/server/RestRequest.ts similarity index 96% rename from src/server/Request.ts rename to src/server/RestRequest.ts index b9dcaaa..ba46caf 100644 --- a/src/server/Request.ts +++ b/src/server/RestRequest.ts @@ -14,8 +14,6 @@ function sanitizeUrl(url: string, forceAbsolute = true) { } } -export const FetchRequest = Request; - export default class RestRequest { public readonly body: unknown; public readonly method: string; diff --git a/src/server/Response.ts b/src/server/RestResponse.ts similarity index 90% rename from src/server/Response.ts rename to src/server/RestResponse.ts index 830c0af..85aa78b 100644 --- a/src/server/Response.ts +++ b/src/server/RestResponse.ts @@ -1,6 +1,4 @@ -import { Headers } from "./Request.ts"; - -export const FetchResponse = Response; +import { Headers } from "./RestRequest.ts"; export default class RestResponse { #status = 200; diff --git a/src/server/Router.ts b/src/server/Router.ts index cb289a5..421f363 100644 --- a/src/server/Router.ts +++ b/src/server/Router.ts @@ -1,16 +1,13 @@ // deno-lint-ignore-file no-explicit-any ban-types import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts"; -import { HttpError, RestRequest, RestResponse, serialize } from "./Server.ts"; +import serialize from "./serialize.ts"; +import HttpError from "./HttpError.ts"; +import RestResponse from "./RestResponse.ts"; +import RestRequest from "./RestRequest.ts"; export type HandlerRes = Promise | RestResponse | undefined; export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE'; -export type BodyType = 'raw' | 'json'; -export type AuthType = 'raw' | 'jwt'; - -export type PrimitiveSchema = 'string' | 'number' | 'boolean' | 'object'; -export type OptionalSchema = `${PrimitiveSchema}?`; -export type Schema = 'any' | PrimitiveSchema | OptionalSchema | Schema[] | ({ $optional?: boolean; } & { [key: string]: Schema }); export interface Handler { handle(req: RestRequest): HandlerRes; @@ -38,84 +35,6 @@ export function makeParameterModifier(func: ProcessFunc) { } } -export function schema(desc: Schema) { - function stringify(desc: Schema): string { - if (typeof desc === 'string') return desc; - if (desc instanceof Array) return desc.map(stringify).join(' | '); - - let res = '{ '; - - for (const key in desc) { - if (key === '$optional') continue; - if (res != '{ ') res += ', '; - res += key + ': '; - res += stringify(desc[key]); - } - res += '}'; - if (desc.$optional) res += '?'; - - return res; - } - function test(path: string[], val: unknown, desc: Schema) { - if (desc === 'any') return; - if (typeof desc === 'string') { - let type: string = desc; - const opt = desc.endsWith('?'); - - if (opt) type = type.substring(0, desc.length - 1); - if (opt && val === undefined) return; - - if (typeof val as any !== type) throw new HttpError(`${path.join('.')}: Expected a ${type}, got ${typeof val} instead.`); - } - else if (desc instanceof Array) { - for (const type of desc) { - try { - test(path, val, type); - return; - } - catch { /**/ } - } - throw new HttpError(`${path.join('.')}: Expected a ${stringify(desc)}, got ${typeof val} instead.`); - } - else { - if (desc.$optional && val === undefined) return; - if (typeof val !== 'object' || val === null) throw new HttpError(`${path.join('.')}: Expected an object, got ${typeof val} instead.`); - - for (const key in desc) { - if (key === '$optional') continue; - test([ ...path, key ], (val as any)[key], desc[key]); - } - } - } - - return makeParameterModifier((_req, val, name) => (test([name], val, desc), val)); -} -export function body(type: BodyType = 'json') { - return makeParameterModifier(async req => { - let body = req.body; - if (type === 'json') { - try { - if (body instanceof Blob) body = await body.text(); - if (typeof body === 'string') body = JSON.parse(body.toString()); - } - catch (e) { - if (e instanceof SyntaxError) throw new HttpError('Body syntax error: ' + e.message); - } - } - return body; - }); -} -export function auth(type: AuthType = 'jwt') { - return makeParameterModifier(req => { - let res = req.headers.authorization; - if (typeof res !== 'string') return undefined; - if (res.startsWith('Bearer')) res = res.substring(6).trimStart(); - - if (type === 'jwt') throw new Error('JWT is not supported.'); - - return res; - }); -} function addMetaQuery(target: any, ...handlers: ((r: Router) => RouterHandler)[]) { let props = Reflect.getOwnMetadata('router:queries', target); diff --git a/src/server/Server.ts b/src/server/Server.ts deleted file mode 100644 index e9260d8..0000000 --- a/src/server/Server.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Router, { rest, route } from "./Router.ts"; -import RestRequest from "./Request.ts"; -import RestResponse from "./Response.ts"; - -export { RestRequest, RestResponse, Router, rest, route }; - -export class HttpError extends Error { - constructor(public readonly body: unknown, public readonly status = 400) { - super(); - serialize(body).then(v => this.message = v.toString()); - } -} - -const undefinedBuff = new Blob([new TextEncoder().encode('undefined')]); -const nullBuff = new Blob([new TextEncoder().encode('null')]); - -export async function serialize(val: unknown, depth = 16): Promise { - while (true) { - if (depth <= 0) throw new Error("Call depth exceeded limit."); - if (val instanceof Promise) val = await val; - else if (val instanceof Function) { - if (val.length !== 0) throw new Error('Can\'t serialize an argument-accepting function'); - val = val(); - } - else break; - depth--; - } - - if (val === undefined) return undefinedBuff; - if (val === null) return nullBuff; - if (val instanceof Blob) return val; - - while (typeof val !== 'string' && val && val.toString !== Object.prototype.toString && val.toString instanceof Function) { - val = val.toString(); - } - - return new Blob([new TextEncoder().encode(JSON.stringify(val))]); -} diff --git a/src/server/decorators.ts b/src/server/decorators.ts new file mode 100644 index 0000000..4fd65ed --- /dev/null +++ b/src/server/decorators.ts @@ -0,0 +1,5 @@ +import { rest, route } from "./Router.ts"; +import body from "./decorators/body.ts"; +import schema from "./decorators/schema.ts"; + +export { body, schema, rest, route }; \ No newline at end of file diff --git a/src/server/decorators/auth.ts b/src/server/decorators/auth.ts new file mode 100644 index 0000000..71d610a --- /dev/null +++ b/src/server/decorators/auth.ts @@ -0,0 +1,15 @@ +import { makeParameterModifier } from "../Router.ts"; + +export type AuthType = 'raw' | 'jwt'; + +export function auth(type: AuthType = 'jwt') { + return makeParameterModifier(req => { + let res = req.headers.authorization; + if (typeof res !== 'string') return undefined; + if (res.startsWith('Bearer')) res = res.substring(6).trimStart(); + + if (type === 'jwt') throw new Error('JWT is not supported.'); + + return res; + }); +} \ No newline at end of file diff --git a/src/server/decorators/body.ts b/src/server/decorators/body.ts new file mode 100644 index 0000000..8de7f1d --- /dev/null +++ b/src/server/decorators/body.ts @@ -0,0 +1,20 @@ +import HttpError from "../HttpError.ts"; +import { makeParameterModifier } from "../Router.ts"; + +export type BodyType = 'raw' | 'json'; + +export default function body(type: BodyType = 'json') { + return makeParameterModifier(async req => { + let body = req.body; + if (type === 'json') { + try { + if (body instanceof Blob) body = await body.text(); + if (typeof body === 'string') body = JSON.parse(body.toString()); + } + catch (e) { + if (e instanceof SyntaxError) throw new HttpError('Body syntax error: ' + e.message); + } + } + return body; + }); +} \ No newline at end of file diff --git a/src/server/decorators/schema.ts b/src/server/decorators/schema.ts new file mode 100644 index 0000000..3e802bf --- /dev/null +++ b/src/server/decorators/schema.ts @@ -0,0 +1,61 @@ +// deno-lint-ignore-file no-explicit-any +import HttpError from "../HttpError.ts"; +import { makeParameterModifier } from "../Router.ts"; + +export type PrimitiveSchema = 'string' | 'number' | 'boolean' | 'object'; +export type OptionalSchema = `${PrimitiveSchema}?`; +export type Schema = 'any' | PrimitiveSchema | OptionalSchema | Schema[] | ({ $optional?: boolean; } & { [key: string]: Schema }); + +export default function schema(desc: Schema) { + function stringify(desc: Schema): string { + if (typeof desc === 'string') return desc; + if (desc instanceof Array) return desc.map(stringify).join(' | '); + + let res = '{ '; + + for (const key in desc) { + if (key === '$optional') continue; + if (res != '{ ') res += ', '; + res += key + ': '; + res += stringify(desc[key]); + } + res += '}'; + if (desc.$optional) res += '?'; + + return res; + } + function test(path: string[], val: unknown, desc: Schema) { + const _path = path.join('.'); + if (desc === 'any') return; + if (typeof desc === 'string') { + let type: string = desc; + const opt = desc.endsWith('?'); + + if (opt) type = type.substring(0, desc.length - 1); + if (opt && val === undefined) return; + + if (typeof val as string !== type) throw new HttpError(`${_path}: Expected a ${type}, got ${typeof val} instead.`); + } + else if (desc instanceof Array) { + for (const type of desc) { + try { + test(path, val, type); + return; + } + catch { /**/ } + } + throw new HttpError(`${_path}: Expected a ${stringify(desc)}, got ${typeof val} instead.`); + } + else { + if (desc.$optional && val === undefined) return; + if (typeof val !== 'object' || val === null) throw new HttpError(`${_path}: Expected an object, got ${typeof val} instead.`); + + for (const key in desc) { + if (key === '$optional') continue; + test([ ...path, key ], (val as any)[key], desc[key]); + } + } + } + + return makeParameterModifier((_req, val, name) => (test([name], val, desc), val)); +} diff --git a/src/server/serialize.ts b/src/server/serialize.ts new file mode 100644 index 0000000..c54ba02 --- /dev/null +++ b/src/server/serialize.ts @@ -0,0 +1,27 @@ +const undefinedBuff = new Blob([new TextEncoder().encode('undefined')]); +const nullBuff = new Blob([new TextEncoder().encode('null')]); + +export default async function serialize(val: unknown, depth = 16): Promise { + while(true) { + if(depth <= 0) throw new Error("Call depth exceeded limit."); + + if(val instanceof Promise) val = await val; + else if(val instanceof Function) { + if(val.length !== 0) throw new Error('Can\'t serialize an argument-accepting function'); + val = val(); + } + else break; + + depth--; + } + + if(val === undefined) return undefinedBuff; + if(val === null) return nullBuff; + if(val instanceof Blob) return val; + + while(typeof val !== 'string' && val && val.toString !== Object.prototype.toString && val.toString instanceof Function) { + val = val.toString(); + } + + return new Blob([new TextEncoder().encode(JSON.stringify(val))]); +} diff --git a/tsconfig.json b/tsconfig.json index 06e1407..0dd04af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,6 @@ { "compilerOptions": { - "moduleResolution": "node", - "outDir": "./dst", - "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "noImplicitAny": false - // "module": "ESNext" } }