diff --git a/lib/context.ts b/lib/context.ts new file mode 100644 index 0000000..8e9f1f4 --- /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 6e4b945..0000000 --- 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 80ce3dd..f3b8e6e 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 df7c292..e3175eb 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 84126ed..fb04622 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 101d9a0..5ad90a8 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 a2c2665..6787c37 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 cd28e9e..ee40dc8 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 404a66d..67ec4f4 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 c0df7dc..54a39d3 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 ebee747..75eecb6 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 3ed4037..6bf6abc 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 cbc7574..966900c 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 d3a1d36..4eae7ea 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 13274a8..88b72de 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 704168b..65e21e5 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 46f3436..defa1af 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 8a70106..2e47b75 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 c8ccbea..0000000 --- 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 1e8cda8..0000000 --- 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 5ec8479..0000000 --- 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 6fc4d91..0000000 --- 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 be48f69..0000000 --- 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 fec0072..e7fbdfb 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 603add7..2b18611 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 0aa1d28..0a9754e 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 caffdf3..da52cf5 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 c66b391..bbb0e40 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); });