@avleon/core 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +601 -561
  2. package/dist/application.js +1 -1
  3. package/dist/cache.d.ts +1 -1
  4. package/dist/cache.js +2 -2
  5. package/dist/collection.d.ts +25 -32
  6. package/dist/collection.js +50 -6
  7. package/dist/collection.test.d.ts +1 -0
  8. package/dist/collection.test.js +59 -0
  9. package/dist/config.d.ts +2 -0
  10. package/dist/config.js +30 -5
  11. package/dist/config.test.d.ts +1 -0
  12. package/dist/config.test.js +40 -0
  13. package/dist/controller.js +2 -2
  14. package/dist/environment-variables.js +42 -5
  15. package/dist/event-dispatcher.d.ts +23 -0
  16. package/dist/event-dispatcher.js +102 -0
  17. package/dist/event-subscriber.d.ts +15 -0
  18. package/dist/event-subscriber.js +96 -0
  19. package/dist/exceptions/http-exceptions.js +1 -1
  20. package/dist/exceptions/index.d.ts +1 -1
  21. package/dist/exceptions/system-exception.js +3 -1
  22. package/dist/file-storage.js +1 -1
  23. package/dist/helpers.js +1 -1
  24. package/dist/icore.d.ts +5 -0
  25. package/dist/icore.js +53 -18
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +6 -1
  28. package/dist/utils/index.d.ts +2 -2
  29. package/dist/utils/optional-require.js +2 -2
  30. package/dist/validation.d.ts +1 -1
  31. package/dist/websocket.d.ts +7 -0
  32. package/dist/websocket.js +20 -0
  33. package/dist/websocket.test.d.ts +0 -0
  34. package/dist/websocket.test.js +1 -0
  35. package/package.json +37 -6
  36. package/src/application.ts +104 -125
  37. package/src/authentication.ts +16 -16
  38. package/src/cache.ts +91 -91
  39. package/src/collection.test.ts +71 -0
  40. package/src/collection.ts +344 -254
  41. package/src/config.test.ts +35 -0
  42. package/src/config.ts +85 -42
  43. package/src/constants.ts +1 -1
  44. package/src/container.ts +54 -54
  45. package/src/controller.ts +125 -127
  46. package/src/decorators.ts +27 -27
  47. package/src/environment-variables.ts +53 -46
  48. package/src/event-dispatcher.ts +100 -0
  49. package/src/event-subscriber.ts +79 -0
  50. package/src/exceptions/http-exceptions.ts +86 -86
  51. package/src/exceptions/index.ts +1 -1
  52. package/src/exceptions/system-exception.ts +35 -34
  53. package/src/file-storage.ts +206 -206
  54. package/src/helpers.ts +324 -328
  55. package/src/icore.ts +1106 -1084
  56. package/src/index.ts +32 -30
  57. package/src/interfaces/avleon-application.ts +32 -40
  58. package/src/logger.ts +72 -72
  59. package/src/map-types.ts +159 -159
  60. package/src/middleware.ts +121 -98
  61. package/src/multipart.ts +116 -116
  62. package/src/openapi.ts +372 -372
  63. package/src/params.ts +111 -111
  64. package/src/queue.ts +126 -126
  65. package/src/response.ts +74 -74
  66. package/src/results.ts +30 -30
  67. package/src/route-methods.ts +186 -186
  68. package/src/swagger-schema.ts +213 -213
  69. package/src/testing.ts +220 -220
  70. package/src/types/app-builder.interface.ts +18 -19
  71. package/src/types/application.interface.ts +7 -9
  72. package/src/utils/hash.ts +8 -5
  73. package/src/utils/index.ts +2 -2
  74. package/src/utils/optional-require.ts +50 -50
  75. package/src/validation.ts +160 -156
  76. package/src/validator-extend.ts +25 -25
  77. package/src/websocket.ts +47 -0
