UNPKG

@softwarecitadel/girouette

Version:

An AdonisJS package allowing decorators-based routing.

266 lines (265 loc) 9.79 kB
import 'reflect-metadata'; import { cwd } from 'node:process'; import { join, relative } from 'node:path'; import { readdir } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; import { REFLECT_RESOURCE_KEY, REFLECT_RESOURCE_MIDDLEWARE_KEY, REFLECT_RESOURCE_NAME_KEY, REFLECT_ROUTES_KEY, REFLECT_GROUP_KEY, REFLECT_GROUP_MIDDLEWARE_KEY, REFLECT_GROUP_DOMAIN_KEY, REFLECT_RESOURCE_ONLY_KEY, REFLECT_RESOURCE_EXCEPT_KEY, REFLECT_RESOURCE_API_ONLY_KEY, } from '../src/constants.js'; /** * The GirouetteProvider is responsible for registering all decorated routes with AdonisJS. * It scans the application's controllers directory and processes route decorators, * resource decorators, and group configurations. * * @example * ```ts * // In your adonisrc.ts * providers: [ * () => import('@adonisjs/core/providers/app_provider'), * () => import('./providers/girouette_provider') * ] * ``` */ export default class GirouetteProvider { app; #router = null; #logger = null; #controllersPath = join(cwd(), 'app'); constructor(app) { this.app = app; } /** * Sets the path to the controllers */ set controllersPath(path) { this.#controllersPath = path; } /** * Boot the provider when the application is ready */ async boot() { // Provider is booted } /** * Starts the provider by initializing the router and registering all routes */ async start() { this.#router = await this.app.container.make('router'); this.#logger = await this.app.container.make('logger'); await this.#scanControllersDirectory(this.#controllersPath); } /** * Recursively scans the directory for controller files and registers their routes */ async #scanControllersDirectory(directory) { const files = await readdir(directory, { withFileTypes: true }); for (const file of files) { const fullPath = join(directory, file.name); if (file.isDirectory()) { await this.#scanControllersDirectory(fullPath); continue; } if (this.#isControllerFile(file.name)) { await this.#processControllerFile(fullPath); } } } /** * Checks if a file is a controller file based on its name */ #isControllerFile(fileName) { return fileName.endsWith('_controller.ts') || fileName.endsWith('_controller.js'); } /** * Processes a controller file by importing it and registering its routes */ async #processControllerFile(filePath) { try { const path = pathToFileURL(filePath); const controllerToProcess = { controller: await import(path.href), importUrl: path, }; this.#registerControllerRoutes(controllerToProcess); this.#registerResourceRoutes(controllerToProcess); } catch (error) { this.#logger?.debug({ error }, '[Girouette] Error processing controller file'); } } /** * Registers all decorated routes from a controller */ #registerControllerRoutes(controller) { try { const routes = Reflect.getMetadata(REFLECT_ROUTES_KEY, controller.controller.default); if (!routes) return; for (const methodName in routes) { this.#registerSingleRoute(controller, methodName, routes[methodName]); } } catch (error) { this.#logger?.debug({ error }, '[Girouette] Error registering controller routes'); } } /** * Registers a single route with the AdonisJS router, applying any group configurations */ #registerSingleRoute(controller, methodName, route) { try { const group = Reflect.getMetadata(REFLECT_GROUP_KEY, controller.controller.default); const groupMiddleware = Reflect.getMetadata(REFLECT_GROUP_MIDDLEWARE_KEY, controller.controller.default); const groupDomain = Reflect.getMetadata(REFLECT_GROUP_DOMAIN_KEY, controller.controller.default); const finalRoute = this.#applyGroupConfiguration(route, group, groupMiddleware); const adonisRoute = this.#createRoute(finalRoute, controller, methodName); this.#configureRoute(adonisRoute, finalRoute, groupDomain); } catch (error) { this.#logger?.debug({ error }, '[Girouette] Error registering single route'); } } /** * Applies group configuration to a route */ #applyGroupConfiguration(route, group, groupMiddleware) { if (!group && !groupMiddleware) return route; return { ...route, pattern: group?.prefix ? this.#prefixRoutePattern(route.pattern, group.prefix) : route.pattern, name: group?.name ? this.#prefixRouteName(route.name, group.name) : route.name, middleware: this.#mergeMiddleware(route.middleware, groupMiddleware), }; } /** * Prefixes a route pattern with a group prefix */ #prefixRoutePattern(pattern, prefix) { const cleanPrefix = prefix.startsWith('/') ? prefix : `/${prefix}`; const cleanPattern = pattern.startsWith('/') ? pattern.slice(1) : pattern; return `${cleanPrefix}/${cleanPattern}`; } /** * Prefixes a route name with a group prefix */ #prefixRouteName(name, prefix) { return name ? `${prefix}.${name}` : name; } /** * Merges route-specific middleware with group middleware */ #mergeMiddleware(routeMiddleware, groupMiddleware) { const middleware = [...(routeMiddleware || [])]; if (groupMiddleware) { if (Array.isArray(groupMiddleware)) { middleware.unshift(...groupMiddleware); } else { middleware.unshift(groupMiddleware); } } return middleware; } /** * Creates a new route in the AdonisJS router */ #createRoute(route, controller, methodName) { const relativePath = relative(this.app.appRoot.pathname, controller.importUrl.pathname) .replaceAll('\\', '/') .replace(/\.ts$/, '.js'); return this.#router.route(route.pattern, [route.method], `./${relativePath}.${methodName}`); } /** * Configures a route with its name, constraints, middleware and domain */ #configureRoute(adonisRoute, route, domain) { if (route.name) { adonisRoute.as(route.name); } if (route.where?.length) { this.#applyRouteConstraints(adonisRoute, route.where); } if (route.middleware?.length) { this.#applyRouteMiddleware(adonisRoute, route.middleware); } if (domain) { adonisRoute.domain(domain); } } /** * Applies route constraints (where clauses) */ #applyRouteConstraints(route, constraints) { for (const { key, matcher } of constraints) { route.where(key, matcher); } } /** * Applies middleware to a route */ #applyRouteMiddleware(route, middleware) { for (const m of middleware) { route.use(m); } } /** * Registers resource routes for a controller */ #registerResourceRoutes(controller) { try { const resourcePattern = Reflect.getMetadata(REFLECT_RESOURCE_KEY, controller.controller.default); if (!resourcePattern) return; const relativePath = relative(this.app.appRoot.pathname, controller.importUrl.pathname) .replaceAll('\\', '/') .replace(/\.ts$/, '.js'); const resource = this.#router.resource(resourcePattern, `./${relativePath}`); this.#configureResource(resource, controller); } catch (error) { this.#logger?.debug({ error }, '[Girouette] Error registering resource routes'); } } /** * Configures a resource with its name and middleware */ #configureResource(resource, controller) { try { const resourceName = Reflect.getMetadata(REFLECT_RESOURCE_NAME_KEY, controller.controller.default); if (resourceName) { resource.as(resourceName); } const resourceMiddleware = Reflect.getMetadata(REFLECT_RESOURCE_MIDDLEWARE_KEY, controller.controller.default); if (resourceMiddleware) { this.#applyResourceMiddleware(resource, resourceMiddleware); } this.#defineResourceActions(resource, controller); } catch (error) { this.#logger?.debug({ error }, '[Girouette] Error configuring resource'); } } /** * Applies middleware to resource routes */ #applyResourceMiddleware(resource, middlewareConfig) { for (const { actions, middleware } of middlewareConfig) { resource.middleware(actions, middleware); } } #defineResourceActions(resource, controller) { const apiOnly = Reflect.getMetadata(REFLECT_RESOURCE_API_ONLY_KEY, controller.controller.default); if (apiOnly) { resource.apiOnly(); } const only = Reflect.getMetadata(REFLECT_RESOURCE_ONLY_KEY, controller.controller.default); if (only) { resource.only(only); } const except = Reflect.getMetadata(REFLECT_RESOURCE_EXCEPT_KEY, controller.controller.default); if (except) { resource.except(except); } } }