restructuring
This commit is contained in:
parent
b3c301e230
commit
c552854ff0
21
package.json
21
package.json
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
@ -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> {
|
||||||
|
@ -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
8
src/server/HttpError.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
@ -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;
|
@ -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);
|
||||||
|
@ -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
5
src/server/decorators.ts
Normal 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 };
|
15
src/server/decorators/auth.ts
Normal file
15
src/server/decorators/auth.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
20
src/server/decorators/body.ts
Normal file
20
src/server/decorators/body.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
61
src/server/decorators/schema.ts
Normal file
61
src/server/decorators/schema.ts
Normal 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
27
src/server/serialize.ts
Normal 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))]);
|
||||||
|
}
|
@ -1,11 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "./dst",
|
|
||||||
"sourceMap": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"noImplicitAny": false
|
|
||||||
// "module": "ESNext"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user