restructuring

This commit is contained in:
TopchetoEU 2023-06-26 17:06:38 +03:00
parent b3c301e230
commit c552854ff0
No known key found for this signature in database
GPG Key ID: 24E57B2E9C61AD19
15 changed files with 151 additions and 160 deletions

View File

@ -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"
}
}

View File

@ -1,8 +1,8 @@
import { Collection, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; import { Collection, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
import UserRouter from "./routers/UserRouter.ts"; import UserRouter from "./routers/UserRouter.ts";
import { Router, rest, route } from "./server/Server.ts";
import User from "./models/User.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 { class RootRouter extends Router {
@route('users/*') users: UserRouter; @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' })); await new RootRouter(salt, db.collection('users')).attach(Deno.listen({ port: 4000, hostname: 'localhost' }));
} }
clonegur(); await clonegur();

View File

@ -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 { export default class AppRouter extends Router {
public onError(_req: RestRequest, error: unknown): RestResponse | HttpError | Promise<RestResponse | HttpError> { public onError(_req: RestRequest, error: unknown): RestResponse | HttpError | Promise<RestResponse | HttpError> {

View File

@ -1,7 +1,8 @@
import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; 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 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 AppRouter from "./AppRouter.ts";
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";

8
src/server/HttpError.ts Normal file
View File

@ -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());
}
}

View File

@ -14,8 +14,6 @@ function sanitizeUrl(url: string, forceAbsolute = true) {
} }
} }
export const FetchRequest = Request;
export default class RestRequest { export default class RestRequest {
public readonly body: unknown; public readonly body: unknown;
public readonly method: string; public readonly method: string;

View File

@ -1,6 +1,4 @@
import { Headers } from "./Request.ts"; import { Headers } from "./RestRequest.ts";
export const FetchResponse = Response;
export default class RestResponse { export default class RestResponse {
#status = 200; #status = 200;

View File

@ -1,16 +1,13 @@
// deno-lint-ignore-file no-explicit-any ban-types // deno-lint-ignore-file no-explicit-any ban-types
import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts"; 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> | RestResponse | undefined; export type HandlerRes = Promise<RestResponse | undefined> | RestResponse | undefined;
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE'; 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 { export interface Handler {
handle(req: RestRequest): HandlerRes; 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)[]) { function addMetaQuery(target: any, ...handlers: ((r: Router) => RouterHandler)[]) {
let props = Reflect.getOwnMetadata('router:queries', target); let props = Reflect.getOwnMetadata('router:queries', target);

View File

@ -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<Blob> {
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))]);
}

5
src/server/decorators.ts Normal file
View File

@ -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 };

View File

@ -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;
});
}

View File

@ -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;
});
}

View File

@ -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));
}

27
src/server/serialize.ts Normal file
View File

@ -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<Blob> {
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))]);
}

View File

@ -1,11 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node",
"outDir": "./dst",
"sourceMap": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"noImplicitAny": false
// "module": "ESNext"
} }
} }