package/src/icore.ts CHANGED
@@ -1,1084 +1,1106 @@
1
- /**
2
- * @copyright 2024
3
- * @author Tareq Hossain
4
- * @email xtrinsic96@gmail.com
5
- * @url https://github.com/xtareq
6
- */
7
- import fastify, {
8
- FastifyInstance,
9
- FastifyReply,
10
- FastifyRequest,
11
- HookHandlerDoneFunction,
12
- preHandlerHookHandler,
13
- RouteGenericInterface,
14
- InjectOptions,
15
- LightMyRequestResponse,
16
- } from "fastify";
17
- import Container, { Constructable } from "typedi";
18
- import fs from "fs/promises";
19
- import path from "path";
20
- import container, {
21
- CONTROLLER_META_KEY,
22
- ROUTE_META_KEY,
23
- PARAM_META_KEY,
24
- QUERY_META_KEY,
25
- REQUEST_BODY_META_KEY,
26
- REQUEST_HEADER_META_KEY,
27
- getRegisteredControllers,
28
- isApiController,
29
- REQUEST_USER_META_KEY,
30
- AUTHORIZATION_META_KEY,
31
- REQUEST_BODY_FILES_KEY,
32
- REQUEST_BODY_FILE_KEY,
33
- FEATURE_KEY,
34
- } from "./container";
35
- import {
36
- Constructor,
37
- formatUrl,
38
- isValidJsonString,
39
- validateObjectByInstance,
40
- } from "./helpers";
41
- import { SystemUseError } from "./exceptions/system-exception";
42
- import { existsSync, PathLike } from "fs";
43
- import { DataSource, DataSourceOptions } from "typeorm";
44
- import { AppMiddleware } from "./middleware";
45
- import {
46
- BadRequestException,
47
- BaseHttpException,
48
- ValidationErrorException,
49
- } from "./exceptions";
50
- import { OpenApiOptions, OpenApiUiOptions } from "./openapi";
51
- import swagger from "@fastify/swagger";
52
- import { AppConfig, IConfig } from "./config";
53
- import { Environment } from "./environment-variables";
54
- import cors, { FastifyCorsOptions } from "@fastify/cors";
55
- import fastifyMultipart, { FastifyMultipartOptions } from "@fastify/multipart";
56
- import { MultipartFile } from "./multipart";
57
- import { validateOrThrow } from "./validation";
58
- import { optionalRequire } from "./utils";
59
-
60
-
61
- export type FuncRoute = {
62
- handler: any;
63
- middlewares?: any[];
64
- schema?: {};
65
- };
66
-
67
- // IRequest
68
- export interface IRequest extends FastifyRequest {
69
- params: any;
70
- query: any;
71
- body: any;
72
- headers: any;
73
- user?: any;
74
- }
75
-
76
-
77
-
78
- export interface DoneFunction extends HookHandlerDoneFunction { }
79
- // IResponse
80
- export interface IResponse extends FastifyReply { }
81
-
82
- export type TestResponseType = LightMyRequestResponse;
83
- export type TestResponse = TestResponseType | Promise<TestResponseType>;
84
-
85
- export interface TestApplication {
86
- get: (url: string, options?: InjectOptions) => TestResponse;
87
- post: (url: string, options?: InjectOptions) => TestResponse;
88
- put: (url: string, options?: InjectOptions) => TestResponse;
89
- patch: (url: string, options?: InjectOptions) => TestResponse;
90
- delete: (url: string, options?: InjectOptions) => TestResponse;
91
- options: (url: string, options?: InjectOptions) => TestResponse;
92
- getController?: <T>(controller: Constructor<T>) => T;
93
- }
94
-
95
- // ParamMetaOptions
96
- export interface ParamMetaOptions {
97
- index: number;
98
- key: string;
99
- name: string;
100
- required: boolean;
101
- validate: boolean;
102
- dataType: any;
103
- validatorClass: boolean;
104
- schema?: any;
105
- type:
106
- | "route:param"
107
- | "route:query"
108
- | "route:body"
109
- | "route:header"
110
- | "route:user"
111
- | "route:file"
112
- | "route:files";
113
- }
114
-
115
- export interface ParamMetaFilesOptions {
116
- index: number;
117
- type: "route:files";
118
- files: MultipartFile[];
119
- fieldName: string;
120
- }
121
-
122
- // Method Param Meta options
123
- export interface MethodParamMeta {
124
- params: ParamMetaOptions[];
125
- query: ParamMetaOptions[];
126
- body: ParamMetaOptions[];
127
- headers: ParamMetaOptions[];
128
- currentUser: ParamMetaOptions[];
129
- swagger?: OpenApiUiOptions;
130
- file?: any[];
131
- files?: ParamMetaFilesOptions[];
132
- }
133
-
134
- interface IRoute {
135
- url: string;
136
- method: string;
137
- controller: string;
138
- }
139
-
140
- const isTsNode =
141
- process.env.TS_NODE_DEV ||
142
- process.env.TS_NODE_PROJECT ||
143
- (process as any)[Symbol.for("ts-node.register.instance")];
144
- const controllerDir = path.join(
145
- process.cwd(),
146
- isTsNode ? "./src/controllers" : "./dist/cotrollers",
147
- );
148
-
149
- type StaticFileOptions = {
150
- path?: PathLike;
151
- prefix?: string;
152
- };
153
-
154
- type MultipartOptions = {
155
- destination: PathLike;
156
- } & FastifyMultipartOptions;
157
-
158
- type AvleonApp = FastifyInstance;
159
-
160
- export type TestAppOptions = {
161
- controllers: Constructor[];
162
- dataSource?: DataSource
163
- };
164
-
165
- export interface AvleonTestAppliction {
166
- addDataSource: (dataSourceOptions: DataSourceOptions) => void;
167
- getApp: (options?: TestAppOptions) => any;
168
- getController: <T>(controller: Constructor<T>, deps: any[]) => T;
169
- }
170
-
171
- export type AutoControllerOptions = {
172
- auto: true,
173
- path?: string
174
- }
175
- export interface IAvleonApplication {
176
- isDevelopment(): boolean;
177
- useCors(corsOptions?: FastifyCorsOptions): void;
178
- useDataSource<
179
- T extends IConfig<R>,
180
- R = ReturnType<InstanceType<Constructable<T>>["config"]>,
181
- >(
182
- ConfigClass: Constructable<T>,
183
- modifyConfig?: (config: R) => R,
184
- ): void;
185
- useOpenApi<
186
- T extends IConfig<R>,
187
- R = ReturnType<InstanceType<Constructable<T>>["config"]>,
188
- >(
189
- ConfigClass: Constructable<T>,
190
- modifyConfig?: (config: R) => R,
191
- ): void;
192
-
193
- useMultipart(options: MultipartOptions): void;
194
- useCache(options: any): void
195
-
196
- useMiddlewares<T extends AppMiddleware>(mclasses: Constructor<T>[]): void;
197
- useAuthoriztion<T extends any>(middleware: Constructor<T>): void;
198
- mapRoute<T extends (...args: any[]) => any>(
199
- method: "get" | "post" | "put" | "delete",
200
- path: string,
201
- fn: T,
202
- ): Promise<void>;
203
- mapGet<T extends (...args: any[]) => any>(path: string, fn: T): any;
204
- mapPost<T extends (...args: any[]) => any>(path: string, fn: T): any;
205
- mapPut<T extends (...args: any[]) => any>(path: string, fn: T): any;
206
- mapDelete<T extends (...args: any[]) => any>(path: string, fn: T): any;
207
- useControllers(controllers: any[]): any;
208
- useControllers(controllersOptions: AutoControllerOptions): any;
209
- useControllers(controllersOrOptions: any[] | AutoControllerOptions): any;
210
- useStaticFiles(options?: StaticFileOptions): void;
211
- run(port?: number): Promise<void>;
212
- getTestApp(): TestApplication;
213
- }
214
- type OpenApiConfigClass<T = any> = Constructable<IConfig<T>>;
215
- type OpenApiConfigInput<T = any> = OpenApiConfigClass<T> | T;
216
-
217
- type ConfigClass<T = any> = Constructable<IConfig<T>>;
218
- type ConfigInput<T = any> = ConfigClass<T> | T;
219
- // InternalApplication
220
- export class AvleonApplication {
221
- private static instance: AvleonApplication;
222
- private static buildOptions: any = {};
223
- private app!: FastifyInstance;
224
- private routeSet = new Set<string>(); // Use Set for fast duplicate detection
225
- private alreadyRun = false;
226
- private routes: Map<string, Function> = new Map();
227
- private middlewares: Map<string, AppMiddleware> = new Map();
228
- private rMap = new Map<string, FuncRoute>();
229
- private hasSwagger = false;
230
- private globalSwaggerOptions: any = {};
231
- private dataSourceOptions?: DataSourceOptions = undefined;
232
- private controllers: any[] = [];
233
- private authorizeMiddleware?: any = undefined;
234
- private appConfig: AppConfig;
235
- private dataSource?: DataSource = undefined;
236
- private isMapFeatures = false;
237
- private registerControllerAuto = false;
238
- private registerControllerPath = './src';
239
-
240
- private metaCache = new Map<string, MethodParamMeta>();
241
- private multipartOptions: FastifyMultipartOptions | undefined;
242
- private constructor() {
243
- this.app = fastify();
244
- this.appConfig = new AppConfig();
245
- }
246
-
247
-
248
- static getApp(): AvleonApplication {
249
- let isTestEnv = process.env.NODE_ENV == "test";
250
- if (!AvleonApplication.instance) {
251
- AvleonApplication.instance = new AvleonApplication();
252
- }
253
- return AvleonApplication.instance;
254
- }
255
-
256
- static getInternalApp(buildOptions: any): AvleonApplication {
257
- let isTestEnv = process.env.NODE_ENV == "test";
258
- if (!AvleonApplication.instance) {
259
- AvleonApplication.instance = new AvleonApplication();
260
- }
261
- AvleonApplication.buildOptions = buildOptions;
262
- if (buildOptions.dataSourceOptions) {
263
- AvleonApplication.instance.dataSourceOptions =
264
- buildOptions.dataSourceOptions;
265
- const typeorm = require("typeorm");
266
- const datasource = new typeorm.DataSource(
267
- buildOptions.dataSourceOptions,
268
- ) as DataSource;
269
-
270
- Container.set<DataSource>("idatasource",
271
- datasource,
272
- );
273
- AvleonApplication.instance.dataSource = datasource;
274
- }
275
- return AvleonApplication.instance;
276
- }
277
-
278
- isDevelopment() {
279
- const env = container.get(Environment);
280
- return env.get("NODE_ENV") == "development";
281
- }
282
-
283
-
284
- private async initSwagger(options: OpenApiUiOptions) {
285
- const { routePrefix, logo, ui, theme, configuration, ...restOptions } =
286
- options;
287
-
288
- this.app.register(swagger, {
289
- openapi: {
290
- openapi: "3.0.0",
291
- ...restOptions,
292
- },
293
- });
294
- const rPrefix = routePrefix ? routePrefix : "/docs";
295
-
296
- if (options.ui && options.ui == "scalar") {
297
- const scalarPlugin = optionalRequire("@scalar/fastify-api-reference", {
298
- failOnMissing: true,
299
- customMessage: 'Install "@scalar/fastify-api-reference" to enable API docs.\n\n npm install @scalar/fastify-api-reference',
300
- });
301
- await this.app.register(scalarPlugin, {
302
- routePrefix: rPrefix as any,
303
- configuration: configuration
304
- ? configuration
305
- : {
306
- metaData: {
307
- title: "Avleon Api",
308
- ogTitle: "Avleon",
309
- },
310
- theme: options.theme ? options.theme : "kepler",
311
- favicon: "/static/favicon.png",
312
- },
313
- });
314
- } else {
315
-
316
- const fastifySwaggerUi = optionalRequire("@fastify/swagger-ui", {
317
- failOnMissing: true,
318
- customMessage: 'Install "@fastify/swagger-ui" to enable API docs.\n\n npm install @fastify/swagger-ui',
319
- });
320
- await this.app.register(fastifySwaggerUi, {
321
- logo: logo ? logo : null,
322
- theme: theme ? theme : {},
323
- routePrefix: rPrefix as any,
324
- });
325
- }
326
- }
327
-
328
- private _isConfigClass<T>(input: any): input is ConfigClass<T> {
329
- return (
330
- typeof input === 'function' &&
331
- typeof input.prototype === 'object' &&
332
- input.prototype?.constructor === input
333
- );
334
- }
335
- useCors<T = FastifyCorsOptions>(corsOptions?: ConfigInput<T>) {
336
- let coptions: any = {};
337
- if (corsOptions) {
338
- if (this._isConfigClass<T>(corsOptions)) {
339
- coptions = this.appConfig.get(corsOptions) as T;
340
- } else {
341
- coptions = corsOptions as T;
342
- }
343
- }
344
- this.app.register(cors, coptions);
345
- }
346
-
347
- useOpenApi<T = OpenApiUiOptions>(
348
- configOrClass: OpenApiConfigInput<T>
349
- ) {
350
- let openApiConfig: T;
351
- if (this._isConfigClass(configOrClass)) {
352
- openApiConfig = this.appConfig.get(configOrClass);
353
- } else {
354
- openApiConfig = configOrClass as T;
355
- }
356
- this.globalSwaggerOptions = openApiConfig;
357
- this.hasSwagger = true;
358
- }
359
-
360
- useMultipart<T extends MultipartOptions>(options: ConfigInput<T>) {
361
- let multipartOptions: T;
362
- if (this._isConfigClass(options)) {
363
- multipartOptions = this.appConfig.get(options);
364
- } else {
365
- multipartOptions = options as T;
366
- }
367
- if (multipartOptions) {
368
- this.multipartOptions = multipartOptions;
369
- this.app.register(fastifyMultipart, {
370
- ...this.multipartOptions,
371
- attachFieldsToBody: true,
372
- });
373
- }
374
- }
375
-
376
-
377
- useDataSource<T extends DataSourceOptions>(options: ConfigInput<T>) {
378
-
379
- let dataSourceOptions: T;
380
- if (this._isConfigClass(options)) {
381
- dataSourceOptions = this.appConfig.get(options);
382
- } else {
383
- dataSourceOptions = options as T;
384
- }
385
-
386
- if (!dataSourceOptions) throw new SystemUseError("Invlaid datasource options.")
387
-
388
- this.dataSourceOptions = dataSourceOptions;
389
- const typeorm = require("typeorm");
390
- const datasource = new typeorm.DataSource(
391
- dataSourceOptions,
392
- ) as DataSource;
393
-
394
- this.dataSource = datasource;
395
-
396
- Container.set<DataSource>("idatasource",
397
- datasource,
398
- );
399
-
400
-
401
- }
402
-
403
- private _useCache(options: any) {
404
-
405
- }
406
-
407
-
408
-
409
- useMiddlewares<T extends AppMiddleware>(mclasses: Constructor<T>[]) {
410
- for (const mclass of mclasses) {
411
- const cls = Container.get<T>(mclass);
412
- this.middlewares.set(mclass.name, cls);
413
- this.app.addHook("preHandler", cls.invoke);
414
- }
415
- }
416
-
417
-
418
- useAuthoriztion<T extends any>(middleware: Constructor<T>) {
419
- this.authorizeMiddleware = middleware as any;
420
- }
421
-
422
-
423
- useStaticFiles(
424
- options: StaticFileOptions = { path: undefined, prefix: undefined },
425
- ) {
426
- this.app.register(require("@fastify/static"), {
427
- root: options.path ? options.path : path.join(process.cwd(), "public"),
428
- prefix: options.prefix ? options.prefix : "/static/",
429
- });
430
- }
431
-
432
- private handleMiddlewares<T extends AppMiddleware>(
433
- mclasses: Constructor<T>[],
434
- ) {
435
- for (const mclass of mclasses) {
436
- const cls = Container.get<T>(mclass.constructor);
437
- this.middlewares.set(mclass.name, cls);
438
- this.app.addHook("preHandler", cls.invoke);
439
- }
440
- }
441
-
442
- private executeMiddlewares(target: any, propertyKey?: string) {
443
- const classMiddlewares =
444
- Reflect.getMetadata("controller:middleware", target.constructor) || [];
445
- const methodMiddlewares = propertyKey
446
- ? Reflect.getMetadata("route:middleware", target, propertyKey) || []
447
- : [];
448
-
449
- return [...classMiddlewares, ...methodMiddlewares];
450
- }
451
-
452
- /**
453
- * build controller
454
- * @param controller
455
- * @returns void
456
- */
457
- private async buildController(controller: any) {
458
- const ctrl: any = Container.get(controller);
459
- const controllerMeta = Reflect.getMetadata(
460
- CONTROLLER_META_KEY,
461
- ctrl.constructor,
462
- );
463
- if (!controllerMeta) return;
464
- const prototype = Object.getPrototypeOf(ctrl);
465
- const methods = Object.getOwnPropertyNames(prototype).filter(
466
- (name) => name !== "constructor",
467
- );
468
- const tag = ctrl.constructor.name.replace("Controller", "");
469
- const swaggerControllerMeta =
470
- Reflect.getMetadata("controller:openapi", ctrl.constructor) || {};
471
- const authClsMeata = Reflect.getMetadata(
472
- AUTHORIZATION_META_KEY,
473
- ctrl.constructor,
474
- ) || { authorize: false, options: undefined };
475
-
476
- for await (const method of methods) {
477
- const methodMeta = Reflect.getMetadata(ROUTE_META_KEY, prototype, method);
478
- if (!methodMeta) continue;
479
- const methodmetaOptions = {
480
- method: methodMeta.method.toLowerCase(),
481
- path: formatUrl(controllerMeta.path + methodMeta.path),
482
- };
483
- const routeKey = `${methodmetaOptions.method}:${methodmetaOptions.path}`;
484
- if (!this.routeSet.has(routeKey)) {
485
- this.routeSet.add(routeKey);
486
- }
487
- const classMiddlewares = this.executeMiddlewares(ctrl, method);
488
-
489
- // handle openapi data
490
- const swaggerMeta =
491
- Reflect.getMetadata("route:openapi", prototype, method) || {};
492
-
493
- const authClsMethodMeata = Reflect.getMetadata(
494
- AUTHORIZATION_META_KEY,
495
- ctrl.constructor,
496
- method,
497
- ) || { authorize: false, options: undefined };
498
- const allMeta = this._processMeta(prototype, method);
499
-
500
- let bodySchema: any = null;
501
- allMeta.body.forEach((r) => {
502
- if (r.schema) {
503
- bodySchema = { ...r.schema };
504
- }
505
- });
506
-
507
- const routePath =
508
- methodmetaOptions.path == "" ? "/" : methodmetaOptions.path;
509
-
510
- let schema = { ...swaggerControllerMeta, ...swaggerMeta, tags: [tag] };
511
- if (!swaggerMeta.body && bodySchema) {
512
- schema = { ...schema, body: bodySchema };
513
- }
514
-
515
- this.app.route({
516
- url: routePath,
517
- method: methodmetaOptions.method.toUpperCase(),
518
- schema: { ...schema },
519
- handler: async (req, res) => {
520
- let reqClone = req as IRequest;
521
-
522
-
523
- // class level authrization
524
- if (authClsMeata.authorize && this.authorizeMiddleware) {
525
- const cls = container.get(this.authorizeMiddleware) as any;
526
- await cls.authorize(reqClone, authClsMeata.options);
527
- if (res.sent) return;
528
- }
529
-
530
- // method level authorization
531
- if (authClsMethodMeata.authorize && this.authorizeMiddleware) {
532
- const cls = container.get(this.authorizeMiddleware) as any;
533
- await cls.authorize(reqClone, authClsMethodMeata.options);
534
- if (res.sent) return;
535
- }
536
- if (classMiddlewares.length > 0) {
537
- for (let m of classMiddlewares) {
538
- const cls = Container.get<AppMiddleware>(m.constructor);
539
- reqClone = (await cls.invoke(reqClone, res)) as IRequest;
540
- if (res.sent) return;
541
- }
542
- }
543
- const args = await this._mapArgs(reqClone, allMeta);
544
-
545
- for (let paramMeta of allMeta.params) {
546
- if (paramMeta.required) {
547
- validateOrThrow({ [paramMeta.key]: args[paramMeta.index] }, { [paramMeta.key]: { type: paramMeta.dataType } }, { location: 'param' })
548
- }
549
- }
550
-
551
- for (let queryMeta of allMeta.query) {
552
- if (queryMeta.validatorClass) {
553
- const err = await validateObjectByInstance(
554
- queryMeta.dataType,
555
- args[queryMeta.index],
556
- );
557
- if (err) {
558
- return await res.code(400).send({
559
- code: 400,
560
- error: "ValidationError",
561
- errors: err,
562
- message: err.message,
563
- });
564
- }
565
- }
566
- if (queryMeta.required) {
567
- validateOrThrow({ [queryMeta.key]: args[queryMeta.index] }, { [queryMeta.key]: { type: queryMeta.dataType } }, { location: 'queryparam' })
568
- }
569
- }
570
-
571
- for (let bodyMeta of allMeta.body) {
572
- if (bodyMeta.validatorClass) {
573
- const err = await validateObjectByInstance(
574
- bodyMeta.dataType,
575
- args[bodyMeta.index],
576
- );
577
- if (err) {
578
- return await res.code(400).send({
579
- code: 400,
580
- error: "ValidationError",
581
- errors: err,
582
- message: err.message,
583
- });
584
- }
585
- }
586
- }
587
- const result = await prototype[method].apply(ctrl, args);
588
- return result;
589
- },
590
- });
591
- }
592
- }
593
-
594
- /**
595
- * map all request parameters
596
- * @param req
597
- * @param meta
598
- * @returns
599
- */
600
- private async _mapArgs(req: IRequest, meta: MethodParamMeta): Promise<any[]> {
601
- if (!req.hasOwnProperty("_argsCache")) {
602
- Object.defineProperty(req, "_argsCache", {
603
- value: new Map<string, any[]>(),
604
- enumerable: false,
605
- });
606
- }
607
-
608
- const cache: Map<string, any[]> = (req as any)._argsCache;
609
- const cacheKey = JSON.stringify(meta); // Faster key-based lookup
610
-
611
- if (cache.has(cacheKey)) {
612
- return cache.get(cacheKey)!;
613
- }
614
-
615
- const args: any[] = meta.params.map((p) => req.params[p.key] || null);
616
- meta.query.forEach(
617
- (q) => (args[q.index] = q.key === "all" ? req.query : req.query[q.key]),
618
- );
619
- meta.body.forEach(
620
- (body) => (args[body.index] = { ...req.body, ...req.formData }),
621
- );
622
- meta.currentUser.forEach((user) => (args[user.index] = req.user));
623
- meta.headers.forEach(
624
- (header) =>
625
- (args[header.index] =
626
- header.key === "all" ? req.headers : req.headers[header.key]),
627
- );
628
-
629
- if (meta.file) {
630
- for await (let f of meta.file) {
631
- args[f.index] = await req.file();
632
- }
633
- }
634
-
635
- if (
636
- meta.files &&
637
- req.headers["content-type"]?.startsWith("multipart/form-data") === true
638
- ) {
639
- const files = await req.saveRequestFiles();
640
- if (!files || files.length === 0) {
641
- throw new BadRequestException({ error: "No files uploaded" });
642
- }
643
-
644
- const fileInfo = files.map((file) => ({
645
- type: file.type,
646
- filepath: file.filepath,
647
- fieldname: file.fieldname,
648
- filename: file.filename,
649
- encoding: file.encoding,
650
- mimetype: file.mimetype,
651
- fields: file.fields,
652
- }));
653
- for await (let f of meta.files) {
654
- const findex = fileInfo.findIndex((x) => x.fieldname == f.fieldName);
655
- if (f.fieldName != "all" && findex == -1) {
656
- throw new BadRequestException(
657
- `${f.fieldName} doesn't exists in request files tree.`,
658
- );
659
- }
660
- args[f.index] =
661
- f.fieldName == "all"
662
- ? fileInfo
663
- : fileInfo.filter((x) => x.fieldname == f.fieldName);
664
- }
665
- }
666
-
667
- cache.set(cacheKey, args);
668
- return args;
669
- }
670
-
671
- /**
672
- * Process Meta for controlelr class methods
673
- * @param prototype
674
- * @param method
675
- * @returns
676
- */
677
- private _processMeta(prototype: any, method: string): MethodParamMeta {
678
- const cacheKey = `${prototype.constructor.name}_${method}`;
679
- if (this.metaCache.has(cacheKey)) {
680
- return this.metaCache.get(cacheKey)!;
681
- }
682
-
683
- const meta: MethodParamMeta = {
684
- params: Reflect.getMetadata(PARAM_META_KEY, prototype, method) || [],
685
- query: Reflect.getMetadata(QUERY_META_KEY, prototype, method) || [],
686
- body: Reflect.getMetadata(REQUEST_BODY_META_KEY, prototype, method) || [],
687
- file: Reflect.getMetadata(REQUEST_BODY_FILE_KEY, prototype, method) || [],
688
- files:
689
- Reflect.getMetadata(REQUEST_BODY_FILES_KEY, prototype, method) || [],
690
- headers:
691
- Reflect.getMetadata(REQUEST_HEADER_META_KEY, prototype, method) || [],
692
- currentUser:
693
- Reflect.getMetadata(REQUEST_USER_META_KEY, prototype, method) || [],
694
- // swagger: Reflect.getMetadata("route:openapi", prototype, method) || {}
695
- };
696
-
697
- this.metaCache.set(cacheKey, meta);
698
- return meta;
699
- }
700
-
701
- private _resolveControllerDir(dir?: string) {
702
- const isTsNode =
703
- process.env.TS_NODE_DEV ||
704
- process.env.TS_NODE_PROJECT ||
705
- (process as any)[Symbol.for("ts-node.register.instance")];
706
- const controllerDir = path.join(
707
- process.cwd(),
708
- this.registerControllerPath
709
- );
710
-
711
- return isTsNode ? controllerDir : controllerDir.replace('src', 'dist')
712
- }
713
-
714
- private async autoControllers(controllersPath?: string) {
715
- const conDir = this._resolveControllerDir(controllersPath);
716
-
717
- const files = await fs.readdir(conDir, { recursive: true });
718
- for (const file of files) {
719
- const isTestFile = /\.(test|spec|e2e-spec)\.ts$/.test(file);
720
- if (isTestFile) continue;
721
- if (isTsNode ? file.endsWith(".ts") : file.endsWith(".js")) {
722
- const filePath = path.join(conDir, file);
723
- const module = await import(filePath);
724
- for (const exported of Object.values(module)) {
725
- if (typeof exported === "function" && isApiController(exported)) {
726
- console.log('adding', exported.name)
727
- if (!this.controllers.some(con => exported.name == con.name)) {
728
- this.controllers.push(exported)
729
- }
730
-
731
- //this.buildController(exported);
732
- }
733
- }
734
- }
735
- }
736
- }
737
-
738
- useControllers(controllers: Constructor[] | AutoControllerOptions) {
739
-
740
- if (Array.isArray(controllers)) {
741
- this.controllers = controllers;
742
-
743
- controllers.forEach(controller => {
744
- if (!this.controllers.includes(controller)) {
745
- this.controllers.push(controller)
746
- }
747
- })
748
- } else {
749
- this.registerControllerAuto = true;
750
- if (controllers.path) {
751
- this.registerControllerPath = controllers.path;
752
- }
753
- }
754
-
755
- }
756
-
757
- // addFeature(feature:{controllers:Function[]}){
758
- // feature.controllers.forEach(c=> this.controllers.push(c))
759
- // }
760
-
761
- // mapFeature(){
762
- // if(!this.isMapFeatures){
763
- // this.isMapFeatures = true;
764
- // }
765
- // }
766
-
767
- private async _mapControllers() {
768
-
769
- if (this.controllers.length > 0) {
770
- for (let controller of this.controllers) {
771
- if (isApiController(controller)) {
772
- this.buildController(controller);
773
- } else {
774
- throw new SystemUseError("Not a api controller.");
775
- }
776
- }
777
- }
778
- }
779
-
780
- // useControllersAuto(controllerPath?:string) {
781
- // this.registerControllerAuto = true;
782
- // //this.autoControllers();
783
- // }
784
-
785
-
786
- private async mapFn(fn: Function) {
787
- const original = fn;
788
- fn = function () { };
789
- return fn;
790
- }
791
-
792
-
793
- private _handleError(error: any): {
794
- code: number;
795
- error: string;
796
- message: any;
797
- } {
798
- if (error instanceof BaseHttpException) {
799
- return {
800
- code: error.code,
801
- error: error.name,
802
- message: isValidJsonString(error.message)
803
- ? JSON.parse(error.message)
804
- : error.message,
805
- };
806
- }
807
- return {
808
- code: 500,
809
- error: "INTERNAL_ERROR",
810
- message: error.message ? error.message : "Something going wrong.",
811
- };
812
- }
813
-
814
- async mapRoute<T extends (...args: any[]) => any>(
815
- method: "get" | "post" | "put" | "delete",
816
- path: string = "",
817
- fn: T,
818
- ) {
819
- await this.mapFn(fn); // Assuming mapFn is needed for all methods
820
-
821
- this.app[method](path, async (req: any, res: any) => {
822
- // Dynamic method call
823
- try {
824
- const result = await fn.apply(this, [req, res]);
825
- if (typeof result === "object" && result !== null) {
826
- res.json(result); // Use res.json for objects
827
- } else {
828
- res.send(result); // Fallback for other types
829
- }
830
- } catch (error) {
831
- console.error(`Error in ${method} route handler:`, error);
832
- const handledErr = this._handleError(error);
833
- res.status(handledErr.code).send(handledErr);
834
- }
835
- });
836
- }
837
- private _routeHandler<T extends (...args: any[]) => any>(
838
- routePath: string,
839
- method: string,
840
- fn: T,
841
- ) {
842
- const routeKey = method + ":" + routePath;
843
- this.rMap.set(routeKey, {
844
- handler: fn,
845
- middlewares: [],
846
- schema: {},
847
- });
848
- // this.mapFn(fn);
849
-
850
- const route = {
851
- useMiddleware: <M extends AppMiddleware>(
852
- middlewares: Constructor<AppMiddleware>[],
853
- ) => {
854
- const midds = Array.isArray(middlewares) ? middlewares : [middlewares];
855
- const ms: any[] = (midds as unknown as any[]).map((mclass) => {
856
- const cls = Container.get<AppMiddleware>(mclass);
857
- this.middlewares.set(mclass.name, cls);
858
- return cls.invoke;
859
- });
860
-
861
- const r = this.rMap.get(routeKey);
862
- if (r) {
863
- r.middlewares = ms;
864
- }
865
- return route;
866
- },
867
-
868
- useOpenApi: (options: OpenApiOptions) => {
869
- const r = this.rMap.get(routeKey);
870
- if (r) {
871
- r.schema = options;
872
- }
873
- return route;
874
- },
875
- };
876
-
877
- return route;
878
- }
879
-
880
-
881
- mapGet<T extends (...args: any[]) => any>(path: string = "", fn: T) {
882
- return this._routeHandler(path, "GET", fn);
883
- }
884
-
885
- mapPost<T extends (...args: any[]) => any>(path: string = "", fn: T) {
886
- return this._routeHandler(path, "POST", fn);
887
- }
888
-
889
- mapPut<T extends (...args: any[]) => any>(path: string = "", fn: T) {
890
- return this._routeHandler(path, "PUT", fn);
891
- }
892
-
893
- mapDelete<T extends (...args: any[]) => any>(path: string = "", fn: T) {
894
- return this._routeHandler(path, "DELETE", fn);
895
- }
896
-
897
-
898
- private _mapFeatures() {
899
- const features = Container.get('features');
900
- console.log('Features', features);
901
- }
902
-
903
- async initializeDatabase() {
904
- if (this.dataSourceOptions && this.dataSource) {
905
- await this.dataSource.initialize();
906
- }
907
- }
908
-
909
- async run(port: number = 4000, fn?: CallableFunction): Promise<void> {
910
- if (this.alreadyRun) throw new SystemUseError("App already running");
911
- this.alreadyRun = true;
912
-
913
- if (this.hasSwagger) {
914
- await this.initSwagger(this.globalSwaggerOptions);
915
- }
916
- await this.initializeDatabase();
917
- if (this.isMapFeatures) {
918
- this._mapFeatures();
919
- }
920
-
921
- if (this.registerControllerAuto) {
922
- await this.autoControllers();
923
- }
924
- await this._mapControllers();
925
-
926
- this.rMap.forEach((value, key) => {
927
- const [m, r] = key.split(":");
928
- this.app.route({
929
- method: m,
930
- url: r,
931
- schema: value.schema || {},
932
- preHandler: value.middlewares ? value.middlewares : [],
933
- handler: async (req, res) => {
934
- const result = await value.handler.apply(this, [req, res]);
935
- return result;
936
- },
937
- });
938
- });
939
- this.app.setErrorHandler(async (error, req, res) => {
940
- const handledErr = this._handleError(error);
941
- if (error instanceof ValidationErrorException || error instanceof BadRequestException) {
942
- return res.status(handledErr.code).send({
943
- code: handledErr.code,
944
- error: handledErr.error,
945
- errors: handledErr.message,
946
- });
947
- }
948
- return res.status(handledErr.code).send(handledErr);
949
- });
950
- await this.app.ready();
951
- await this.app.listen({ port });
952
- console.log(`Application running on http://127.0.0.1:${port}`);
953
- }
954
- getTestApp(buildOptions?: any): TestApplication {
955
- try {
956
- // }
957
- // this.initializeDatabase();
958
- this._mapControllers();
959
- this.rMap.forEach((value, key) => {
960
- const [m, r] = key.split(":");
961
- this.app.route({
962
- method: m,
963
- url: r,
964
- schema: value.schema || {},
965
- preHandler: value.middlewares ? value.middlewares : [],
966
- handler: async (req, res) => {
967
- const result = await value.handler.apply(this, [req, res]);
968
- return result;
969
- },
970
- });
971
- });
972
-
973
- this.app.setErrorHandler(async (error, req, res) => {
974
- const handledErr = this._handleError(error);
975
- if (error instanceof ValidationErrorException) {
976
- return res.status(handledErr.code).send({
977
- code: handledErr.code,
978
- error: handledErr.error,
979
- errors: handledErr.message,
980
- });
981
- }
982
- return res.status(handledErr.code).send(handledErr);
983
- });
984
- // return this.app as any;
985
- //
986
- return {
987
- get: async (url: string, options?: InjectOptions) =>
988
- this.app.inject({ method: "GET", url, ...options }),
989
- post: async (url: string, options?: InjectOptions) =>
990
- this.app.inject({ method: "POST", url, ...options }),
991
- put: async (url: string, options?: InjectOptions) =>
992
- this.app.inject({ method: "PUT", url, ...options }),
993
- patch: async (url: string, options?: InjectOptions) =>
994
- this.app.inject({ method: "PATCH", url, ...options }),
995
- delete: async (url: string, options?: InjectOptions) =>
996
- this.app.inject({ method: "DELETE", url, ...options }),
997
- options: async (url: string, options?: InjectOptions) =>
998
- this.app.inject({ method: "OPTIONS", url, ...options }),
999
- getController: <T>(controller: Constructor<T>, deps: any[] = []) => {
1000
- const paramTypes = Reflect.getMetadata('design:paramtypes', controller) || [];
1001
-
1002
- deps.forEach((dep, i) => {
1003
- Container.set(paramTypes[i], dep);
1004
- });
1005
-
1006
- return Container.get(controller);
1007
-
1008
- },
1009
- };
1010
- } catch (error) {
1011
- console.log(error);
1012
- throw new SystemUseError("Can't get test appliction");
1013
- }
1014
- }
1015
- }
1016
-
1017
- export type Application = typeof AvleonApplication;
1018
-
1019
- // Applciation Builder
1020
- export interface ITestBuilder {
1021
- getTestApplication(): AvleonTestAppliction;
1022
- createTestApplication(options: any): AvleonTestAppliction;
1023
- }
1024
-
1025
- export interface IAppBuilder {
1026
- registerPlugin<T extends Function, S extends {}>(
1027
- plugin: T,
1028
- options: S,
1029
- ): Promise<void>;
1030
- addDataSource<
1031
- T extends IConfig<R>,
1032
- R = ReturnType<InstanceType<Constructable<T>>["config"]>,
1033
- >(
1034
- ConfigClass: Constructable<T>,
1035
- modifyConfig?: (config: R) => R,
1036
- ): void;
1037
- build<T extends IAvleonApplication>(): T;
1038
- }
1039
-
1040
- export class AvleonTest {
1041
- private constructor() {
1042
- process.env.NODE_ENV = "test";
1043
- }
1044
- static getController<T>(controller: Constructor<T>, deps: any[] = []) {
1045
- const paramTypes = Reflect.getMetadata('design:paramtypes', controller) || [];
1046
-
1047
- deps.forEach((dep, i) => {
1048
- Container.set(paramTypes[i], dep);
1049
- });
1050
-
1051
- return Container.get(controller);
1052
- }
1053
-
1054
- private getService<T>(service: Constructor<T>) {
1055
- return Container.get(service);
1056
- }
1057
-
1058
-
1059
- static createTestApplication(options: TestAppOptions) {
1060
- const app = AvleonApplication.getInternalApp({
1061
- dataSourceOptions: options.dataSource ? options.dataSource : null,
1062
- });
1063
- app.useControllers([...options.controllers]);
1064
- return app.getTestApp();
1065
- }
1066
-
1067
- static from(app: AvleonApplication) {
1068
- return app.getTestApp();
1069
- }
1070
-
1071
- static clean() {
1072
- Container.reset();
1073
- }
1074
- }
1075
-
1076
- export class Avleon {
1077
-
1078
- static createApplication() {
1079
- const app = AvleonApplication.getApp();
1080
- return app
1081
- }
1082
-
1083
-
1084
- }
1
+ /**
2
+ * @copyright 2024
3
+ * @author Tareq Hossain
4
+ * @email xtrinsic96@gmail.com
5
+ * @url https://github.com/xtareq
6
+ */
7
+ import fastify, {
8
+ FastifyInstance,
9
+ FastifyReply,
10
+ FastifyRequest,
11
+ HookHandlerDoneFunction,
12
+ preHandlerHookHandler,
13
+ RouteGenericInterface,
14
+ InjectOptions,
15
+ LightMyRequestResponse,
16
+ } from "fastify";
17
+ import Container, { Constructable } from "typedi";
18
+ import fs from "fs/promises";
19
+ import path from "path";
20
+ import container, {
21
+ CONTROLLER_META_KEY,
22
+ ROUTE_META_KEY,
23
+ PARAM_META_KEY,
24
+ QUERY_META_KEY,
25
+ REQUEST_BODY_META_KEY,
26
+ REQUEST_HEADER_META_KEY,
27
+ getRegisteredControllers,
28
+ isApiController,
29
+ REQUEST_USER_META_KEY,
30
+ AUTHORIZATION_META_KEY,
31
+ REQUEST_BODY_FILES_KEY,
32
+ REQUEST_BODY_FILE_KEY,
33
+ FEATURE_KEY,
34
+ } from "./container";
35
+ import {
36
+ Constructor,
37
+ formatUrl,
38
+ isValidJsonString,
39
+ validateObjectByInstance,
40
+ } from "./helpers";
41
+ import { SystemUseError } from "./exceptions/system-exception";
42
+ import { existsSync, PathLike } from "fs";
43
+ import { DataSource, DataSourceOptions } from "typeorm";
44
+ import { AvleonMiddleware } from "./middleware";
45
+ import {
46
+ BadRequestException,
47
+ BaseHttpException,
48
+ ValidationErrorException,
49
+ } from "./exceptions";
50
+ import { OpenApiOptions, OpenApiUiOptions } from "./openapi";
51
+ import swagger from "@fastify/swagger";
52
+ import { AvleonConfig, IConfig } from "./config";
53
+ import { Environment } from "./environment-variables";
54
+ import cors, { FastifyCorsOptions } from "@fastify/cors";
55
+ import fastifyMultipart, { FastifyMultipartOptions } from "@fastify/multipart";
56
+ import { MultipartFile } from "./multipart";
57
+ import { validateOrThrow } from "./validation";
58
+ import { optionalRequire } from "./utils";
59
+
60
+ import { ServerOptions, Server as SocketIoServer } from "socket.io";
61
+ import { SocketContextService } from "./event-dispatcher";
62
+ import { EventSubscriberRegistry } from "./event-subscriber";
63
+
64
+ export type FuncRoute = {
65
+ handler: any;
66
+ middlewares?: any[];
67
+ schema?: {};
68
+ };
69
+
70
+ // IRequest
71
+ export interface IRequest extends FastifyRequest {
72
+ params: any;
73
+ query: any;
74
+ body: any;
75
+ headers: any;
76
+ user?: any;
77
+ }
78
+
79
+ export interface DoneFunction extends HookHandlerDoneFunction { }
80
+ // IResponse
81
+ export interface IResponse extends FastifyReply { }
82
+
83
+ export type TestResponseType = LightMyRequestResponse;
84
+ export type TestResponse = TestResponseType | Promise<TestResponseType>;
85
+
86
+ export interface TestApplication {
87
+ get: (url: string, options?: InjectOptions) => TestResponse;
88
+ post: (url: string, options?: InjectOptions) => TestResponse;
89
+ put: (url: string, options?: InjectOptions) => TestResponse;
90
+ patch: (url: string, options?: InjectOptions) => TestResponse;
91
+ delete: (url: string, options?: InjectOptions) => TestResponse;
92
+ options: (url: string, options?: InjectOptions) => TestResponse;
93
+ getController?: <T>(controller: Constructor<T>) => T;
94
+ }
95
+
96
+ // ParamMetaOptions
97
+ export interface ParamMetaOptions {
98
+ index: number;
99
+ key: string;
100
+ name: string;
101
+ required: boolean;
102
+ validate: boolean;
103
+ dataType: any;
104
+ validatorClass: boolean;
105
+ schema?: any;
106
+ type:
107
+ | "route:param"
108
+ | "route:query"
109
+ | "route:body"
110
+ | "route:header"
111
+ | "route:user"
112
+ | "route:file"
113
+ | "route:files";
114
+ }
115
+
116
+ export interface ParamMetaFilesOptions {
117
+ index: number;
118
+ type: "route:files";
119
+ files: MultipartFile[];
120
+ fieldName: string;
121
+ }
122
+
123
+ // Method Param Meta options
124
+ export interface MethodParamMeta {
125
+ params: ParamMetaOptions[];
126
+ query: ParamMetaOptions[];
127
+ body: ParamMetaOptions[];
128
+ headers: ParamMetaOptions[];
129
+ currentUser: ParamMetaOptions[];
130
+ swagger?: OpenApiUiOptions;
131
+ file?: any[];
132
+ files?: ParamMetaFilesOptions[];
133
+ }
134
+
135
+ interface IRoute {
136
+ url: string;
137
+ method: string;
138
+ controller: string;
139
+ }
140
+
141
+ const isTsNode =
142
+ process.env.TS_NODE_DEV ||
143
+ process.env.TS_NODE_PROJECT ||
144
+ (process as any)[Symbol.for("ts-node.register.instance")];
145
+ const controllerDir = path.join(
146
+ process.cwd(),
147
+ isTsNode ? "./src/controllers" : "./dist/cotrollers",
148
+ );
149
+
150
+ type StaticFileOptions = {
151
+ path?: PathLike;
152
+ prefix?: string;
153
+ };
154
+
155
+ type MultipartOptions = {
156
+ destination: PathLike;
157
+ } & FastifyMultipartOptions;
158
+
159
+ type AvleonApp = FastifyInstance;
160
+
161
+ export type TestAppOptions = {
162
+ controllers: Constructor[];
163
+ dataSource?: DataSource;
164
+ };
165
+
166
+ export interface AvleonTestAppliction {
167
+ addDataSource: (dataSourceOptions: DataSourceOptions) => void;
168
+ getApp: (options?: TestAppOptions) => any;
169
+ getController: <T>(controller: Constructor<T>, deps: any[]) => T;
170
+ }
171
+
172
+ export type AutoControllerOptions = {
173
+ auto: true;
174
+ path?: string;
175
+ };
176
+ export interface IAvleonApplication {
177
+ isDevelopment(): boolean;
178
+ useCors(corsOptions?: FastifyCorsOptions): void;
179
+ useDataSource<
180
+ T extends IConfig<R>,
181
+ R = ReturnType<InstanceType<Constructable<T>>["config"]>,
182
+ >(
183
+ ConfigClass: Constructable<T>,
184
+ modifyConfig?: (config: R) => R,
185
+ ): void;
186
+ useOpenApi<
187
+ T extends IConfig<R>,
188
+ R = ReturnType<InstanceType<Constructable<T>>["config"]>,
189
+ >(
190
+ ConfigClass: Constructable<T>,
191
+ modifyConfig?: (config: R) => R,
192
+ ): void;
193
+
194
+ useMultipart(options: MultipartOptions): void;
195
+ useCache(options: any): void;
196
+
197
+ useMiddlewares<T extends AvleonMiddleware>(mclasses: Constructor<T>[]): void;
198
+ useAuthoriztion<T extends any>(middleware: Constructor<T>): void;
199
+ mapRoute<T extends (...args: any[]) => any>(
200
+ method: "get" | "post" | "put" | "delete",
201
+ path: string,
202
+ fn: T,
203
+ ): Promise<void>;
204
+ mapGet<T extends (...args: any[]) => any>(path: string, fn: T): any;
205
+ mapPost<T extends (...args: any[]) => any>(path: string, fn: T): any;
206
+ mapPut<T extends (...args: any[]) => any>(path: string, fn: T): any;
207
+ mapDelete<T extends (...args: any[]) => any>(path: string, fn: T): any;
208
+ useControllers(controllers: any[]): any;
209
+ useControllers(controllersOptions: AutoControllerOptions): any;
210
+ useControllers(controllersOrOptions: any[] | AutoControllerOptions): any;
211
+ useStaticFiles(options?: StaticFileOptions): void;
212
+ run(port?: number): Promise<void>;
213
+ getTestApp(): TestApplication;
214
+ }
215
+ type OpenApiConfigClass<T = any> = Constructable<IConfig<T>>;
216
+ type OpenApiConfigInput<T = any> = OpenApiConfigClass<T> | T;
217
+
218
+ type ConfigClass<T = any> = Constructable<IConfig<T>>;
219
+ type ConfigInput<T = any> = ConfigClass<T> | T;
220
+
221
+ interface FastifyWithIO extends FastifyInstance {
222
+ io?: any
223
+ }
224
+
225
+ const subscriberRegistry = Container.get(EventSubscriberRegistry);
226
+ // InternalApplication
227
+ export class AvleonApplication {
228
+ private static instance: AvleonApplication;
229
+ private static buildOptions: any = {};
230
+ private app!: FastifyWithIO;
231
+ private routeSet = new Set<string>(); // Use Set for fast duplicate detection
232
+ private alreadyRun = false;
233
+ private routes: Map<string, Function> = new Map();
234
+ private middlewares: Map<string, AvleonMiddleware> = new Map();
235
+ private rMap = new Map<string, FuncRoute>();
236
+ private hasSwagger = false;
237
+ private _hasWebsocket = false;
238
+ private globalSwaggerOptions: any = {};
239
+ private dataSourceOptions?: DataSourceOptions = undefined;
240
+ private controllers: any[] = [];
241
+ private authorizeMiddleware?: any = undefined;
242
+ private appConfig: AvleonConfig;
243
+ private dataSource?: DataSource = undefined;
244
+ private isMapFeatures = false;
245
+ private registerControllerAuto = false;
246
+ private registerControllerPath = "./src";
247
+
248
+ private metaCache = new Map<string, MethodParamMeta>();
249
+ private multipartOptions: FastifyMultipartOptions | undefined;
250
+ private constructor() {
251
+ this.app = fastify();
252
+ this.appConfig = new AvleonConfig();
253
+ }
254
+
255
+ static getApp(): AvleonApplication {
256
+ let isTestEnv = process.env.NODE_ENV == "test";
257
+ if (!AvleonApplication.instance) {
258
+ AvleonApplication.instance = new AvleonApplication();
259
+ }
260
+ return AvleonApplication.instance;
261
+ }
262
+
263
+ static getInternalApp(buildOptions: any): AvleonApplication {
264
+ let isTestEnv = process.env.NODE_ENV == "test";
265
+ if (!AvleonApplication.instance) {
266
+ AvleonApplication.instance = new AvleonApplication();
267
+ }
268
+ AvleonApplication.buildOptions = buildOptions;
269
+ if (buildOptions.dataSourceOptions) {
270
+ AvleonApplication.instance.dataSourceOptions =
271
+ buildOptions.dataSourceOptions;
272
+ const typeorm = require("typeorm");
273
+ const datasource = new typeorm.DataSource(
274
+ buildOptions.dataSourceOptions,
275
+ ) as DataSource;
276
+
277
+ Container.set<DataSource>("idatasource", datasource);
278
+ AvleonApplication.instance.dataSource = datasource;
279
+ }
280
+ return AvleonApplication.instance;
281
+ }
282
+
283
+ isDevelopment() {
284
+ const env = container.get(Environment);
285
+ return env.get("NODE_ENV") == "development";
286
+ }
287
+
288
+ private async initSwagger(options: OpenApiUiOptions) {
289
+ const { routePrefix, logo, ui, theme, configuration, ...restOptions } =
290
+ options;
291
+
292
+ this.app.register(swagger, {
293
+ openapi: {
294
+ openapi: "3.0.0",
295
+ ...restOptions,
296
+ },
297
+ });
298
+ const rPrefix = routePrefix ? routePrefix : "/docs";
299
+
300
+ if (options.ui && options.ui == "scalar") {
301
+ const scalarPlugin = optionalRequire("@scalar/fastify-api-reference", {
302
+ failOnMissing: true,
303
+ customMessage:
304
+ "Install \"@scalar/fastify-api-reference\" to enable API docs.\n\n npm install @scalar/fastify-api-reference",
305
+ });
306
+ await this.app.register(scalarPlugin, {
307
+ routePrefix: rPrefix as any,
308
+ configuration: configuration
309
+ ? configuration
310
+ : {
311
+ metaData: {
312
+ title: "Avleon Api",
313
+ ogTitle: "Avleon",
314
+ },
315
+ theme: options.theme ? options.theme : "kepler",
316
+ favicon: "/static/favicon.png",
317
+ },
318
+ });
319
+ } else {
320
+ const fastifySwaggerUi = optionalRequire("@fastify/swagger-ui", {
321
+ failOnMissing: true,
322
+ customMessage:
323
+ "Install \"@fastify/swagger-ui\" to enable API docs.\n\n npm install @fastify/swagger-ui",
324
+ });
325
+ await this.app.register(fastifySwaggerUi, {
326
+ logo: logo ? logo : null,
327
+ theme: theme ? theme : {},
328
+ routePrefix: rPrefix as any,
329
+ });
330
+ }
331
+ }
332
+
333
+ private _isConfigClass<T>(input: any): input is ConfigClass<T> {
334
+ return (
335
+ typeof input === "function" &&
336
+ typeof input.prototype === "object" &&
337
+ input.prototype?.constructor === input
338
+ );
339
+ }
340
+ useCors<T = FastifyCorsOptions>(corsOptions?: ConfigInput<T>) {
341
+ let coptions: any = {};
342
+ if (corsOptions) {
343
+ if (this._isConfigClass<T>(corsOptions)) {
344
+ coptions = this.appConfig.get(corsOptions) as T;
345
+ } else {
346
+ coptions = corsOptions as T;
347
+ }
348
+ }
349
+ this.app.register(cors, coptions);
350
+ }
351
+
352
+ useWebSocket<T = Partial<ServerOptions>>(socketOptions: ConfigInput<T>) {
353
+ this._hasWebsocket = true;
354
+ this._initWebSocket(socketOptions);
355
+ }
356
+
357
+ private async _initWebSocket(options:any) {
358
+ const fsSocketIO = optionalRequire("fastify-socket.io", {
359
+ failOnMissing: true,
360
+ customMessage:
361
+ "Install \"fastify-socket.io\" to enable socket.io.\n\n run pnpm install fastify-socket.io",
362
+ });
363
+ await this.app.register(fsSocketIO, options);
364
+ const socketIO = await this.app.io;
365
+ Container.set(SocketIoServer, socketIO);
366
+ }
367
+
368
+ useOpenApi<T = OpenApiUiOptions>(configOrClass: OpenApiConfigInput<T>) {
369
+ let openApiConfig: T;
370
+ if (this._isConfigClass(configOrClass)) {
371
+ openApiConfig = this.appConfig.get(configOrClass);
372
+ } else {
373
+ openApiConfig = configOrClass as T;
374
+ }
375
+ this.globalSwaggerOptions = openApiConfig;
376
+ this.hasSwagger = true;
377
+ }
378
+
379
+ useMultipart<T extends MultipartOptions>(options: ConfigInput<T>) {
380
+ let multipartOptions: T;
381
+ if (this._isConfigClass(options)) {
382
+ multipartOptions = this.appConfig.get(options);
383
+ } else {
384
+ multipartOptions = options as T;
385
+ }
386
+ if (multipartOptions) {
387
+ this.multipartOptions = multipartOptions;
388
+ this.app.register(fastifyMultipart, {
389
+ ...this.multipartOptions,
390
+ attachFieldsToBody: true,
391
+ });
392
+ }
393
+ }
394
+
395
+ useDataSource<T extends DataSourceOptions>(options: ConfigInput<T>) {
396
+ let dataSourceOptions: T;
397
+ if (this._isConfigClass(options)) {
398
+ dataSourceOptions = this.appConfig.get(options);
399
+ } else {
400
+ dataSourceOptions = options as T;
401
+ }
402
+
403
+ if (!dataSourceOptions)
404
+ throw new SystemUseError("Invlaid datasource options.");
405
+
406
+ this.dataSourceOptions = dataSourceOptions;
407
+ const typeorm = require("typeorm");
408
+ const datasource = new typeorm.DataSource(dataSourceOptions) as DataSource;
409
+
410
+ this.dataSource = datasource;
411
+
412
+ Container.set<DataSource>("idatasource", datasource);
413
+ }
414
+
415
+ private _useCache(options: any) { }
416
+
417
+ useMiddlewares<T extends AvleonMiddleware>(mclasses: Constructor<T>[]) {
418
+ for (const mclass of mclasses) {
419
+ const cls = Container.get<T>(mclass);
420
+ this.middlewares.set(mclass.name, cls);
421
+ this.app.addHook("preHandler", cls.invoke);
422
+ }
423
+ }
424
+
425
+ useAuthoriztion<T extends any>(middleware: Constructor<T>) {
426
+ this.authorizeMiddleware = middleware as any;
427
+ }
428
+
429
+ useStaticFiles(
430
+ options: StaticFileOptions = { path: undefined, prefix: undefined },
431
+ ) {
432
+ this.app.register(require("@fastify/static"), {
433
+ root: options.path ? options.path : path.join(process.cwd(), "public"),
434
+ prefix: options.prefix ? options.prefix : "/static/",
435
+ });
436
+ }
437
+
438
+ private handleMiddlewares<T extends AvleonMiddleware>(
439
+ mclasses: Constructor<T>[],
440
+ ) {
441
+ for (const mclass of mclasses) {
442
+ const cls = Container.get<T>(mclass.constructor);
443
+ this.middlewares.set(mclass.name, cls);
444
+ this.app.addHook("preHandler", cls.invoke);
445
+ }
446
+ }
447
+
448
+ private executeMiddlewares(target: any, propertyKey?: string) {
449
+ const classMiddlewares =
450
+ Reflect.getMetadata("controller:middleware", target.constructor) || [];
451
+ const methodMiddlewares = propertyKey
452
+ ? Reflect.getMetadata("route:middleware", target, propertyKey) || []
453
+ : [];
454
+
455
+ return [...classMiddlewares, ...methodMiddlewares];
456
+ }
457
+
458
+ /**
459
+ * build controller
460
+ * @param controller
461
+ * @returns void
462
+ */
463
+ private async buildController(controller: any) {
464
+ const ctrl: any = Container.get(controller);
465
+ const controllerMeta = Reflect.getMetadata(
466
+ CONTROLLER_META_KEY,
467
+ ctrl.constructor,
468
+ );
469
+ if (!controllerMeta) return;
470
+ const prototype = Object.getPrototypeOf(ctrl);
471
+ const methods = Object.getOwnPropertyNames(prototype).filter(
472
+ (name) => name !== "constructor",
473
+ );
474
+ const tag = ctrl.constructor.name.replace("Controller", "");
475
+ const swaggerControllerMeta =
476
+ Reflect.getMetadata("controller:openapi", ctrl.constructor) || {};
477
+ const authClsMeata = Reflect.getMetadata(
478
+ AUTHORIZATION_META_KEY,
479
+ ctrl.constructor,
480
+ ) || { authorize: false, options: undefined };
481
+
482
+ for await (const method of methods) {
483
+ const methodMeta = Reflect.getMetadata(ROUTE_META_KEY, prototype, method);
484
+ if (!methodMeta) continue;
485
+ const methodmetaOptions = {
486
+ method: methodMeta.method.toLowerCase(),
487
+ path: formatUrl(controllerMeta.path + methodMeta.path),
488
+ };
489
+ const routeKey = `${methodmetaOptions.method}:${methodmetaOptions.path}`;
490
+ if (!this.routeSet.has(routeKey)) {
491
+ this.routeSet.add(routeKey);
492
+ }
493
+ const classMiddlewares = this.executeMiddlewares(ctrl, method);
494
+
495
+ // handle openapi data
496
+ const swaggerMeta =
497
+ Reflect.getMetadata("route:openapi", prototype, method) || {};
498
+
499
+ const authClsMethodMeata = Reflect.getMetadata(
500
+ AUTHORIZATION_META_KEY,
501
+ ctrl.constructor,
502
+ method,
503
+ ) || { authorize: false, options: undefined };
504
+ const allMeta = this._processMeta(prototype, method);
505
+
506
+ let bodySchema: any = null;
507
+ allMeta.body.forEach((r) => {
508
+ if (r.schema) {
509
+ bodySchema = { ...r.schema };
510
+ }
511
+ });
512
+
513
+ const routePath =
514
+ methodmetaOptions.path == "" ? "/" : methodmetaOptions.path;
515
+
516
+ let schema = { ...swaggerControllerMeta, ...swaggerMeta, tags: [tag] };
517
+ if (!swaggerMeta.body && bodySchema) {
518
+ schema = { ...schema, body: bodySchema };
519
+ }
520
+
521
+ this.app.route({
522
+ url: routePath,
523
+ method: methodmetaOptions.method.toUpperCase(),
524
+ schema: { ...schema },
525
+ handler: async (req, res) => {
526
+ let reqClone = req as IRequest;
527
+
528
+ // class level authrization
529
+ if (authClsMeata.authorize && this.authorizeMiddleware) {
530
+ const cls = container.get(this.authorizeMiddleware) as any;
531
+ await cls.authorize(reqClone, authClsMeata.options);
532
+ if (res.sent) return;
533
+ }
534
+
535
+ // method level authorization
536
+ if (authClsMethodMeata.authorize && this.authorizeMiddleware) {
537
+ const cls = container.get(this.authorizeMiddleware) as any;
538
+ await cls.authorize(reqClone, authClsMethodMeata.options);
539
+ if (res.sent) return;
540
+ }
541
+ if (classMiddlewares.length > 0) {
542
+ for (let m of classMiddlewares) {
543
+ const cls = Container.get<AvleonMiddleware>(m.constructor);
544
+ reqClone = (await cls.invoke(reqClone, res)) as IRequest;
545
+ if (res.sent) return;
546
+ }
547
+ }
548
+ const args = await this._mapArgs(reqClone, allMeta);
549
+
550
+ for (let paramMeta of allMeta.params) {
551
+ if (paramMeta.required) {
552
+ validateOrThrow(
553
+ { [paramMeta.key]: args[paramMeta.index] },
554
+ { [paramMeta.key]: { type: paramMeta.dataType } },
555
+ { location: "param" },
556
+ );
557
+ }
558
+ }
559
+
560
+ for (let queryMeta of allMeta.query) {
561
+ if (queryMeta.validatorClass) {
562
+ const err = await validateObjectByInstance(
563
+ queryMeta.dataType,
564
+ args[queryMeta.index],
565
+ );
566
+ if (err) {
567
+ return await res.code(400).send({
568
+ code: 400,
569
+ error: "ValidationError",
570
+ errors: err,
571
+ message: err.message,
572
+ });
573
+ }
574
+ }
575
+ if (queryMeta.required) {
576
+ validateOrThrow(
577
+ { [queryMeta.key]: args[queryMeta.index] },
578
+ { [queryMeta.key]: { type: queryMeta.dataType } },
579
+ { location: "queryparam" },
580
+ );
581
+ }
582
+ }
583
+
584
+ for (let bodyMeta of allMeta.body) {
585
+ if (bodyMeta.validatorClass) {
586
+ const err = await validateObjectByInstance(
587
+ bodyMeta.dataType,
588
+ args[bodyMeta.index],
589
+ );
590
+ if (err) {
591
+ return await res.code(400).send({
592
+ code: 400,
593
+ error: "ValidationError",
594
+ errors: err,
595
+ message: err.message,
596
+ });
597
+ }
598
+ }
599
+ }
600
+ const result = await prototype[method].apply(ctrl, args);
601
+ return result;
602
+ },
603
+ });
604
+ }
605
+ }
606
+
607
+ /**
608
+ * map all request parameters
609
+ * @param req
610
+ * @param meta
611
+ * @returns
612
+ */
613
+ private async _mapArgs(req: IRequest, meta: MethodParamMeta): Promise<any[]> {
614
+ if (!req.hasOwnProperty("_argsCache")) {
615
+ Object.defineProperty(req, "_argsCache", {
616
+ value: new Map<string, any[]>(),
617
+ enumerable: false,
618
+ });
619
+ }
620
+
621
+ const cache: Map<string, any[]> = (req as any)._argsCache;
622
+ const cacheKey = JSON.stringify(meta); // Faster key-based lookup
623
+
624
+ if (cache.has(cacheKey)) {
625
+ return cache.get(cacheKey)!;
626
+ }
627
+
628
+ const args: any[] = meta.params.map((p) => req.params[p.key] || null);
629
+ meta.query.forEach(
630
+ (q) => (args[q.index] = q.key === "all" ? req.query : req.query[q.key]),
631
+ );
632
+ meta.body.forEach(
633
+ (body) => (args[body.index] = { ...req.body, ...req.formData }),
634
+ );
635
+ meta.currentUser.forEach((user) => (args[user.index] = req.user));
636
+ meta.headers.forEach(
637
+ (header) =>
638
+ (args[header.index] =
639
+ header.key === "all" ? req.headers : req.headers[header.key]),
640
+ );
641
+
642
+ if (meta.file) {
643
+ for await (let f of meta.file) {
644
+ args[f.index] = await req.file();
645
+ }
646
+ }
647
+
648
+ if (
649
+ meta.files &&
650
+ req.headers["content-type"]?.startsWith("multipart/form-data") === true
651
+ ) {
652
+ const files = await req.saveRequestFiles();
653
+ if (!files || files.length === 0) {
654
+ throw new BadRequestException({ error: "No files uploaded" });
655
+ }
656
+
657
+ const fileInfo = files.map((file) => ({
658
+ type: file.type,
659
+ filepath: file.filepath,
660
+ fieldname: file.fieldname,
661
+ filename: file.filename,
662
+ encoding: file.encoding,
663
+ mimetype: file.mimetype,
664
+ fields: file.fields,
665
+ }));
666
+ for await (let f of meta.files) {
667
+ const findex = fileInfo.findIndex((x) => x.fieldname == f.fieldName);
668
+ if (f.fieldName != "all" && findex == -1) {
669
+ throw new BadRequestException(
670
+ `${f.fieldName} doesn't exists in request files tree.`,
671
+ );
672
+ }
673
+ args[f.index] =
674
+ f.fieldName == "all"
675
+ ? fileInfo
676
+ : fileInfo.filter((x) => x.fieldname == f.fieldName);
677
+ }
678
+ }
679
+
680
+ cache.set(cacheKey, args);
681
+ return args;
682
+ }
683
+
684
+ /**
685
+ * Process Meta for controlelr class methods
686
+ * @param prototype
687
+ * @param method
688
+ * @returns
689
+ */
690
+ private _processMeta(prototype: any, method: string): MethodParamMeta {
691
+ const cacheKey = `${prototype.constructor.name}_${method}`;
692
+ if (this.metaCache.has(cacheKey)) {
693
+ return this.metaCache.get(cacheKey)!;
694
+ }
695
+
696
+ const meta: MethodParamMeta = {
697
+ params: Reflect.getMetadata(PARAM_META_KEY, prototype, method) || [],
698
+ query: Reflect.getMetadata(QUERY_META_KEY, prototype, method) || [],
699
+ body: Reflect.getMetadata(REQUEST_BODY_META_KEY, prototype, method) || [],
700
+ file: Reflect.getMetadata(REQUEST_BODY_FILE_KEY, prototype, method) || [],
701
+ files:
702
+ Reflect.getMetadata(REQUEST_BODY_FILES_KEY, prototype, method) || [],
703
+ headers:
704
+ Reflect.getMetadata(REQUEST_HEADER_META_KEY, prototype, method) || [],
705
+ currentUser:
706
+ Reflect.getMetadata(REQUEST_USER_META_KEY, prototype, method) || [],
707
+ // swagger: Reflect.getMetadata("route:openapi", prototype, method) || {}
708
+ };
709
+
710
+ this.metaCache.set(cacheKey, meta);
711
+ return meta;
712
+ }
713
+
714
+ private _resolveControllerDir(dir?: string) {
715
+ const isTsNode =
716
+ process.env.TS_NODE_DEV ||
717
+ process.env.TS_NODE_PROJECT ||
718
+ (process as any)[Symbol.for("ts-node.register.instance")];
719
+ const controllerDir = path.join(process.cwd(), this.registerControllerPath);
720
+
721
+ return isTsNode ? controllerDir : controllerDir.replace("src", "dist");
722
+ }
723
+
724
+ private async autoControllers(controllersPath?: string) {
725
+ const conDir = this._resolveControllerDir(controllersPath);
726
+
727
+ const files = await fs.readdir(conDir, { recursive: true });
728
+ for (const file of files) {
729
+ const isTestFile = /\.(test|spec|e2e-spec)\.ts$/.test(file);
730
+ if (isTestFile) continue;
731
+ if (isTsNode ? file.endsWith(".ts") : file.endsWith(".js")) {
732
+ const filePath = path.join(conDir, file);
733
+ const module = await import(filePath);
734
+ for (const exported of Object.values(module)) {
735
+ if (typeof exported === "function" && isApiController(exported)) {
736
+ console.log("adding", exported.name);
737
+ if (!this.controllers.some((con) => exported.name == con.name)) {
738
+ this.controllers.push(exported);
739
+ }
740
+
741
+ //this.buildController(exported);
742
+ }
743
+ }
744
+ }
745
+ }
746
+ }
747
+
748
+ useControllers(controllers: Constructor[] | AutoControllerOptions) {
749
+ if (Array.isArray(controllers)) {
750
+ this.controllers = controllers;
751
+
752
+ controllers.forEach((controller) => {
753
+ if (!this.controllers.includes(controller)) {
754
+ this.controllers.push(controller);
755
+ }
756
+ });
757
+ } else {
758
+ this.registerControllerAuto = true;
759
+ if (controllers.path) {
760
+ this.registerControllerPath = controllers.path;
761
+ }
762
+ }
763
+ }
764
+
765
+ // addFeature(feature:{controllers:Function[]}){
766
+ // feature.controllers.forEach(c=> this.controllers.push(c))
767
+ // }
768
+
769
+ // mapFeature(){
770
+ // if(!this.isMapFeatures){
771
+ // this.isMapFeatures = true;
772
+ // }
773
+ // }
774
+
775
+ private async _mapControllers() {
776
+ if (this.controllers.length > 0) {
777
+ for (let controller of this.controllers) {
778
+ if (isApiController(controller)) {
779
+ this.buildController(controller);
780
+ } else {
781
+ throw new SystemUseError("Not a api controller.");
782
+ }
783
+ }
784
+ }
785
+ }
786
+
787
+ // useControllersAuto(controllerPath?:string) {
788
+ // this.registerControllerAuto = true;
789
+ // //this.autoControllers();
790
+ // }
791
+
792
+ private async mapFn(fn: Function) {
793
+ const original = fn;
794
+ fn = function () { };
795
+ return fn;
796
+ }
797
+
798
+ private _handleError(error: any): {
799
+ code: number;
800
+ error: string;
801
+ message: any;
802
+ } {
803
+ if (error instanceof BaseHttpException) {
804
+ return {
805
+ code: error.code,
806
+ error: error.name,
807
+ message: isValidJsonString(error.message)
808
+ ? JSON.parse(error.message)
809
+ : error.message,
810
+ };
811
+ }
812
+ return {
813
+ code: 500,
814
+ error: "INTERNAL_ERROR",
815
+ message: error.message ? error.message : "Something going wrong.",
816
+ };
817
+ }
818
+
819
+ async mapRoute<T extends (...args: any[]) => any>(
820
+ method: "get" | "post" | "put" | "delete",
821
+ path: string = "",
822
+ fn: T,
823
+ ) {
824
+ await this.mapFn(fn);
825
+
826
+ this.app[method](path, async (req: any, res: any) => {
827
+
828
+ try {
829
+ const result = await fn.apply(this, [req, res]);
830
+ if (typeof result === "object" && result !== null) {
831
+ res.json(result);
832
+ } else {
833
+ res.send(result);
834
+ }
835
+ } catch (error) {
836
+ console.error(`Error in ${method} route handler:`, error);
837
+ const handledErr = this._handleError(error);
838
+ res.status(handledErr.code).send(handledErr);
839
+ }
840
+ });
841
+ }
842
+ private _routeHandler<T extends (...args: any[]) => any>(
843
+ routePath: string,
844
+ method: string,
845
+ fn: T,
846
+ ) {
847
+ const routeKey = method + ":" + routePath;
848
+ this.rMap.set(routeKey, {
849
+ handler: fn,
850
+ middlewares: [],
851
+ schema: {},
852
+ });
853
+
854
+ const route = {
855
+ useMiddleware: <M extends AvleonMiddleware>(
856
+ middlewares: Constructor<AvleonMiddleware>[],
857
+ ) => {
858
+ const midds = Array.isArray(middlewares) ? middlewares : [middlewares];
859
+ const ms: any[] = (midds as unknown as any[]).map((mclass) => {
860
+ const cls = Container.get<AvleonMiddleware>(mclass);
861
+ this.middlewares.set(mclass.name, cls);
862
+ return cls.invoke;
863
+ });
864
+
865
+ const r = this.rMap.get(routeKey);
866
+ if (r) {
867
+ r.middlewares = ms;
868
+ }
869
+ return route;
870
+ },
871
+
872
+ useOpenApi: (options: OpenApiOptions) => {
873
+ const r = this.rMap.get(routeKey);
874
+ if (r) {
875
+ r.schema = options;
876
+ }
877
+ return route;
878
+ },
879
+ };
880
+
881
+ return route;
882
+ }
883
+
884
+ mapGet<T extends (...args: any[]) => any>(path: string = "", fn: T) {
885
+ return this._routeHandler(path, "GET", fn);
886
+ }
887
+
888
+ mapPost<T extends (...args: any[]) => any>(path: string = "", fn: T) {
889
+ return this._routeHandler(path, "POST", fn);
890
+ }
891
+
892
+ mapPut<T extends (...args: any[]) => any>(path: string = "", fn: T) {
893
+ return this._routeHandler(path, "PUT", fn);
894
+ }
895
+
896
+ mapDelete<T extends (...args: any[]) => any>(path: string = "", fn: T) {
897
+ return this._routeHandler(path, "DELETE", fn);
898
+ }
899
+
900
+ private _mapFeatures() {
901
+ const features = Container.get("features");
902
+ console.log("Features", features);
903
+ }
904
+
905
+ async initializeDatabase() {
906
+ if (this.dataSourceOptions && this.dataSource) {
907
+ await this.dataSource.initialize();
908
+ }
909
+ }
910
+
911
+
912
+ handleSocket(socket: any) {
913
+ subscriberRegistry.register(socket);
914
+ }
915
+
916
+ async run(port: number = 4000, fn?: CallableFunction): Promise<void> {
917
+ if (this.alreadyRun) throw new SystemUseError("App already running");
918
+ this.alreadyRun = true;
919
+
920
+ if (this.hasSwagger) {
921
+ await this.initSwagger(this.globalSwaggerOptions);
922
+ }
923
+ await this.initializeDatabase();
924
+ if (this.isMapFeatures) {
925
+ this._mapFeatures();
926
+ }
927
+
928
+ if (this.registerControllerAuto) {
929
+ await this.autoControllers();
930
+ }
931
+ await this._mapControllers();
932
+
933
+ this.rMap.forEach((value, key) => {
934
+ const [m, r] = key.split(":");
935
+ this.app.route({
936
+ method: m,
937
+ url: r,
938
+ schema: value.schema || {},
939
+ preHandler: value.middlewares ? value.middlewares : [],
940
+ handler: async (req, res) => {
941
+ const result = await value.handler.apply(this, [req, res]);
942
+ return result;
943
+ },
944
+ });
945
+ });
946
+ this.app.setErrorHandler(async (error, req, res) => {
947
+ const handledErr = this._handleError(error);
948
+ if (
949
+ error instanceof ValidationErrorException ||
950
+ error instanceof BadRequestException
951
+ ) {
952
+ return res.status(handledErr.code).send({
953
+ code: handledErr.code,
954
+ error: handledErr.error,
955
+ errors: handledErr.message,
956
+ });
957
+ }
958
+ return res.status(handledErr.code).send(handledErr);
959
+ });
960
+
961
+ await this.app.ready();
962
+ if (this._hasWebsocket) {
963
+ await this.app.io.on("connection", this.handleSocket);
964
+ await this.app.io.use((socket: { handshake: { auth: { token: any; }; }; data: { user: any; }; }, next:any) => {
965
+ const token = socket.handshake.auth.token;
966
+ try {
967
+ console.log("token", token);
968
+ const user = {id:1, name:"tareq"};
969
+ socket.data.user = user; // this powers @AuthUser()
970
+ next();
971
+ } catch {
972
+ next(new Error("Unauthorized"));
973
+ }
974
+ });
975
+ }
976
+ await this.app.listen({ port });
977
+ console.log(`Application running on http://127.0.0.1:${port}`);
978
+ }
979
+ getTestApp(buildOptions?: any): TestApplication {
980
+ try {
981
+ // }
982
+ // this.initializeDatabase();
983
+ this._mapControllers();
984
+ this.rMap.forEach((value, key) => {
985
+ const [m, r] = key.split(":");
986
+ this.app.route({
987
+ method: m,
988
+ url: r,
989
+ schema: value.schema || {},
990
+ preHandler: value.middlewares ? value.middlewares : [],
991
+ handler: async (req, res) => {
992
+ const result = await value.handler.apply(this, [req, res]);
993
+ return result;
994
+ },
995
+ });
996
+ });
997
+
998
+ this.app.setErrorHandler(async (error, req, res) => {
999
+ const handledErr = this._handleError(error);
1000
+ if (error instanceof ValidationErrorException) {
1001
+ return res.status(handledErr.code).send({
1002
+ code: handledErr.code,
1003
+ error: handledErr.error,
1004
+ errors: handledErr.message,
1005
+ });
1006
+ }
1007
+ return res.status(handledErr.code).send(handledErr);
1008
+ });
1009
+ // return this.app as any;
1010
+ //
1011
+ return {
1012
+ get: async (url: string, options?: InjectOptions) =>
1013
+ this.app.inject({ method: "GET", url, ...options }),
1014
+ post: async (url: string, options?: InjectOptions) =>
1015
+ this.app.inject({ method: "POST", url, ...options }),
1016
+ put: async (url: string, options?: InjectOptions) =>
1017
+ this.app.inject({ method: "PUT", url, ...options }),
1018
+ patch: async (url: string, options?: InjectOptions) =>
1019
+ this.app.inject({ method: "PATCH", url, ...options }),
1020
+ delete: async (url: string, options?: InjectOptions) =>
1021
+ this.app.inject({ method: "DELETE", url, ...options }),
1022
+ options: async (url: string, options?: InjectOptions) =>
1023
+ this.app.inject({ method: "OPTIONS", url, ...options }),
1024
+ getController: <T>(controller: Constructor<T>, deps: any[] = []) => {
1025
+ const paramTypes =
1026
+ Reflect.getMetadata("design:paramtypes", controller) || [];
1027
+
1028
+ deps.forEach((dep, i) => {
1029
+ Container.set(paramTypes[i], dep);
1030
+ });
1031
+
1032
+ return Container.get(controller);
1033
+ },
1034
+ };
1035
+ } catch (error) {
1036
+ console.log(error);
1037
+ throw new SystemUseError("Can't get test appliction");
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ export type Application = typeof AvleonApplication;
1043
+
1044
+ // Applciation Builder
1045
+ export interface ITestBuilder {
1046
+ getTestApplication(): AvleonTestAppliction;
1047
+ createTestApplication(options: any): AvleonTestAppliction;
1048
+ }
1049
+
1050
+ export interface IAppBuilder {
1051
+ registerPlugin<T extends Function, S extends {}>(
1052
+ plugin: T,
1053
+ options: S,
1054
+ ): Promise<void>;
1055
+ addDataSource<
1056
+ T extends IConfig<R>,
1057
+ R = ReturnType<InstanceType<Constructable<T>>["config"]>,
1058
+ >(
1059
+ ConfigClass: Constructable<T>,
1060
+ modifyConfig?: (config: R) => R,
1061
+ ): void;
1062
+ build<T extends IAvleonApplication>(): T;
1063
+ }
1064
+
1065
+ export class AvleonTest {
1066
+ private constructor() {
1067
+ process.env.NODE_ENV = "test";
1068
+ }
1069
+ static getController<T>(controller: Constructor<T>, deps: any[] = []) {
1070
+ const paramTypes =
1071
+ Reflect.getMetadata("design:paramtypes", controller) || [];
1072
+
1073
+ deps.forEach((dep, i) => {
1074
+ Container.set(paramTypes[i], dep);
1075
+ });
1076
+
1077
+ return Container.get(controller);
1078
+ }
1079
+
1080
+ private getService<T>(service: Constructor<T>) {
1081
+ return Container.get(service);
1082
+ }
1083
+
1084
+ static createTestApplication(options: TestAppOptions) {
1085
+ const app = AvleonApplication.getInternalApp({
1086
+ dataSourceOptions: options.dataSource ? options.dataSource : null,
1087
+ });
1088
+ app.useControllers([...options.controllers]);
1089
+ return app.getTestApp();
1090
+ }
1091
+
1092
+ static from(app: AvleonApplication) {
1093
+ return app.getTestApp();
1094
+ }
1095
+
1096
+ static clean() {
1097
+ Container.reset();
1098
+ }
1099
+ }
1100
+
1101
+ export class Avleon {
1102
+ static createApplication() {
1103
+ const app = AvleonApplication.getApp();
1104
+ return app;
1105
+ }
1106
+ }