diff --git a/examples/CRUD/package-lock.json b/examples/CRUD/package-lock.json index 86956ae..d60cc13 100644 --- a/examples/CRUD/package-lock.json +++ b/examples/CRUD/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@types/node": "^22.5.0", "glands": "^1.0.3", + "graphql": "^16.9.0", "ts-node": "^10.9.2", "typescript": "^5.5.4" }, @@ -288,6 +289,15 @@ "node": ">= 20" } }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/examples/CRUD/package.json b/examples/CRUD/package.json index 06c6f36..5a3d42d 100644 --- a/examples/CRUD/package.json +++ b/examples/CRUD/package.json @@ -14,6 +14,7 @@ "dependencies": { "@types/node": "^22.5.0", "glands": "^1.0.3", + "graphql": "^16.9.0", "ts-node": "^10.9.2", "typescript": "^5.5.4" } diff --git a/examples/CRUD/router/crud.ts b/examples/CRUD/router/crud.ts index 3f335fb..8fc4bc4 100644 --- a/examples/CRUD/router/crud.ts +++ b/examples/CRUD/router/crud.ts @@ -1,4 +1,42 @@ -import { exposed, Get, Route, Delete, Post, Put, Context } from '../../../dist'; +import { graphql, buildSchema } from 'graphql'; +import { exposed, Route, Context, All, Get, Post, Delete, Put } from '../../../dist'; +// Define a simple GraphQL schema +const schema = buildSchema(` + type Query { + hello(name: String): String +} +`); + +// Define the root resolver +const root = { + hello: ({ name }: { name: string }) => `Hello, ${name || 'world'}!`, +}; + + +@Route('/graphql') +@exposed +class GraphQLHandler { + @All() + async handleGraphQL(ctx: Context) { + const { query, variables } = ctx.body as { query: string; variables?: any }; + + // Execute the GraphQL query + const result = await graphql({ + schema, + source: query, + variableValues: variables, + rootValue: root, + }); + + // Send the result back as the response + ctx.writeHead(200, { + 'Content-Type': 'application/json', + }); + ctx.write(JSON.stringify(result)); + ctx.end(); + } +} + import p from 'path'; import * as fs from 'fs'; const db = p.join(__dirname, '..', 'db', 'index.json'); @@ -99,4 +137,4 @@ class CRUD { ctx.end(); } -} +} \ No newline at end of file diff --git a/lib/core/router/index.ts b/lib/core/router/index.ts index 9788b23..2c69a9e 100644 --- a/lib/core/router/index.ts +++ b/lib/core/router/index.ts @@ -18,8 +18,13 @@ function generator(method: string) { }; }; } +export const All = function allGenerator(): MethodDecorator | any { + return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): void => { + Reflect.init('method', 'ALL', target, propertyKey); + Reflect.init('path', '*', target, propertyKey); + }; +}; export const { Get, Post, Put, Delete, Patch, Options, Head } = methods; - export namespace Router { export function set(controllerClassOrFunction: RouteHandler, routePath?: string): void { if (typeof controllerClassOrFunction === 'function') { @@ -40,29 +45,34 @@ export namespace Router { } export function findMatch(path: string, method: string, base: string): { controller: any; handlerKey: string; fullRoutePath: string; params: Record } | null { for (const [routePath, controller] of routes.entries()) { - console.log('routePath:', routePath); - console.log('path:', path); if (path.startsWith(routePath)) { if (isClass(controller)) { const routeInstance = new controller(); const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(routeInstance)).filter((key) => key !== 'constructor'); - console.log('KEYS:', keys); - for (const key of keys) { const handlerMethod = Reflect.get('method', controller.prototype, key); const handlerPath = Reflect.get('path', controller.prototype, key); - console.log('handlerPath:', handlerPath); - console.log('handlerMethod:', handlerMethod); let fullRoutePath = handlerPath ? `${routePath}${handlerPath}` : routePath; const parsedURL = new Parser.URI(path, base, fullRoutePath); - if (handlerPath && handlerPath.startsWith('/:')) { const paramName = handlerPath.split(':')[1]; fullRoutePath = `${routePath}/${parsedURL.params[paramName]}`; } - if (handlerMethod === method && fullRoutePath === path) { - return { controller, handlerKey: key, fullRoutePath, params: parsedURL.params }; + if ((handlerMethod === method || handlerMethod === 'ALL') && fullRoutePath === path) { + return { + controller, + handlerKey: key, + fullRoutePath, + params: parsedURL.params, + }; + } else if (handlerMethod === 'ALL' && fullRoutePath.startsWith(path)) { + return { + controller, + handlerKey: key, + fullRoutePath, + params: parsedURL.params, + }; } } } else if (typeof controller === 'function') { @@ -85,10 +95,6 @@ export namespace Router { const globalMids = Gmids.get(); const allMids = [...globalMids, ...classMids, ...methodMids]; - // Parse JSON body if the method is POST, PUT, or PATCH - if (['POST', 'PUT', 'PATCH'].includes(ctx.method!)) { - ctx.body = await ctx.json(); - } // Execute global middlewares first await execute(ctx, GlMid); // Execute class and method middlewares with the handler diff --git a/lib/core/server.ts b/lib/core/server.ts index 3c80d29..797d1d7 100644 --- a/lib/core/server.ts +++ b/lib/core/server.ts @@ -1,13 +1,13 @@ import { IncomingMessage, Server, ServerResponse, METHODS } from 'http'; import { Parser } from '../helper/parser'; -import { Gland } from '../types'; +import { Gland, ListenArgs, NxtFunction } from '../types'; import { ServerUtils } from '../helper'; import { WebContext } from './context'; import { Router } from './router'; import { LoadModules } from '../helper/load'; import { Context } from '../types'; import { midManager } from './middleware'; -export class WebServer extends Server implements Gland.Listener, Gland.APP { +export class WebServer extends Server implements Gland.APP { private middlewares: Gland.Middleware[] = []; constructor() { super(); @@ -25,7 +25,7 @@ export class WebServer extends Server implements Gland.Listener, Gland.APP { const isAlreadyAdded = this.middlewares.some((middleware) => (middleware as any).key === middlewareKey); if (!isAlreadyAdded) { - const middleware = async (ctx: Context, next: () => Promise) => { + const middleware = async (ctx: Context, next: NxtFunction) => { if (ctx.url!.startsWith(path) && ctx.method === method) { await handler(ctx); if (ctx.writableEnded) { @@ -53,6 +53,7 @@ export class WebServer extends Server implements Gland.Listener, Gland.APP { const { controller, handlerKey, params } = matchingRoute; ctx.query = Object.fromEntries(url.searchParams.entries()); ctx.params = params; + ctx.body = await ctx.json(); // Check if the controller is a class or a function if (typeof controller === 'function' && !handlerKey) { const middlewareStack = Array.from(new Set([...this.middlewares, controller])); @@ -64,26 +65,48 @@ export class WebServer extends Server implements Gland.Listener, Gland.APP { } } } - init(opts: Gland.ListenOptions = {}, listeningListener?: (info: Gland.ListenOptions) => void): Server { - // Assign default values if not provided by the user - opts.port ??= 3000; - opts.host ??= 'localhost'; - opts.logger ??= true; - this.on('request', this.lifecycle.bind(this)); + listen(...args: ListenArgs): this { + let port: number | undefined; + let host: string | undefined; + let backlog: number | undefined; + let listener: (() => void) | undefined; + + if (typeof args[0] === 'object' && args[0] !== null && !(args[0] instanceof Function)) { + const opts = args[0] as Gland.ListenOptions; + port = opts.port ?? 3000; + host = opts.host ?? 'localhost'; + backlog = opts.backlog; + listener = args[1] as (() => void) | undefined; - const listener = ServerUtils.Tools.listener(opts, listeningListener); - if (opts.logger) { - ServerUtils.Tools.log(opts); + if (opts.logger) { + ServerUtils.Tools.log(opts); + } + } else { + if (typeof args[0] === 'number') port = args[0]; + if (typeof args[1] === 'string') host = args[1]; + if (typeof args[1] === 'number') backlog = args[1]; + if (typeof args[2] === 'number') backlog = args[2]; + if (typeof args[1] === 'function') listener = args[1]; + if (typeof args[2] === 'function') listener = args[2]; + if (typeof args[3] === 'function') listener = args[3]; } - if (opts.path) { - this.listen(opts.path, opts.backlog, listener); + + this.on('request', this.lifecycle.bind(this)); + + if (typeof args[0] === 'string') { + super.listen(args[0], backlog, listener); } else { - this.listen(opts.port, opts.host, opts.backlog, listener); + super.listen(port, host, backlog, listener); } + return this; } + + // Reuse ListenArgs type for init method + init(...args: ListenArgs): this { + return this.listen(...args); + } async load(paths: string = './*.ts') { await LoadModules.load(paths); } } -export const g = new WebServer(); diff --git a/lib/index.ts b/lib/index.ts index 82d10de..3be9f15 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,7 +3,7 @@ import { Qiu } from './cli/Qiu'; import { DbTypes, Context } from './types'; import { Logger } from './helper/logger'; import { exposed, Route } from './core/decorators'; -import { Delete, Get, Head, Options, Patch, Post, Put } from './core/router/index'; +import { Delete, Get, Head, Options, Patch, Post, Put, All } from './core/router/index'; import { NxtFunction } from './types/index'; import { mid, mids } from './core/decorators/index'; import { Gmids } from './core/middleware'; @@ -19,5 +19,5 @@ export default class gland extends WebServer { return Logger; } } -export { Get, Post, Put, Delete, Patch, Head, Options, Route, exposed, mid, mids }; +export { Get, Post, Put, Delete, Patch, Head, Options, Route, exposed, mid, mids, All }; export var Gmid = Gmids.set; diff --git a/lib/types/index.ts b/lib/types/index.ts index e2c88f6..2ec5d09 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -9,9 +9,6 @@ export namespace Gland { exclusive?: boolean; logger?: boolean; } - export interface Listener { - init(opts: ListenOptions): void; - } export interface StaticOptions { dotfiles?: 'allow' | 'deny' | 'ignore'; etag?: boolean; @@ -58,3 +55,11 @@ export type MidsFn = (ctx: Context, next: NxtFunction) => any; export type RouteHandler = new (...args: any[]) => any | ((...args: any[]) => any); export type DbTypes = 'mariadb' | 'postgres' | 'sqlite' | 'sqlserver' | 'mysql'; export type NxtFunction = () => Promise; +export type ListenArgs = + | [port?: number, hostname?: string, backlog?: number, listeningListener?: () => void] + | [port?: number, hostname?: string, listeningListener?: () => void] + | [port?: number, backlog?: number, listeningListener?: () => void] + | [port?: number, listeningListener?: () => void] + | [path: string, backlog?: number, listeningListener?: () => void] + | [path: string, listeningListener?: () => void] + | [options: Gland.ListenOptions, listeningListener?: () => void]; diff --git a/package.json b/package.json index 12a27a8..900ca20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "glands", - "version": "1.0.5", + "version": "1.0.6", "description": "Glands is a lightweight framework for Node.js designed for simplicity and high performance.", "main": "dist/index.js", "types": "dist/index.d.ts",