From eb16859984ff489c1cdeeea831d207a6ef549c28 Mon Sep 17 00:00:00 2001 From: L2jLiga Date: Mon, 29 Jul 2024 02:46:23 +0200 Subject: [PATCH] docs: introduced tsdoc eslint plugin --- lib/bootstrap/bootstrap.ts | 28 ++++++++++-- lib/decorators/helpers/class-metadata.ts | 2 +- lib/decorators/helpers/container.ts | 25 ----------- .../helpers/request-decorators.factory.ts | 3 +- lib/decorators/strategies/controller-type.ts | 3 +- lib/index.ts | 11 +++++ lib/plugins/class-loader.ts | 22 +++++++++ lib/plugins/index.ts | 12 ++++- lib/plugins/life-cycle.ts | 45 ++++++++++++------- lib/plugins/shared-interfaces.ts | 4 ++ .../helpers => registry}/container.spec.ts | 0 lib/registry/container.ts | 42 +++++++++++++++++ lib/registry/hooks-registry.ts | 17 +++++++ plugins/typedi/src/use-container.spec.ts | 42 ++++++++--------- plugins/typedi/src/use-container.ts | 2 +- 15 files changed, 185 insertions(+), 73 deletions(-) delete mode 100644 lib/decorators/helpers/container.ts rename lib/{decorators/helpers => registry}/container.spec.ts (100%) create mode 100644 lib/registry/container.ts create mode 100644 lib/registry/hooks-registry.ts diff --git a/lib/bootstrap/bootstrap.ts b/lib/bootstrap/bootstrap.ts index 4a7810de..448a1b8f 100644 --- a/lib/bootstrap/bootstrap.ts +++ b/lib/bootstrap/bootstrap.ts @@ -4,13 +4,18 @@ import { lstatSync, PathLike } from 'node:fs'; import { opendir } from 'node:fs/promises'; import type { AutoLoadConfig } from '../interfaces/bootstrap-config.js'; import type { BootstrapConfig } from '../interfaces/index.js'; -import { CLASS_LOADER, ClassLoader, Constructable, hooksRegistry } from '../plugins/index.js'; +import { CLASS_LOADER, ClassLoader, Constructable } from '../plugins/index.js'; import { CREATOR } from '../symbols/index.js'; import { transformAndWait } from '../utils/transform-and-wait.js'; import { isValidRegistrable } from '../utils/validators.js'; +import { hooksRegistry } from '../registry/hooks-registry.js'; const defaultMask = /\.(handler|controller)\./; +/** + * Fastify plugin responsible for bootstraping + * fastify-decorators. + */ export const bootstrap = fp( async (fastifyInstance: FastifyInstance, config: BootstrapConfig): Promise => { // 1. Load all modules @@ -44,15 +49,30 @@ export const bootstrap = fp( }, ); +/** + * Automatically loads modules from filesystem + */ function autoLoadModules(config: AutoLoadConfig): AsyncIterable> { const flags = config.mask instanceof RegExp ? config.mask.flags.replace('g', '') : ''; const mask = config.mask ? new RegExp(config.mask, flags) : defaultMask; - return readModulesRecursively(parsePath(config.directory), mask); + return readModulesRecursively(getBaseDirOf(config.directory), mask); } -function parsePath(directory: PathLike): URL { - const urlLike = directory.toString('utf8'); +/** + * Function accepts anything path-like and transforms + * it to URL object linking to the base directory. + * + * @example + * ```typescript + * parsePath(import.meta.url) // returns URL to directory containing file from which function was called + * parsePath(__filename) // same as above + * parsePath(__dirname) // converts dirname into URL + * parsePath(process.cwd) // converts process working directory into URL + * ``` + */ +function getBaseDirOf(pathLike: PathLike): URL { + const urlLike = pathLike.toString('utf8'); const url = urlLike.startsWith('file://') ? new URL(urlLike) : new URL('file://' + urlLike); if (lstatSync(url).isFile()) url.pathname += './..'; diff --git a/lib/decorators/helpers/class-metadata.ts b/lib/decorators/helpers/class-metadata.ts index d875862e..8977bb33 100644 --- a/lib/decorators/helpers/class-metadata.ts +++ b/lib/decorators/helpers/class-metadata.ts @@ -1,6 +1,6 @@ import { IErrorHandler, IHandler, IHook } from '../../interfaces/index.js'; import { ERROR_HANDLERS, HANDLERS, HOOKS, METADATA } from '../../symbols/index.js'; -import { Container } from './container.js'; +import { Container } from '../../registry/container.js'; // TODO: Support for ES Decorators export function getHandlersContainer(target: T): Container { diff --git a/lib/decorators/helpers/container.ts b/lib/decorators/helpers/container.ts deleted file mode 100644 index 5e1c50b6..00000000 --- a/lib/decorators/helpers/container.ts +++ /dev/null @@ -1,25 +0,0 @@ -export class Container implements Iterable { - private _values: T[] = []; - - constructor(private _parent?: Container) {} - - *[Symbol.iterator](): IterableIterator { - if (this._parent) yield* this._parent; - yield* this._values; - } - - setParent(parent: Container): void { - this._parent = parent; - } - - push(...items: T[]) { - this._values.push(...items); - } - - get length() { - let length = 0; - if (this._parent) length += this._parent.length; - length += this._values.length; - return length; - } -} diff --git a/lib/decorators/helpers/request-decorators.factory.ts b/lib/decorators/helpers/request-decorators.factory.ts index 11ea881c..c00e38c9 100644 --- a/lib/decorators/helpers/request-decorators.factory.ts +++ b/lib/decorators/helpers/request-decorators.factory.ts @@ -1,11 +1,12 @@ import type { FastifyInstance, FastifyRequest, RouteShorthandOptions } from 'fastify'; import { HttpMethods, RequestHandler, RouteConfig } from '../../interfaces/index.js'; -import { Constructable, getErrorHandlerContainer, getHandlersContainer, getHooksContainer, hooksRegistry, Registrable } from '../../plugins/index.js'; +import { Constructable, getErrorHandlerContainer, getHandlersContainer, getHooksContainer, Registrable } from '../../plugins/index.js'; import { CREATOR } from '../../symbols/index.js'; import { transformAndWait } from '../../utils/transform-and-wait.js'; import { getHandlerContainerMetadata } from './class-metadata.js'; import { createErrorsHandler } from './create-errors-handler.js'; import { ensureRegistrable } from './ensure-registrable.js'; +import { hooksRegistry } from '../../registry/hooks-registry.js'; type ParsedRouteConfig = { url: string; options: RouteShorthandOptions }; diff --git a/lib/decorators/strategies/controller-type.ts b/lib/decorators/strategies/controller-type.ts index 051779e8..39cc59f3 100644 --- a/lib/decorators/strategies/controller-type.ts +++ b/lib/decorators/strategies/controller-type.ts @@ -1,10 +1,11 @@ import type { FastifyInstance, FastifyReply, FastifyRequest, FastifySchema } from 'fastify'; import { onRequestHookHandler } from 'fastify/types/hooks.js'; -import { CLASS_LOADER, ClassLoader, hooksRegistry, Registrable } from '../../plugins/index.js'; +import { CLASS_LOADER, ClassLoader, Registrable } from '../../plugins/index.js'; import { ControllerType } from '../../registry/controller-type.js'; import { transformAndWait } from '../../utils/transform-and-wait.js'; import { getErrorHandlerContainer, getHandlersContainer, getHooksContainer } from '../helpers/class-metadata.js'; import { injectTagsIntoSwagger, TagObject } from '../helpers/swagger-helper.js'; +import { hooksRegistry } from '../../registry/hooks-registry.js'; const controllersCache = new WeakMap(); diff --git a/lib/index.ts b/lib/index.ts index f707050e..7f28e007 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,14 @@ +/** + * Fastify-decorators is a library-wrapper + * around the Fastify, it providers set of + * decorators. + * + * Decorators can be used to declare controllers, + * their requests handlers and life-cycle hooks. + * + * @packageDocumentation + */ + export { bootstrap } from './bootstrap/bootstrap.js'; export { BootstrapConfig } from './interfaces/bootstrap-config.js'; export { RequestHandler } from './interfaces/request-handler.js'; diff --git a/lib/plugins/class-loader.ts b/lib/plugins/class-loader.ts index 98b5d790..f7a5171c 100644 --- a/lib/plugins/class-loader.ts +++ b/lib/plugins/class-loader.ts @@ -7,8 +7,30 @@ declare module 'fastify' { } } +/** + * Symbol for getting class-loader from + * FastifyInstance. + */ export const CLASS_LOADER = Symbol.for('fastify-decorators.class-loader'); +/** + * Dependency scope, used to determine in + * which context class isntance was requested. + * By default all class instances are requested + * in FastifyInstance context and thus they + * are singletons, with one exception - when using + * per-request controller strategy. + */ export type Scope = FastifyInstance | FastifyRequest; +/** + * ClassLoader is the function which fastify-decorators + * uses under the hood to get class instance for given + * scope. + * Currently only 2 scopes are supported: + * - FastifyInstance - classes instantiated in this + * scope are singletons + * - FastifyRequest - classes instantiated in this + * scope destroyed when request finished + */ export type ClassLoader = (constructor: Constructable, scope: Scope) => C; diff --git a/lib/plugins/index.ts b/lib/plugins/index.ts index ac074122..65ded911 100644 --- a/lib/plugins/index.ts +++ b/lib/plugins/index.ts @@ -1,3 +1,13 @@ +/** + * Entrypoint for plugin development and integration. + * Provides utilities to interact with library + * life-cycle events, can be used in order to + * customize controller creation, app startup + * and teardown behavior. + * + * @packageDocumentation + */ + export * from './class-loader.js'; export * from './life-cycle.js'; export { Constructable, Registrable } from './shared-interfaces.js'; @@ -6,5 +16,5 @@ export { getHandlersContainer, getHooksContainer, getErrorHandlerContainer } fro export { IHook, IHandler, IErrorHandler } from '../interfaces/controller.js'; export { CREATOR } from '../symbols/index.js'; -export { Container } from '../decorators/helpers/container.js'; +export { Container } from '../registry/container.js'; export { CLASS_LOADER } from './class-loader.js'; diff --git a/lib/plugins/life-cycle.ts b/lib/plugins/life-cycle.ts index 6ecb57fb..a586aecb 100644 --- a/lib/plugins/life-cycle.ts +++ b/lib/plugins/life-cycle.ts @@ -1,28 +1,41 @@ import { FastifyInstance } from 'fastify'; import { Registrable } from './shared-interfaces.js'; +import { HooksRegistry, hooksRegistry } from '../registry/hooks-registry.js'; +/** + * Hook that executed when Fastify starts + * fastify-decorators loading + */ export type AppInitHook = (fastifyInstance: FastifyInstance) => unknown | Promise; + +/** + * Hooks that executed before each controller + * class instantiation. + */ export type BeforeControllerCreationHook = (fastifyInstance: FastifyInstance, target: Registrable) => unknown | Promise; + +/** + * Hooks that executed after each controller + * class were instantiated. + */ export type AfterControllerCreationHook = (fastifyInstance: FastifyInstance, target: Registrable, instance: unknown) => unknown | Promise; -export type AppReadyHook = (fastifyInstance: FastifyInstance) => unknown | Promise; -export type AppDestroyHook = (fastifyInstance: FastifyInstance) => unknown | Promise; -export interface HooksRegistry { - appInit: AppInitHook[]; - beforeControllerCreation: BeforeControllerCreationHook[]; - afterControllerCreation: AfterControllerCreationHook[]; - appReady: AppReadyHook[]; - appDestroy: AppDestroyHook[]; -} +/** + * Hooks that executed when all controllers + * are instantiated. + */ +export type AppReadyHook = (fastifyInstance: FastifyInstance) => unknown | Promise; -export const hooksRegistry: HooksRegistry = { - appInit: [], - beforeControllerCreation: [], - afterControllerCreation: [], - appReady: [], - appDestroy: [], -}; +/** + * Hooks that executed when fastify instance + * is going to close + */ +export type AppDestroyHook = (fastifyInstance: FastifyInstance) => unknown | Promise; +/** + * Helper function for hooking fastify-decorators, + * see overloads and hooks description. + */ export function createInitializationHook(stage: T, hookFn: AppInitHook): void; export function createInitializationHook(stage: T, hookFn: BeforeControllerCreationHook): void; export function createInitializationHook(stage: T, hookFn: AfterControllerCreationHook): void; diff --git a/lib/plugins/shared-interfaces.ts b/lib/plugins/shared-interfaces.ts index eceeeae5..7908f7ef 100644 --- a/lib/plugins/shared-interfaces.ts +++ b/lib/plugins/shared-interfaces.ts @@ -5,6 +5,10 @@ import type { CREATOR } from '../symbols/index.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructable = new (...args: any) => T; +/** + * Registrable is special classes that + * could be instantiated with class-loader + */ export interface Registrable extends Constructable { [CREATOR]: { register(instance: FastifyInstance, prefix?: string): Promise | void; diff --git a/lib/decorators/helpers/container.spec.ts b/lib/registry/container.spec.ts similarity index 100% rename from lib/decorators/helpers/container.spec.ts rename to lib/registry/container.spec.ts diff --git a/lib/registry/container.ts b/lib/registry/container.ts new file mode 100644 index 00000000..8028c9f4 --- /dev/null +++ b/lib/registry/container.ts @@ -0,0 +1,42 @@ +/** + * Container for storing objects + * Can handle hierarchy as well + * + * @experimental + */ +export class Container implements Iterable { + private readonly _values = new Map(); + + constructor(private _parent?: Container) {} + + public *[Symbol.iterator](): IterableIterator { + if (this._parent) yield* this._parent; + yield* this._values.values(); + } + + public setParent(parent: Container): void { + this._parent = parent; + } + + public set(key: K, value: T): void { + this._values.set(key, value); + } + + public get(key: K): T | null { + if (this._values.has(key)) { + return this._values.get(key) ?? null; + } + return this._parent?.get(key) ?? null; + } + + public push(...items: T[]): void { + items.forEach((item) => this._values.set(item as unknown as K, item)); + } + + public get length(): number { + let length = 0; + if (this._parent) length += this._parent.length; + length += this._values.size; + return length; + } +} diff --git a/lib/registry/hooks-registry.ts b/lib/registry/hooks-registry.ts new file mode 100644 index 00000000..31b97eb3 --- /dev/null +++ b/lib/registry/hooks-registry.ts @@ -0,0 +1,17 @@ +import { AfterControllerCreationHook, AppDestroyHook, AppInitHook, AppReadyHook, BeforeControllerCreationHook } from '../plugins/life-cycle.js'; + +export interface HooksRegistry { + appInit: AppInitHook[]; + beforeControllerCreation: BeforeControllerCreationHook[]; + afterControllerCreation: AfterControllerCreationHook[]; + appReady: AppReadyHook[]; + appDestroy: AppDestroyHook[]; +} + +export const hooksRegistry: HooksRegistry = { + appInit: [], + beforeControllerCreation: [], + afterControllerCreation: [], + appReady: [], + appDestroy: [], +}; diff --git a/plugins/typedi/src/use-container.spec.ts b/plugins/typedi/src/use-container.spec.ts index 78a1f1f2..b527c3de 100644 --- a/plugins/typedi/src/use-container.spec.ts +++ b/plugins/typedi/src/use-container.spec.ts @@ -1,34 +1,30 @@ -import { FastifyInstance } from 'fastify'; -import { hooksRegistry, Registrable } from 'fastify-decorators/plugins'; -import { Container } from 'typedi'; +import { fastify } from 'fastify'; +import { bootstrap, Controller } from 'fastify-decorators'; +import { Container, Service } from 'typedi'; import { useContainer } from './index.js'; +import { CLASS_LOADER } from 'fastify-decorators/plugins'; describe('Use container', () => { - beforeEach(() => { - hooksRegistry.beforeControllerCreation = []; - hooksRegistry.appInit = []; - Container.reset(); - }); - - it('should register before controller creation hook', () => { + beforeAll(() => { useContainer(Container); - - expect(hooksRegistry.beforeControllerCreation).toHaveLength(1); }); - it('should register controller in container on beforeControllerCreation', () => { - useContainer(Container); - - class Test {} + it('should create controller with injected dependency', async () => { + const instance = fastify(); + instance.register(bootstrap, { controllers: [SampleController] }); - hooksRegistry.beforeControllerCreation[0]({} as FastifyInstance, Test as Registrable); + await instance.ready(); - expect(Container.get(Test)).toBeInstanceOf(Test); + expect(Container.has(SampleController)).toBeTruthy(); + expect(instance[CLASS_LOADER](SampleController, instance)).toBeInstanceOf(SampleController); + expect(instance[CLASS_LOADER](SampleController, instance).dependency).toBeInstanceOf(Dependency); }); +}); - it('should register after controller creation hook', () => { - useContainer(Container); +@Service() +class Dependency {} - expect(hooksRegistry.appInit).toHaveLength(1); - }); -}); +@Controller() +class SampleController { + constructor(public dependency: Dependency) {} +} diff --git a/plugins/typedi/src/use-container.ts b/plugins/typedi/src/use-container.ts index 37e9bb2b..14995440 100644 --- a/plugins/typedi/src/use-container.ts +++ b/plugins/typedi/src/use-container.ts @@ -1,5 +1,5 @@ import { CLASS_LOADER, createInitializationHook } from 'fastify-decorators/plugins'; -import { Constructable } from 'fastify-decorators/plugins/index.js'; +import { Constructable } from 'fastify-decorators/plugins'; import type { Container as TypeDIContainer, ServiceOptions } from 'typedi'; export function useContainer(Container: typeof TypeDIContainer) {