diff --git a/dist/ctx.js b/dist/ctx.js index 2d016cd..960dbb1 100644 --- a/dist/ctx.js +++ b/dist/ctx.js @@ -1 +1 @@ -function x(F,L,G){let z=new Headers,M={},U=!1,I,Y=null,_,Z,$={};return{req:F,server:L,url:G,getUser(){return $},setUser(w){if(w)$=w},getIP(){return this.server.requestIP(this.req)},async getBody(){if(!Z)Z=await R(F);if(Z.error)return new Response(JSON.stringify({error:Z.error}),{status:400});return Z},setHeader(w,E){return z.set(w,E),this},set(w,E){if(typeof w!=="string")throw new Error("Key must be string type!");if(!E)throw new Error("value paramter is missing pls pass value after key");return M[w]=E,this},get(w){return w?M[w]:null},setAuth(w){return U=w,this},getAuth(){return U},text(w,E){return new Response(w,{status:E,headers:z})},json(w,E){return new Response(JSON.stringify(w),{status:E,headers:z})},file(w,E){return new Response(Bun.file(w),{status:E,headers:z})},redirect(w,E){return z.set("Location",w),new Response(null,{status:E??302,headers:z})},setCookie(w,E,J={}){let X=`${encodeURIComponent(w)}=${encodeURIComponent(E)}`;if(J.maxAge)X+=`; Max-Age=${J.maxAge}`;if(J.expires)X+=`; Expires=${J.expires.toUTCString()}`;if(J.path)X+=`; Path=${J.path}`;if(J.domain)X+=`; Domain=${J.domain}`;if(J.secure)X+="; Secure";if(J.httpOnly)X+="; HttpOnly";if(J.sameSite)X+=`; SameSite=${J.sameSite}`;return z?.append("Set-Cookie",X),this},getParams(w){if(!_&&F?.routePattern)_=O(F?.routePattern,G?.pathname);if(w)return _[w]??{};return w},getQuery(w){try{if(!I)I=Object.fromEntries(G.searchParams);return w?I[w]||{}:I}catch(E){return{}}},getCookie(w){if(!Y){let E=F.headers.get("cookie");if(E)Y=K(E);else return null}if(!Y)return null;if(w)return Y[w]??null;else return Y}}}function K(F){let L={},G=F?.split(";");for(let z=0;z{return this.FilterRoutes=G,this.filter()},permitAll:()=>{for(let G of this?.FilterRoutes)this.filters.add(G);return this.FilterRoutes=null,this.filter()},require:(G)=>{if(G)this.filterFunction=G}}}cors(G){return this.corsConfig=G,this}addHooks(G,z){if(typeof G!=="string")throw new Error("hookName must be a string");if(typeof z!=="function")throw new Error("callback must be a instance of function");switch(G){case"onRequest":this.hooks.onRequest=z,this.hasOnReqHook=!0;break;case"preHandler":this.hooks.preHandler=z,this.hasPreHandlerHook=!0;break;case"postHandler":this.hooks.postHandler=z,this.hasPostHandlerHook=!0;break;case"onSend":this.hooks.onSend=z,this.hasOnSendHook=!0;break;case"onError":this.hooks.onError=z,this.hasOnError=!0;break;case"onClose":this.hooks.onClose=z;break;default:throw new Error(`Unknown hook type: ${G}`)}return this}compile(){if(this.globalMiddlewares.length>0)this.hasMiddleware=!0;for(let[G,z]of this.middlewares.entries())if(z.length>0){this.hasMiddleware=!0;break}if(this.hooks.onRequest)this.hasOnReqHook=!0;if(this.hooks.preHandler)this.hasPreHandlerHook=!0;if(this.hooks.postHandler)this.hasPostHandlerHook=!0;if(this.hooks.onSend)this.hasOnSendHook=!0;if(this.hooks.onError)this.hasOnError=!0;this.tempRoutes=new Map}listen(G=3000,...z){if(typeof Bun==="undefined")throw new Error(".listen() is designed to run on Bun only...");if(!G||typeof G!=="number")throw new Error("port is required and should be a number type");let L="0.0.0.0",J=void 0,U={};for(let Y of z)if(typeof Y==="string")L=Y;else if(typeof Y==="function")J=Y;else if(typeof Y==="object"&&Y!==null)U=Y;let X={port:G,hostname:L,fetch:async(Y,Z)=>{let $=new URL(Y.url);try{return await B(Y,Z,$,this)}catch(F){if(this.hasOnError&&this.hooks.onError){let W=await this.hooks.onError(F,Y,$,Z);if(W)return W}return new Response(JSON.stringify({message:"Internal Server Error",error:F.message,staus:500}),{status:500})}}};if(U.sslCert&&U.sslKey)X.certFile=U.sslCert,X.keyFile=U.sslKey;if(this.compile(),this.serverInstance=Bun?.serve(X),J)return J();if(U.sslCert&&U.sslKey)console.log(`HTTPS server is running on https://localhost:${G}`);else console.log(`HTTP server is running on http://localhost:${G}`);return this.serverInstance}close(){if(this.serverInstance)this.serverInstance.stop(!0),this.serverInstance=null,console.log("Server has been stopped.");else console.warn("Server is not running.")}route(G,z){if(!G||typeof G!=="string")throw new Error("Path must be a string");let L=Object.fromEntries(z.tempRoutes);return Object.entries(L).forEach(([U,X])=>{let Y=`${G}${U}`;if(!this.middlewares.has(Y))this.middlewares.set(Y,[]);X.handlers.slice(0,-1).forEach((W)=>{if(!this.middlewares.get(Y)?.includes(W))this.middlewares.get(Y)?.push(W)});let $=X.handlers[X.handlers.length-1],F=X.method;try{this.trie.insert(Y,{handler:$,method:F})}catch(W){console.error(`Error inserting ${Y}:`,W)}}),z=null,this}register(G,z){return this.route(G,z)}addRoute(G,z,L){if(typeof z!=="string")throw new Error(`Error in ${L[L.length-1]}: Path must be a string. Received: ${typeof z}`);if(typeof G!=="string")throw new Error(`Error in addRoute: Method must be a string. Received: ${typeof G}`);this.tempRoutes.set(z,{method:G,handlers:L});let J=L.slice(0,-1),U=L[L.length-1];if(!this.middlewares.has(z))this.middlewares.set(z,[]);J.forEach((X)=>{if(z==="/")this.globalMiddlewares=[...new Set([...this.globalMiddlewares,...J])];else if(!this.middlewares.get(z)?.includes(X))this.middlewares.get(z)?.push(X)});try{this.trie.insert(z,{handler:U,method:G})}catch(X){console.error(`Error inserting ${z}:`,X)}}use(G,z){if(Array.isArray(G))G.forEach((J)=>{if(typeof J==="function")this.globalMiddlewares.push(J)});if(typeof G==="function"){if(this.globalMiddlewares.push(G),Array.isArray(z))z.forEach((J)=>{this.globalMiddlewares.push(J)});return}return(Array.isArray(G)?G.filter((J)=>typeof J==="string"):[G].filter((J)=>typeof J==="string")).forEach((J)=>{if(!this.middlewares.has(J))this.middlewares.set(J,[]);if(z)(Array.isArray(z)?z:[z]).forEach((X)=>{this.middlewares.get(J)?.push(X)})}),this}get(G,...z){return this.addRoute("GET",G,z),this}post(G,...z){return this.addRoute("POST",G,z),this}put(G,...z){return this.addRoute("PUT",G,z),this}patch(G,...z){return this.addRoute("PATCH",G,z),this}delete(G,...z){return this.addRoute("DELETE",G,z),this}}export{j as default}; +class Q{children;isEndOfWord;handler;isDynamic;pattern;path;method;subMiddlewares;constructor(){this.children={},this.isEndOfWord=!1,this.handler=[],this.isDynamic=!1,this.pattern="",this.path="",this.method=[],this.subMiddlewares=new Map}}class D{root;constructor(){this.root=new Q}insert(J,G){let L=this.root,z=J.split("/").filter(Boolean);if(J==="/"){L.isEndOfWord=!0,L.handler.push(G.handler),L.path=J,L.method.push(G.method);return}for(let X of z){let Y=!1,U=X;if(X.startsWith(":"))Y=!0,U=":";if(!L.children[U])L.children[U]=new Q;L=L.children[U],L.isDynamic=Y,L.pattern=X,L.method.push(G.method),L.handler.push(G.handler),L.path=J}L.isEndOfWord=!0,L.method.push(G.method),L.handler.push(G.handler),L.path=J}search(J,G){let L=this.root,z=J.split("/").filter(Boolean),X=z.length;for(let _ of z){let $=_;if(!L.children[$])if(L.children[":"])L=L.children[":"];else return null;else L=L.children[$]}let Y=L.path.split("/").filter(Boolean);if(X!==Y.length)return null;let U=L.method.indexOf(G);if(U!==-1)return{path:L.path,handler:L.handler[U],isDynamic:L.isDynamic,pattern:L.pattern,method:L.method[U]};return{path:L.path,handler:L.handler,isDynamic:L.isDynamic,pattern:L.pattern,method:L.method[U]}}}function V(J,G,L){let z=new Headers({"X-Powered-By":"DieselJS","Cache-Control":"no-cache"});z.set("X-Powered-By","DieselJS");let X={},Y=!1,U,_=null,$,K,W={};return{req:J,server:G,url:L,setHeader(Z,F){return z.set(Z,F),this},getUser(){return W},setUser(Z){if(Z)W=Z},getIP(){return this.server.requestIP(this.req)},async getBody(){if(!K)K=await N(J);if(K.error)return new Response(JSON.stringify({error:K.error}),{status:400});return K},set(Z,F){if(typeof Z!=="string")throw new Error("Key must be string type!");if(!F)throw new Error("value paramter is missing pls pass value after key");return X[Z]=F,this},get(Z){return Z?X[Z]:null},setAuth(Z){return Y=Z,this},getAuth(){return Y},text(Z,F){if(!z.has("Content-Type"))z.set("Content-Type","text/plain; charset=utf-8");return new Response(Z,{status:F,headers:z})},send(Z,F){if(typeof Z==="string"){if(!z.has("Content-Type"))z.set("Content-Type","text/plain; charset=utf-8")}else if(typeof Z==="object"){if(!z.has("Content-Type"))z.set("Content-Type","application/json; charset=utf-8");Z=JSON.stringify(Z)}else if(Z instanceof Uint8Array||Z instanceof ArrayBuffer){if(!z.has("Content-Type"))z.set("Content-Type","application/octet-stream")}return new Response(Z,{status:F,headers:z})},json(Z,F){if(!z.has("Content-Type"))z.set("Content-Type","application/json; charset=utf-8");return new Response(JSON.stringify(Z),{status:F,headers:z})},file(Z,F){let E=Bun.file(Z).type||"application/octet-stream";if(!z.has("Content-Type"))z.set("Content-Type",E);return new Response(Bun.file(Z),{status:F,headers:z})},redirect(Z,F){return z.set("Location",Z),new Response(null,{status:F??302,headers:z})},setCookie(Z,F,E={}){let A=`${encodeURIComponent(Z)}=${encodeURIComponent(F)}`;if(E.maxAge)A+=`; Max-Age=${E.maxAge}`;if(E.expires)A+=`; Expires=${E.expires.toUTCString()}`;if(E.path)A+=`; Path=${E.path}`;if(E.domain)A+=`; Domain=${E.domain}`;if(E.secure)A+="; Secure";if(E.httpOnly)A+="; HttpOnly";if(E.sameSite)A+=`; SameSite=${E.sameSite}`;return z?.append("Set-Cookie",A),this},getParams(Z){if(!$&&J?.routePattern)$=M(J?.routePattern,L?.pathname);if(Z)if($)return $[Z];else return;if($)return Z;else return},getQuery(Z){try{if(!U)U=Object.fromEntries(L.searchParams);if(Z)return U[Z]??void 0;return U}catch(F){return}},getCookie(Z){if(!_){let F=J.headers.get("cookie");if(F)_=I(F);else return}if(!_)return;if(Z)return _[Z]??void 0;else return _}}}function I(J){let G={},L=J?.split(";");for(let z=0;z{return this.FilterRoutes=J,this.filter()},permitAll:()=>{for(let J of this?.FilterRoutes)this.filters.add(J);return this.FilterRoutes=null,this.filter()},require:(J)=>{if(J)this.filterFunction=J}}}cors(J){return this.corsConfig=J,this}addHooks(J,G){if(typeof J!=="string")throw new Error("hookName must be a string");if(typeof G!=="function")throw new Error("callback must be a instance of function");switch(J){case"onRequest":this.hooks.onRequest=G,this.hasOnReqHook=!0;break;case"preHandler":this.hooks.preHandler=G,this.hasPreHandlerHook=!0;break;case"postHandler":this.hooks.postHandler=G,this.hasPostHandlerHook=!0;break;case"onSend":this.hooks.onSend=G,this.hasOnSendHook=!0;break;case"onError":this.hooks.onError=G,this.hasOnError=!0;break;case"onClose":this.hooks.onClose=G;break;default:throw new Error(`Unknown hook type: ${J}`)}return this}compile(){if(this.globalMiddlewares.length>0)this.hasMiddleware=!0;for(let[J,G]of this.middlewares.entries())if(G.length>0){this.hasMiddleware=!0;break}if(this.hooks.onRequest)this.hasOnReqHook=!0;if(this.hooks.preHandler)this.hasPreHandlerHook=!0;if(this.hooks.postHandler)this.hasPostHandlerHook=!0;if(this.hooks.onSend)this.hasOnSendHook=!0;if(this.hooks.onError)this.hasOnError=!0;this.tempRoutes=new Map}listen(J=3000,...G){if(typeof Bun==="undefined")throw new Error(".listen() is designed to run on Bun only...");if(!J||typeof J!=="number")throw new Error("port is required and should be a number type");let L="0.0.0.0",z=void 0,X={};for(let U of G)if(typeof U==="string")L=U;else if(typeof U==="function")z=U;else if(typeof U==="object"&&U!==null)X=U;let Y={port:J,hostname:L,fetch:async(U,_)=>{let $=new URL(U.url);try{return await B(U,_,$,this)}catch(K){if(this.hasOnError&&this.hooks.onError){let W=await this.hooks.onError(K,U,$,_);if(W)return W}return new Response(JSON.stringify({message:"Internal Server Error",error:K.message,staus:500}),{status:500})}}};if(X.sslCert&&X.sslKey)Y.certFile=X.sslCert,Y.keyFile=X.sslKey;if(this.compile(),this.serverInstance=Bun?.serve(Y),z)return z();if(X.sslCert&&X.sslKey)console.log(`HTTPS server is running on https://localhost:${J}`);else console.log(`HTTP server is running on http://localhost:${J}`);return this.serverInstance}close(){if(this.serverInstance)this.serverInstance.stop(!0),this.serverInstance=null,console.log("Server has been stopped.");else console.warn("Server is not running.")}route(J,G){if(!J||typeof J!=="string")throw new Error("Path must be a string");let L=Object.fromEntries(G.tempRoutes);return Object.entries(L).forEach(([X,Y])=>{let U=`${J}${X}`;if(!this.middlewares.has(U))this.middlewares.set(U,[]);Y.handlers.slice(0,-1).forEach((W)=>{if(!this.middlewares.get(U)?.includes(W))this.middlewares.get(U)?.push(W)});let $=Y.handlers[Y.handlers.length-1],K=Y.method;try{this.trie.insert(U,{handler:$,method:K})}catch(W){console.error(`Error inserting ${U}:`,W)}}),G=null,this}register(J,G){return this.route(J,G)}addRoute(J,G,L){if(typeof G!=="string")throw new Error(`Error in ${L[L.length-1]}: Path must be a string. Received: ${typeof G}`);if(typeof J!=="string")throw new Error(`Error in addRoute: Method must be a string. Received: ${typeof J}`);this.tempRoutes.set(G,{method:J,handlers:L});let z=L.slice(0,-1),X=L[L.length-1];if(!this.middlewares.has(G))this.middlewares.set(G,[]);z.forEach((Y)=>{if(G==="/")this.globalMiddlewares=[...new Set([...this.globalMiddlewares,...z])];else if(!this.middlewares.get(G)?.includes(Y))this.middlewares.get(G)?.push(Y)});try{this.trie.insert(G,{handler:X,method:J})}catch(Y){console.error(`Error inserting ${G}:`,Y)}}use(J,G){if(Array.isArray(J))J.forEach((z)=>{if(typeof z==="function")this.globalMiddlewares.push(z)});if(typeof J==="function"){if(this.globalMiddlewares.push(J),Array.isArray(G))G.forEach((z)=>{this.globalMiddlewares.push(z)});return}return(Array.isArray(J)?J.filter((z)=>typeof z==="string"):[J].filter((z)=>typeof z==="string")).forEach((z)=>{if(!this.middlewares.has(z))this.middlewares.set(z,[]);if(G)(Array.isArray(G)?G:[G]).forEach((Y)=>{this.middlewares.get(z)?.push(Y)})}),this}get(J,...G){return this.addRoute("GET",J,G),this}post(J,...G){return this.addRoute("POST",J,G),this}put(J,...G){return this.addRoute("PUT",J,G),this}patch(J,...G){return this.addRoute("PATCH",J,G),this}delete(J,...G){return this.addRoute("DELETE",J,G),this}options(J,...G){return this.addRoute("OPTIONS",J,G),this}}export{j as default}; diff --git a/dist/trie.d.ts b/dist/trie.d.ts index 7325d69..fd26e19 100644 --- a/dist/trie.d.ts +++ b/dist/trie.d.ts @@ -20,6 +20,12 @@ export default class Trie { isDynamic: boolean; pattern: string; method: string; + } | { + path: string; + handler: handlerFunction[]; + isDynamic: boolean; + pattern: string; + method: string; } | null; } export {}; diff --git a/dist/trie.js b/dist/trie.js index 635eb81..fda4fa8 100644 --- a/dist/trie.js +++ b/dist/trie.js @@ -1 +1 @@ -class E{children;isEndOfWord;handler;isDynamic;pattern;path;method;subMiddlewares;constructor(){this.children={},this.isEndOfWord=!1,this.handler=[],this.isDynamic=!1,this.pattern="",this.path="",this.method=[],this.subMiddlewares=new Map}}class F{root;constructor(){this.root=new E}insert(q,w){let b=this.root,z=q.split("/").filter(Boolean);if(q==="/"){b.isEndOfWord=!0,b.handler=[w.handler],b.path=q,b.method=[w.method];return}for(let j=0;j boolean; json: (data: Object, status?: number) => Response; text: (data: string, status?: number) => Response; + send: (data: string, status?: number) => Response; file: (filePath: string, status?: number) => Response; redirect: (path: string, status?: number) => Response; getParams: (props?: any) => any; diff --git a/example/main.ts b/example/main.ts index cd89925..e7730be 100644 --- a/example/main.ts +++ b/example/main.ts @@ -8,12 +8,12 @@ const app = new Diesel(); const SECRET_KEY = "linux"; const port = 3000 -app.cors({ - origin: "http://localhost:3000", - methods: ["GET", "POST", "PUT", "DELETE"], - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, -}); +// app.cors({ +// origin: "http://localhost:3000", +// methods: ["GET", "POST", "PUT", "DELETE"], +// allowedHeaders: ["Content-Type", "Authorization"], +// credentials: true, +// }); // Authentication Middleware export async function authJwt(ctx: ContextType): Promise { @@ -40,7 +40,7 @@ const h2 = () => { // .filter() // .routeMatcher("/cookie") // .permitAll() -// .require(authJwt as middlewareFunc) +// .require() // Error Handling Hook app.addHooks("onError", (error: any, req: Request, url: URL) => { @@ -53,17 +53,30 @@ app.addHooks("onError", (error: any, req: Request, url: URL) => { // app.use(["/home","/user"],[h1,h2]) // app.use([h1,h2]) // app.use(h1) -app.use(h1,[h2,h1]) +// app.use(h1,[h2,h1]) // Routes + +// app.use(authJwt) app - .get("/",async (ctx) => { - // const user = ctx.getUser(); - return ctx.json({msg:"Hello world"}) - }) + // .get("/",async (ctx) => { + // return ctx.json({msg:"where is john?"}) + // }) // .get("/:id",async (ctx) => { // const id= ctx.getParams("id") // return ctx.json({id}) // }) + // .get("/api/param/:id/:username",(ctx) =>{ + // const id = ctx.getParams("id") + // const username = ctx.getParams("username") + // return ctx.json({id,username}) + // }) + app + .get("/post",(ctx) =>{ + return ctx.json({msg:"get"}) + }) + .post("/post",(ctx) =>{ + return ctx.json({msg:"post"}) + }) // .get("/:name?",(ctx:ContextType) =>{ // return ctx.text("hello world") // }) @@ -74,13 +87,15 @@ app // return ctx.redirect("/") // }) // .get("/test/:id/:name", async (ctx) => { - // const query = ctx.getQuery(); - // const params = ctx.getParams(); - // return ctx.json({ msg: "Hello World", query, params }); - // }) - // .get("/ok", (ctx:ContextType) => { - // return ctx.text("How are you?"); + // // const query = ctx.getQuery(); + // // const params = ctx.getParams(); + // return ctx.json({ msg: "Hello World" }); // }) + .get("/test/:id/:name", (ctx:ContextType) => { + const id = ctx.getParams("id") + const name = ctx.getParams("name") + return ctx.text("How are you?"+id+name); + }) // .get("/cookie", async (ctx) => { // const user = { name: "pk", age: 22 }; @@ -109,4 +124,4 @@ app // Start the Server app.listen(port) -app.close() \ No newline at end of file +// app.close() \ No newline at end of file diff --git a/src/ctx.ts b/src/ctx.ts index 18a7427..5a26fd0 100644 --- a/src/ctx.ts +++ b/src/ctx.ts @@ -4,12 +4,16 @@ import { Server } from "bun"; import type { ContextType, CookieOptions, ParseBodyResult } from "./types"; export default function createCtx( req: Request, server: Server, url: URL): ContextType { - let headers: Headers = new Headers(); + const headers: Headers = new Headers({ + "X-Powered-By": "DieselJS", // Branding header + "Cache-Control": "no-cache", // Prevent caching for dynamic responses + }); + headers.set("X-Powered-By", "DieselJS") let settedValue: Record = {}; let isAuthenticated: boolean = false; let parsedQuery: any; let parsedCookie: any = null; - let parsedParams: any; + let parsedParams: any ; let parsedBody: ParseBodyResult | null; // let responseStatus: number = 200; let user: any = {}; @@ -18,8 +22,12 @@ export default function createCtx( req: Request, server: Server, url: URL): Cont req, server, url, - - // + + setHeader(key: string, value: any): ContextType { + headers.set(key, value); + return this; + }, + getUser() { return user; }, @@ -51,11 +59,6 @@ export default function createCtx( req: Request, server: Server, url: URL): Cont return parsedBody; }, - setHeader(key: string, value: any): ContextType { - headers.set(key, value); - return this; - }, - set(key: string, value: any): ContextType { if (typeof key !== "string") throw new Error("Key must be string type!"); if (!value) @@ -79,23 +82,43 @@ export default function createCtx( req: Request, server: Server, url: URL): Cont // Response methods with optional status text(data: string, status?: number) { + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "text/plain; charset=utf-8") + } return new Response(data, { status, - headers, + headers }); }, - // send(data: string, status?: number) { - // return new Response(data, { - // status: status ?? responseStatus, - // headers, - // }); - // }, + send(data: any, status?: number): Response { + if (typeof data === "string") { + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "text/plain; charset=utf-8"); + } + } else if (typeof data === "object") { + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json; charset=utf-8"); + } + data = JSON.stringify(data); + } else if (data instanceof Uint8Array || data instanceof ArrayBuffer) { + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/octet-stream"); + } + } + return new Response(data, { + status, + headers, + }); + }, json(data: any, status?: number): Response { + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json; charset=utf-8"); + } return new Response(JSON.stringify(data), { status, - headers, + headers }); }, @@ -107,6 +130,10 @@ export default function createCtx( req: Request, server: Server, url: URL): Cont // }, file(filePath: string, status?: number): Response { + const mimeType = Bun.file(filePath).type || "application/octet-stream"; + if (!headers.has("Content-Type")) { + headers.set("Content-Type", mimeType); + } return new Response(Bun.file(filePath), { status, headers, @@ -145,38 +172,49 @@ export default function createCtx( req: Request, server: Server, url: URL): Cont return this; }, - getParams(props: string): string | Record | {} { + getParams(props: string): string | Record | undefined { if (!parsedParams && req?.routePattern) { parsedParams = extractDynamicParams(req?.routePattern, url?.pathname); } if (props) { - return parsedParams[props] ?? {}; + if (parsedParams) { + return parsedParams[props] + }else{ + return undefined; + } + } + if (parsedParams) { + return props + } else{ + return undefined; } - return props; }, - getQuery(props?: any): string | Record | {} { + getQuery(props?: any): string | Record | undefined { try { if (!parsedQuery) { parsedQuery = Object.fromEntries(url.searchParams); } - return props ? parsedQuery[props] || {} : parsedQuery; + if (props) { + return parsedQuery[props] ?? undefined; + } + return parsedQuery; } catch (error) { - return {}; + return undefined; } }, - getCookie(cookieName?: string) { + getCookie(cookieName?: string): string | null | undefined { if (!parsedCookie) { const cookieHeader = req.headers.get("cookie"); if (cookieHeader) { parsedCookie = parseCookie(cookieHeader); - } else return null; + } else return undefined; } - if (!parsedCookie) return null; + if (!parsedCookie) return undefined; if (cookieName) { - return parsedCookie[cookieName] ?? null; + return parsedCookie[cookieName] ?? undefined; } else { return parsedCookie; } diff --git a/src/handleRequest.ts b/src/handleRequest.ts index 916199a..8d8c34f 100644 --- a/src/handleRequest.ts +++ b/src/handleRequest.ts @@ -3,13 +3,10 @@ import createCtx from "./ctx"; import type { ContextType, corsT, DieselT, RouteHandlerT } from "./types"; export default async function handleRequest(req: Request, server: Server, url: URL, diesel: DieselT): Promise { - // Try to find the route handler in the trie const routeHandler: RouteHandlerT | undefined = diesel.trie.search(url.pathname, req.method); - // If the route is dynamic, we only set routePattern if necessary if (routeHandler?.isDynamic) req.routePattern = routeHandler.path; - // create the context which contains the methods Req,Res, many more const ctx: ContextType = createCtx(req, server, url); @@ -34,7 +31,11 @@ export default async function handleRequest(req: Request, server: Server, url: U const filterResult = await diesel.filterFunction(ctx, server) if (filterResult) return filterResult } else { - return ctx.json({ message: "Authentication required" },400) + return ctx.json({ + error:true, + message: "Protected route,authentication required", + status:400 + },400) } } } @@ -58,10 +59,24 @@ export default async function handleRequest(req: Request, server: Server, url: U } if (!routeHandler || routeHandler.method !== req.method) { - const message = routeHandler ? "Method not allowed" : `Route not found for ${url.pathname}`; const status = routeHandler ? 405 : 404; - return new Response(JSON.stringify({ message }), {status}); + const message = routeHandler + ? "Method not allowed" + : `Route not found for ${url.pathname}`; + + return new Response( + JSON.stringify({ + error: true, + message, + status + }), + { + status, + headers: { 'Content-Type': 'application/json' }, + } + ); } + // Run preHandler hooks 2 if (diesel.hasPreHandlerHook && diesel.hooks.preHandler) { @@ -71,7 +86,6 @@ export default async function handleRequest(req: Request, server: Server, url: U // Finally, execute the route handler and return its result const result = await routeHandler.handler(ctx) as Response | null | void; - // 3. run the postHandler hooks if (diesel.hasPostHandlerHook && diesel.hooks.postHandler) { await diesel.hooks.postHandler(ctx) @@ -84,7 +98,11 @@ export default async function handleRequest(req: Request, server: Server, url: U } // Default Response if Handler is Void - return result ?? ctx.json({ message:"No response from this handler" },204) + return result ?? ctx.json({ + error:true, + message:"No response from this handler", + status:204 + },204) } diff --git a/src/main.ts b/src/main.ts index 8a11ef0..b1440c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,7 +33,7 @@ export default class Diesel { filters: Set; filterFunction: middlewareFunc | null; hasFilterEnabled: boolean; - private serverInstance: Server | null + private serverInstance: Server | null; constructor() { this.tempRoutes = new Map(); @@ -59,7 +59,7 @@ export default class Diesel { this.filters = new Set(); this.filterFunction = null; this.hasFilterEnabled = false; - this.serverInstance = null + this.serverInstance = null; } filter(): FilterMethods { @@ -194,7 +194,7 @@ export default class Diesel { JSON.stringify({ message: "Internal Server Error", error: error.message, - staus: 500 + staus: 500, }), { status: 500 } ); @@ -208,7 +208,6 @@ export default class Diesel { } this.compile(); - this.serverInstance = Bun?.serve(ServerOptions); // Bun?.gc(false) diff --git a/src/trie.ts b/src/trie.ts index a756b32..6b17a19 100644 --- a/src/trie.ts +++ b/src/trie.ts @@ -30,28 +30,26 @@ class TrieNode { this.root = new TrieNode(); } - insert(path: string, route: RouteT): void { + insert(path:string, route:RouteT) : void { let node = this.root; - const pathSegments = path.split('/').filter(Boolean); + const pathSegments = path.split('/').filter(Boolean); // Split path by segments - // Special handling for root path + // If it's the root path '/', treat it separately if (path === '/') { node.isEndOfWord = true; - node.handler = [route.handler]; + node.handler.push(route.handler) node.path = path; - node.method = [route.method]; + node.method.push(route.method) return; } - for (let i = 0; i < pathSegments.length; i++) { - const segment = pathSegments[i]; + for (const segment of pathSegments) { let isDynamic = false; let key = segment; - // Handle dynamic segments if (segment.startsWith(':')) { isDynamic = true; - key = ':'; // Dynamic segments are stored under ':' + key = ':'; // Store dynamic routes under the key ':' } if (!node.children[key]) { @@ -59,20 +57,20 @@ class TrieNode { } node = node.children[key]; + // Set dynamic route information if applicable node.isDynamic = isDynamic; node.pattern = segment; - - // Only assign handlers and methods at the last segment - if (i === pathSegments.length - 1) { - node.handler = [route.handler]; - node.method = [route.method]; - node.isEndOfWord = true; - node.path = path; // Store the full path at the endpoint - } + node.method.push(route.method) + node.handler.push(route.handler) + node.path = path; // Store the actual pattern like ':id' } + // After looping through the entire path, assign route details + node.isEndOfWord = true; + node.method.push(route.method); + node.handler.push(route.handler) + node.path = path; // Store the original path } - // insertMidl(midl:handlerFunction): void { // if (!this.root.subMiddlewares.has(midl)) { // this.root.subMiddlewares.set(midl) @@ -83,26 +81,33 @@ class TrieNode { search(path: string, method: HttpMethod) { let node = this.root; const pathSegments = path.split('/').filter(Boolean); + const totalSegments = pathSegments.length; for (const segment of pathSegments) { let key = segment; - // Check for an exact match + // Check for exact match first (static) if (!node.children[key]) { - // Attempt a dynamic match + // Try dynamic match (e.g., ':id') if (node.children[':']) { node = node.children[':']; } else { - return null; // No match found + return null; // No match } } else { node = node.children[key]; } } - // Verify the endpoint and method match + // Check if the number of segments matches the number of dynamic segments + const routeSegments = node.path.split('/').filter(Boolean); + if (totalSegments !== routeSegments.length) { + return null; + } + + // Method matching const routeMethodIndex = node.method.indexOf(method); - if (node.isEndOfWord && routeMethodIndex !== -1) { + if (routeMethodIndex !== -1) { return { path: node.path, handler: node.handler[routeMethodIndex], @@ -112,37 +117,13 @@ class TrieNode { }; } - // No match found - return null; + return { + path: node.path, + handler: node.handler, + isDynamic: node.isDynamic, + pattern: node.pattern, + method: node.method[routeMethodIndex] + }; } - - - - // New getAllRoutes method - - // getAllRoutes() { - // const routes = []; - // // Helper function to recursively collect all routes - // const traverse = (node, currentPath) => { - // if (node.isEndOfWord) { - // routes.push({ - // path: currentPath, - // handler: node.handler, - // isImportant: node.isImportant, - // isDynamic: node.isDynamic, - // pattern: node.pattern, - // }); - // } - // // Recursively traverse all children - // for (const key in node.children) { - // const child = node.children[key]; - // const newPath = currentPath + (key === ':' ? '/:' + child.pattern : '/' + key); // Reconstruct the full path - // traverse(child, newPath); - // } - // }; - // // Start traversal from the root - // traverse(this.root, ""); - // return routes; - // } - } + } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index c1834a0..6b955f8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,7 +52,7 @@ export interface ContextType { getAuth: () => boolean; json: (data: Object, status?: number) => Response; text: (data: string, status?: number) => Response; - // send: (data: string, status?: number) => Response; + send: (data: string, status?: number) => Response; // html: (filePath: string, status?: number) => Response; file: (filePath: string, status?: number) => Response; redirect: (path: string, status?: number) => Response; diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index 9b1ee42..0000000 --- a/test/.gitignore +++ /dev/null @@ -1,175 +0,0 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Caches - -.cache - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/test/README.md b/test/README.md deleted file mode 100644 index f5d0440..0000000 --- a/test/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# . - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.30. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/test/bun.lockb b/test/bun.lockb deleted file mode 100755 index 4a7a97d..0000000 Binary files a/test/bun.lockb and /dev/null differ diff --git a/test/index.test.ts b/test/index.test.ts index f60e53c..f4dbf56 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,75 +1,170 @@ -import { describe, it, expect, beforeAll, afterAll } from 'bun:test' -import {app} from './server' -const port = process.env.PORT || 3000 +import { describe, it, expect, beforeAll, afterAll } from "bun:test"; +import { app } from "./server"; +const port = process.env.PORT || 3000; beforeAll(async () => { - app.listen( port as number, () => { - console.log('Server running on http://localhost:3000') - }) - - await Bun.sleep(1000) -}) + app.listen(port as number, () => { + console.log(`Server running on http://localhost:${port}`); + }); + + await Bun.sleep(1000); +}); afterAll(async () => { - app.close() + app.close(); console.log("Server closed."); -}) +}); describe("GET /api/user/register", () => { it("should return a message", async () => { - const response = await fetch("http://localhost:3000/api/user/register") - const data = await response.json() - expect(response.status).toBe(200) - expect(data.msg).toBe("This is a public route. No authentication needed.") - }) -}) + const response = await fetch("http://localhost:3000/api/user/register"); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data.msg).toBe("This is a public route. No authentication needed."); + }); + it("should return 405 for unsupported method on /api/user/register", async () => { + const response = await fetch("http://localhost:3000/api/user/register", { + method: "POST", + }); + expect(response.status).toBe(405); + }); + it("should return 405 for unsupported method on /api/user/register", async () => { + const response = await fetch("http://localhost:3000/api/user/register", { + method: "PUT", + }); + expect(response.status).toBe(405); + }); + it("should return 405 for unsupported method on /api/user/register", async () => { + const response = await fetch("http://localhost:3000/api/user/register", { + method: "DELETE", + }); + expect(response.status).toBe(405); + }); + it("should return 200 again for supported method on /api/user/register", async () => { + const response = await fetch("http://localhost:3000/api/user/register", { + method: "GET", + }); + expect(response.status).toBe(200); + }); + + it("should set Content-Type to application/json for JSON responses", async () => { + const response = await fetch("http://localhost:3000/api/user/register"); + console.log(response.headers) + expect(response.headers.get("Content-Type")).toBe("application/json; charset=utf-8"); + }); + + +}); describe("GET /error", () => { it("should trigger onError hook", async () => { - const response = await fetch("http://localhost:3000/error") - const data = await response.json() - expect(response.status).toBe(500) - expect(data.message).toBe("Something went wrong!") - }) -}) + const response = await fetch("http://localhost:3000/error"); + const data = await response.json(); + expect(response.status).toBe(500); + expect(data.message).toBe("Something went wrong!"); + }); +}); describe("GET /api/protected", () => { - it("should return 401 if no token is provided", async () => { - const response = await fetch("http://localhost:3000/api/protected") - const data = await response.json() - expect(response.status).toBe(401) - expect(data.message).toBe("Authentication token missing") - }) - - it("should return 200 if token is valid", async () => { + it("should return 401 when no cookie is provided", async () => { + const response = await fetch("http://localhost:3000/api/protected"); + const data = await response.json(); + expect(response.status).toBe(401); + expect(data.message).toBe("Authentication token missing"); + }); + + it("should return 200 if accesToken cookie is given", async () => { // Simulate sending a valid token const response = await fetch("http://localhost:3000/api/protected", { headers: { Cookie: "accessToken=validToken", }, - }) - const data = await response.json() - expect(response.status).toBe(200) - expect(data.msg).toBe('Authenticated user') - }) -}) + }); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data.msg).toBe("Authenticated user"); + }); +}); describe("CORS Testing", () => { + it("should allow POST requests from allowed origin", async () => { + const response = await fetch("http://localhost:3000/api/hello", { + method: "POST", + headers: { + Origin: "http://localhost:3000", + }, + }); + expect(response.status).toBe(200); + }); + it("should allow requests from allowed origin", async () => { const response = await fetch("http://localhost:3000/api/hello", { headers: { - 'Origin': 'http://localhost:3000' - } - }) - const data = await response.json() - expect(response.status).toBe(200) - expect(data.msg).toBe('Hello world!') - }) + Origin: "http://localhost:3000", + }, + }); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data.msg).toBe("Hello world!"); + }); it("should deny requests from disallowed origin", async () => { const response = await fetch("http://localhost:3000/api/hello", { headers: { - 'Origin': 'http://evil.com' - } - }) - expect(response.status).toBe(403) - }) -}) \ No newline at end of file + Origin: "http://evil.com", + }, + }); + expect(response.status).toBe(403); + }); + it("should deny PUT requests from disallowed origin", async () => { + const response = await fetch("http://localhost:3000/api/hello", { + method: "PUT", + headers: { + Origin: "http://evil.com", + }, + }); + expect(response.status).toBe(403); + }); +}); + +describe("Testing Dynamic routes - /api/param/:id/:username", () => { + it("it should return 404 as we have set route- /api/param/:id/:username", async () => { + const response = await fetch("http://localhost:3000/api/param"); + expect(response.status).toBe(404); + }); + + // it("it should return 404 as we have give only /id in param", async () => { + // const response = await fetch("http://localhost:3000/api/param/99"); + // expect(response.status).toBe(404); + // }); + it("it should return 200 as we have give only /id/username also in param", async () => { + const response = await fetch("http://localhost:3000/api/param/99/pradeep"); + expect(response.status).toBe(200); + }); +}); + +describe("Testing for Query Route", () => { + const baseUrl = "http://localhost:3000/query"; + + it("should return 200 when name and age are provided as query parameters", async () => { + const response = await fetch(`${baseUrl}?name=pradeep&age=23`); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ name: "pradeep", age: "23" }); + }); + + it("should return 200 when only name is provided as query parameter", async () => { + const response = await fetch(`${baseUrl}?name=pradeep`); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ name: "pradeep", age: undefined }); + }); + + it("should return 200 when no query parameters are provided", async () => { + const response = await fetch(baseUrl); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ name: undefined, age: undefined }); + }); +}); diff --git a/test/package.json b/test/package.json deleted file mode 100644 index d57848c..0000000 --- a/test/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": ".", - "module": "index.ts", - "type": "module", - "scripts": { - "test": "" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - } -} \ No newline at end of file diff --git a/test/server.ts b/test/server.ts index 3ef146f..6faeefa 100644 --- a/test/server.ts +++ b/test/server.ts @@ -5,7 +5,7 @@ import type { ContextType } from '../dist/types' export const app = new Diesel() -async function authJwt(ctx:ContextType, server:Server) { +async function authJwt(ctx:ContextType, server:Server):Promise { try { const token = await ctx.getCookie('accessToken') @@ -30,15 +30,23 @@ app.cors({ allowedHeaders: 'Content-Type,Authorization' }) -app.get("/api/hello", async (ctx) => { +app +.get("/api/hello", async (ctx) => { return ctx.json({ msg: "Hello world!" }) }) +.post("/api/hello",(ctx) =>{ + return ctx.json({msg:"Hello world from post"}) +}) app.get("/error", (ctx) => { return ctx.json({ message: "Something went wrong!" }, 500); }); -app.get('/api/protected', authJwt, async (ctx) => { +app +.get('/api/protected', authJwt, async (ctx) => { + return ctx.json({ msg: 'Authenticated user' }) +}) +.post('/api/protected', authJwt, async (ctx) => { return ctx.json({ msg: 'Authenticated user' }) }) @@ -46,6 +54,16 @@ app.get("/api/user/register", async (ctx) => { return ctx.json({ msg: "This is a public route. No authentication needed." }) }) +app.get("/api/param/:id/:username", async (ctx) => { + const id = ctx.getParams('id') + return ctx.json({ msg: `This is a public route. No authentication needed. User id: ${id}` }) +}) +app.get("/query",async(ctx) =>{ + const name = ctx.getQuery('name') + const age = ctx.getQuery('age') + return ctx.json({ name , age }) +}) +// app.listen(3000) \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index 238655f..0000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -}