From 1b2734f44fd05ef7bba0e9117154f7dbb6f5c25f Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Mon, 26 Aug 2024 14:46:46 +0100 Subject: [PATCH 01/12] feat: HTTP + graph routes --- lib/Router.ts | 48 +++++++++++++++++++++++++++++-------------- lib/types.ts | 13 ++++++++++-- lib/utils/Profiler.ts | 2 +- tests/Router_test.ts | 16 +++++++-------- wrangler.toml | 3 +-- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/lib/Router.ts b/lib/Router.ts index bc172d80..27e23743 100644 --- a/lib/Router.ts +++ b/lib/Router.ts @@ -1,5 +1,5 @@ import { Cascade } from "./utils/Cascade.ts"; -import { Middleware, Handler, Route } from "./types.ts"; +import { Middleware, Handler, Route, HttpRouteConfig, GraphRouteConfig } from "./types.ts"; export class RequestContext> { url: URL; @@ -12,7 +12,7 @@ export class RequestContext> { } } -export class _Route implements Route { +export class HttpRoute implements Route { path: `/${string}`; params: Record = {}; regexPath: RegExp; @@ -20,14 +20,11 @@ export class _Route implements Route { middleware: Middleware[] = []; handler: Handler; - constructor(routeObj: Route) { - if (!routeObj.path) throw new Error("Route is missing path"); - if (!routeObj.handler) throw new Error("Route is missing handler"); - + constructor(routeObj: HttpRouteConfig) { this.path = routeObj.path[routeObj.path.length - 1] === "/" - ? (routeObj.path.slice(0, -1) as Route["path"]) - : routeObj.path; + ? (routeObj.path.slice(0, -1) as HttpRoute["path"]) + : (routeObj.path as HttpRoute["path"]); this.path.split("/").forEach((str, i) => { if (str[0] === ":") this.params[str.slice(1)] = i; @@ -39,7 +36,27 @@ export class _Route implements Route { ) : new RegExp(`^${this.path}\/?$`); - this.method = routeObj.method || "GET"; + this.method = (routeObj.method as HttpRoute["method"]) || "GET"; + this.handler = Cascade.promisify(routeObj.handler!) as Handler; + this.middleware = [routeObj.middleware] + .flat() + .filter(Boolean) + .map((mware) => Cascade.promisify(mware!)); + } +} + +export class GraphRoute implements Route { + path: string; + params: Record = {}; + regexPath: RegExp; + method?: "QUERY" | "MUTATION" | "RESOLVE"; + middleware: Middleware[] = []; + handler: Handler; + + constructor(routeObj: GraphRouteConfig) { + this.path = routeObj.path; + this.regexPath = new RegExp(`^${this.path}$`); + this.method = routeObj.method as GraphRoute["method"]; this.handler = Cascade.promisify(routeObj.handler!) as Handler; this.middleware = [routeObj.middleware] .flat() @@ -50,7 +67,8 @@ export class _Route implements Route { export class Router { constructor( - public routes: _Route[] = [], + public httpRoutes: HttpRoute[] = [], + public graphRoutes: GraphRoute[] = [], public middleware: Middleware[] = [] ) {} @@ -111,10 +129,10 @@ export class Router { handler: arg3, }; - const fullRoute = new _Route(routeObj as Route); + const fullRoute = new HttpRoute(routeObj as HttpRouteConfig); if ( - this.routes.find( + this.httpRoutes.find( (existing) => existing.regexPath.toString() === fullRoute.regexPath.toString() ) @@ -122,7 +140,7 @@ export class Router { throw new Error(`Route with path ${routeObj.path} already exists!`); } - this.routes.push(fullRoute); + this.httpRoutes.push(fullRoute); this.middleware.push(function RouteMiddleware(ctx) { if ( fullRoute.regexPath.test(ctx.url.pathname) && @@ -203,9 +221,9 @@ export class Router { * @returns Route - removed route */ removeRoute(route: Route["path"]): Route | undefined { - const routeToRemove = this.routes.find((r) => r.path === route); + const routeToRemove = this.httpRoutes.find((r) => r.path === route); if (routeToRemove) { - this.routes.splice(this.routes.indexOf(routeToRemove), 1); + this.httpRoutes.splice(this.httpRoutes.indexOf(routeToRemove), 1); } return routeToRemove; diff --git a/lib/types.ts b/lib/types.ts index 9b7536e3..ba621cca 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,12 +1,21 @@ import { RequestContext } from "./Router.ts"; export interface Route { - path: `/${string}`; - method?: "GET" | "POST" | "PUT" | "DELETE"; + path: string; + method?: string; middleware?: Middleware[] | Middleware; handler: Handler; } +export interface HttpRouteConfig extends Route { + path: `/${string}`; + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; +} + +export interface GraphRouteConfig extends Route { + method?: "QUERY" | "MUTATION" | "RESOLVER"; +} + export type Result = void | Response | undefined; export type Next = () => Promise | Result; diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts index cd6ce5e3..d2cb2035 100644 --- a/lib/utils/Profiler.ts +++ b/lib/utils/Profiler.ts @@ -35,7 +35,7 @@ export class Profiler { const results: ProfileResults = {}; - for (const route of router.routes) { + for (const route of router.httpRoutes) { results[route.path] = { avgTime: 0, requests: [] }; if (!excludedRoutes.includes(route)) { diff --git a/tests/Router_test.ts b/tests/Router_test.ts index 5c24e260..91efc7c2 100644 --- a/tests/Router_test.ts +++ b/tests/Router_test.ts @@ -25,7 +25,7 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { testHandler ); - assert(router.routes.length === 4); + assert(router.httpRoutes.length === 4); const request1 = new Request("http://localhost:7777/route1"); const request2 = new Request("http://localhost:7777/route2"); @@ -50,7 +50,7 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { router.removeRoute("/route3"); router.removeRoute("/route4"); - assert(router.routes.length === 0); + assert(router.httpRoutes.length === 0); }); await t.step("routers on server can be subsequently editted", () => { @@ -65,8 +65,8 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { aRouter.removeRoute("/route"); - assert(!aRouter.routes.find((route) => route.path === "/route")); - assert(aRouter.routes.length === 2); + assert(!aRouter.httpRoutes.find((route) => route.path === "/route")); + assert(aRouter.httpRoutes.length === 2); }); await t.step("http shorthand methods work correctly", () => { @@ -89,7 +89,7 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { handler: () => new Response("DELETE"), }); - assert(router.routes.length === 4); + assert(router.httpRoutes.length === 4); assert(getRoute.method === "GET"); assert(postRoute.method === "POST"); assert(putRoute.method === "PUT"); @@ -100,8 +100,8 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { const router = new Router(); router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")); - assert(router.routes[0].params["id"] === 2); - assert(router.routes[0].params["name"] === 4); + assert(router.httpRoutes[0].params["id"] === 2); + assert(router.httpRoutes[0].params["name"] === 4); }); }); @@ -121,7 +121,7 @@ Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { const res = await newRouter.handle( new Request("http://localhost:7777/hello/123/world/bruno") ); - const json = await res.json(); + const json = await res.json() as { id: string; name: string }; assert(json.id === "123" && json.name === "bruno"); }); diff --git a/wrangler.toml b/wrangler.toml index a33dabdd..9fef41d8 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,5 +6,4 @@ compatibility_date = "2024-02-23" ENVIRONMENT = 'production' [env.dev.vars] -ENVIRONMENT = 'dev' - +ENVIRONMENT = 'dev' \ No newline at end of file From 775fcee24f9dc617025a47f7e400df3da67b03f6 Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Tue, 27 Aug 2024 21:47:28 +0100 Subject: [PATCH 02/12] feat: schemaRouter refactor --- lib/handlers/graph.ts | 2 +- lib/handlers/ssr.ts | 2 +- lib/{Router.ts => routers/httpRouter.ts} | 149 ++++++-------------- lib/routers/schemaRouter.ts | 164 +++++++++++++++++++++++ lib/types.ts | 86 ++++++++++-- lib/utils/CacheItem.ts | 2 +- lib/utils/Cascade.ts | 2 +- lib/utils/Profiler.ts | 11 +- lib/utils/Schema.ts | 2 +- mod.ts | 6 +- tests/Router_test.ts | 42 +++--- tests/handlers/file_test.ts | 5 +- tests/handlers/sse_test.ts | 4 +- tests/handlers/ssr_test.ts | 4 +- tests/middleware/authenticator_test.ts | 12 +- tests/middleware/cacher_test.ts | 18 +-- tests/middleware/logger_test.ts | 4 +- tests/mocks/middleware.ts | 4 +- tests/mocks/profileRouter.ts | 4 +- tests/utils/CacheItem_test.ts | 4 +- tests/utils/Cascade_test.ts | 4 +- tests/utils/Profiler_test.ts | 4 +- 22 files changed, 346 insertions(+), 189 deletions(-) rename lib/{Router.ts => routers/httpRouter.ts} (50%) create mode 100644 lib/routers/schemaRouter.ts diff --git a/lib/handlers/graph.ts b/lib/handlers/graph.ts index ff332318..6e4b9458 100644 --- a/lib/handlers/graph.ts +++ b/lib/handlers/graph.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../Router.ts"; +import { RequestContext } from "../routers/httpRouter.ts"; import { mergeHeaders } from "../utils/helpers.ts"; import { Schema } from "../utils/Schema.ts"; import { Handler, HandlerOptions } from "../types.ts"; diff --git a/lib/handlers/ssr.ts b/lib/handlers/ssr.ts index 2061a549..80ce3dd2 100644 --- a/lib/handlers/ssr.ts +++ b/lib/handlers/ssr.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../Router.ts"; +import { RequestContext } from "../routers/httpRouter.ts"; import { Crypto } from "../utils/Crypto.ts"; import { mergeHeaders } from "../utils/helpers.ts"; import { Handler, HandlerOptions } from "../types.ts"; diff --git a/lib/Router.ts b/lib/routers/httpRouter.ts similarity index 50% rename from lib/Router.ts rename to lib/routers/httpRouter.ts index 27e23743..f1904f65 100644 --- a/lib/Router.ts +++ b/lib/routers/httpRouter.ts @@ -1,24 +1,25 @@ -import { Cascade } from "./utils/Cascade.ts"; -import { Middleware, Handler, Route, HttpRouteConfig, GraphRouteConfig } from "./types.ts"; - -export class RequestContext> { - url: URL; - state: T; - params: Record = {}; - - constructor(public router: Router, public request: Request, state?: T) { - this.url = new URL(request.url); - this.state = state ? state : ({} as T); - } +import { Cascade } from "../utils/Cascade.ts"; +import { + Middleware, + Handler, + BaseRouter, + BaseRoute, + RequestContext, +} from "../types.ts"; + +export interface HttpRouteConfig extends Partial { + path: `/${string}`; + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + handler: Handler; } -export class HttpRoute implements Route { +export class HttpRoute implements BaseRoute { path: `/${string}`; - params: Record = {}; regexPath: RegExp; - method?: "GET" | "POST" | "PUT" | "DELETE"; middleware: Middleware[] = []; handler: Handler; + params: Record = {}; + method: "GET" | "POST" | "PUT" | "DELETE"; constructor(routeObj: HttpRouteConfig) { this.path = @@ -45,49 +46,18 @@ export class HttpRoute implements Route { } } -export class GraphRoute implements Route { - path: string; - params: Record = {}; - regexPath: RegExp; - method?: "QUERY" | "MUTATION" | "RESOLVE"; - middleware: Middleware[] = []; - handler: Handler; - - constructor(routeObj: GraphRouteConfig) { - this.path = routeObj.path; - this.regexPath = new RegExp(`^${this.path}$`); - this.method = routeObj.method as GraphRoute["method"]; - this.handler = Cascade.promisify(routeObj.handler!) as Handler; - this.middleware = [routeObj.middleware] - .flat() - .filter(Boolean) - .map((mware) => Cascade.promisify(mware!)); - } -} - -export class Router { +export class HttpRouter implements BaseRouter { constructor( - public httpRoutes: HttpRoute[] = [], - public graphRoutes: GraphRoute[] = [], + public routes: HttpRoute[] = [], public middleware: Middleware[] = [] ) {} - /** - * Running Request through middleware cascade for Response. - * @param request: Request - * @returns Promise - */ async handle(request: Request): Promise { const ctx = new RequestContext(this, request); const res = await new Cascade(ctx, this.middleware).run(); return res ? res : new Response("", { status: 404 }); } - /** - * Add global middleware or another router's middleware - * @param middleware: Middleware[] | Middleware | Router - * @returns number - server.middleware.length - */ use(middleware: Middleware | Middleware[]) { if (Array.isArray(middleware)) { middleware.forEach((mware) => this.use(mware)); @@ -97,42 +67,36 @@ export class Router { return this; } - /** - * Add Route - * @param route: Route | Route["path"] - * @param arg2?: Partial | Middleware | Middleware[], - * @param arg3?: Handler - * @returns route: Route - added route object - */ - addRoute(route: Route): Route; - addRoute(route: Route["path"], data: Handler | Partial): Route; + addRoute(route: HttpRouteConfig): HttpRoute; + addRoute(route: HttpRouteConfig["path"], data: Handler): HttpRoute; addRoute( - route: Route["path"], + route: HttpRouteConfig["path"], middleware: Middleware | Middleware[], handler: Handler - ): Route; + ): HttpRoute; addRoute( - arg1: Route | Route["path"], - arg2?: Partial | Middleware | Middleware[], + arg1: HttpRouteConfig | HttpRoute["path"], + arg2?: Middleware | Middleware[], arg3?: Handler - ): Route { - const routeObj: Partial = + ): HttpRoute { + // overload resolution + const routeObj: HttpRouteConfig = typeof arg1 !== "string" ? arg1 : arguments.length === 2 - ? typeof arg2 === "function" - ? { path: arg1, handler: arg2 as Handler } - : { path: arg1, ...(arg2 as Partial) } + ? { path: arg1, handler: arg2 as Handler } : { path: arg1, middleware: arg2 as Middleware | Middleware[], - handler: arg3, + handler: arg3 as Handler, }; - const fullRoute = new HttpRoute(routeObj as HttpRouteConfig); + // create new Route object + const fullRoute = new HttpRoute(routeObj); + // check if route already exists if ( - this.httpRoutes.find( + this.routes.find( (existing) => existing.regexPath.toString() === fullRoute.regexPath.toString() ) @@ -140,7 +104,8 @@ export class Router { throw new Error(`Route with path ${routeObj.path} already exists!`); } - this.httpRoutes.push(fullRoute); + // add route to appropriate routes and middleware + this.routes.push(fullRoute); this.middleware.push(function RouteMiddleware(ctx) { if ( fullRoute.regexPath.test(ctx.url.pathname) && @@ -162,10 +127,7 @@ export class Router { return fullRoute; } - /** - * Add Route with method "GET" (same as default addRoute behaviour) - * @returns route: Route - added route object - */ + get: typeof this.addRoute = function () { // @ts-ignore supply overload args const newRoute = this.addRoute(...arguments); @@ -173,10 +135,6 @@ export class Router { return newRoute; }; - /** - * Add Route with method "POST" - * @returns route: Route - added route object - */ post: typeof this.addRoute = function () { // @ts-ignore supply overload args const newRoute = this.addRoute(...arguments); @@ -184,10 +142,6 @@ export class Router { return newRoute; }; - /** - * Add Route with method "PUT" - * @returns route: Route - added route object - */ put: typeof this.addRoute = function () { // @ts-ignore supply overload args const newRoute = this.addRoute(...arguments); @@ -195,10 +149,6 @@ export class Router { return newRoute; }; - /** - * Add Route with method "DELETE" - * @returns route: Route - added route object - */ delete: typeof this.addRoute = function () { // @ts-ignore supply overload args const newRoute = this.addRoute(...arguments); @@ -206,37 +156,22 @@ export class Router { return newRoute; }; - /** - * Add Routes - * @param routes: Route[] - middleware can be Middlewares or Middleware - * @returns Route[] - added routes - */ - addRoutes(routes: Route[]): Route[] { + addRoutes(routes: HttpRouteConfig[]): HttpRoute[] { return routes.map((route) => this.addRoute(route)); } - /** - * Remove Route from Peko server - * @param route: Route["path"] of route to remove - * @returns Route - removed route - */ - removeRoute(route: Route["path"]): Route | undefined { - const routeToRemove = this.httpRoutes.find((r) => r.path === route); + removeRoute(route: HttpRoute["path"]): HttpRoute | undefined { + const routeToRemove = this.routes.find((r) => r.path === route); if (routeToRemove) { - this.httpRoutes.splice(this.httpRoutes.indexOf(routeToRemove), 1); + this.routes.splice(this.routes.indexOf(routeToRemove), 1); } return routeToRemove; } - /** - * Remove Routes - * @param routes: Route["path"] of routes to remove - * @returns Array - removed routes - */ - removeRoutes(routes: Route["path"][]): Array { + removeRoutes(routes: HttpRoute["path"][]): Array { return routes.map((route) => this.removeRoute(route)); } } +export { RequestContext }; -export default Router; diff --git a/lib/routers/schemaRouter.ts b/lib/routers/schemaRouter.ts new file mode 100644 index 00000000..f523b1b2 --- /dev/null +++ b/lib/routers/schemaRouter.ts @@ -0,0 +1,164 @@ +import { Cascade } from "../utils/Cascade.ts"; +import { + Middleware, + Handler, + BaseRoute, + BaseRouteConfig, + RequestContext, +} from "../types.ts"; + +export interface GraphRouteConfig extends BaseRouteConfig { + method?: "QUERY" | "MUTATION" | "RESOLVER"; +} + +export class GraphRoute implements BaseRoute { + path: string; + params: Record = {}; + regexPath: RegExp; + method: "QUERY" | "MUTATION" | "RESOLVER"; + middleware: Middleware[] = []; + handler: Handler; + + constructor(routeObj: GraphRouteConfig) { + this.path = routeObj.path; + this.regexPath = new RegExp(`^${this.path}$`); + this.method = (routeObj.method as GraphRoute["method"]) || "QUERY"; + this.handler = Cascade.promisify(routeObj.handler!) as Handler; + this.middleware = [routeObj.middleware] + .flat() + .filter(Boolean) + .map((mware) => Cascade.promisify(mware!)); + } +} + +export class GraphRouter { + constructor( + public routes: GraphRoute[] = [], + public middleware: Middleware[] = [] + ) {} + + /** + * Running Request through middleware cascade for Response. + * @param request: Request + * @returns Promise + */ + async handle(request: Request): Promise { + const ctx = new RequestContext(this, request); + const res = await new Cascade(ctx, this.middleware).run(); + return res ? res : new Response("", { status: 404 }); + } + + /** + * Add global middleware or another router's middleware + * @param middleware: Middleware[] | Middleware | Router + * @returns number - server.middleware.length + */ + use(middleware: Middleware | Middleware[]) { + if (Array.isArray(middleware)) { + middleware.forEach((mware) => this.use(mware)); + } else { + this.middleware.push(Cascade.promisify(middleware)); + } + return this; + } + + /** + * Add Route + * @param route: Route | Route["path"] + * @param arg2?: Partial | Middleware | Middleware[], + * @param arg3?: Handler + * @returns route: Route - added route object + */ + addRoute(route: GraphRouteConfig): GraphRoute; + addRoute(route: GraphRouteConfig["path"], data: Handler): GraphRoute; + addRoute( + route: GraphRouteConfig["path"], + middleware: Middleware | Middleware[], + handler: Handler + ): GraphRoute; + addRoute( + arg1: GraphRouteConfig | GraphRouteConfig["path"], + arg2?: Middleware | Middleware[], + arg3?: Handler + ): GraphRoute { + // overload resolution + const routeObj: GraphRouteConfig = + typeof arg1 !== "string" + ? arg1 + : arguments.length === 2 + ? { path: arg1, handler: arg2 as Handler } + : { + path: arg1, + middleware: arg2 as Middleware | Middleware[], + handler: arg3 as Handler, + }; + + // create new Route object + const fullRoute = new GraphRoute(routeObj as GraphRouteConfig); + + // check if route already exists + if ( + this.routes.find( + (existing) => + existing.regexPath.toString() === fullRoute.regexPath.toString() + ) + ) { + throw new Error(`Route with path ${routeObj.path} already exists!`); + } + + // add route to appropriate routes and middleware + this.routes.push(fullRoute); + this.middleware.push(function RouteMiddleware(ctx) { + if ( + fullRoute.regexPath.test(ctx.url.pathname) && + fullRoute.method === ctx.request.method + ) { + if (fullRoute?.params) { + const pathBits = ctx.url.pathname.split("/"); + for (const param in fullRoute.params) { + ctx.params[param] = pathBits[fullRoute.params[param]]; + } + } + + return new Cascade(ctx, [ + ...fullRoute.middleware, + fullRoute.handler, + ]).run(); + } + }); + + return fullRoute; + } + + /** + * Add Routes + * @param routes: Route[] - middleware can be Middlewares or Middleware + * @returns Route[] - added routes + */ + addRoutes(routes: GraphRouteConfig[]): GraphRoute[] { + return routes.map((route) => this.addRoute(route)); + } + + /** + * Remove Route from Peko server + * @param route: Route["path"] of route to remove + * @returns Route - removed route + */ + removeRoute(route: GraphRouteConfig["path"]): GraphRoute | undefined { + const routeToRemove = this.routes.find((r) => r.path === route); + if (routeToRemove) { + this.routes.splice(this.routes.indexOf(routeToRemove), 1); + } + + return routeToRemove; + } + + /** + * Remove Routes + * @param routes: Route["path"] of routes to remove + * @returns Array - removed routes + */ + removeRoutes(routes: GraphRouteConfig["path"][]): Array { + return routes.map((route) => this.removeRoute(route)); + } +} diff --git a/lib/types.ts b/lib/types.ts index ba621cca..a86149aa 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,19 +1,76 @@ -import { RequestContext } from "./Router.ts"; - -export interface Route { +export interface BaseRouteConfig { path: string; method?: string; - middleware?: Middleware[] | Middleware; + middleware?: Middleware | Middleware[]; handler: Handler; } -export interface HttpRouteConfig extends Route { - path: `/${string}`; - method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; +export interface BaseRoute { + path: string; + regexPath: RegExp; + middleware: Middleware[] | Middleware; + handler: Handler; + method: string; } -export interface GraphRouteConfig extends Route { - method?: "QUERY" | "MUTATION" | "RESOLVER"; +export interface BaseRouter { + routes: BaseRoute[]; + middleware: Middleware[]; + + /** + * Running Request through middleware cascade for Response. + * @param request: Request + * @returns Promise + */ + handle(request: Request): Promise; + + /** + * Add global middleware or another router's middleware + * @param middleware: Middleware[] | Middleware | Router + * @returns number - server.middleware.length + */ + use(middleware: Middleware | Middleware[]): void; + + /** + * Add Route + * @param route: Route | Route["path"] + * @param arg2?: Partial | Middleware | Middleware[], + * @param arg3?: Handler + * @returns route: Route - added route object + */ + addRoute(route: BaseRouteConfig): BaseRoute; + addRoute(route: BaseRouteConfig["path"], data: Handler): BaseRoute; + addRoute( + route: BaseRouteConfig["path"], + middleware: Middleware | Middleware[], + handler: Handler + ): BaseRoute; + addRoute( + arg1: BaseRouteConfig | BaseRouteConfig["path"], + arg2?: Middleware | Middleware[], + arg3?: Handler + ): BaseRoute; + + /** + * Add Routes + * @param routes: Route[] - middleware can be Middlewares or Middleware + * @returns Route[] - added routes + */ + addRoutes(routes: BaseRouteConfig[]): BaseRoute[]; + + /** + * Remove Route from Peko server + * @param route: Route["path"] of route to remove + * @returns Route - removed route + */ + removeRoute(route: BaseRoute["path"]): BaseRoute | undefined; + + /** + * Remove Routes + * @param routes: Route["path"] of routes to remove + * @returns Array - removed routes + */ + removeRoutes(routes: BaseRoute["path"][]): Array; } export type Result = void | Response | undefined; @@ -26,6 +83,17 @@ export type Middleware = ( export type Handler = (ctx: RequestContext) => Promise | Response; export type HandlerOptions = { headers?: Headers }; +export class RequestContext> { + url: URL; + state: T; + params: Record = {}; + + constructor(public router: BaseRouter, public request: Request, state?: T) { + this.url = new URL(request.url); + this.state = state ? state : ({} as T); + } +} + export type BodyInit = | string | Blob diff --git a/lib/utils/CacheItem.ts b/lib/utils/CacheItem.ts index 3050150e..4e975609 100644 --- a/lib/utils/CacheItem.ts +++ b/lib/utils/CacheItem.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../Router.ts"; +import { RequestContext } from "../routers/httpRouter.ts"; export class CacheItem { key: string; diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts index 2b7091c0..f899c1bf 100644 --- a/lib/utils/Cascade.ts +++ b/lib/utils/Cascade.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../Router.ts"; +import { RequestContext } from "../types.ts"; import { Middleware, Result, Next } from "../types.ts"; export type PromiseMiddleware = ( diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts index d2cb2035..e1f9385b 100644 --- a/lib/utils/Profiler.ts +++ b/lib/utils/Profiler.ts @@ -1,15 +1,14 @@ -import { Router } from "../Router.ts"; -import { Route } from "../types.ts"; +import { BaseRouter, BaseRoute } from "../types.ts"; type ProfileConfig = { mode?: "serve" | "handle"; url?: string; count?: number; - excludedRoutes?: Route[]; + excludedRoutes?: BaseRoute[]; }; type ProfileResults = Record< - Route["path"], +BaseRoute["path"], { avgTime: number; requests: { @@ -27,7 +26,7 @@ export class Profiler { * @param config * @returns results: ProfileResults */ - static async run(router: Router, config?: ProfileConfig) { + static async run(router: BaseRouter, config?: ProfileConfig) { const url = (config && config.url) || `http://localhost:7777`; const count = (config && config.count) || 100; const excludedRoutes = (config && config.excludedRoutes) || []; @@ -35,7 +34,7 @@ export class Profiler { const results: ProfileResults = {}; - for (const route of router.httpRoutes) { + for (const route of router.routes) { results[route.path] = { avgTime: 0, requests: [] }; if (!excludedRoutes.includes(route)) { diff --git a/lib/utils/Schema.ts b/lib/utils/Schema.ts index 76a9ba0d..a6b2dc24 100644 --- a/lib/utils/Schema.ts +++ b/lib/utils/Schema.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../Router.ts"; +import { RequestContext } from "../types.ts"; import { Middleware } from "../types.ts"; export class ID extends String {} diff --git a/mod.ts b/mod.ts index e0d89e4d..dcb8d76d 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,8 @@ */ // Core classes, functions & types -export * from "./lib/Router.ts"; +export * from "./lib/routers/httpRouter.ts"; +export * from "./lib/routers/schemaRouter.ts"; export * from "./lib/types.ts"; // Handlers @@ -22,6 +23,3 @@ export * from "./lib/utils/Cascade.ts"; export * from "./lib/utils/Crypto.ts"; export * from "./lib/utils/helpers.ts"; export * from "./lib/utils/Profiler.ts"; - -import { Router } from "./lib/Router.ts"; -export default Router; diff --git a/tests/Router_test.ts b/tests/Router_test.ts index 91efc7c2..51c1209c 100644 --- a/tests/Router_test.ts +++ b/tests/Router_test.ts @@ -1,5 +1,5 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "./../lib/Router.ts"; +import { HttpRouter } from "../lib/routers/httpRouter.ts"; import { testMiddleware2, testMiddleware3, @@ -8,39 +8,32 @@ import { } from "./mocks/middleware.ts"; Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { - const router = new Router(); + const router = new HttpRouter(); await t.step( "routes added with full route and string arg options", async () => { - router.addRoute({ path: "/route1", handler: testHandler }); + router.addRoute("/route1", testHandler); router.addRoute("/route2", testMiddleware1, testHandler); - router.addRoute("/route3", { - middleware: testMiddleware1, - handler: testHandler, - }); router.addRoute( - "/route4", + "/route3", [testMiddleware1, testMiddleware2], testHandler ); - assert(router.httpRoutes.length === 4); + assert(router.routes.length === 3); const request1 = new Request("http://localhost:7777/route1"); const request2 = new Request("http://localhost:7777/route2"); const request3 = new Request("http://localhost:7777/route3"); - const request4 = new Request("http://localhost:7777/route4"); const response1 = await router.handle(request1); const response2 = await router.handle(request2); const response3 = await router.handle(request3); - const response4 = await router.handle(request4); assert(response1.status === 200); assert(response2.status === 200); assert(response3.status === 200); - assert(response4.status === 200); } ); @@ -48,15 +41,14 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { router.removeRoute("/route1"); router.removeRoute("/route2"); router.removeRoute("/route3"); - router.removeRoute("/route4"); - assert(router.httpRoutes.length === 0); + assert(router.routes.length === 0); }); await t.step("routers on server can be subsequently editted", () => { - const aRouter = new Router(); + const aRouter = new HttpRouter(); aRouter.addRoutes([ - { path: "/route", handler: testHandler }, + { path: "/route", middleware: [], handler: testHandler }, { path: "/route2", handler: testHandler }, { path: "/route3", handler: testHandler }, ]); @@ -65,12 +57,12 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { aRouter.removeRoute("/route"); - assert(!aRouter.httpRoutes.find((route) => route.path === "/route")); - assert(aRouter.httpRoutes.length === 2); + assert(!aRouter.routes.find((route) => route.path === "/route")); + assert(aRouter.routes.length === 2); }); await t.step("http shorthand methods work correctly", () => { - const router = new Router(); + const router = new HttpRouter(); const getRoute = router.get({ path: "/get", @@ -89,7 +81,7 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { handler: () => new Response("DELETE"), }); - assert(router.httpRoutes.length === 4); + assert(router.routes.length === 4); assert(getRoute.method === "GET"); assert(postRoute.method === "POST"); assert(putRoute.method === "PUT"); @@ -97,20 +89,20 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { }); await t.step("Params correctly stored", () => { - const router = new Router(); + const router = new HttpRouter(); router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")); - assert(router.httpRoutes[0].params["id"] === 2); - assert(router.httpRoutes[0].params["name"] === 4); + assert(router.routes[0].params["id"] === 2); + assert(router.routes[0].params["name"] === 4); }); }); Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { - const router = new Router(); + const router = new HttpRouter(); router.middleware = []; await t.step("params discovered in RequestContext creation", async () => { - const newRouter = new Router(); + const newRouter = new HttpRouter(); newRouter.addRoute("/hello/:id/world/:name", (ctx) => { return new Response( diff --git a/tests/handlers/file_test.ts b/tests/handlers/file_test.ts index 4e0b7d74..ecc02ab7 100644 --- a/tests/handlers/file_test.ts +++ b/tests/handlers/file_test.ts @@ -1,9 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; import { file } from "../../lib/handlers/file.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: File", async (t) => { - const server = new Router(); + const server = new HttpRouter(); const ctx = new RequestContext(server, new Request("http://localhost")); const fileURL = new URL(import.meta.url); const decoder = new TextDecoder(); diff --git a/tests/handlers/sse_test.ts b/tests/handlers/sse_test.ts index accdf8bf..0e47ac4a 100644 --- a/tests/handlers/sse_test.ts +++ b/tests/handlers/sse_test.ts @@ -1,9 +1,9 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { sse } from "../../lib/handlers/sse.ts"; Deno.test("HANDLER: Server-sent events", async (t) => { - const router = new Router(); + const router = new HttpRouter(); const ctx = new RequestContext(router, new Request("http://localhost")); const eventTarget = new EventTarget(); const decoder = new TextDecoder(); diff --git a/tests/handlers/ssr_test.ts b/tests/handlers/ssr_test.ts index ecb9f6e3..c94d46a4 100644 --- a/tests/handlers/ssr_test.ts +++ b/tests/handlers/ssr_test.ts @@ -1,9 +1,9 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { ssr } from "../../lib/handlers/ssr.ts"; Deno.test("HANDLER: Server-side render", async (t) => { - const server = new Router(); + const server = new HttpRouter(); const ctx = new RequestContext(server, new Request("http://localhost")); const decoder = new TextDecoder(); const cacheControl = "max-age=60, stale-while-revalidate=10"; diff --git a/tests/middleware/authenticator_test.ts b/tests/middleware/authenticator_test.ts index fe6d68aa..5ce70a8d 100644 --- a/tests/middleware/authenticator_test.ts +++ b/tests/middleware/authenticator_test.ts @@ -1,12 +1,12 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { authenticator } from "../../lib/middleware/authenticator.ts"; import { Crypto } from "../../lib/utils/Crypto.ts"; Deno.test("MIDDLEWARE: Authenticator", async (t) => { const successString = "Authorized!"; const crypto = new Crypto("test_key"); - const server = new Router(); + const server = new HttpRouter(); const testPayload = { iat: Date.now(), @@ -25,8 +25,8 @@ Deno.test("MIDDLEWARE: Authenticator", async (t) => { () => new Response(successString) ); - assert((await response?.text()) === successString); - assert(response?.status === 200); + assert((response instanceof Response && await response.text()) === successString); + assert(response instanceof Response && response.status === 200); assert(JSON.stringify(ctx.state.auth) === JSON.stringify(testPayload)); }); @@ -37,7 +37,7 @@ Deno.test("MIDDLEWARE: Authenticator", async (t) => { () => new Response(successString) ); - assert(response?.status === 401); + assert(response instanceof Response && response.status === 401); assert(!ctx.state.auth); }); @@ -51,7 +51,7 @@ Deno.test("MIDDLEWARE: Authenticator", async (t) => { () => new Response(successString) ); - assert(response?.status === 200); + assert(response instanceof Response && response.status === 200); assert(ctx.state.auth); }); }); diff --git a/tests/middleware/cacher_test.ts b/tests/middleware/cacher_test.ts index 102da6af..2184f7df 100644 --- a/tests/middleware/cacher_test.ts +++ b/tests/middleware/cacher_test.ts @@ -1,11 +1,11 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { cacher } from "../../lib/middleware/cacher.ts"; import { testHandler } from "../mocks/middleware.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; Deno.test("MIDDLEWARE: Cacher", async (t) => { - const router = new Router(); + const router = new HttpRouter(); const successString = "Success!"; const testData = { foo: "bar", @@ -63,12 +63,12 @@ Deno.test("MIDDLEWARE: Cacher", async (t) => { assert(ctx_custom.state.responseFromCache); assert(ctx_custom.state.foo === testData.foo); - const default_body1 = await default_response1?.text(); - const default_body2 = await default_response2?.text(); + const default_body1 = default_response1 && await default_response1.text(); + const default_body2 = default_response2 && await default_response2.text(); assert(default_body1 === successString && default_body2 === successString); - const custom_body1 = await custom_response1?.text(); - const custom_body2 = await custom_response2?.text(); + const custom_body1 = custom_response1 && await custom_response1.text(); + const custom_body2 = custom_response2 && await custom_response2.text(); assert(custom_body1 === successString); assert(custom_body2 === successString); }); @@ -79,7 +79,7 @@ Deno.test("MIDDLEWARE: Cacher", async (t) => { ...testData, }); const response = await customCacher(ctx, () => new Response(successString)); - const body = await response?.text(); + const body = response && await response.text(); assert(!ctx.state.responseFromCache && ctx.state.foo === testData.foo); assert(body === successString); @@ -94,9 +94,9 @@ Deno.test("MIDDLEWARE: Cacher", async (t) => { assert(memRes); const testJSON = await testRes.json(); - const memJSON2 = await memRes.json(); + const memJSON2 = memRes && await memRes.json(); - assert(testJSON.foo === memJSON2.foo); + assert(testJSON["foo"] === memJSON2["foo"]); }); await t.step("return 304 with matching ETAG", async () => { diff --git a/tests/middleware/logger_test.ts b/tests/middleware/logger_test.ts index c5768520..fee88b16 100644 --- a/tests/middleware/logger_test.ts +++ b/tests/middleware/logger_test.ts @@ -1,12 +1,12 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { logger } from "../../lib/middleware/logger.ts"; Deno.test("MIDDLEWARE: Logger", async (t) => { const successString = "Success!"; let logOutput: unknown; - const server = new Router(); + const server = new HttpRouter(); const testData = { foo: "bar", diff --git a/tests/mocks/middleware.ts b/tests/mocks/middleware.ts index 97ad4d2e..835dcd3a 100644 --- a/tests/mocks/middleware.ts +++ b/tests/mocks/middleware.ts @@ -1,5 +1,5 @@ import { Middleware, Handler } from "../../lib/types.ts"; -import { Router } from "../../lib/Router.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; export const testMiddleware1: Middleware = async (ctx, next) => { const start = performance.now(); @@ -36,7 +36,7 @@ export const testHandler: Handler = async (ctx) => { }; export const getTestRouter = () => { - const router = new Router(); + const router = new HttpRouter(); router.addRoute( "/test", [testMiddleware1, testMiddleware2, testMiddleware3], diff --git a/tests/mocks/profileRouter.ts b/tests/mocks/profileRouter.ts index 913c9f8c..916bf0d9 100644 --- a/tests/mocks/profileRouter.ts +++ b/tests/mocks/profileRouter.ts @@ -1,4 +1,4 @@ -import { Router } from "../../lib/Router.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; import { testMiddleware2, testMiddleware3, @@ -6,7 +6,7 @@ import { testMiddleware1, } from "./middleware.ts"; -const router = new Router(); +const router = new HttpRouter(); router.addRoute( "/test", [testMiddleware1, testMiddleware2, testMiddleware3], diff --git a/tests/utils/CacheItem_test.ts b/tests/utils/CacheItem_test.ts index b363ec8a..a109d3ec 100644 --- a/tests/utils/CacheItem_test.ts +++ b/tests/utils/CacheItem_test.ts @@ -1,5 +1,5 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; Deno.test("UTIL: CacheItem", async (t) => { @@ -16,7 +16,7 @@ Deno.test("UTIL: CacheItem", async (t) => { }); await t.step("defaultKeyGen generates correct key", () => { - const mockRouter = new Router(); + const mockRouter = new HttpRouter(); const mockRequest = new Request("http://localhost:3000/path?query=param"); const mockState = { user: "Alice" }; diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index 28c4129f..7161877c 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -1,5 +1,5 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router, RequestContext } from "../../lib/Router.ts"; +import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; import { Cascade } from "../../lib/utils/Cascade.ts"; import { testMiddleware1, @@ -9,7 +9,7 @@ import { } from "../../tests/mocks/middleware.ts"; Deno.test("UTIL: Cascade", async (t) => { - const testServer = new Router(); + const testServer = new HttpRouter(); const testContext = new RequestContext( testServer, new Request("http://localhost") diff --git a/tests/utils/Profiler_test.ts b/tests/utils/Profiler_test.ts index 29835082..223349e5 100644 --- a/tests/utils/Profiler_test.ts +++ b/tests/utils/Profiler_test.ts @@ -1,9 +1,9 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/Router.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; import { Profiler } from "../../lib/utils/Profiler.ts"; Deno.test("UTIL: Profiler", async (t) => { - const router = new Router(); + const router = new HttpRouter(); router.addRoute("/hello", () => { return new Response("Hello, World!"); From 1fb8db2b11dc977f006272bfaeb3dbcb57e2b20f Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Tue, 27 Aug 2024 21:49:30 +0100 Subject: [PATCH 03/12] fix: reactSSR example --- example/reactSSR/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/reactSSR/router.ts b/example/reactSSR/router.ts index bf9b7f7d..6cd09718 100644 --- a/example/reactSSR/router.ts +++ b/example/reactSSR/router.ts @@ -1,4 +1,4 @@ -import { Router, logger, cacher } from "../../mod.ts"; //"https://deno.land/x/peko/mod.ts" +import { HttpRouter, logger, cacher } from "../../mod.ts"; //"https://deno.land/x/peko/mod.ts" import { reactHandler } from "./handlers/react.handler.ts"; import { githubHandler } from "./handlers/github.handler.ts"; import { reqTime } from "./middleware/reqTime.middleware.ts"; @@ -7,7 +7,7 @@ import { parrotHandler } from "./handlers/parrot.handler.ts"; import About from "./src/pages/About.tsx"; import Home from "./src/pages/Home.tsx"; -const router = new Router(); +const router = new HttpRouter(); router.use(logger(console.log)); // SSR, with cache because static page From 3fb404d59d987308cc2eba508e120e0602007a15 Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 28 Aug 2024 01:39:13 +0100 Subject: [PATCH 04/12] feat: base router class --- lib/routers/_router.ts | 158 ++++++++++++++++++ lib/routers/httpRouter.ts | 155 ++++------------- lib/routers/schemaRouter.ts | 158 ++---------------- lib/types.ts | 91 +--------- lib/utils/CacheItem.ts | 2 +- lib/utils/Cascade.ts | 3 +- lib/utils/Profiler.ts | 8 +- tests/handlers/file_test.ts | 4 +- tests/handlers/sse_test.ts | 5 +- tests/handlers/ssr_test.ts | 5 +- tests/middleware/authenticator_test.ts | 5 +- tests/middleware/cacher_test.ts | 5 +- tests/middleware/logger_test.ts | 5 +- .../_router_test.ts} | 49 +----- tests/routers/httpRouter_test.ts | 39 +++++ tests/utils/CacheItem_test.ts | 5 +- tests/utils/Cascade_test.ts | 5 +- 17 files changed, 284 insertions(+), 418 deletions(-) create mode 100644 lib/routers/_router.ts rename tests/{Router_test.ts => routers/_router_test.ts} (74%) create mode 100644 tests/routers/httpRouter_test.ts diff --git a/lib/routers/_router.ts b/lib/routers/_router.ts new file mode 100644 index 00000000..9ab68fcf --- /dev/null +++ b/lib/routers/_router.ts @@ -0,0 +1,158 @@ +import { Handler, Middleware, RequestContext } from "../types.ts"; +import { Cascade } from "../utils/Cascade.ts"; + +export interface RouteConfig { + path: string; + method?: string; + middleware?: Middleware | Middleware[]; + handler: Handler; +} + +export class Route { + path: string; + middleware: Middleware[]; + handler: Handler; + method: string; + + constructor(routeObj: RouteConfig) { + this.path = routeObj.path; + this.method = routeObj.method || ""; + this.handler = Cascade.promisify(routeObj.handler!) as Handler; + this.middleware = [routeObj.middleware] + .flat() + .filter(Boolean) + .map((mware) => Cascade.promisify(mware!)); + } + + get regexPath() { + return new RegExp(this.path); + } + + match(ctx: RequestContext): boolean { + return this.regexPath.test(ctx.url.pathname); + } +} + +export class Router { + Route = Route; + + constructor( + public routes: Route[] = [], // <- use this as a hashmap for routes + public middleware: Middleware[] = [] + ) {} + + /** + * Running Request through middleware cascade for Response. + * @param request: Request + * @returns Promise + */ + async handle(request: Request): Promise { + const ctx = new RequestContext(this, request); + const res = await new Cascade(ctx, this.middleware).run(); + return res ? res : new Response("", { status: 404 }); + } + + /** + * Add global middleware or another router's middleware + * @param middleware: Middleware[] | Middleware | Router + * @returns number - server.middleware.length + */ + use(middleware: Middleware | Middleware[]) { + if (Array.isArray(middleware)) { + middleware.forEach((mware) => this.use(mware)); + } else { + this.middleware.push(Cascade.promisify(middleware)); + } + return this; + } + + /** + * Add Route + * @param route: Route | Route["path"] + * @param arg2?: Partial | Middleware | Middleware[], + * @param arg3?: Handler + * @returns route: Route - added route object + */ + addRoute(route: RouteConfig): Route; + addRoute(route: RouteConfig["path"], data: Handler): Route; + addRoute( + route: RouteConfig["path"], + middleware: Middleware | Middleware[], + handler: Handler + ): Route; + addRoute( + arg1: RouteConfig | Route["path"], + arg2?: Middleware | Middleware[], + arg3?: Handler + ): Route { + // overload resolution + const routeObj: RouteConfig = + typeof arg1 !== "string" + ? arg1 + : arguments.length === 2 + ? { path: arg1, handler: arg2 as Handler } + : { + path: arg1, + middleware: arg2 as Middleware | Middleware[], + handler: arg3 as Handler, + }; + + // create new Route object + const fullRoute = new this.Route(routeObj); + + // check if route already exists + if ( + this.routes.find( + (existing) => + existing.regexPath.toString() === fullRoute.regexPath.toString() + ) + ) { + throw new Error(`Route with path ${routeObj.path} already exists!`); + } + + // add route to appropriate routes and middleware + this.routes.push(fullRoute); + this.middleware.push(function RouteMiddleware(ctx) { + if (fullRoute.match(ctx)) { + return new Cascade(ctx, [ + ...fullRoute.middleware, + fullRoute.handler, + ]).run(); + } + }); + + return fullRoute; + } + + /** + * Add Routes + * @param routes: Route[] - middleware can be Middlewares or Middleware + * @returns Route[] - added routes + */ + addRoutes(routes: RouteConfig[]): Route[] { + return routes.map((route) => this.addRoute(route)); + } + + /** + * Remove Route from Peko server + * @param route: Route["path"] of route to remove + * @returns Route - removed route + */ + removeRoute(route: Route["path"]): Route | undefined { + const routeToRemove = this.routes.find((r) => r.path === route); + if (routeToRemove) { + this.routes.splice(this.routes.indexOf(routeToRemove), 1); + } + + return routeToRemove; + } + + /** + * Remove Routes + * @param routes: Route["path"] of routes to remove + * @returns Array - removed routes + */ + removeRoutes(routes: Route["path"][]): Array { + return routes.map((route) => this.removeRoute(route)); + } +} diff --git a/lib/routers/httpRouter.ts b/lib/routers/httpRouter.ts index f1904f65..04cced68 100644 --- a/lib/routers/httpRouter.ts +++ b/lib/routers/httpRouter.ts @@ -1,131 +1,58 @@ -import { Cascade } from "../utils/Cascade.ts"; -import { - Middleware, - Handler, - BaseRouter, - BaseRoute, - RequestContext, -} from "../types.ts"; +import { Middleware, Handler, RequestContext } from "../types.ts"; +import { Route, Router, RouteConfig } from "./_router.ts"; -export interface HttpRouteConfig extends Partial { +export interface HttpRouteConfig extends RouteConfig { path: `/${string}`; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; handler: Handler; } -export class HttpRoute implements BaseRoute { - path: `/${string}`; - regexPath: RegExp; - middleware: Middleware[] = []; - handler: Handler; - params: Record = {}; - method: "GET" | "POST" | "PUT" | "DELETE"; +export class HttpRoute extends Route { + declare path: `/${string}`; + declare method: HttpRouteConfig["method"]; constructor(routeObj: HttpRouteConfig) { - this.path = - routeObj.path[routeObj.path.length - 1] === "/" - ? (routeObj.path.slice(0, -1) as HttpRoute["path"]) - : (routeObj.path as HttpRoute["path"]); + super(routeObj); + this.method = routeObj.method || "GET"; + } + get params() { + const x: Record = {}; this.path.split("/").forEach((str, i) => { - if (str[0] === ":") this.params[str.slice(1)] = i; + if (str[0] === ":") x[str.slice(1)] = i; }); + return x; + } - this.regexPath = this.params + get regexPath() { + return this.params ? new RegExp( `^${this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*")}\/?$` ) : new RegExp(`^${this.path}\/?$`); + } - this.method = (routeObj.method as HttpRoute["method"]) || "GET"; - this.handler = Cascade.promisify(routeObj.handler!) as Handler; - this.middleware = [routeObj.middleware] - .flat() - .filter(Boolean) - .map((mware) => Cascade.promisify(mware!)); + match(ctx: RequestContext): boolean { + if ( + this.regexPath.test(ctx.url.pathname) && + this.method === ctx.request.method + ) { + const pathBits = ctx.url.pathname.split("/"); + for (const param in this.params) { + ctx.params[param] = pathBits[this.params[param]]; + } + return true; + } + return false; } } -export class HttpRouter implements BaseRouter { +export class HttpRouter extends Router { constructor( public routes: HttpRoute[] = [], public middleware: Middleware[] = [] - ) {} - - async handle(request: Request): Promise { - const ctx = new RequestContext(this, request); - const res = await new Cascade(ctx, this.middleware).run(); - return res ? res : new Response("", { status: 404 }); - } - - use(middleware: Middleware | Middleware[]) { - if (Array.isArray(middleware)) { - middleware.forEach((mware) => this.use(mware)); - } else { - this.middleware.push(Cascade.promisify(middleware)); - } - return this; - } - - addRoute(route: HttpRouteConfig): HttpRoute; - addRoute(route: HttpRouteConfig["path"], data: Handler): HttpRoute; - addRoute( - route: HttpRouteConfig["path"], - middleware: Middleware | Middleware[], - handler: Handler - ): HttpRoute; - addRoute( - arg1: HttpRouteConfig | HttpRoute["path"], - arg2?: Middleware | Middleware[], - arg3?: Handler - ): HttpRoute { - // overload resolution - const routeObj: HttpRouteConfig = - typeof arg1 !== "string" - ? arg1 - : arguments.length === 2 - ? { path: arg1, handler: arg2 as Handler } - : { - path: arg1, - middleware: arg2 as Middleware | Middleware[], - handler: arg3 as Handler, - }; - - // create new Route object - const fullRoute = new HttpRoute(routeObj); - - // check if route already exists - if ( - this.routes.find( - (existing) => - existing.regexPath.toString() === fullRoute.regexPath.toString() - ) - ) { - throw new Error(`Route with path ${routeObj.path} already exists!`); - } - - // add route to appropriate routes and middleware - this.routes.push(fullRoute); - this.middleware.push(function RouteMiddleware(ctx) { - if ( - fullRoute.regexPath.test(ctx.url.pathname) && - fullRoute.method === ctx.request.method - ) { - if (fullRoute?.params) { - const pathBits = ctx.url.pathname.split("/"); - for (const param in fullRoute.params) { - ctx.params[param] = pathBits[fullRoute.params[param]]; - } - } - - return new Cascade(ctx, [ - ...fullRoute.middleware, - fullRoute.handler, - ]).run(); - } - }); - - return fullRoute; + ) { + super(); } get: typeof this.addRoute = function () { @@ -155,23 +82,5 @@ export class HttpRouter implements BaseRouter { newRoute.method = "DELETE"; return newRoute; }; - - addRoutes(routes: HttpRouteConfig[]): HttpRoute[] { - return routes.map((route) => this.addRoute(route)); - } - - removeRoute(route: HttpRoute["path"]): HttpRoute | undefined { - const routeToRemove = this.routes.find((r) => r.path === route); - if (routeToRemove) { - this.routes.splice(this.routes.indexOf(routeToRemove), 1); - } - - return routeToRemove; - } - - removeRoutes(routes: HttpRoute["path"][]): Array { - return routes.map((route) => this.removeRoute(route)); - } } export { RequestContext }; - diff --git a/lib/routers/schemaRouter.ts b/lib/routers/schemaRouter.ts index f523b1b2..eec1a63f 100644 --- a/lib/routers/schemaRouter.ts +++ b/lib/routers/schemaRouter.ts @@ -1,164 +1,26 @@ -import { Cascade } from "../utils/Cascade.ts"; -import { - Middleware, - Handler, - BaseRoute, - BaseRouteConfig, - RequestContext, -} from "../types.ts"; +import { Middleware } from "../types.ts"; +import { Router, Route, RouteConfig } from "./_router.ts"; -export interface GraphRouteConfig extends BaseRouteConfig { +export interface GraphRouteConfig extends RouteConfig { method?: "QUERY" | "MUTATION" | "RESOLVER"; } -export class GraphRoute implements BaseRoute { - path: string; - params: Record = {}; - regexPath: RegExp; - method: "QUERY" | "MUTATION" | "RESOLVER"; - middleware: Middleware[] = []; - handler: Handler; +export class GraphRoute extends Route { + declare method: GraphRouteConfig["method"]; constructor(routeObj: GraphRouteConfig) { - this.path = routeObj.path; - this.regexPath = new RegExp(`^${this.path}$`); + super(routeObj); this.method = (routeObj.method as GraphRoute["method"]) || "QUERY"; - this.handler = Cascade.promisify(routeObj.handler!) as Handler; - this.middleware = [routeObj.middleware] - .flat() - .filter(Boolean) - .map((mware) => Cascade.promisify(mware!)); } } -export class GraphRouter { +export class GraphRouter extends Router { constructor( public routes: GraphRoute[] = [], public middleware: Middleware[] = [] - ) {} - - /** - * Running Request through middleware cascade for Response. - * @param request: Request - * @returns Promise - */ - async handle(request: Request): Promise { - const ctx = new RequestContext(this, request); - const res = await new Cascade(ctx, this.middleware).run(); - return res ? res : new Response("", { status: 404 }); - } - - /** - * Add global middleware or another router's middleware - * @param middleware: Middleware[] | Middleware | Router - * @returns number - server.middleware.length - */ - use(middleware: Middleware | Middleware[]) { - if (Array.isArray(middleware)) { - middleware.forEach((mware) => this.use(mware)); - } else { - this.middleware.push(Cascade.promisify(middleware)); - } - return this; - } - - /** - * Add Route - * @param route: Route | Route["path"] - * @param arg2?: Partial | Middleware | Middleware[], - * @param arg3?: Handler - * @returns route: Route - added route object - */ - addRoute(route: GraphRouteConfig): GraphRoute; - addRoute(route: GraphRouteConfig["path"], data: Handler): GraphRoute; - addRoute( - route: GraphRouteConfig["path"], - middleware: Middleware | Middleware[], - handler: Handler - ): GraphRoute; - addRoute( - arg1: GraphRouteConfig | GraphRouteConfig["path"], - arg2?: Middleware | Middleware[], - arg3?: Handler - ): GraphRoute { - // overload resolution - const routeObj: GraphRouteConfig = - typeof arg1 !== "string" - ? arg1 - : arguments.length === 2 - ? { path: arg1, handler: arg2 as Handler } - : { - path: arg1, - middleware: arg2 as Middleware | Middleware[], - handler: arg3 as Handler, - }; - - // create new Route object - const fullRoute = new GraphRoute(routeObj as GraphRouteConfig); - - // check if route already exists - if ( - this.routes.find( - (existing) => - existing.regexPath.toString() === fullRoute.regexPath.toString() - ) - ) { - throw new Error(`Route with path ${routeObj.path} already exists!`); - } - - // add route to appropriate routes and middleware - this.routes.push(fullRoute); - this.middleware.push(function RouteMiddleware(ctx) { - if ( - fullRoute.regexPath.test(ctx.url.pathname) && - fullRoute.method === ctx.request.method - ) { - if (fullRoute?.params) { - const pathBits = ctx.url.pathname.split("/"); - for (const param in fullRoute.params) { - ctx.params[param] = pathBits[fullRoute.params[param]]; - } - } - - return new Cascade(ctx, [ - ...fullRoute.middleware, - fullRoute.handler, - ]).run(); - } - }); - - return fullRoute; - } - - /** - * Add Routes - * @param routes: Route[] - middleware can be Middlewares or Middleware - * @returns Route[] - added routes - */ - addRoutes(routes: GraphRouteConfig[]): GraphRoute[] { - return routes.map((route) => this.addRoute(route)); + ) { + super(); } - /** - * Remove Route from Peko server - * @param route: Route["path"] of route to remove - * @returns Route - removed route - */ - removeRoute(route: GraphRouteConfig["path"]): GraphRoute | undefined { - const routeToRemove = this.routes.find((r) => r.path === route); - if (routeToRemove) { - this.routes.splice(this.routes.indexOf(routeToRemove), 1); - } - - return routeToRemove; - } - - /** - * Remove Routes - * @param routes: Route["path"] of routes to remove - * @returns Array - removed routes - */ - removeRoutes(routes: GraphRouteConfig["path"][]): Array { - return routes.map((route) => this.removeRoute(route)); - } + // schema stuff goes here... } diff --git a/lib/types.ts b/lib/types.ts index a86149aa..101d9a02 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,76 +1,14 @@ -export interface BaseRouteConfig { - path: string; - method?: string; - middleware?: Middleware | Middleware[]; - handler: Handler; -} - -export interface BaseRoute { - path: string; - regexPath: RegExp; - middleware: Middleware[] | Middleware; - handler: Handler; - method: string; -} - -export interface BaseRouter { - routes: BaseRoute[]; - middleware: Middleware[]; - - /** - * Running Request through middleware cascade for Response. - * @param request: Request - * @returns Promise - */ - handle(request: Request): Promise; - - /** - * Add global middleware or another router's middleware - * @param middleware: Middleware[] | Middleware | Router - * @returns number - server.middleware.length - */ - use(middleware: Middleware | Middleware[]): void; - - /** - * Add Route - * @param route: Route | Route["path"] - * @param arg2?: Partial | Middleware | Middleware[], - * @param arg3?: Handler - * @returns route: Route - added route object - */ - addRoute(route: BaseRouteConfig): BaseRoute; - addRoute(route: BaseRouteConfig["path"], data: Handler): BaseRoute; - addRoute( - route: BaseRouteConfig["path"], - middleware: Middleware | Middleware[], - handler: Handler - ): BaseRoute; - addRoute( - arg1: BaseRouteConfig | BaseRouteConfig["path"], - arg2?: Middleware | Middleware[], - arg3?: Handler - ): BaseRoute; +import { Router } from "./routers/_router.ts"; - /** - * Add Routes - * @param routes: Route[] - middleware can be Middlewares or Middleware - * @returns Route[] - added routes - */ - addRoutes(routes: BaseRouteConfig[]): BaseRoute[]; - - /** - * Remove Route from Peko server - * @param route: Route["path"] of route to remove - * @returns Route - removed route - */ - removeRoute(route: BaseRoute["path"]): BaseRoute | undefined; +export class RequestContext> { + url: URL; + state: T; + params: Record = {}; - /** - * Remove Routes - * @param routes: Route["path"] of routes to remove - * @returns Array - removed routes - */ - removeRoutes(routes: BaseRoute["path"][]): Array; + constructor(public router: Router, public request: Request, state?: T) { + this.url = new URL(request.url); + this.state = state ? state : ({} as T); + } } export type Result = void | Response | undefined; @@ -83,17 +21,6 @@ export type Middleware = ( export type Handler = (ctx: RequestContext) => Promise | Response; export type HandlerOptions = { headers?: Headers }; -export class RequestContext> { - url: URL; - state: T; - params: Record = {}; - - constructor(public router: BaseRouter, public request: Request, state?: T) { - this.url = new URL(request.url); - this.state = state ? state : ({} as T); - } -} - export type BodyInit = | string | Blob diff --git a/lib/utils/CacheItem.ts b/lib/utils/CacheItem.ts index 4e975609..a2c26657 100644 --- a/lib/utils/CacheItem.ts +++ b/lib/utils/CacheItem.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../routers/httpRouter.ts"; +import { RequestContext } from "../types.ts"; export class CacheItem { key: string; diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts index f899c1bf..967f2d48 100644 --- a/lib/utils/Cascade.ts +++ b/lib/utils/Cascade.ts @@ -1,5 +1,4 @@ -import { RequestContext } from "../types.ts"; -import { Middleware, Result, Next } from "../types.ts"; +import { RequestContext, Middleware, Result, Next } from "../types.ts"; export type PromiseMiddleware = ( ctx: RequestContext, diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts index e1f9385b..404a66d4 100644 --- a/lib/utils/Profiler.ts +++ b/lib/utils/Profiler.ts @@ -1,14 +1,14 @@ -import { BaseRouter, BaseRoute } from "../types.ts"; +import { Router, Route } from "../routers/_router.ts"; type ProfileConfig = { mode?: "serve" | "handle"; url?: string; count?: number; - excludedRoutes?: BaseRoute[]; + excludedRoutes?: Route[]; }; type ProfileResults = Record< -BaseRoute["path"], +Route["path"], { avgTime: number; requests: { @@ -26,7 +26,7 @@ export class Profiler { * @param config * @returns results: ProfileResults */ - static async run(router: BaseRouter, config?: ProfileConfig) { + static async run(router: Router, config?: ProfileConfig) { const url = (config && config.url) || `http://localhost:7777`; const count = (config && config.count) || 100; const excludedRoutes = (config && config.excludedRoutes) || []; diff --git a/tests/handlers/file_test.ts b/tests/handlers/file_test.ts index ecc02ab7..3ed40372 100644 --- a/tests/handlers/file_test.ts +++ b/tests/handlers/file_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { file } from "../../lib/handlers/file.ts"; import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: File", async (t) => { - const server = new HttpRouter(); + const server = new Router(); const ctx = new RequestContext(server, new Request("http://localhost")); const fileURL = new URL(import.meta.url); const decoder = new TextDecoder(); diff --git a/tests/handlers/sse_test.ts b/tests/handlers/sse_test.ts index 0e47ac4a..cbc75744 100644 --- a/tests/handlers/sse_test.ts +++ b/tests/handlers/sse_test.ts @@ -1,9 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { sse } from "../../lib/handlers/sse.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: Server-sent events", async (t) => { - const router = new HttpRouter(); + const router = new Router(); const ctx = new RequestContext(router, new Request("http://localhost")); const eventTarget = new EventTarget(); const decoder = new TextDecoder(); diff --git a/tests/handlers/ssr_test.ts b/tests/handlers/ssr_test.ts index c94d46a4..d3a1d362 100644 --- a/tests/handlers/ssr_test.ts +++ b/tests/handlers/ssr_test.ts @@ -1,9 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { ssr } from "../../lib/handlers/ssr.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: Server-side render", async (t) => { - const server = new HttpRouter(); + const server = new Router(); const ctx = new RequestContext(server, new Request("http://localhost")); const decoder = new TextDecoder(); const cacheControl = "max-age=60, stale-while-revalidate=10"; diff --git a/tests/middleware/authenticator_test.ts b/tests/middleware/authenticator_test.ts index 5ce70a8d..13274a8d 100644 --- a/tests/middleware/authenticator_test.ts +++ b/tests/middleware/authenticator_test.ts @@ -1,12 +1,13 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { authenticator } from "../../lib/middleware/authenticator.ts"; import { Crypto } from "../../lib/utils/Crypto.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Authenticator", async (t) => { const successString = "Authorized!"; const crypto = new Crypto("test_key"); - const server = new HttpRouter(); + const server = new Router(); const testPayload = { iat: Date.now(), diff --git a/tests/middleware/cacher_test.ts b/tests/middleware/cacher_test.ts index 2184f7df..704168b8 100644 --- a/tests/middleware/cacher_test.ts +++ b/tests/middleware/cacher_test.ts @@ -1,11 +1,12 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { cacher } from "../../lib/middleware/cacher.ts"; import { testHandler } from "../mocks/middleware.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Cacher", async (t) => { - const router = new HttpRouter(); + const router = new Router(); const successString = "Success!"; const testData = { foo: "bar", diff --git a/tests/middleware/logger_test.ts b/tests/middleware/logger_test.ts index fee88b16..46f34366 100644 --- a/tests/middleware/logger_test.ts +++ b/tests/middleware/logger_test.ts @@ -1,12 +1,13 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { logger } from "../../lib/middleware/logger.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Logger", async (t) => { const successString = "Success!"; let logOutput: unknown; - const server = new HttpRouter(); + const server = new Router(); const testData = { foo: "bar", diff --git a/tests/Router_test.ts b/tests/routers/_router_test.ts similarity index 74% rename from tests/Router_test.ts rename to tests/routers/_router_test.ts index 51c1209c..b0509a0c 100644 --- a/tests/Router_test.ts +++ b/tests/routers/_router_test.ts @@ -1,14 +1,14 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter } from "../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { testMiddleware2, testMiddleware3, testMiddleware1, testHandler, -} from "./mocks/middleware.ts"; +} from "../mocks/middleware.ts"; Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { - const router = new HttpRouter(); + const router = new Router(); await t.step( "routes added with full route and string arg options", @@ -46,7 +46,7 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { }); await t.step("routers on server can be subsequently editted", () => { - const aRouter = new HttpRouter(); + const aRouter = new Router(); aRouter.addRoutes([ { path: "/route", middleware: [], handler: testHandler }, { path: "/route2", handler: testHandler }, @@ -60,49 +60,14 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { assert(!aRouter.routes.find((route) => route.path === "/route")); assert(aRouter.routes.length === 2); }); - - await t.step("http shorthand methods work correctly", () => { - const router = new HttpRouter(); - - const getRoute = router.get({ - path: "/get", - handler: () => new Response("GET"), - }); - const postRoute = router.post({ - path: "/post", - handler: () => new Response("POST"), - }); - const putRoute = router.put({ - path: "/put", - handler: () => new Response("PUT"), - }); - const deleteRoute = router.delete({ - path: "/delete", - handler: () => new Response("DELETE"), - }); - - assert(router.routes.length === 4); - assert(getRoute.method === "GET"); - assert(postRoute.method === "POST"); - assert(putRoute.method === "PUT"); - assert(deleteRoute.method === "DELETE"); - }); - - await t.step("Params correctly stored", () => { - const router = new HttpRouter(); - router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")); - - assert(router.routes[0].params["id"] === 2); - assert(router.routes[0].params["name"] === 4); - }); }); Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { - const router = new HttpRouter(); + const router = new Router(); router.middleware = []; await t.step("params discovered in RequestContext creation", async () => { - const newRouter = new HttpRouter(); + const newRouter = new Router(); newRouter.addRoute("/hello/:id/world/:name", (ctx) => { return new Response( @@ -144,7 +109,7 @@ Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { return new Response("Error! :(", { status: 500 }); } }); - router.get("/error-test", () => { + router.addRoute("/error-test", () => { throw new Error("Oopsie!"); }); diff --git a/tests/routers/httpRouter_test.ts b/tests/routers/httpRouter_test.ts new file mode 100644 index 00000000..ffc68b91 --- /dev/null +++ b/tests/routers/httpRouter_test.ts @@ -0,0 +1,39 @@ +import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; + +Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { + await t.step("http shorthand methods work correctly", () => { + const router = new HttpRouter(); + + const getRoute = router.get({ + path: "/get", + handler: () => new Response("GET"), + }); + const postRoute = router.post({ + path: "/post", + handler: () => new Response("POST"), + }); + const putRoute = router.put({ + path: "/put", + handler: () => new Response("PUT"), + }); + const deleteRoute = router.delete({ + path: "/delete", + handler: () => new Response("DELETE"), + }); + + assert(router.routes.length === 4); + assert(getRoute.method === "GET"); + assert(postRoute.method === "POST"); + assert(putRoute.method === "PUT"); + assert(deleteRoute.method === "DELETE"); + }); + + await t.step("Params correctly stored", () => { + const router = new HttpRouter(); + router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")); + + assert(router.routes[0].params["id"] === 2); + assert(router.routes[0].params["name"] === 4); + }); +}); diff --git a/tests/utils/CacheItem_test.ts b/tests/utils/CacheItem_test.ts index a109d3ec..0aa1d289 100644 --- a/tests/utils/CacheItem_test.ts +++ b/tests/utils/CacheItem_test.ts @@ -1,6 +1,7 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; +import { RequestContext } from "../../lib/types.ts"; Deno.test("UTIL: CacheItem", async (t) => { await t.step("constructor sets properties correctly", () => { @@ -16,7 +17,7 @@ Deno.test("UTIL: CacheItem", async (t) => { }); await t.step("defaultKeyGen generates correct key", () => { - const mockRouter = new HttpRouter(); + const mockRouter = new Router(); const mockRequest = new Request("http://localhost:3000/path?query=param"); const mockState = { user: "Alice" }; diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index 7161877c..fb4026f2 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -1,5 +1,6 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { HttpRouter, RequestContext } from "../../lib/routers/httpRouter.ts"; +import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/types.ts"; import { Cascade } from "../../lib/utils/Cascade.ts"; import { testMiddleware1, @@ -9,7 +10,7 @@ import { } from "../../tests/mocks/middleware.ts"; Deno.test("UTIL: Cascade", async (t) => { - const testServer = new HttpRouter(); + const testServer = new Router(); const testContext = new RequestContext( testServer, new Request("http://localhost") From c2e9b17043b71febe8d617beb2931b2a7b17c8d1 Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 28 Aug 2024 11:55:10 +0100 Subject: [PATCH 05/12] fix: types across routers --- lib/routers/_router.ts | 31 ++++++++++++++----------------- lib/routers/httpRouter.ts | 6 ++++-- lib/routers/schemaRouter.ts | 4 ++-- mod.ts | 2 +- tests/routers/_router_test.ts | 20 ++------------------ tests/routers/httpRouter_test.ts | 19 ++++++++++++++++++- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/routers/_router.ts b/lib/routers/_router.ts index 9ab68fcf..b095042f 100644 --- a/lib/routers/_router.ts +++ b/lib/routers/_router.ts @@ -3,7 +3,6 @@ import { Cascade } from "../utils/Cascade.ts"; export interface RouteConfig { path: string; - method?: string; middleware?: Middleware | Middleware[]; handler: Handler; } @@ -12,11 +11,9 @@ export class Route { path: string; middleware: Middleware[]; handler: Handler; - method: string; constructor(routeObj: RouteConfig) { this.path = routeObj.path; - this.method = routeObj.method || ""; this.handler = Cascade.promisify(routeObj.handler!) as Handler; this.middleware = [routeObj.middleware] .flat() @@ -33,11 +30,11 @@ export class Route { } } -export class Router { +export class Router { Route = Route; constructor( - public routes: Route[] = [], // <- use this as a hashmap for routes + public routes: R[] = [], // <- use this as a hashmap for routes public middleware: Middleware[] = [] ) {} @@ -73,20 +70,20 @@ export class Router { * @param arg3?: Handler * @returns route: Route - added route object */ - addRoute(route: RouteConfig): Route; - addRoute(route: RouteConfig["path"], data: Handler): Route; + addRoute(route: Config): R; + addRoute(route: Config["path"], data: Handler): R; addRoute( - route: RouteConfig["path"], + route: Config["path"], middleware: Middleware | Middleware[], handler: Handler - ): Route; + ): R; addRoute( - arg1: RouteConfig | Route["path"], + arg1: Config | Config["path"], arg2?: Middleware | Middleware[], arg3?: Handler - ): Route { + ): R { // overload resolution - const routeObj: RouteConfig = + const routeObj = typeof arg1 !== "string" ? arg1 : arguments.length === 2 @@ -111,7 +108,7 @@ export class Router { } // add route to appropriate routes and middleware - this.routes.push(fullRoute); + this.routes.push(fullRoute as R); this.middleware.push(function RouteMiddleware(ctx) { if (fullRoute.match(ctx)) { return new Cascade(ctx, [ @@ -121,7 +118,7 @@ export class Router { } }); - return fullRoute; + return fullRoute as R; } /** @@ -129,7 +126,7 @@ export class Router { * @param routes: Route[] - middleware can be Middlewares or Middleware * @returns Route[] - added routes */ - addRoutes(routes: RouteConfig[]): Route[] { + addRoutes(routes: Config[]): R[] { return routes.map((route) => this.addRoute(route)); } @@ -138,7 +135,7 @@ export class Router { * @param route: Route["path"] of route to remove * @returns Route - removed route */ - removeRoute(route: Route["path"]): Route | undefined { + removeRoute(route: R["path"]): R | undefined { const routeToRemove = this.routes.find((r) => r.path === route); if (routeToRemove) { this.routes.splice(this.routes.indexOf(routeToRemove), 1); @@ -152,7 +149,7 @@ export class Router { * @param routes: Route["path"] of routes to remove * @returns Array - removed routes */ - removeRoutes(routes: Route["path"][]): Array { + removeRoutes(routes: R["path"][]): Array { return routes.map((route) => this.removeRoute(route)); } } diff --git a/lib/routers/httpRouter.ts b/lib/routers/httpRouter.ts index 04cced68..84126ed8 100644 --- a/lib/routers/httpRouter.ts +++ b/lib/routers/httpRouter.ts @@ -47,9 +47,11 @@ export class HttpRoute extends Route { } } -export class HttpRouter extends Router { +export class HttpRouter extends Router { + Route = HttpRoute; + constructor( - public routes: HttpRoute[] = [], + public routes: R[] = [], public middleware: Middleware[] = [] ) { super(); diff --git a/lib/routers/schemaRouter.ts b/lib/routers/schemaRouter.ts index eec1a63f..709e53a4 100644 --- a/lib/routers/schemaRouter.ts +++ b/lib/routers/schemaRouter.ts @@ -14,9 +14,9 @@ export class GraphRoute extends Route { } } -export class GraphRouter extends Router { +export class GraphRouter extends Router { constructor( - public routes: GraphRoute[] = [], + public routes: R[] = [], public middleware: Middleware[] = [] ) { super(); diff --git a/mod.ts b/mod.ts index dcb8d76d..ebee7470 100644 --- a/mod.ts +++ b/mod.ts @@ -2,7 +2,7 @@ * Featherweight apps on the edge */ -// Core classes, functions & types +// Routers & types export * from "./lib/routers/httpRouter.ts"; export * from "./lib/routers/schemaRouter.ts"; export * from "./lib/types.ts"; diff --git a/tests/routers/_router_test.ts b/tests/routers/_router_test.ts index b0509a0c..69200aeb 100644 --- a/tests/routers/_router_test.ts +++ b/tests/routers/_router_test.ts @@ -7,7 +7,7 @@ import { testHandler, } from "../mocks/middleware.ts"; -Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { +Deno.test("ROUTER: Router managing routes", async (t) => { const router = new Router(); await t.step( @@ -62,26 +62,10 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { }); }); -Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { +Deno.test("ROUTER: Router - request handling", async (t) => { const router = new Router(); router.middleware = []; - await t.step("params discovered in RequestContext creation", async () => { - const newRouter = new Router(); - - newRouter.addRoute("/hello/:id/world/:name", (ctx) => { - return new Response( - JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] }) - ); - }); - - const res = await newRouter.handle( - new Request("http://localhost:7777/hello/123/world/bruno") - ); - const json = await res.json() as { id: string; name: string }; - assert(json.id === "123" && json.name === "bruno"); - }); - await t.step("no route found triggers basic 404", async () => { const request = new Request("http://localhost:7777/404"); const response = await router.handle(request); diff --git a/tests/routers/httpRouter_test.ts b/tests/routers/httpRouter_test.ts index ffc68b91..603add74 100644 --- a/tests/routers/httpRouter_test.ts +++ b/tests/routers/httpRouter_test.ts @@ -1,7 +1,7 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; import { HttpRouter } from "../../lib/routers/httpRouter.ts"; -Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { +Deno.test("ROUTER: HttpRouter", async (t) => { await t.step("http shorthand methods work correctly", () => { const router = new HttpRouter(); @@ -36,4 +36,21 @@ Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { assert(router.routes[0].params["id"] === 2); assert(router.routes[0].params["name"] === 4); }); + + await t.step("params discovered in RequestContext creation", async () => { + const newRouter = new HttpRouter(); + + newRouter.addRoute("/hello/:id/world/:name", (ctx) => { + return new Response( + JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] }) + ); + }); + + const res = await newRouter.handle( + new Request("http://localhost:7777/hello/123/world/bruno") + ); + const json = await res.json() as { id: string; name: string }; + assert(json.id === "123" && json.name === "bruno"); + }); }); + From 85d12655b67e08446b7f1750f0c12f042cf1d75e Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 28 Aug 2024 12:00:19 +0100 Subject: [PATCH 06/12] fix: types --- lib/routers/_router.ts | 22 ++++++++++++++-------- lib/utils/Cascade.ts | 9 +++++++-- tests/utils/Cascade_test.ts | 3 ++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/routers/_router.ts b/lib/routers/_router.ts index b095042f..9f25aa94 100644 --- a/lib/routers/_router.ts +++ b/lib/routers/_router.ts @@ -4,17 +4,17 @@ import { Cascade } from "../utils/Cascade.ts"; export interface RouteConfig { path: string; middleware?: Middleware | Middleware[]; - handler: Handler; + handler?: Handler; } export class Route { path: string; middleware: Middleware[]; - handler: Handler; + handler?: Handler; constructor(routeObj: RouteConfig) { this.path = routeObj.path; - this.handler = Cascade.promisify(routeObj.handler!) as Handler; + this.handler = routeObj.handler && Cascade.promisify(routeObj.handler); this.middleware = [routeObj.middleware] .flat() .filter(Boolean) @@ -30,7 +30,10 @@ export class Route { } } -export class Router { +export class Router< + Config extends RouteConfig = RouteConfig, + R extends Route = Route +> { Route = Route; constructor( @@ -107,14 +110,17 @@ export class Router Promise; export type PromiseMiddleware = ( ctx: RequestContext, next: Next @@ -14,7 +17,9 @@ export class Cascade { constructor(public ctx: RequestContext, public middleware: Middleware[]) {} - static promisify(fcn: Middleware): PromiseMiddleware { + static promisify(fcn: Handler): PromiseHandler + static promisify(fcn: Middleware): PromiseMiddleware + static promisify(fcn: Handler | Middleware): PromiseHandler | PromiseMiddleware { return fcn.constructor.name === "AsyncFunction" ? (fcn as PromiseMiddleware) : (ctx, next) => diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index fb4026f2..caffdf32 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -8,6 +8,7 @@ import { testMiddleware3, testHandler, } from "../../tests/mocks/middleware.ts"; +import { Middleware } from "../../mod.ts"; Deno.test("UTIL: Cascade", async (t) => { const testServer = new Router(); @@ -26,7 +27,7 @@ Deno.test("UTIL: Cascade", async (t) => { const result = await cascade.run(); await t.step("promisify works", () => { - const testMW = () => new Response("hello"); + const testMW: Middleware = () => new Response("hello"); const testMWProm = Cascade.promisify(testMW); assert(testMWProm(testContext, () => {}) instanceof Promise); }); From 0521f35c9a64428ec73a17aeb2ef5523be81541a Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 28 Aug 2024 15:34:36 +0100 Subject: [PATCH 07/12] feat: schema utils --- lib/routers/_router.ts | 10 +- lib/routers/schemaRouter.ts | 88 +++++++++- lib/utils/Schema.ts | 316 ++++++++++++++++------------------ tests/mocks/httpRouter.ts | 19 ++ tests/mocks/middleware.ts | 14 +- tests/mocks/profileRouter.ts | 17 -- tests/mocks/schemaRouter.ts | 129 ++++++++++++++ tests/routers/_router_test.ts | 34 ++-- tests/utils/Schema_test.ts | 166 ++---------------- 9 files changed, 415 insertions(+), 378 deletions(-) create mode 100644 tests/mocks/httpRouter.ts delete mode 100644 tests/mocks/profileRouter.ts create mode 100644 tests/mocks/schemaRouter.ts diff --git a/lib/routers/_router.ts b/lib/routers/_router.ts index 9f25aa94..df7c2927 100644 --- a/lib/routers/_router.ts +++ b/lib/routers/_router.ts @@ -74,7 +74,7 @@ export class Router< * @returns route: Route - added route object */ addRoute(route: Config): R; - addRoute(route: Config["path"], data: Handler): R; + addRoute(route: Config["path"], data: Omit | Handler): R; addRoute( route: Config["path"], middleware: Middleware | Middleware[], @@ -82,15 +82,17 @@ export class Router< ): R; addRoute( arg1: Config | Config["path"], - arg2?: Middleware | Middleware[], + arg2?: Middleware | Middleware[] | Omit | Handler, arg3?: Handler ): R { // overload resolution const routeObj = typeof arg1 !== "string" ? arg1 - : arguments.length === 2 - ? { path: arg1, handler: arg2 as Handler } + : arguments.length === 2 + ? arg2 instanceof Function + ? { path: arg1, handler: arg2 as Handler } + : { path: arg1, ...arg2 as Omit } : { path: arg1, middleware: arg2 as Middleware | Middleware[], diff --git a/lib/routers/schemaRouter.ts b/lib/routers/schemaRouter.ts index 709e53a4..8baa9537 100644 --- a/lib/routers/schemaRouter.ts +++ b/lib/routers/schemaRouter.ts @@ -1,26 +1,96 @@ -import { Middleware } from "../types.ts"; +import { Middleware, RequestContext } from "../types.ts"; +import { defaultScalars, DTO, Fields, ResolvedType } from "../utils/Schema.ts"; import { Router, Route, RouteConfig } from "./_router.ts"; -export interface GraphRouteConfig extends RouteConfig { +export type Resolver | DTO[]> = ( + ctx: RequestContext +) => Promise> | ResolvedType; + +export interface SchemaRouteConfig | DTO[]> + extends RouteConfig { method?: "QUERY" | "MUTATION" | "RESOLVER"; + args: Fields; + data: O; + middleware?: Middleware[]; + resolver: Resolver; } -export class GraphRoute extends Route { - declare method: GraphRouteConfig["method"]; +export class SchemaRoute | DTO[]> extends Route { + declare method: SchemaRouteConfig["method"]; + args: Fields; + data: O; + resolver: Resolver; - constructor(routeObj: GraphRouteConfig) { + constructor(routeObj: SchemaRouteConfig) { super(routeObj); - this.method = (routeObj.method as GraphRoute["method"]) || "QUERY"; + this.method = + (routeObj.method as SchemaRouteConfig["method"]) || "QUERY"; + this.args = routeObj.args; + this.data = routeObj.data as O; + this.resolver = routeObj.resolver; } } -export class GraphRouter extends Router { +export class SchemaRouter< + Config extends SchemaRouteConfig> = SchemaRouteConfig< + DTO + >, + R extends SchemaRoute> = SchemaRoute> +> extends Router { + Route = SchemaRoute; + constructor( public routes: R[] = [], - public middleware: Middleware[] = [] + public middleware: Middleware[] = [], + public scalars: Record unknown> = { + ...defaultScalars, + } ) { super(); } - // schema stuff goes here... + // TODO: reimplement handle method to return JSON data and errors from ctx + // (accumulated by routes) + + query | DTO[]>( + path: SchemaRouteConfig["path"], + data: Omit, "path"> + ) { + const input = { + path: path, + method: "QUERY", + ...data, + }; + // @ts-ignore supply overload args∏ + const newRoute = this.addRoute(input); + return newRoute; + } + + mutation | DTO[]>( + path: SchemaRouteConfig["path"], + data: Omit, "path"> + ) { + const input = { + path: path, + method: "MUTATION", + ...data, + }; + // @ts-ignore supply overload args + const newRoute = this.addRoute(input); + return newRoute; + } + + resolver | DTO[]>( + path: SchemaRouteConfig["path"], + data: Omit, "path"> + ) { + const input = { + path: path, + method: "RESOLVER", + ...data, + }; + // @ts-ignore supply overload args + const newRoute = this.addRoute(input); + return newRoute; + } } diff --git a/lib/utils/Schema.ts b/lib/utils/Schema.ts index a6b2dc24..0430f639 100644 --- a/lib/utils/Schema.ts +++ b/lib/utils/Schema.ts @@ -1,219 +1,199 @@ +import { SchemaRoute } from "../routers/schemaRouter.ts"; import { RequestContext } from "../types.ts"; -import { Middleware } from "../types.ts"; export class ID extends String {} export class Int extends Number {} export class Float extends Number {} -export const defaultScalars = { ID, Int, Float, Boolean, Date, String }; -export type Scalars = (typeof defaultScalars)[keyof typeof defaultScalars]; - -export class Enum> { +export class Enum = Record> { constructor(public name: string, public values: T) {} } +export const defaultScalars = { ID, Int, Float, Boolean, Date, String }; +type Scalars = (typeof defaultScalars)[keyof typeof defaultScalars]; type GroupedTypes = Scalars | Enum> | DTO; -type FieldType = GroupedTypes | GroupedTypes[]; -interface Fields { - [key: string]: Field; -} +export type FieldType = GroupedTypes | GroupedTypes[]; interface FieldConfig { nullable?: boolean; validator?: ( x: ResolvedType ) => boolean | { pass: boolean; message: string }; - resolver?: (x: RequestContext) => Promise>[]>; + resolver?: ( + x: RequestContext + ) => Promise>[]> | ResolvedField>[]; } export class Field { constructor(public type: T, public config: FieldConfig = {}) {} } - -interface DTOConfig { - fields: F; -} -export class DTO { - constructor(public name: string, public config: DTOConfig) {} -} -export class Input extends DTO {} -export class Type extends DTO {} - -export class Query | Type[]> { - public type = "query"; - constructor(public name: string, public config: QueryConfig) {} -} - -export class Mutation< - O extends Type | Type[] -> extends Query { - public type = "mutation"; +export interface Fields { + [key: string]: Field; } -interface QueryConfig | Type[]> { - args: Fields; - data: O; - resolver: (ctx: RequestContext) => Promise>>; - middleware?: Middleware[]; +export class DTO { + constructor(public name: string, public fields: F) {} } type ResolvedFields = { [P in keyof Fields]: ResolvedField; }; -type ResolvedField = T extends Field +export type ResolvedField = T extends Field ? F extends GroupedTypes[] ? ResolvedType[] : ResolvedType : never; -export type ResolvedType = T extends Type +export type ResolvedType = T extends DTO ? ResolvedFields : T extends Scalars ? InstanceType : T extends Enum> ? T["values"][keyof T["values"]] + : T extends Array // New condition for handling arrays + ? ResolvedType[] : never; -export class Schema { - public scalars: Record = { - ...defaultScalars, - }; +export const generateSchema = ({ + scalars, + routes, +}: { + scalars: Record unknown>; + routes: SchemaRoute>[]; +}) => { + let schema = "# GraphQL Schema (autogenerated)\n\n"; + schema += generateScalarSchema(scalars); + schema += "\n\n"; + schema += generateEnumSchema(routes); + + schema += "type Query {\n"; + schema += generateOperationsSchema("QUERY", routes); + schema += "\n}\n\n"; + + schema += "type Mutation {\n"; + schema += generateOperationsSchema("MUTATION", routes); + schema += "\n}\n\n"; + + schema += generateDtosSchema(routes); + schema += "\n\n"; + + return schema; +}; - constructor( - public operations: Query | Type[]>[], - additionalScalars: Record = {} - ) { - Object.keys(additionalScalars).forEach( - (key) => (this.scalars[key] = additionalScalars[key]) - ); - } - - toString() { - let schema = "# GraphQL Schema (autogenerated)\n\n"; - schema += this.generateScalars(); - schema += "\n\n"; - schema += this.generateEnums(); - - schema += "type Query {\n"; - schema += this.generateOperationFields("query"); - schema += "\n}\n\n"; - - schema += "type Mutation {\n"; - schema += this.generateOperationFields("mutation"); - schema += "\n}\n\n"; - - schema += this.generateDTOs(); - schema += "\n\n"; - - return schema; - } - - private generateScalars(): string { - return Object.keys(this.scalars) - .map((key) => `scalar ${key}`) - .join("\n"); - } - - private generateEnums(): string { - const enums = new Set>(); - let enumsString = ""; - - const collectFromFields = (fields: Fields) => { - Object.values(fields).forEach((field) => { - if (field instanceof Input || field instanceof Type) { - collectFromFields(field.config.fields); - } else { - const fieldType = field.type; - if (fieldType instanceof Enum) { - enums.add(fieldType); - } else if (Array.isArray(fieldType) && fieldType[0] instanceof Enum) { - enums.add(fieldType[0]); - } - } - }); - }; +export const generateScalarSchema = ( + scalars: Record unknown> +): string => { + return Object.keys(scalars) + .map((key) => `scalar ${key}`) + .join("\n"); +}; + +export const generateEnumSchema = ( + routes: SchemaRoute>[] +): string => { + const enums = new Set(); + let enumsString = ""; - this.operations.forEach((operation) => { - collectFromFields(operation.config.args); - if (Array.isArray(operation.config.data)) { - collectFromFields(operation.config.data[0].config.fields); + const collectFromFields = (fields: Fields) => { + Object.values(fields).forEach((field) => { + if (field instanceof DTO) { + collectFromFields(field.fields); } else { - collectFromFields(operation.config.data.config.fields); + const fieldType = field.type; + if (fieldType instanceof Enum) { + enums.add(fieldType); + } else if (Array.isArray(fieldType) && fieldType[0] instanceof Enum) { + enums.add(fieldType[0]); + } } }); + }; - enums.forEach((enumType) => { - enumsString += `enum ${enumType.name} {\n`; - Object.values(enumType.values).forEach((value) => { - enumsString += ` ${value}\n`; - }); - enumsString += "}\n\n"; - }); - - return enumsString; - } - - private generateOperationFields(type: "query" | "mutation"): string { - return this.operations - .filter((operation) => operation.type === type) - .map((operation) => { - const args = Object.entries(operation.config.args) - .map(([name, field]) => `${name}: ${this.generateFieldString(field)}`) - .join(", "); - const outputType = Array.isArray(operation.config.data) - ? `[${operation.config.data[0].name}]!` - : `${operation.config.data.name}!`; - return ` ${operation.name}(${args}): ${outputType}`; - }) - .join("\n"); - } - - private generateDTOs(): string { - const DTOs = new Set<{ - dtoType: "input" | "type"; - dto: DTO | DTO[]; - }>(); - let dtoString = ""; - - this.operations.forEach((operation) => { - Object.values(operation.config.args).forEach((arg) => { - if (arg.type instanceof Input) { - DTOs.add({ dtoType: "input", dto: arg.type }); - } - }); - DTOs.add({ dtoType: "type", dto: operation.config.data }); + routes.forEach((route) => { + collectFromFields(route.args); + if (Array.isArray(route.data)) { + collectFromFields(route.data[0].fields); + } else { + collectFromFields(route.data.fields); + } + }); + + enums.forEach((enumType) => { + enumsString += `enum ${enumType.name} {\n`; + Object.values(enumType.values).forEach((value) => { + enumsString += ` ${value}\n`; }); + enumsString += "}\n\n"; + }); - DTOs.forEach((dto) => { - dtoString += this.generateDTOFields(dto); - }); + return enumsString; +}; - return dtoString; - } +export const generateOperationsSchema = ( + type: "QUERY" | "MUTATION", + routes: SchemaRoute>[] +): string => { + return routes + .filter((route) => route.method === type) + .map((route) => { + const args = Object.entries(route.args) + .map(([name, field]) => `${name}: ${generateFieldString(field)}`) + .join(", "); + const outputType = Array.isArray(route.data) + ? `[${route.data[0].name}]!` + : `${route.data.name}!`; + return ` ${route.path}(${args}): ${outputType}`; + }) + .join("\n"); +}; - private generateDTOFields(input: { +export const generateDtosSchema = ( + routes: SchemaRoute>[] +): string => { + const DTOs = new Set<{ dtoType: "input" | "type"; dto: DTO | DTO[]; - }): string { - const dtoInput = input.dto; - const isArray = Array.isArray(dtoInput); - const dto = isArray ? dtoInput[0] : dtoInput; - - const fieldsString = Object.entries(dto.config.fields) - .map(([fieldName, field]) => { - const fieldTypeString = this.generateFieldString(field); - return ` ${fieldName}: ${fieldTypeString}`; - }) - .join("\n"); - - return `${input.dtoType} ${dto.name} {\n${fieldsString}\n}\n\n`; - } - - private generateFieldString(field: Field): string { - const isArray = Array.isArray(field.type); - const baseType = Array.isArray(field.type) ? field.type[0] : field.type; - const typeName = baseType instanceof Type ? baseType.name : baseType.name; - return `${isArray ? `[${typeName}]` : typeName}${ - field.config.nullable ? "" : "!" - }`; - } -} + }>(); + let dtoString = ""; + + routes.forEach((route) => { + Object.values(route.args).forEach((arg) => { + if (arg.type instanceof DTO) { + DTOs.add({ dtoType: "input", dto: arg.type }); + } + }); + DTOs.add({ dtoType: "type", dto: route.data }); + }); + + DTOs.forEach((dto) => { + dtoString += generateDtoSchema(dto); + }); + + return dtoString; +}; + +const generateDtoSchema = (input: { + dtoType: "input" | "type"; + dto: DTO | DTO[]; +}): string => { + const dtoInput = input.dto; + const isArray = Array.isArray(dtoInput); + const dto = isArray ? dtoInput[0] : dtoInput; + + const fieldsString = Object.entries(dto.fields) + .map(([fieldName, field]) => { + const fieldTypeString = generateFieldString(field); + return ` ${fieldName}: ${fieldTypeString}`; + }) + .join("\n"); + + return `${input.dtoType} ${dto.name} {\n${fieldsString}\n}\n\n`; +}; + +const generateFieldString = (field: Field): string => { + const isArray = Array.isArray(field.type); + const baseType = Array.isArray(field.type) ? field.type[0] : field.type; + const typeName = baseType instanceof DTO ? baseType.name : baseType.name; + return `${isArray ? `[${typeName}]` : typeName}${ + field.config.nullable ? "" : "!" + }`; +}; diff --git a/tests/mocks/httpRouter.ts b/tests/mocks/httpRouter.ts new file mode 100644 index 00000000..8a70106f --- /dev/null +++ b/tests/mocks/httpRouter.ts @@ -0,0 +1,19 @@ +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; +import { + testMiddleware2, + testMiddleware3, + testHandler, + testMiddleware1, +} from "./middleware.ts"; + +export const mockHttpRouter = () => { + const router = new HttpRouter(); + router.addRoute( + "/test", + [testMiddleware1, testMiddleware2, testMiddleware3], + testHandler + ); + router.get("/bench", () => new Response("Hello, bench!")); + return router; +}; + diff --git a/tests/mocks/middleware.ts b/tests/mocks/middleware.ts index 835dcd3a..e6807180 100644 --- a/tests/mocks/middleware.ts +++ b/tests/mocks/middleware.ts @@ -1,5 +1,4 @@ import { Middleware, Handler } from "../../lib/types.ts"; -import { HttpRouter } from "../../lib/routers/httpRouter.ts"; export const testMiddleware1: Middleware = async (ctx, next) => { const start = performance.now(); @@ -33,15 +32,4 @@ export const testHandler: Handler = async (ctx) => { return new Response( JSON.stringify({ ...ctx.state, createdAt: performance.now() }) ); -}; - -export const getTestRouter = () => { - const router = new HttpRouter(); - router.addRoute( - "/test", - [testMiddleware1, testMiddleware2, testMiddleware3], - testHandler - ); - router.get("/bench", () => new Response("Hello, bench!")); - return router; -}; +}; \ No newline at end of file diff --git a/tests/mocks/profileRouter.ts b/tests/mocks/profileRouter.ts deleted file mode 100644 index 916bf0d9..00000000 --- a/tests/mocks/profileRouter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HttpRouter } from "../../lib/routers/httpRouter.ts"; -import { - testMiddleware2, - testMiddleware3, - testHandler, - testMiddleware1, -} from "./middleware.ts"; - -const router = new HttpRouter(); -router.addRoute( - "/test", - [testMiddleware1, testMiddleware2, testMiddleware3], - testHandler -); -router.get("/bench", () => new Response("Hello, bench!")); - -export default router; diff --git a/tests/mocks/schemaRouter.ts b/tests/mocks/schemaRouter.ts new file mode 100644 index 00000000..6be177e6 --- /dev/null +++ b/tests/mocks/schemaRouter.ts @@ -0,0 +1,129 @@ +import { SchemaRouter } from "../../lib/routers/schemaRouter.ts"; +import { DTO, Enum, Field, Int, ResolvedType } from "../../lib/utils/Schema.ts"; + +export const mockSchemaRouter = () => { + const schemaRouter = new SchemaRouter(); + + // User stuff + const emailField = new Field(String, { + validator: (x) => x.includes("@") && x.includes("."), + }); + const ageField = new Field(Int, { + nullable: true, + validator: (x) => typeof x === "number" && x > 0, + }); + const user = new DTO("User", { + email: emailField, + age: ageField, + }); + const mockUser: ResolvedType = { + email: "test@test.com", + age: 20, + }; + + schemaRouter.mutation("RegisterUser", { + args: { + email: emailField, + age: ageField, + }, + data: user, + resolver: () => mockUser, + }); + + // Content stuff + const content = new DTO("Content", { + title: new Field(String), + content: new Field(String), + }); + const mockContent: ResolvedType = { + title: "Hello", + content: "World", + }; + + enum PostStatus { + draft = "draft", + published = "published", + } + + schemaRouter.mutation("CreateContent", { + args: { + content: new Field( + new DTO("CreateContentInput", { + ...content.fields, + }) + ), + }, + data: content, + resolver: () => mockContent, + }); + + // Post stuff + const post = new DTO("Post", { + author: new Field(user, { + resolver: () => [mockUser], + }), + content: new Field(content, { + resolver: () => [mockContent], + }), + status: new Field(new Enum("PostStatus", PostStatus)), + likes: new Field([user], { + resolver: () => [[mockUser]], + }), + }); + const mockPost: ResolvedType = { + author: mockUser, + content: mockContent, + status: PostStatus.published, + likes: [mockUser], + }; + + schemaRouter.mutation("PostContent", { + args: { + content: new Field( + new DTO("PostContentInput", { + ...post.fields, + }) + ), + }, + data: post, + resolver: () => mockPost, + }); + + // Comment stuff + const comment = new DTO("Comment", { + author: new Field(user, { + resolver: () => [mockUser], + }), + post: new Field(post, { + resolver: () => [mockPost], + }), + text: new Field(String), + }); + const mockComment: ResolvedType = { + author: mockUser, + post: mockPost, + text: "Hello", + }; + + schemaRouter.mutation("CommentOnPostInput", { + args: { + comment: new Field( + new DTO("CommentOnPost", { + ...comment.fields, + }) + ), + }, + data: comment, + resolver: () => mockComment, + }); + + schemaRouter.query("GetComments", { + args: { + post: new Field(post), + }, + data: [comment], + resolver: () => [mockComment], + }); + + return schemaRouter; +}; diff --git a/tests/routers/_router_test.ts b/tests/routers/_router_test.ts index 69200aeb..fec00724 100644 --- a/tests/routers/_router_test.ts +++ b/tests/routers/_router_test.ts @@ -13,34 +13,42 @@ Deno.test("ROUTER: Router managing routes", async (t) => { await t.step( "routes added with full route and string arg options", async () => { - router.addRoute("/route1", testHandler); - router.addRoute("/route2", testMiddleware1, testHandler); + router.addRoute("route1", testHandler); + router.addRoute("route2", testMiddleware1, testHandler); + router.addRoute("route3", { + middleware: [testMiddleware1, testMiddleware2], + handler: testHandler, + }) router.addRoute( - "/route3", + "route4", [testMiddleware1, testMiddleware2], testHandler ); - assert(router.routes.length === 3); + assert(router.routes.length === 4); const request1 = new Request("http://localhost:7777/route1"); const request2 = new Request("http://localhost:7777/route2"); const request3 = new Request("http://localhost:7777/route3"); + const request4 = new Request("http://localhost:7777/route4"); const response1 = await router.handle(request1); const response2 = await router.handle(request2); const response3 = await router.handle(request3); + const response4 = await router.handle(request4); assert(response1.status === 200); assert(response2.status === 200); assert(response3.status === 200); + assert(response4.status === 200); } ); await t.step("routes removed", () => { - router.removeRoute("/route1"); - router.removeRoute("/route2"); - router.removeRoute("/route3"); + router.removeRoute("route1"); + router.removeRoute("route2"); + router.removeRoute("route3"); + router.removeRoute("route4"); assert(router.routes.length === 0); }); @@ -48,16 +56,16 @@ Deno.test("ROUTER: Router managing routes", async (t) => { await t.step("routers on server can be subsequently editted", () => { const aRouter = new Router(); aRouter.addRoutes([ - { path: "/route", middleware: [], handler: testHandler }, - { path: "/route2", handler: testHandler }, - { path: "/route3", handler: testHandler }, + { path: "route", middleware: [], handler: testHandler }, + { path: "route2", handler: testHandler }, + { path: "route3", handler: testHandler }, ]); aRouter.use(aRouter.middleware); - aRouter.removeRoute("/route"); + aRouter.removeRoute("route"); - assert(!aRouter.routes.find((route) => route.path === "/route")); + assert(!aRouter.routes.find((route) => route.path === "route")); assert(aRouter.routes.length === 2); }); }); @@ -106,7 +114,7 @@ Deno.test("ROUTER: Router - request handling", async (t) => { await t.step("all middleware and handlers run", async () => { router.addRoute({ - path: "/test", + path: "test", middleware: [testMiddleware1, testMiddleware2, testMiddleware3], handler: testHandler, }); diff --git a/tests/utils/Schema_test.ts b/tests/utils/Schema_test.ts index bbb0e403..f32d2098 100644 --- a/tests/utils/Schema_test.ts +++ b/tests/utils/Schema_test.ts @@ -1,158 +1,16 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { - Type, - Enum, - Field, - Int, - Input, - Schema, - Mutation, - Query, - ResolvedType, -} from "../../lib/utils/Schema.ts"; - -Deno.test("UTIL: Profiler", async (t) => { - const emailField = new Field(String, { - validator: (x) => x.includes("@") && x.includes("."), - }); - - const ageField = new Field(Int, { - nullable: true, - validator: (x) => typeof x === "number" && x > 0, - }); - - const user = new Type("User", { - fields: { - email: emailField, - age: ageField, - }, - }); - - const mockUser: ResolvedType = { - email: "test@test.com", - age: 20, - }; - - const content = new Type("Content", { - fields: { - title: new Field(String), - content: new Field(String), - }, - }); - - const mockContent: ResolvedType = { - title: "Hello", - content: "World", - }; - - enum PostStatus { - draft = "draft", - published = "published", - } - - const post = new Type("Post", { - fields: { - author: new Field(user, { - resolver: async (ctx) => [mockUser], - }), - content: new Field(content, { - resolver: async (ctx) => [mockContent], - }), - status: new Field(new Enum("PostStatus", PostStatus)), - likes: new Field([user], { - resolver: async (ctx) => [[mockUser]], - }), - }, - }); - - const mockPost: ResolvedType = { - author: mockUser, - content: mockContent, - status: PostStatus.published, - likes: [mockUser], - }; - - const comment = new Type("Comment", { - fields: { - author: new Field(user, { - resolver: async (ctx) => [mockUser], - }), - post: new Field(post, { - resolver: async (ctx) => [mockPost], - }), - text: new Field(String), - }, - }); - - const mockComment: ResolvedType = { - author: mockUser, - post: mockPost, - text: "Hello", - }; - - const registerUser = new Mutation("RegisterUser", { - args: { - email: emailField, - age: ageField, - }, - data: user, - resolver: async (ctx) => mockUser, - }); - - const createContent = new Mutation("CreateContent", { - args: { - content: new Field( - new Input("CreateContentInput", { - fields: content.config.fields, - }) - ), - }, - data: content, - resolver: async (ctx) => mockContent, - }); - - const postContent = new Mutation("PostContent", { - args: { - content: new Field( - new Input("PostContentInput", { - fields: post.config.fields, - }) - ), - }, - data: post, - resolver: async (ctx) => mockPost, - }); - - const commentOnPost = new Mutation("CommentOnPostInput", { - args: { - comment: new Field( - new Input("CommentOnPost", { - fields: comment.config.fields, - }) - ), - }, - data: comment, - resolver: async (ctx) => mockComment, - }); - - const getComments = new Query("GetComments", { - args: { - post: new Field(post), - }, - data: [comment], - resolver: async (ctx) => [mockComment], - }); - - await t.step("creates the correct schema string", async () => { - const schema = new Schema([ - registerUser, - createContent, - postContent, - commentOnPost, - getComments, - ]); - - const schemaString = schema.toString(); +import { mockSchemaRouter } from "../mocks/schemaRouter.ts"; +import { generateSchema } from "../../lib/utils/Schema.ts"; + +Deno.test("ROUTER: SchemaRouter", async (t) => { + const router = mockSchemaRouter(); + + await t.step("creates the correct schema string", () => { + const schemaString = generateSchema({ + scalars: router.scalars, + routes: router.routes, + }); + console.log(schemaString); assert(schemaString); }); From ea6892e7e2000910987340718794580fb34fb6aa Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Sun, 1 Sep 2024 15:36:37 +0100 Subject: [PATCH 08/12] fix: dedupe DTOs in schema --- lib/routers/schemaRouter.ts | 12 +-- lib/utils/Schema.ts | 72 ++++++++-------- tests/mocks/schema/_schemaRouter.ts | 62 +++++++++++++ tests/mocks/schema/comment.ts | 19 ++++ tests/mocks/schema/content.ts | 16 ++++ tests/mocks/schema/post.ts | 29 +++++++ tests/mocks/schema/user.ts | 20 +++++ tests/mocks/schemaRouter.ts | 129 ---------------------------- tests/utils/Schema_test.ts | 4 +- 9 files changed, 189 insertions(+), 174 deletions(-) create mode 100644 tests/mocks/schema/_schemaRouter.ts create mode 100644 tests/mocks/schema/comment.ts create mode 100644 tests/mocks/schema/content.ts create mode 100644 tests/mocks/schema/post.ts create mode 100644 tests/mocks/schema/user.ts delete mode 100644 tests/mocks/schemaRouter.ts diff --git a/lib/routers/schemaRouter.ts b/lib/routers/schemaRouter.ts index 8baa9537..68ed6949 100644 --- a/lib/routers/schemaRouter.ts +++ b/lib/routers/schemaRouter.ts @@ -1,25 +1,21 @@ import { Middleware, RequestContext } from "../types.ts"; -import { defaultScalars, DTO, Fields, ResolvedType } from "../utils/Schema.ts"; +import { Scalars, DTO, Fields, ResolvedType } from "../utils/Schema.ts"; import { Router, Route, RouteConfig } from "./_router.ts"; -export type Resolver | DTO[]> = ( - ctx: RequestContext -) => Promise> | ResolvedType; - export interface SchemaRouteConfig | DTO[]> extends RouteConfig { method?: "QUERY" | "MUTATION" | "RESOLVER"; args: Fields; data: O; middleware?: Middleware[]; - resolver: Resolver; + resolver: (ctx: RequestContext) => Promise> | ResolvedType; } export class SchemaRoute | DTO[]> extends Route { declare method: SchemaRouteConfig["method"]; args: Fields; data: O; - resolver: Resolver; + resolver: (ctx: RequestContext) => Promise> | ResolvedType; constructor(routeObj: SchemaRouteConfig) { super(routeObj); @@ -43,7 +39,7 @@ export class SchemaRouter< public routes: R[] = [], public middleware: Middleware[] = [], public scalars: Record unknown> = { - ...defaultScalars, + ...Scalars, } ) { super(); diff --git a/lib/utils/Schema.ts b/lib/utils/Schema.ts index 0430f639..c0df7dc4 100644 --- a/lib/utils/Schema.ts +++ b/lib/utils/Schema.ts @@ -4,14 +4,27 @@ import { RequestContext } from "../types.ts"; export class ID extends String {} export class Int extends Number {} export class Float extends Number {} +export const Scalars = { ID, Int, Float, Boolean, Date, String }; +type ScalarTypes = (typeof Scalars)[keyof typeof Scalars]; + export class Enum = Record> { constructor(public name: string, public values: T) {} } -export const defaultScalars = { ID, Int, Float, Boolean, Date, String }; -type Scalars = (typeof defaultScalars)[keyof typeof defaultScalars]; -type GroupedTypes = Scalars | Enum> | DTO; -export type FieldType = GroupedTypes | GroupedTypes[]; +type SchemaTypes = ScalarTypes | Enum> | DTO; +type FieldType = SchemaTypes | SchemaTypes[]; + +export class DTO { + constructor(public name: string, public fields: F) {} +} + +export interface Fields { + [key: string]: Field; +} + +export class Field { + constructor(public type: T, public config: FieldConfig = {}) {} +} interface FieldConfig { nullable?: boolean; @@ -19,40 +32,30 @@ interface FieldConfig { x: ResolvedType ) => boolean | { pass: boolean; message: string }; resolver?: ( - x: RequestContext - ) => Promise>[]> | ResolvedField>[]; -} -export class Field { - constructor(public type: T, public config: FieldConfig = {}) {} -} -export interface Fields { - [key: string]: Field; + ctx: RequestContext + ) => Promise[]> | ResolvedType[]; } -export class DTO { - constructor(public name: string, public fields: F) {} -} +export type ResolvedType = T extends DTO + ? ResolvedFields + : T extends ScalarTypes + ? InstanceType + : T extends Enum> + ? T["values"][keyof T["values"]] + : T extends Array + ? ResolvedType[] + : never; type ResolvedFields = { [P in keyof Fields]: ResolvedField; }; export type ResolvedField = T extends Field - ? F extends GroupedTypes[] + ? F extends SchemaTypes[] ? ResolvedType[] : ResolvedType : never; -export type ResolvedType = T extends DTO - ? ResolvedFields - : T extends Scalars - ? InstanceType - : T extends Enum> - ? T["values"][keyof T["values"]] - : T extends Array // New condition for handling arrays - ? ResolvedType[] - : never; - export const generateSchema = ({ scalars, routes, @@ -149,23 +152,22 @@ export const generateOperationsSchema = ( export const generateDtosSchema = ( routes: SchemaRoute>[] ): string => { - const DTOs = new Set<{ - dtoType: "input" | "type"; - dto: DTO | DTO[]; - }>(); let dtoString = ""; routes.forEach((route) => { + const results: string[] = []; Object.values(route.args).forEach((arg) => { if (arg.type instanceof DTO) { - DTOs.add({ dtoType: "input", dto: arg.type }); + results.push(generateDtoSchema({ dtoType: "input", dto: arg.type })); } }); - DTOs.add({ dtoType: "type", dto: route.data }); - }); + results.push(generateDtoSchema({ dtoType: "type", dto: route.data })); - DTOs.forEach((dto) => { - dtoString += generateDtoSchema(dto); + results.forEach((result) => { + if (!dtoString.includes(result)) { + dtoString += result; + } + }); }); return dtoString; diff --git a/tests/mocks/schema/_schemaRouter.ts b/tests/mocks/schema/_schemaRouter.ts new file mode 100644 index 00000000..c8ccbea8 --- /dev/null +++ b/tests/mocks/schema/_schemaRouter.ts @@ -0,0 +1,62 @@ +import { SchemaRouter } from "../../../lib/routers/schemaRouter.ts"; +import { DTO, Field } from "../../../lib/utils/Schema.ts"; +import { comment, mockComment } from "./comment.ts"; +import { content, mockContent } from "./content.ts"; +import { mockPost, post } from "./post.ts"; +import { ageField, emailField, mockUser, user } from "./user.ts"; + +export const mockSchemaRouter = () => { + const schemaRouter = new SchemaRouter(); + + schemaRouter.mutation("RegisterUser", { + args: { + email: emailField, + age: ageField, + }, + data: user, + resolver: () => mockUser, + }); + + schemaRouter.mutation("CreateContent", { + args: { + content: new Field( + new DTO("CreateContentInput", { + ...content.fields, + }) + ), + }, + data: content, + resolver: () => mockContent, + }); + + schemaRouter.mutation("PostContent", { + args: { + contentId: new Field(String), + }, + data: post, + resolver: () => mockPost, + }); + + schemaRouter.mutation("CommentOnPostInput", { + args: { + comment: new Field( + new DTO("CommentInput", { + postId: new Field(String), + text: new Field(String), + }) + ), + }, + data: comment, + resolver: () => mockComment, + }); + + schemaRouter.query("GetComments", { + args: { + postId: new Field(String), + }, + data: [comment], + resolver: () => [mockComment], + }); + + return schemaRouter; +}; diff --git a/tests/mocks/schema/comment.ts b/tests/mocks/schema/comment.ts new file mode 100644 index 00000000..1e8cda80 --- /dev/null +++ b/tests/mocks/schema/comment.ts @@ -0,0 +1,19 @@ +import { DTO, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; +// import { mockPost, post } from "./post.ts"; +import { mockUser, user } from "./user.ts"; + +export const comment = new DTO("Comment", { + author: new Field(user, { + resolver: () => [mockUser], + }), + // post: new Field(() => post, { + // resolver: () => mockPost, + // }), + text: new Field(String), +}); + +export const mockComment: ResolvedType = { + author: mockUser, + text: "Hello", + // post: mockPost, +}; diff --git a/tests/mocks/schema/content.ts b/tests/mocks/schema/content.ts new file mode 100644 index 00000000..5ec84797 --- /dev/null +++ b/tests/mocks/schema/content.ts @@ -0,0 +1,16 @@ +import { DTO, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; +import { mockUser, user } from "./user.ts"; + +export const content = new DTO("Content", { + title: new Field(String), + content: new Field(String), + creator: new Field(user, { + resolver: () => [mockUser], + }), +}); + +export const mockContent: ResolvedType = { + title: "Hello", + content: "World", + creator: mockUser, +}; diff --git a/tests/mocks/schema/post.ts b/tests/mocks/schema/post.ts new file mode 100644 index 00000000..6fc4d915 --- /dev/null +++ b/tests/mocks/schema/post.ts @@ -0,0 +1,29 @@ +import { DTO, Enum, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; +import { comment, mockComment } from "./comment.ts"; +import { content, mockContent } from "./content.ts"; +import { mockUser, user } from "./user.ts"; + +enum PostStatus { + draft = "draft", + published = "published", +} + +export const post = new DTO("Post", { + author: new Field(user, { + resolver: () => [mockUser], + }), + content: new Field(content, { + resolver: () => [mockContent], + }), + status: new Field(new Enum("PostStatus", PostStatus)), + comments: new Field([comment], { + resolver: () => [[mockComment]], + }), +}); + +export const mockPost: ResolvedType = { + author: mockUser, + content: mockContent, + status: PostStatus.published, + comments: [mockComment], +}; \ No newline at end of file diff --git a/tests/mocks/schema/user.ts b/tests/mocks/schema/user.ts new file mode 100644 index 00000000..be48f696 --- /dev/null +++ b/tests/mocks/schema/user.ts @@ -0,0 +1,20 @@ +import { DTO, Field, Int, ResolvedType } from "../../../lib/utils/Schema.ts"; + +export const emailField = new Field(String, { + validator: (x) => x.includes("@") && x.includes("."), +}); + +export const ageField = new Field(Int, { + nullable: true, + validator: (x) => typeof x === "number" && x > 0, +}); + +export const user = new DTO("User", { + email: emailField, + age: ageField, +}); + +export const mockUser: ResolvedType = { + email: "test@test.com", + age: 20, +}; diff --git a/tests/mocks/schemaRouter.ts b/tests/mocks/schemaRouter.ts deleted file mode 100644 index 6be177e6..00000000 --- a/tests/mocks/schemaRouter.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { SchemaRouter } from "../../lib/routers/schemaRouter.ts"; -import { DTO, Enum, Field, Int, ResolvedType } from "../../lib/utils/Schema.ts"; - -export const mockSchemaRouter = () => { - const schemaRouter = new SchemaRouter(); - - // User stuff - const emailField = new Field(String, { - validator: (x) => x.includes("@") && x.includes("."), - }); - const ageField = new Field(Int, { - nullable: true, - validator: (x) => typeof x === "number" && x > 0, - }); - const user = new DTO("User", { - email: emailField, - age: ageField, - }); - const mockUser: ResolvedType = { - email: "test@test.com", - age: 20, - }; - - schemaRouter.mutation("RegisterUser", { - args: { - email: emailField, - age: ageField, - }, - data: user, - resolver: () => mockUser, - }); - - // Content stuff - const content = new DTO("Content", { - title: new Field(String), - content: new Field(String), - }); - const mockContent: ResolvedType = { - title: "Hello", - content: "World", - }; - - enum PostStatus { - draft = "draft", - published = "published", - } - - schemaRouter.mutation("CreateContent", { - args: { - content: new Field( - new DTO("CreateContentInput", { - ...content.fields, - }) - ), - }, - data: content, - resolver: () => mockContent, - }); - - // Post stuff - const post = new DTO("Post", { - author: new Field(user, { - resolver: () => [mockUser], - }), - content: new Field(content, { - resolver: () => [mockContent], - }), - status: new Field(new Enum("PostStatus", PostStatus)), - likes: new Field([user], { - resolver: () => [[mockUser]], - }), - }); - const mockPost: ResolvedType = { - author: mockUser, - content: mockContent, - status: PostStatus.published, - likes: [mockUser], - }; - - schemaRouter.mutation("PostContent", { - args: { - content: new Field( - new DTO("PostContentInput", { - ...post.fields, - }) - ), - }, - data: post, - resolver: () => mockPost, - }); - - // Comment stuff - const comment = new DTO("Comment", { - author: new Field(user, { - resolver: () => [mockUser], - }), - post: new Field(post, { - resolver: () => [mockPost], - }), - text: new Field(String), - }); - const mockComment: ResolvedType = { - author: mockUser, - post: mockPost, - text: "Hello", - }; - - schemaRouter.mutation("CommentOnPostInput", { - args: { - comment: new Field( - new DTO("CommentOnPost", { - ...comment.fields, - }) - ), - }, - data: comment, - resolver: () => mockComment, - }); - - schemaRouter.query("GetComments", { - args: { - post: new Field(post), - }, - data: [comment], - resolver: () => [mockComment], - }); - - return schemaRouter; -}; diff --git a/tests/utils/Schema_test.ts b/tests/utils/Schema_test.ts index f32d2098..c66b391c 100644 --- a/tests/utils/Schema_test.ts +++ b/tests/utils/Schema_test.ts @@ -1,5 +1,5 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { mockSchemaRouter } from "../mocks/schemaRouter.ts"; +import { mockSchemaRouter } from "../mocks/schema/_schemaRouter.ts"; import { generateSchema } from "../../lib/utils/Schema.ts"; Deno.test("ROUTER: SchemaRouter", async (t) => { @@ -10,7 +10,7 @@ Deno.test("ROUTER: SchemaRouter", async (t) => { scalars: router.scalars, routes: router.routes, }); - + console.log(schemaString); assert(schemaString); }); From 9c22dc09922f6cbdd50a9173738f4e185276508a Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 11 Sep 2024 10:57:32 +0100 Subject: [PATCH 09/12] feat: base router abstracted --- lib/context.ts | 12 + lib/handlers/graph.ts | 26 -- lib/handlers/ssr.ts | 2 +- lib/routers/_router.ts | 21 +- lib/routers/httpRouter.ts | 20 +- lib/routers/schemaRouter.ts | 92 ----- lib/types.ts | 13 +- lib/utils/CacheItem.ts | 2 +- lib/utils/Cascade.ts | 3 +- lib/utils/Profiler.ts | 8 +- lib/utils/Schema.ts | 340 +++++++++--------- mod.ts | 1 - tests/handlers/file_test.ts | 6 +- tests/handlers/sse_test.ts | 6 +- tests/handlers/ssr_test.ts | 6 +- tests/middleware/authenticator_test.ts | 6 +- tests/middleware/cacher_test.ts | 6 +- tests/middleware/logger_test.ts | 6 +- tests/mocks/httpRouter.ts | 6 +- tests/mocks/schema/_schemaRouter.ts | 62 ---- tests/mocks/schema/comment.ts | 19 - tests/mocks/schema/content.ts | 16 - tests/mocks/schema/post.ts | 29 -- tests/mocks/schema/user.ts | 20 -- .../{_router_test.ts => baseRouter_test.ts} | 22 +- tests/routers/httpRouter_test.ts | 24 +- tests/utils/CacheItem_test.ts | 8 +- tests/utils/Cascade_test.ts | 6 +- tests/utils/Schema_test.ts | 160 ++++++++- 29 files changed, 423 insertions(+), 525 deletions(-) create mode 100644 lib/context.ts delete mode 100644 lib/handlers/graph.ts delete mode 100644 lib/routers/schemaRouter.ts delete mode 100644 tests/mocks/schema/_schemaRouter.ts delete mode 100644 tests/mocks/schema/comment.ts delete mode 100644 tests/mocks/schema/content.ts delete mode 100644 tests/mocks/schema/post.ts delete mode 100644 tests/mocks/schema/user.ts rename tests/routers/{_router_test.ts => baseRouter_test.ts} (86%) diff --git a/lib/context.ts b/lib/context.ts new file mode 100644 index 00000000..8e9f1f4f --- /dev/null +++ b/lib/context.ts @@ -0,0 +1,12 @@ +import { BaseRouter } from "./routers/_Router.ts"; + +export class RequestContext> { + url: URL; + state: T; + params: Record = {}; + + constructor(public router: BaseRouter, public request: Request, state?: T) { + this.url = new URL(request.url); + this.state = state ? state : ({} as T); + } + } \ No newline at end of file diff --git a/lib/handlers/graph.ts b/lib/handlers/graph.ts deleted file mode 100644 index 6e4b9458..00000000 --- a/lib/handlers/graph.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RequestContext } from "../routers/httpRouter.ts"; -import { mergeHeaders } from "../utils/helpers.ts"; -import { Schema } from "../utils/Schema.ts"; -import { Handler, HandlerOptions } from "../types.ts"; - -export interface graphQLHandlerOptions extends HandlerOptions { - loaders: Record Promise>; -} - -export const graphQL = ( - schema: Schema, - opts: graphQLHandlerOptions -): Handler => { - return async function GraphQLHandler(ctx: RequestContext) { - // THIS IS WIP - - return new Response("WIP", { - headers: mergeHeaders( - new Headers({ - "Content-Type": "application/json", - }), - opts.headers - ), - }); - }; -}; diff --git a/lib/handlers/ssr.ts b/lib/handlers/ssr.ts index 80ce3dd2..f3b8e6e2 100644 --- a/lib/handlers/ssr.ts +++ b/lib/handlers/ssr.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../routers/httpRouter.ts"; +import { RequestContext } from "../context.ts"; import { Crypto } from "../utils/Crypto.ts"; import { mergeHeaders } from "../utils/helpers.ts"; import { Handler, HandlerOptions } from "../types.ts"; diff --git a/lib/routers/_router.ts b/lib/routers/_router.ts index df7c2927..e3175eb8 100644 --- a/lib/routers/_router.ts +++ b/lib/routers/_router.ts @@ -1,18 +1,19 @@ -import { Handler, Middleware, RequestContext } from "../types.ts"; +import { RequestContext } from "../context.ts"; +import { Handler, Middleware } from "../types.ts"; import { Cascade } from "../utils/Cascade.ts"; -export interface RouteConfig { +export interface BaseRouteConfig { path: string; middleware?: Middleware | Middleware[]; handler?: Handler; } -export class Route { +export class BaseRoute { path: string; middleware: Middleware[]; handler?: Handler; - constructor(routeObj: RouteConfig) { + constructor(routeObj: BaseRouteConfig) { this.path = routeObj.path; this.handler = routeObj.handler && Cascade.promisify(routeObj.handler); this.middleware = [routeObj.middleware] @@ -30,11 +31,11 @@ export class Route { } } -export class Router< - Config extends RouteConfig = RouteConfig, - R extends Route = Route +export class BaseRouter< + Config extends BaseRouteConfig = BaseRouteConfig, + R extends BaseRoute = BaseRoute > { - Route = Route; + Route = BaseRoute; constructor( public routes: R[] = [], // <- use this as a hashmap for routes @@ -89,10 +90,10 @@ export class Router< const routeObj = typeof arg1 !== "string" ? arg1 - : arguments.length === 2 + : arguments.length === 2 ? arg2 instanceof Function ? { path: arg1, handler: arg2 as Handler } - : { path: arg1, ...arg2 as Omit } + : { path: arg1, ...(arg2 as Omit) } : { path: arg1, middleware: arg2 as Middleware | Middleware[], diff --git a/lib/routers/httpRouter.ts b/lib/routers/httpRouter.ts index 84126ed8..fb046229 100644 --- a/lib/routers/httpRouter.ts +++ b/lib/routers/httpRouter.ts @@ -1,13 +1,14 @@ -import { Middleware, Handler, RequestContext } from "../types.ts"; -import { Route, Router, RouteConfig } from "./_router.ts"; +import { RequestContext } from "../context.ts"; +import { Middleware, Handler } from "../types.ts"; +import { BaseRoute, BaseRouter, BaseRouteConfig } from "./_Router.ts"; -export interface HttpRouteConfig extends RouteConfig { +export interface HttpRouteConfig extends BaseRouteConfig { path: `/${string}`; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; handler: Handler; } -export class HttpRoute extends Route { +export class HttpRoute extends BaseRoute { declare path: `/${string}`; declare method: HttpRouteConfig["method"]; @@ -47,13 +48,13 @@ export class HttpRoute extends Route { } } -export class HttpRouter extends Router { +export class HttpRouter< + Config extends HttpRouteConfig = HttpRouteConfig, + R extends HttpRoute = HttpRoute +> extends BaseRouter { Route = HttpRoute; - constructor( - public routes: R[] = [], - public middleware: Middleware[] = [] - ) { + constructor(public routes: R[] = [], public middleware: Middleware[] = []) { super(); } @@ -85,4 +86,3 @@ export class HttpRouter | DTO[]> - extends RouteConfig { - method?: "QUERY" | "MUTATION" | "RESOLVER"; - args: Fields; - data: O; - middleware?: Middleware[]; - resolver: (ctx: RequestContext) => Promise> | ResolvedType; -} - -export class SchemaRoute | DTO[]> extends Route { - declare method: SchemaRouteConfig["method"]; - args: Fields; - data: O; - resolver: (ctx: RequestContext) => Promise> | ResolvedType; - - constructor(routeObj: SchemaRouteConfig) { - super(routeObj); - this.method = - (routeObj.method as SchemaRouteConfig["method"]) || "QUERY"; - this.args = routeObj.args; - this.data = routeObj.data as O; - this.resolver = routeObj.resolver; - } -} - -export class SchemaRouter< - Config extends SchemaRouteConfig> = SchemaRouteConfig< - DTO - >, - R extends SchemaRoute> = SchemaRoute> -> extends Router { - Route = SchemaRoute; - - constructor( - public routes: R[] = [], - public middleware: Middleware[] = [], - public scalars: Record unknown> = { - ...Scalars, - } - ) { - super(); - } - - // TODO: reimplement handle method to return JSON data and errors from ctx - // (accumulated by routes) - - query | DTO[]>( - path: SchemaRouteConfig["path"], - data: Omit, "path"> - ) { - const input = { - path: path, - method: "QUERY", - ...data, - }; - // @ts-ignore supply overload args∏ - const newRoute = this.addRoute(input); - return newRoute; - } - - mutation | DTO[]>( - path: SchemaRouteConfig["path"], - data: Omit, "path"> - ) { - const input = { - path: path, - method: "MUTATION", - ...data, - }; - // @ts-ignore supply overload args - const newRoute = this.addRoute(input); - return newRoute; - } - - resolver | DTO[]>( - path: SchemaRouteConfig["path"], - data: Omit, "path"> - ) { - const input = { - path: path, - method: "RESOLVER", - ...data, - }; - // @ts-ignore supply overload args - const newRoute = this.addRoute(input); - return newRoute; - } -} diff --git a/lib/types.ts b/lib/types.ts index 101d9a02..5ad90a8c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,15 +1,4 @@ -import { Router } from "./routers/_router.ts"; - -export class RequestContext> { - url: URL; - state: T; - params: Record = {}; - - constructor(public router: Router, public request: Request, state?: T) { - this.url = new URL(request.url); - this.state = state ? state : ({} as T); - } -} +import { RequestContext } from "./context.ts"; export type Result = void | Response | undefined; export type Next = () => Promise | Result; diff --git a/lib/utils/CacheItem.ts b/lib/utils/CacheItem.ts index a2c26657..6787c379 100644 --- a/lib/utils/CacheItem.ts +++ b/lib/utils/CacheItem.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "../types.ts"; +import { RequestContext } from "../context.ts"; export class CacheItem { key: string; diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts index cd28e9e4..ee40dc8e 100644 --- a/lib/utils/Cascade.ts +++ b/lib/utils/Cascade.ts @@ -1,4 +1,5 @@ -import { RequestContext, Middleware, Result, Next, Handler } from "../types.ts"; +import { RequestContext } from "../context.ts"; +import { Middleware, Result, Next, Handler } from "../types.ts"; export type PromiseHandler = ( ctx: RequestContext, diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts index 404a66d4..67ec4f48 100644 --- a/lib/utils/Profiler.ts +++ b/lib/utils/Profiler.ts @@ -1,14 +1,14 @@ -import { Router, Route } from "../routers/_router.ts"; +import { BaseRouter, BaseRoute } from "../routers/_Router.ts"; type ProfileConfig = { mode?: "serve" | "handle"; url?: string; count?: number; - excludedRoutes?: Route[]; + excludedRoutes?: BaseRoute[]; }; type ProfileResults = Record< -Route["path"], + BaseRoute["path"], { avgTime: number; requests: { @@ -26,7 +26,7 @@ export class Profiler { * @param config * @returns results: ProfileResults */ - static async run(router: Router, config?: ProfileConfig) { + static async run(router: BaseRouter, config?: ProfileConfig) { const url = (config && config.url) || `http://localhost:7777`; const count = (config && config.count) || 100; const excludedRoutes = (config && config.excludedRoutes) || []; diff --git a/lib/utils/Schema.ts b/lib/utils/Schema.ts index c0df7dc4..54a39d36 100644 --- a/lib/utils/Schema.ts +++ b/lib/utils/Schema.ts @@ -1,201 +1,219 @@ -import { SchemaRoute } from "../routers/schemaRouter.ts"; -import { RequestContext } from "../types.ts"; +import { RequestContext } from "../context.ts"; +import { Middleware } from "../types.ts"; export class ID extends String {} export class Int extends Number {} export class Float extends Number {} -export const Scalars = { ID, Int, Float, Boolean, Date, String }; -type ScalarTypes = (typeof Scalars)[keyof typeof Scalars]; +export const defaultScalars = { ID, Int, Float, Boolean, Date, String }; +export type Scalars = (typeof defaultScalars)[keyof typeof defaultScalars]; -export class Enum = Record> { +export class Enum> { constructor(public name: string, public values: T) {} } -type SchemaTypes = ScalarTypes | Enum> | DTO; -type FieldType = SchemaTypes | SchemaTypes[]; - -export class DTO { - constructor(public name: string, public fields: F) {} -} - -export interface Fields { +type GroupedTypes = Scalars | Enum> | DTO; +type FieldType = GroupedTypes | GroupedTypes[]; +interface Fields { [key: string]: Field; } -export class Field { - constructor(public type: T, public config: FieldConfig = {}) {} -} - interface FieldConfig { nullable?: boolean; validator?: ( x: ResolvedType ) => boolean | { pass: boolean; message: string }; - resolver?: ( - ctx: RequestContext - ) => Promise[]> | ResolvedType[]; + resolver?: (x: RequestContext) => Promise>[]>; +} +export class Field { + constructor(public type: T, public config: FieldConfig = {}) {} } -export type ResolvedType = T extends DTO - ? ResolvedFields - : T extends ScalarTypes - ? InstanceType - : T extends Enum> - ? T["values"][keyof T["values"]] - : T extends Array - ? ResolvedType[] - : never; +interface DTOConfig { + fields: F; +} +export class DTO { + constructor(public name: string, public config: DTOConfig) {} +} +export class Input extends DTO {} +export class Type extends DTO {} + +export class Query | Type[]> { + public type = "query"; + constructor(public name: string, public config: QueryConfig) {} +} + +export class Mutation< + O extends Type | Type[] +> extends Query { + public type = "mutation"; +} + +interface QueryConfig | Type[]> { + args: Fields; + data: O; + resolver: (ctx: RequestContext) => Promise>>; + middleware?: Middleware[]; +} type ResolvedFields = { [P in keyof Fields]: ResolvedField; }; -export type ResolvedField = T extends Field - ? F extends SchemaTypes[] +type ResolvedField = T extends Field + ? F extends GroupedTypes[] ? ResolvedType[] : ResolvedType : never; -export const generateSchema = ({ - scalars, - routes, -}: { - scalars: Record unknown>; - routes: SchemaRoute>[]; -}) => { - let schema = "# GraphQL Schema (autogenerated)\n\n"; - schema += generateScalarSchema(scalars); - schema += "\n\n"; - schema += generateEnumSchema(routes); - - schema += "type Query {\n"; - schema += generateOperationsSchema("QUERY", routes); - schema += "\n}\n\n"; - - schema += "type Mutation {\n"; - schema += generateOperationsSchema("MUTATION", routes); - schema += "\n}\n\n"; - - schema += generateDtosSchema(routes); - schema += "\n\n"; - - return schema; -}; +export type ResolvedType = T extends Type + ? ResolvedFields + : T extends Scalars + ? InstanceType + : T extends Enum> + ? T["values"][keyof T["values"]] + : never; -export const generateScalarSchema = ( - scalars: Record unknown> -): string => { - return Object.keys(scalars) - .map((key) => `scalar ${key}`) - .join("\n"); -}; +export class Schema { + public scalars: Record = { + ...defaultScalars, + }; -export const generateEnumSchema = ( - routes: SchemaRoute>[] -): string => { - const enums = new Set(); - let enumsString = ""; + constructor( + public operations: Query | Type[]>[], + additionalScalars: Record = {} + ) { + Object.keys(additionalScalars).forEach( + (key) => (this.scalars[key] = additionalScalars[key]) + ); + } + + toString() { + let schema = "# GraphQL Schema (autogenerated)\n\n"; + schema += this.generateScalars(); + schema += "\n\n"; + schema += this.generateEnums(); + + schema += "type Query {\n"; + schema += this.generateOperationFields("query"); + schema += "\n}\n\n"; + + schema += "type Mutation {\n"; + schema += this.generateOperationFields("mutation"); + schema += "\n}\n\n"; + + schema += this.generateDTOs(); + schema += "\n\n"; + + return schema; + } + + private generateScalars(): string { + return Object.keys(this.scalars) + .map((key) => `scalar ${key}`) + .join("\n"); + } + + private generateEnums(): string { + const enums = new Set>(); + let enumsString = ""; + + const collectFromFields = (fields: Fields) => { + Object.values(fields).forEach((field) => { + if (field instanceof Input || field instanceof Type) { + collectFromFields(field.config.fields); + } else { + const fieldType = field.type; + if (fieldType instanceof Enum) { + enums.add(fieldType); + } else if (Array.isArray(fieldType) && fieldType[0] instanceof Enum) { + enums.add(fieldType[0]); + } + } + }); + }; - const collectFromFields = (fields: Fields) => { - Object.values(fields).forEach((field) => { - if (field instanceof DTO) { - collectFromFields(field.fields); + this.operations.forEach((operation) => { + collectFromFields(operation.config.args); + if (Array.isArray(operation.config.data)) { + collectFromFields(operation.config.data[0].config.fields); } else { - const fieldType = field.type; - if (fieldType instanceof Enum) { - enums.add(fieldType); - } else if (Array.isArray(fieldType) && fieldType[0] instanceof Enum) { - enums.add(fieldType[0]); - } + collectFromFields(operation.config.data.config.fields); } }); - }; - routes.forEach((route) => { - collectFromFields(route.args); - if (Array.isArray(route.data)) { - collectFromFields(route.data[0].fields); - } else { - collectFromFields(route.data.fields); - } - }); - - enums.forEach((enumType) => { - enumsString += `enum ${enumType.name} {\n`; - Object.values(enumType.values).forEach((value) => { - enumsString += ` ${value}\n`; + enums.forEach((enumType) => { + enumsString += `enum ${enumType.name} {\n`; + Object.values(enumType.values).forEach((value) => { + enumsString += ` ${value}\n`; + }); + enumsString += "}\n\n"; }); - enumsString += "}\n\n"; - }); - - return enumsString; -}; - -export const generateOperationsSchema = ( - type: "QUERY" | "MUTATION", - routes: SchemaRoute>[] -): string => { - return routes - .filter((route) => route.method === type) - .map((route) => { - const args = Object.entries(route.args) - .map(([name, field]) => `${name}: ${generateFieldString(field)}`) - .join(", "); - const outputType = Array.isArray(route.data) - ? `[${route.data[0].name}]!` - : `${route.data.name}!`; - return ` ${route.path}(${args}): ${outputType}`; - }) - .join("\n"); -}; -export const generateDtosSchema = ( - routes: SchemaRoute>[] -): string => { - let dtoString = ""; - - routes.forEach((route) => { - const results: string[] = []; - Object.values(route.args).forEach((arg) => { - if (arg.type instanceof DTO) { - results.push(generateDtoSchema({ dtoType: "input", dto: arg.type })); - } + return enumsString; + } + + private generateOperationFields(type: "query" | "mutation"): string { + return this.operations + .filter((operation) => operation.type === type) + .map((operation) => { + const args = Object.entries(operation.config.args) + .map(([name, field]) => `${name}: ${this.generateFieldString(field)}`) + .join(", "); + const outputType = Array.isArray(operation.config.data) + ? `[${operation.config.data[0].name}]!` + : `${operation.config.data.name}!`; + return ` ${operation.name}(${args}): ${outputType}`; + }) + .join("\n"); + } + + private generateDTOs(): string { + const DTOs = new Set<{ + dtoType: "input" | "type"; + dto: DTO | DTO[]; + }>(); + let dtoString = ""; + + this.operations.forEach((operation) => { + Object.values(operation.config.args).forEach((arg) => { + if (arg.type instanceof Input) { + DTOs.add({ dtoType: "input", dto: arg.type }); + } + }); + DTOs.add({ dtoType: "type", dto: operation.config.data }); }); - results.push(generateDtoSchema({ dtoType: "type", dto: route.data })); - results.forEach((result) => { - if (!dtoString.includes(result)) { - dtoString += result; - } + DTOs.forEach((dto) => { + dtoString += this.generateDTOFields(dto); }); - }); - - return dtoString; -}; -const generateDtoSchema = (input: { - dtoType: "input" | "type"; - dto: DTO | DTO[]; -}): string => { - const dtoInput = input.dto; - const isArray = Array.isArray(dtoInput); - const dto = isArray ? dtoInput[0] : dtoInput; - - const fieldsString = Object.entries(dto.fields) - .map(([fieldName, field]) => { - const fieldTypeString = generateFieldString(field); - return ` ${fieldName}: ${fieldTypeString}`; - }) - .join("\n"); - - return `${input.dtoType} ${dto.name} {\n${fieldsString}\n}\n\n`; -}; - -const generateFieldString = (field: Field): string => { - const isArray = Array.isArray(field.type); - const baseType = Array.isArray(field.type) ? field.type[0] : field.type; - const typeName = baseType instanceof DTO ? baseType.name : baseType.name; - return `${isArray ? `[${typeName}]` : typeName}${ - field.config.nullable ? "" : "!" - }`; -}; + return dtoString; + } + + private generateDTOFields(input: { + dtoType: "input" | "type"; + dto: DTO | DTO[]; + }): string { + const dtoInput = input.dto; + const isArray = Array.isArray(dtoInput); + const dto = isArray ? dtoInput[0] : dtoInput; + + const fieldsString = Object.entries(dto.config.fields) + .map(([fieldName, field]) => { + const fieldTypeString = this.generateFieldString(field); + return ` ${fieldName}: ${fieldTypeString}`; + }) + .join("\n"); + + return `${input.dtoType} ${dto.name} {\n${fieldsString}\n}\n\n`; + } + + private generateFieldString(field: Field): string { + const isArray = Array.isArray(field.type); + const baseType = Array.isArray(field.type) ? field.type[0] : field.type; + const typeName = baseType instanceof Type ? baseType.name : baseType.name; + return `${isArray ? `[${typeName}]` : typeName}${ + field.config.nullable ? "" : "!" + }`; + } +} \ No newline at end of file diff --git a/mod.ts b/mod.ts index ebee7470..75eecb60 100644 --- a/mod.ts +++ b/mod.ts @@ -4,7 +4,6 @@ // Routers & types export * from "./lib/routers/httpRouter.ts"; -export * from "./lib/routers/schemaRouter.ts"; export * from "./lib/types.ts"; // Handlers diff --git a/tests/handlers/file_test.ts b/tests/handlers/file_test.ts index 3ed40372..6bf6abcd 100644 --- a/tests/handlers/file_test.ts +++ b/tests/handlers/file_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { file } from "../../lib/handlers/file.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: File", async (t) => { - const server = new Router(); + const server = new BaseRouter(); const ctx = new RequestContext(server, new Request("http://localhost")); const fileURL = new URL(import.meta.url); const decoder = new TextDecoder(); diff --git a/tests/handlers/sse_test.ts b/tests/handlers/sse_test.ts index cbc75744..966900c7 100644 --- a/tests/handlers/sse_test.ts +++ b/tests/handlers/sse_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { sse } from "../../lib/handlers/sse.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: Server-sent events", async (t) => { - const router = new Router(); + const router = new BaseRouter(); const ctx = new RequestContext(router, new Request("http://localhost")); const eventTarget = new EventTarget(); const decoder = new TextDecoder(); diff --git a/tests/handlers/ssr_test.ts b/tests/handlers/ssr_test.ts index d3a1d362..4eae7ead 100644 --- a/tests/handlers/ssr_test.ts +++ b/tests/handlers/ssr_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { ssr } from "../../lib/handlers/ssr.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("HANDLER: Server-side render", async (t) => { - const server = new Router(); + const server = new BaseRouter(); const ctx = new RequestContext(server, new Request("http://localhost")); const decoder = new TextDecoder(); const cacheControl = "max-age=60, stale-while-revalidate=10"; diff --git a/tests/middleware/authenticator_test.ts b/tests/middleware/authenticator_test.ts index 13274a8d..88b72de2 100644 --- a/tests/middleware/authenticator_test.ts +++ b/tests/middleware/authenticator_test.ts @@ -1,13 +1,13 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { authenticator } from "../../lib/middleware/authenticator.ts"; import { Crypto } from "../../lib/utils/Crypto.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Authenticator", async (t) => { const successString = "Authorized!"; const crypto = new Crypto("test_key"); - const server = new Router(); + const server = new BaseRouter(); const testPayload = { iat: Date.now(), diff --git a/tests/middleware/cacher_test.ts b/tests/middleware/cacher_test.ts index 704168b8..65e21e51 100644 --- a/tests/middleware/cacher_test.ts +++ b/tests/middleware/cacher_test.ts @@ -1,12 +1,12 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { cacher } from "../../lib/middleware/cacher.ts"; import { testHandler } from "../mocks/middleware.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Cacher", async (t) => { - const router = new Router(); + const router = new BaseRouter(); const successString = "Success!"; const testData = { foo: "bar", diff --git a/tests/middleware/logger_test.ts b/tests/middleware/logger_test.ts index 46f34366..defa1aff 100644 --- a/tests/middleware/logger_test.ts +++ b/tests/middleware/logger_test.ts @@ -1,13 +1,13 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { logger } from "../../lib/middleware/logger.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("MIDDLEWARE: Logger", async (t) => { const successString = "Success!"; let logOutput: unknown; - const server = new Router(); + const server = new BaseRouter(); const testData = { foo: "bar", diff --git a/tests/mocks/httpRouter.ts b/tests/mocks/httpRouter.ts index 8a70106f..2e47b75d 100644 --- a/tests/mocks/httpRouter.ts +++ b/tests/mocks/httpRouter.ts @@ -1,4 +1,4 @@ -import { HttpRouter } from "../../lib/routers/httpRouter.ts"; +import { HttpBaseRouter } from "../../lib/routers/httpBaseRouter.ts"; import { testMiddleware2, testMiddleware3, @@ -6,8 +6,8 @@ import { testMiddleware1, } from "./middleware.ts"; -export const mockHttpRouter = () => { - const router = new HttpRouter(); +export const mockHttpBaseRouter = () => { + const router = new HttpBaseRouter(); router.addRoute( "/test", [testMiddleware1, testMiddleware2, testMiddleware3], diff --git a/tests/mocks/schema/_schemaRouter.ts b/tests/mocks/schema/_schemaRouter.ts deleted file mode 100644 index c8ccbea8..00000000 --- a/tests/mocks/schema/_schemaRouter.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { SchemaRouter } from "../../../lib/routers/schemaRouter.ts"; -import { DTO, Field } from "../../../lib/utils/Schema.ts"; -import { comment, mockComment } from "./comment.ts"; -import { content, mockContent } from "./content.ts"; -import { mockPost, post } from "./post.ts"; -import { ageField, emailField, mockUser, user } from "./user.ts"; - -export const mockSchemaRouter = () => { - const schemaRouter = new SchemaRouter(); - - schemaRouter.mutation("RegisterUser", { - args: { - email: emailField, - age: ageField, - }, - data: user, - resolver: () => mockUser, - }); - - schemaRouter.mutation("CreateContent", { - args: { - content: new Field( - new DTO("CreateContentInput", { - ...content.fields, - }) - ), - }, - data: content, - resolver: () => mockContent, - }); - - schemaRouter.mutation("PostContent", { - args: { - contentId: new Field(String), - }, - data: post, - resolver: () => mockPost, - }); - - schemaRouter.mutation("CommentOnPostInput", { - args: { - comment: new Field( - new DTO("CommentInput", { - postId: new Field(String), - text: new Field(String), - }) - ), - }, - data: comment, - resolver: () => mockComment, - }); - - schemaRouter.query("GetComments", { - args: { - postId: new Field(String), - }, - data: [comment], - resolver: () => [mockComment], - }); - - return schemaRouter; -}; diff --git a/tests/mocks/schema/comment.ts b/tests/mocks/schema/comment.ts deleted file mode 100644 index 1e8cda80..00000000 --- a/tests/mocks/schema/comment.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DTO, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; -// import { mockPost, post } from "./post.ts"; -import { mockUser, user } from "./user.ts"; - -export const comment = new DTO("Comment", { - author: new Field(user, { - resolver: () => [mockUser], - }), - // post: new Field(() => post, { - // resolver: () => mockPost, - // }), - text: new Field(String), -}); - -export const mockComment: ResolvedType = { - author: mockUser, - text: "Hello", - // post: mockPost, -}; diff --git a/tests/mocks/schema/content.ts b/tests/mocks/schema/content.ts deleted file mode 100644 index 5ec84797..00000000 --- a/tests/mocks/schema/content.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { DTO, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; -import { mockUser, user } from "./user.ts"; - -export const content = new DTO("Content", { - title: new Field(String), - content: new Field(String), - creator: new Field(user, { - resolver: () => [mockUser], - }), -}); - -export const mockContent: ResolvedType = { - title: "Hello", - content: "World", - creator: mockUser, -}; diff --git a/tests/mocks/schema/post.ts b/tests/mocks/schema/post.ts deleted file mode 100644 index 6fc4d915..00000000 --- a/tests/mocks/schema/post.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DTO, Enum, Field, ResolvedType } from "../../../lib/utils/Schema.ts"; -import { comment, mockComment } from "./comment.ts"; -import { content, mockContent } from "./content.ts"; -import { mockUser, user } from "./user.ts"; - -enum PostStatus { - draft = "draft", - published = "published", -} - -export const post = new DTO("Post", { - author: new Field(user, { - resolver: () => [mockUser], - }), - content: new Field(content, { - resolver: () => [mockContent], - }), - status: new Field(new Enum("PostStatus", PostStatus)), - comments: new Field([comment], { - resolver: () => [[mockComment]], - }), -}); - -export const mockPost: ResolvedType = { - author: mockUser, - content: mockContent, - status: PostStatus.published, - comments: [mockComment], -}; \ No newline at end of file diff --git a/tests/mocks/schema/user.ts b/tests/mocks/schema/user.ts deleted file mode 100644 index be48f696..00000000 --- a/tests/mocks/schema/user.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DTO, Field, Int, ResolvedType } from "../../../lib/utils/Schema.ts"; - -export const emailField = new Field(String, { - validator: (x) => x.includes("@") && x.includes("."), -}); - -export const ageField = new Field(Int, { - nullable: true, - validator: (x) => typeof x === "number" && x > 0, -}); - -export const user = new DTO("User", { - email: emailField, - age: ageField, -}); - -export const mockUser: ResolvedType = { - email: "test@test.com", - age: 20, -}; diff --git a/tests/routers/_router_test.ts b/tests/routers/baseRouter_test.ts similarity index 86% rename from tests/routers/_router_test.ts rename to tests/routers/baseRouter_test.ts index fec00724..e7fbdfbe 100644 --- a/tests/routers/_router_test.ts +++ b/tests/routers/baseRouter_test.ts @@ -1,5 +1,5 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { testMiddleware2, testMiddleware3, @@ -7,8 +7,8 @@ import { testHandler, } from "../mocks/middleware.ts"; -Deno.test("ROUTER: Router managing routes", async (t) => { - const router = new Router(); +Deno.test("ROUTER: BaseRouter managing routes", async (t) => { + const router = new BaseRouter(); await t.step( "routes added with full route and string arg options", @@ -54,24 +54,24 @@ Deno.test("ROUTER: Router managing routes", async (t) => { }); await t.step("routers on server can be subsequently editted", () => { - const aRouter = new Router(); - aRouter.addRoutes([ + const aBaseRouter = new BaseRouter(); + aBaseRouter.addRoutes([ { path: "route", middleware: [], handler: testHandler }, { path: "route2", handler: testHandler }, { path: "route3", handler: testHandler }, ]); - aRouter.use(aRouter.middleware); + aBaseRouter.use(aBaseRouter.middleware); - aRouter.removeRoute("route"); + aBaseRouter.removeRoute("route"); - assert(!aRouter.routes.find((route) => route.path === "route")); - assert(aRouter.routes.length === 2); + assert(!aBaseRouter.routes.find((route) => route.path === "route")); + assert(aBaseRouter.routes.length === 2); }); }); -Deno.test("ROUTER: Router - request handling", async (t) => { - const router = new Router(); +Deno.test("ROUTER: BaseRouter - request handling", async (t) => { + const router = new BaseRouter(); router.middleware = []; await t.step("no route found triggers basic 404", async () => { diff --git a/tests/routers/httpRouter_test.ts b/tests/routers/httpRouter_test.ts index 603add74..2b186113 100644 --- a/tests/routers/httpRouter_test.ts +++ b/tests/routers/httpRouter_test.ts @@ -1,32 +1,32 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; import { HttpRouter } from "../../lib/routers/httpRouter.ts"; -Deno.test("ROUTER: HttpRouter", async (t) => { +Deno.test("ROUTER: HttpBaseRouter", async (t) => { await t.step("http shorthand methods work correctly", () => { const router = new HttpRouter(); - const getRoute = router.get({ + const getBaseRoute = router.get({ path: "/get", handler: () => new Response("GET"), }); - const postRoute = router.post({ + const postBaseRoute = router.post({ path: "/post", handler: () => new Response("POST"), }); - const putRoute = router.put({ + const putBaseRoute = router.put({ path: "/put", handler: () => new Response("PUT"), }); - const deleteRoute = router.delete({ + const deleteBaseRoute = router.delete({ path: "/delete", handler: () => new Response("DELETE"), }); assert(router.routes.length === 4); - assert(getRoute.method === "GET"); - assert(postRoute.method === "POST"); - assert(putRoute.method === "PUT"); - assert(deleteRoute.method === "DELETE"); + assert(getBaseRoute.method === "GET"); + assert(postBaseRoute.method === "POST"); + assert(putBaseRoute.method === "PUT"); + assert(deleteBaseRoute.method === "DELETE"); }); await t.step("Params correctly stored", () => { @@ -38,15 +38,15 @@ Deno.test("ROUTER: HttpRouter", async (t) => { }); await t.step("params discovered in RequestContext creation", async () => { - const newRouter = new HttpRouter(); + const newBaseRouter = new HttpRouter(); - newRouter.addRoute("/hello/:id/world/:name", (ctx) => { + newBaseRouter.addRoute("/hello/:id/world/:name", (ctx) => { return new Response( JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] }) ); }); - const res = await newRouter.handle( + const res = await newBaseRouter.handle( new Request("http://localhost:7777/hello/123/world/bruno") ); const json = await res.json() as { id: string; name: string }; diff --git a/tests/utils/CacheItem_test.ts b/tests/utils/CacheItem_test.ts index 0aa1d289..0a9754e9 100644 --- a/tests/utils/CacheItem_test.ts +++ b/tests/utils/CacheItem_test.ts @@ -1,7 +1,7 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { CacheItem, defaultKeyGen } from "../../lib/utils/CacheItem.ts"; -import { RequestContext } from "../../lib/types.ts"; Deno.test("UTIL: CacheItem", async (t) => { await t.step("constructor sets properties correctly", () => { @@ -17,11 +17,11 @@ Deno.test("UTIL: CacheItem", async (t) => { }); await t.step("defaultKeyGen generates correct key", () => { - const mockRouter = new Router(); + const mockBaseRouter = new BaseRouter(); const mockRequest = new Request("http://localhost:3000/path?query=param"); const mockState = { user: "Alice" }; - const ctx = new RequestContext(mockRouter, mockRequest, mockState); + const ctx = new RequestContext(mockBaseRouter, mockRequest, mockState); const result = defaultKeyGen(ctx); assert(result, 'GET-/path?query=param-{"user":"Alice"}'); diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index caffdf32..da52cf51 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -1,6 +1,6 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { Router } from "../../lib/routers/_router.ts"; -import { RequestContext } from "../../lib/types.ts"; +import { RequestContext } from "../../lib/context.ts"; +import { BaseRouter } from "../../lib/routers/_Router.ts"; import { Cascade } from "../../lib/utils/Cascade.ts"; import { testMiddleware1, @@ -11,7 +11,7 @@ import { import { Middleware } from "../../mod.ts"; Deno.test("UTIL: Cascade", async (t) => { - const testServer = new Router(); + const testServer = new BaseRouter(); const testContext = new RequestContext( testServer, new Request("http://localhost") diff --git a/tests/utils/Schema_test.ts b/tests/utils/Schema_test.ts index c66b391c..bbb0e403 100644 --- a/tests/utils/Schema_test.ts +++ b/tests/utils/Schema_test.ts @@ -1,16 +1,158 @@ import { assert } from "https://deno.land/std@0.218.0/assert/mod.ts"; -import { mockSchemaRouter } from "../mocks/schema/_schemaRouter.ts"; -import { generateSchema } from "../../lib/utils/Schema.ts"; +import { + Type, + Enum, + Field, + Int, + Input, + Schema, + Mutation, + Query, + ResolvedType, +} from "../../lib/utils/Schema.ts"; -Deno.test("ROUTER: SchemaRouter", async (t) => { - const router = mockSchemaRouter(); +Deno.test("UTIL: Profiler", async (t) => { + const emailField = new Field(String, { + validator: (x) => x.includes("@") && x.includes("."), + }); + + const ageField = new Field(Int, { + nullable: true, + validator: (x) => typeof x === "number" && x > 0, + }); + + const user = new Type("User", { + fields: { + email: emailField, + age: ageField, + }, + }); + + const mockUser: ResolvedType = { + email: "test@test.com", + age: 20, + }; + + const content = new Type("Content", { + fields: { + title: new Field(String), + content: new Field(String), + }, + }); + + const mockContent: ResolvedType = { + title: "Hello", + content: "World", + }; + + enum PostStatus { + draft = "draft", + published = "published", + } + + const post = new Type("Post", { + fields: { + author: new Field(user, { + resolver: async (ctx) => [mockUser], + }), + content: new Field(content, { + resolver: async (ctx) => [mockContent], + }), + status: new Field(new Enum("PostStatus", PostStatus)), + likes: new Field([user], { + resolver: async (ctx) => [[mockUser]], + }), + }, + }); + + const mockPost: ResolvedType = { + author: mockUser, + content: mockContent, + status: PostStatus.published, + likes: [mockUser], + }; + + const comment = new Type("Comment", { + fields: { + author: new Field(user, { + resolver: async (ctx) => [mockUser], + }), + post: new Field(post, { + resolver: async (ctx) => [mockPost], + }), + text: new Field(String), + }, + }); + + const mockComment: ResolvedType = { + author: mockUser, + post: mockPost, + text: "Hello", + }; + + const registerUser = new Mutation("RegisterUser", { + args: { + email: emailField, + age: ageField, + }, + data: user, + resolver: async (ctx) => mockUser, + }); + + const createContent = new Mutation("CreateContent", { + args: { + content: new Field( + new Input("CreateContentInput", { + fields: content.config.fields, + }) + ), + }, + data: content, + resolver: async (ctx) => mockContent, + }); + + const postContent = new Mutation("PostContent", { + args: { + content: new Field( + new Input("PostContentInput", { + fields: post.config.fields, + }) + ), + }, + data: post, + resolver: async (ctx) => mockPost, + }); + + const commentOnPost = new Mutation("CommentOnPostInput", { + args: { + comment: new Field( + new Input("CommentOnPost", { + fields: comment.config.fields, + }) + ), + }, + data: comment, + resolver: async (ctx) => mockComment, + }); + + const getComments = new Query("GetComments", { + args: { + post: new Field(post), + }, + data: [comment], + resolver: async (ctx) => [mockComment], + }); - await t.step("creates the correct schema string", () => { - const schemaString = generateSchema({ - scalars: router.scalars, - routes: router.routes, - }); + await t.step("creates the correct schema string", async () => { + const schema = new Schema([ + registerUser, + createContent, + postContent, + commentOnPost, + getComments, + ]); + const schemaString = schema.toString(); console.log(schemaString); assert(schemaString); }); From e991e46d7cdfb53f1716c19945a9ce02439cfb73 Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Wed, 11 Sep 2024 11:04:08 +0100 Subject: [PATCH 10/12] fix: examples --- example/reactSSR/handlers/react.handler.ts | 5 +++-- example/singleFileAuth/app.ts | 2 +- mod.ts | 1 + package.json | 1 + scripts/deno/auth.ts | 15 +++++++++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 scripts/deno/auth.ts diff --git a/example/reactSSR/handlers/react.handler.ts b/example/reactSSR/handlers/react.handler.ts index eb2b9634..d723a7f9 100644 --- a/example/reactSSR/handlers/react.handler.ts +++ b/example/reactSSR/handlers/react.handler.ts @@ -1,17 +1,18 @@ +import { JSX, ReactNode } from "react"; import { renderToString } from "react-dom/server"; import { Handler, RequestContext, ssr } from "../../../mod.ts"; import htmlTemplate from "../src/document.ts"; export const reactHandler = ( - component: (props: Record) => unknown, + component: (props: Record) => JSX.Element, title: string, entrypoint: string ): Handler => (ctx: RequestContext<{ env?: { ENVIRONMENT: string } }>) => { return ssr( () => { - const ssrHTML = renderToString(component(ctx.state), null); + const ssrHTML = renderToString(component(ctx.state as Record)); return htmlTemplate({ title, ssrHTML, diff --git a/example/singleFileAuth/app.ts b/example/singleFileAuth/app.ts index 9d38166a..6e366951 100644 --- a/example/singleFileAuth/app.ts +++ b/example/singleFileAuth/app.ts @@ -1,7 +1,7 @@ import * as Peko from "../../mod.ts"; // "https://deno.land/x/peko/mod.ts" const html = String; -const router = new Peko.Router(); +const router = new Peko.HttpRouter(); const crypto = new Peko.Crypto("SUPER_SECRET_KEY_123"); // <-- replace from env const user = { // <-- replace with db / auth provider query diff --git a/mod.ts b/mod.ts index 75eecb60..33864738 100644 --- a/mod.ts +++ b/mod.ts @@ -4,6 +4,7 @@ // Routers & types export * from "./lib/routers/httpRouter.ts"; +export * from "./lib/context.ts"; export * from "./lib/types.ts"; // Handlers diff --git a/package.json b/package.json index 30fc4b58..1ed216b6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "scripts": { "build": "esbuild --bundle --sourcemap --target=es2020 --platform=browser --format=esm --outdir=./example/reactSSR/dist/pages --external:react --jsx=automatic --tsconfig-raw={} ./example/reactSSR/src/pages/*.tsx", "start": "deno run --allow-net --allow-read --allow-env --allow-run scripts/deno/main.ts", + "start:auth": "deno run --allow-net --allow-read --allow-env --allow-run scripts/deno/auth.ts", "test": "deno test --allow-read --allow-net", "profile:deno": "deno run --allow-read --allow-net scripts/deno/profile.ts", "profile:bun": "bun run scripts/bun/profile.ts", diff --git a/scripts/deno/auth.ts b/scripts/deno/auth.ts new file mode 100644 index 00000000..09f4c9de --- /dev/null +++ b/scripts/deno/auth.ts @@ -0,0 +1,15 @@ +import router from "../../example/singleFileAuth/app.ts"; + +router.middleware.unshift((ctx) => { + ctx.state.env = Deno.env.toObject(); +}); + +// Start Deno server with Peko router :^) +Deno.serve( + { + port: 7777, + }, + (req) => router.handle(req) +); + +console.log("Deno server running with Peko router <3"); From e75ce632a494f40c3e983d1f5476f1a41c9f698e Mon Sep 17 00:00:00 2001 From: Sebastien Ringrose Date: Sat, 21 Sep 2024 15:13:48 +0100 Subject: [PATCH 11/12] fix: scripts --- example/{singleFileAuth => simpleAuth}/app.ts | 0 scripts/bun/profile.ts | 4 ++-- scripts/deno/auth.ts | 2 +- scripts/deno/profile.ts | 4 ++-- scripts/wrangler/profile.ts | 4 ++-- tests/mocks/httpRouter.ts | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) rename example/{singleFileAuth => simpleAuth}/app.ts (100%) diff --git a/example/singleFileAuth/app.ts b/example/simpleAuth/app.ts similarity index 100% rename from example/singleFileAuth/app.ts rename to example/simpleAuth/app.ts diff --git a/scripts/bun/profile.ts b/scripts/bun/profile.ts index eade3a18..dfc8d5a0 100644 --- a/scripts/bun/profile.ts +++ b/scripts/bun/profile.ts @@ -1,7 +1,7 @@ import Profiler from "../../lib/utils/Profiler.ts"; -import { getTestRouter } from "../../tests/mocks/middleware.ts"; +import { testHttpRouter } from "../../tests/mocks/httpRouter.ts"; -const testRouter = getTestRouter(); +const testRouter = testHttpRouter(); const server = Bun.serve({ port: 8080, fetch(req) { diff --git a/scripts/deno/auth.ts b/scripts/deno/auth.ts index 09f4c9de..6412d75b 100644 --- a/scripts/deno/auth.ts +++ b/scripts/deno/auth.ts @@ -1,4 +1,4 @@ -import router from "../../example/singleFileAuth/app.ts"; +import router from "../../example/simpleAuth/app.ts"; router.middleware.unshift((ctx) => { ctx.state.env = Deno.env.toObject(); diff --git a/scripts/deno/profile.ts b/scripts/deno/profile.ts index 9f388fce..56879d7f 100644 --- a/scripts/deno/profile.ts +++ b/scripts/deno/profile.ts @@ -1,7 +1,7 @@ import Profiler from "../../lib/utils/Profiler.ts"; -import { getTestRouter } from "../../tests/mocks/middleware.ts"; +import { testHttpRouter } from "../../tests/mocks/httpRouter.ts"; -const testRouter = getTestRouter(); +const testRouter = testHttpRouter(); const abortController = new AbortController(); Deno.serve( { diff --git a/scripts/wrangler/profile.ts b/scripts/wrangler/profile.ts index ca0e2993..5e625863 100644 --- a/scripts/wrangler/profile.ts +++ b/scripts/wrangler/profile.ts @@ -1,7 +1,7 @@ import Profiler from "../../lib/utils/Profiler.ts"; -import { getTestRouter } from "../../tests/mocks/middleware.ts"; +import { testHttpRouter } from "../../tests/mocks/httpRouter.ts"; -const testRouter = getTestRouter(); +const testRouter = testHttpRouter(); const handleResults = await Profiler.run(testRouter, { mode: "handle", diff --git a/tests/mocks/httpRouter.ts b/tests/mocks/httpRouter.ts index 2e47b75d..60d97860 100644 --- a/tests/mocks/httpRouter.ts +++ b/tests/mocks/httpRouter.ts @@ -1,4 +1,4 @@ -import { HttpBaseRouter } from "../../lib/routers/httpBaseRouter.ts"; +import { HttpRouter } from "../../lib/routers/httpRouter.ts"; import { testMiddleware2, testMiddleware3, @@ -6,8 +6,8 @@ import { testMiddleware1, } from "./middleware.ts"; -export const mockHttpBaseRouter = () => { - const router = new HttpBaseRouter(); +export const testHttpRouter = () => { + const router = new HttpRouter(); router.addRoute( "/test", [testMiddleware1, testMiddleware2, testMiddleware3], From 40e3d957746479824c32afd81c6f036b08d65ebd Mon Sep 17 00:00:00 2001 From: Seb Ringrose Date: Sat, 21 Sep 2024 15:17:27 +0100 Subject: [PATCH 12/12] fix: _router.ts to _Router.ts --- lib/routers/{_router.ts => _Router.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/routers/{_router.ts => _Router.ts} (100%) diff --git a/lib/routers/_router.ts b/lib/routers/_Router.ts similarity index 100% rename from lib/routers/_router.ts rename to lib/routers/_Router.ts