Skip to main content
Module

x/fastro/core/server.ts

Fast and simple web application framework for deno
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
import { serve, ServerRequest, Server, decode, MultipartReader, FormFile, isFormFile, v4,} from "../deps.ts";import { version } from "../mod.ts";
export class Fastro { constructor(options?: ServerOptions) { if (options && options.payload) this.payloadEnabled = true; if (options && options.static) this.staticEnabled = true; if (options && options.logger) this.loggerEnabled = true; }
private server!: Server; private middlewareList: Middleware[] = []; private routerList: Router[] = []; private staticEnabled!: boolean; private loggerEnabled!: boolean; private payloadEnabled!: boolean; private staticFileList: Static[] = []; private pluginList: Instance[] = [];
public route(router: Router) { const [exist] = this.routerList.filter((r) => { return (r.url === router.url) && (r.method === router.method); }); if (!exist) this.routerList.push(router); return this; }
public get(url: string, handler: Handler) { return this.route({ method: "GET", url, handler }); }
public post(url: string, handler: Handler) { return this.route({ method: "POST", url, handler }); }
public head(url: string, handler: Handler) { return this.route({ method: "HEAD", url, handler }); }
public patch(url: string, handler: Handler) { return this.route({ method: "PATCH", url, handler }); }
public put(url: string, handler: Handler) { return this.route({ method: "PUT", url, handler }); }
public options(url: string, handler: Handler) { return this.route({ method: "OPTIONS", url, handler }); }
public delete(url: string, handler: Handler) { return this.route({ method: "DELETE", url, handler }); }
public async close(): Promise<void> { if (this.server) { this.server.close(); } }
public static(urlOrFolder: string, folder?: string, options?: StaticOptions) { if (urlOrFolder && !folder) { this.staticFileList.push({ url: "/", folder: urlOrFolder, options }); } if (urlOrFolder && folder) { this.staticFileList.push({ url: urlOrFolder, folder, options }); } return this; }
private inlinePluginHandler(fastro: Fastro) { try { const fplugin = this.pluginList.filter((p) => p.prefix); this.generatePluginId(fastro, fplugin); let finalMutateRoute: { router: Router; prefix: string }[] = []; this.pluginList.forEach((p) => { this.routerList.filter((r) => r.id === p.id).forEach((m) => { if (p && p.prefix) { finalMutateRoute.push({ router: m, prefix: p.prefix }); } }); });
const beforeConcat = finalMutateRoute.map((x) => { const url = x.router.url === "/" ? "" : x.router.url; x.router.url = `/${x.prefix}${url}`; return x.router; }); this.routerList = this.routerList.concat(beforeConcat); } catch (error) { throw fastroError("INLINE_PLUGIN_HANDLE_ERROR", error); } }
private generatePluginId(fastro: Fastro, plugins: Instance[]) { plugins.forEach((p) => p.plugin(fastro, () => { const id = v4.generate(); p.id = id; this.routerList .filter((r) => r.id === undefined) .forEach((r) => r.id = id); }) ); }
private loadPlugin(fastro: Fastro) { try { const fplugin = this.pluginList.filter((p) => !p.prefix); if (fplugin.length < 1) this.inlinePluginHandler(fastro); else this.generatePluginId(fastro, fplugin);
const filteredRoutes: { prefix: string; id: string }[] = []; const prefixPlugins = this.pluginList.filter((p) => p.prefix); if (prefixPlugins.length > 0) { prefixPlugins.forEach((pre) => { const [p] = this.pluginList.filter((p) => { return (p.plugin === pre.plugin) && p.id; }); if (pre.prefix && p && p.id) { filteredRoutes.push({ prefix: pre.prefix, id: p.id }); } }); }
let clonedRoutes: Router[] = deepCopyFunction(this.routerList); let finalMutateRoute: { router: Router; prefix: string }[] = [];
filteredRoutes.forEach((c) => { clonedRoutes.filter((r) => { return r.id === c.id; }).forEach((m) => { finalMutateRoute.push({ router: m, prefix: c.prefix }); }); });
const beforeConcat = finalMutateRoute.map((x) => { const url = x.router.url === "/" ? "" : x.router.url; x.router.url = `/${x.prefix}${url}`; return x.router; });
this.routerList = this.routerList.concat(beforeConcat); } catch (error) { throw fastroError("LOAD_PLUGIN_ERROR", error); } }
register(plugin: Plugin): Fastro; register(prefix: string, plugin: Plugin): Fastro; register(prefixOrPlugin: string | Plugin, plugin?: Plugin) { if (typeof prefixOrPlugin !== "string") { this.pluginList.push({ plugin: prefixOrPlugin }); } if (typeof prefixOrPlugin === "string" && plugin) { this.pluginList.push({ prefix: prefixOrPlugin, plugin }); } return this; }
public use(handler: Handler): Fastro; public use(url: string, handler: Handler): Fastro; public use(handlerOrUrl: string | Handler, handler?: Handler) { if (typeof handlerOrUrl !== "string") { this.middlewareList.push({ url: "/", handler: handlerOrUrl }); } if (handler && (typeof handlerOrUrl === "string")) { this.middlewareList.push({ url: handlerOrUrl, handler }); } return this; }
private send<T>( payload: string | T, status: number | undefined = 200, headers: Headers | undefined = new Headers(), req: ServerRequest, ) { try { // deno-lint-ignore no-explicit-any let body: any; if ( typeof payload === "string" || payload instanceof Uint8Array ) { body = payload; } else if (typeof payload === "undefined") body = "undefined"; else if (Array.isArray(payload)) body = JSON.stringify(payload); else if ( typeof payload === "number" || typeof payload === "boolean" || typeof payload === "bigint" || typeof payload === "function" || typeof payload === "symbol" ) { body = payload.toString(); } else body = JSON.stringify(payload);
headers.set( "x-powered-by", `Fastro/${version.fastro},Deno/${version.deno},V8/${version.v8},Typescript/${version.typescript}`, ); headers.set("Connection", "keep-alive"); headers.set("Date", new Date().toUTCString()); req.respond({ body, status, headers }); } catch (e) { throw fastroError("SEND_ERROR", e); } }
private async formUrlEncodedHandler(req: ServerRequest, contentType: string) { try { const bodyString = decode(await Deno.readAll(req.body)); const body = bodyString .replace(contentType, "") .split("&");
// deno-lint-ignore no-explicit-any const data: any[] = []; body.forEach((i) => { if (i.includes("=")) { const [key, value] = i.split("="); const decodedV = decodeURIComponent(value); const decodedK = decodeURIComponent(key); // deno-lint-ignore no-explicit-any const obj: any = {}; obj[decodedK] = decodedV; data.push(obj); } else { const obj = JSON.parse(i); data.push(obj); } }); if (data.length > 1) return data; if (data.length === 1) { const [d] = data; return d; } } catch (error) { throw fastroError("FORM_URL_ENCODED_HANDLER_ERROR", error); } }
private async multipartHandler(req: ServerRequest, contentType: string) { try { const boundaries = contentType?.match(/boundary=([^\s]+)/); let boundary; const arr: MultiPartData[] = []; if (boundaries && boundaries?.length > 0) [, boundary] = boundaries; if (boundary) { const reader = new MultipartReader(req.body, boundary); const form = await reader.readForm(1024 * 1024); const map = new Map(form.entries()); map.forEach((v, key) => { const content = form.file(key); if (isFormFile(content)) { const v = decode(content.content); arr.push({ key, value: v, filename: content.filename }); } else { arr.push({ key, value: v }); } }); await form.removeAll(); }
return arr; } catch (error) { throw fastroError("MULTIPART_HANDLER_ERROR", error); } }
private async getPayload(req: ServerRequest) { const contentType = req.headers.get("content-type"); if (contentType?.includes("multipart/form-data")) { return this.multipartHandler(req, contentType); } else if ( contentType?.includes("application/x-www-form-urlencoded") ) { return this.formUrlEncodedHandler(req, contentType); } else if ( contentType?.includes("application/json") ) { const payload = decode(await Deno.readAll(req.body)); return JSON.parse(payload); } else { const payload = decode(await Deno.readAll(req.body)); return payload; } }
private getParameter(incoming: string) { try { let incomingSplit = incoming.substr(1, incoming.length).split("/"); const params: string[] = []; incomingSplit .map((path, idx) => { return { path, idx }; }) .forEach((value) => params.push(incomingSplit[value.idx]));
return params; } catch (error) { if (this.loggerEnabled) throw fastroError("GET_PARAMETER_ERROR", error); } }
private checkUrl(incoming: string, registered: string) { if (incoming.length > 1 && (registered === "/")) return false; return incoming.includes(registered); }
private async routeHandler(req: Request) { try { const [router] = this.routerList.filter((router) => { return this.checkUrl(req.url, router.url) && (req.method === router.method); }); if (!router) return req.send("not found", 404); router.handler(req, () => {}); } catch (error) { if (this.loggerEnabled) throw fastroError("ROUTE_HANDLER_ERROR", error); } }
private checkExtension(url: string) { const split = url.split("/"); const extension = split[split.length - 1].includes("."); return extension; }
// deno-lint-ignore no-explicit-any private formatDirText(contentList: any[]) { let formattedText: string = ""; contentList.forEach((e) => { // deno-lint-ignore no-explicit-any const text: any = e.name; formattedText = formattedText.concat(text + "\n"); }); return formattedText; }
private readIdxFile(path: string) { try { let content; const [exist] = this.fileCache.filter((f) => f.path === path); if (!exist) { content = Deno.readFileSync(path); this.fileCache.push({ path, content }); } else { content = exist.content; } const decoder = new TextDecoder("utf-8"); return decoder.decode(content); } catch (error) { return undefined; } }
private async listFile(req: Request, f: Static) { const contentList = await readDir(f.folder); const contentText = this.formatDirText(contentList); req.send(contentText); }
private async defaultStaticRoot(req: Request, f: Static) { const [defaultFolder] = this.staticFileList.filter((staticFile) => { return (req.url === staticFile.url) && !this.checkExtension(req.url); }); if (!defaultFolder) return req.send("file not found", 404); if (f.options?.index) { const path = `${f.folder}/${f.options.index}`; const idxFile = this.readIdxFile(path); if (!idxFile) return this.listFile(req, f); return req.send(idxFile); }
this.listFile(req, f); }
private fileCache: StaticFileCache[] = [];
private getFileExtension(url: string) { const split = url.split("/"); const [, extension] = split[split.length - 1].split("."); return extension; }
private async readStaticFileHandler( req: Request, f: Static, ) { const filePath = req.url.replace(f.url, "").replace("/", ""); const finalPath = `${f.folder}/${filePath}`; try { const [exist] = this.fileCache.filter((cache) => { return cache.path === finalPath; }); let file; if (!exist) { file = await Deno.readFile(finalPath); this.fileCache.push({ path: finalPath, content: file }); } else { file = exist.content; } const header = new Headers(); const ext = this.getFileExtension(finalPath); if (ext === "svg") header.set("content-type", "image/svg+xml"); else if (ext === "png") header.set("content-type", "image/png"); else header.set("content-type", "text/plain; charset=utf-8"); req.send(file, 200, header); } catch (e) { this.defaultStaticRoot(req, f); } }
// deno-lint-ignore no-explicit-any private staticContainer = new Map<string, any>();
private addStaticRoute(url: string, staticFile: Static) { this.staticContainer.set(url, staticFile); return this.get(url, (req) => { const [router] = this.routerList.filter((r) => r.url === req.url); const staticFile = this.staticContainer.get(router.url); this.readStaticFileHandler(req, staticFile); }); }
private async staticHandler(request: Request) { const [rootStaticFile] = this.staticFileList.filter((staticFile) => { return request.url === staticFile.url; });
if (rootStaticFile) return this.addStaticRoute(request.url, rootStaticFile);
const [staticFile] = this.staticFileList.filter((staticFile) => { if (staticFile.url === "/") return false; const staticLength = staticFile.url.split("/").length; const requestLength = request.url.split("/").length; const diff = requestLength - staticLength; return request.url.includes(staticFile.url) && diff < 2; }); if (staticFile) return this.addStaticRoute(request.url, staticFile); const [idx] = this.staticFileList.filter((f) => f.url === "/"); this.addStaticRoute(request.url, idx); }
private nonRootMidHandler(req: Request) { try { const midddlewares = this.middlewareList.filter((m) => { if (m.url === "/") return false; return m.url && this.checkUrl(req.url, m.url); });
if (midddlewares.length < 1) return this.routeHandler(req); else { midddlewares.forEach((m) => { m.handler(req, () => { this.routeHandler(req); }); }); } } catch (error) { if (this.loggerEnabled) { throw fastroError("NON_ROOT_MIDDLEWARE_HANDLER_ERROR", error); } } }
private async middlewareHandler(req: Request) { try { const rootMiddleware = this.middlewareList.filter((m) => m.url === "/"); if (rootMiddleware.length > 0) { rootMiddleware.forEach((m) => { m.handler(req, () => { this.nonRootMidHandler(req); }); }); } else this.nonRootMidHandler(req); } catch (error) { if (this.loggerEnabled) { throw fastroError("MIDDLEWARE_HANDLER_ERROR", error); } } }
private async requestHandler(request: ServerRequest) { try { const req = request as Request; if (this.payloadEnabled) { const p = await this.getPayload(request); req.payload = p; } if (this.loggerEnabled) { const msg = [ new Date(), req.proto, req.method, req.payload, `${req.headers.get("host")}${req.url}`, `${req.headers.get("user-agent")}`, ]; console.info(JSON.stringify(msg)); } req.parameter = this.getParameter(req.url) ?? []; req.send = (payload, status, headers) => { return this.send(payload, status, headers, req); }; if (this.staticEnabled && this.staticFileList.length > 0) { this.staticHandler(req); } if (this.middlewareList.length > 0) return this.middlewareHandler(req); this.routeHandler(req); } catch (error) { if (this.loggerEnabled) throw fastroError("REQUEST_HANDLER_ERROR", error); } }
public async listen( options?: ListenOptions, callback?: (error: Error | undefined, address: string | undefined) => void, ) { try { let opt = options ? options : { port: 3000 }; this.server = serve(opt); this.loadPlugin(this); if (!callback) console.info("Server listen on port", opt.port); // deno-lint-ignore no-explicit-any else callback(undefined, opt as any); for await (const req of this.server) { await this.requestHandler(req); } } catch (error) { throw fastroError("LISTEN_ERROR", error); } }}
export class Request extends ServerRequest { parameter!: string[]; // deno-lint-ignore no-explicit-any payload!: any | any[] | undefined; send!: { <T>(payload: string | T, status?: number, headers?: Headers): void; }; // deno-lint-ignore no-explicit-any [key: string]: any}
export function fastroError(name: string, error: Error) { const msg = [new Date(), name, error.message, error.stack]; console.error(JSON.stringify(msg));}
export interface ServerOptions { payload?: boolean; static?: boolean; logger?: boolean;}
export interface ListenOptions { port: number; hostname?: string;}
interface Router { method: string; url: string; id?: string; handler(req: Request, callback: Function): void; // deno-lint-ignore no-explicit-any [key: string]: any;}
interface Middleware { url?: string; handler(req: Request, callback: Function): void;}
interface Handler { (req: Request, callback: Function): void;}
type StaticOptions = { index: string;};
interface Static { url: string; folder: string; options?: StaticOptions;}
interface StaticFileCache { path: string; // deno-lint-ignore no-explicit-any content: any;}
interface Plugin { (fastro: Fastro, callback: Function): void;}interface Instance { plugin: Plugin; prefix?: string; id?: string;}
type MultiPartData = { key: string; value: string | FormFile | FormFile[] | undefined; filename?: string;};
type UrlEncData = { key: string; value: string;};
async function readDir(target: string) { type entry = { name: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; }; let files: entry[] = []; const results = Deno.readDir(target); for await (const dirEntry of results) { let file = target + "/" + dirEntry.name; files.push(dirEntry); } return files;}
// deno-lint-ignore no-explicit-anyconst deepCopyFunction = (inObject: any) => { // deno-lint-ignore no-explicit-any let outObject: any, value, key; if (typeof inObject !== "object" || inObject === null) { return inObject; } outObject = Array.isArray(inObject) ? [] : {}; for (key in inObject) { value = inObject[key]; outObject[key] = deepCopyFunction(value); } return outObject;};