@avleon/core 0.0.20 → 0.0.24
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.
- package/README.md +559 -2
- package/dist/application.d.ts +26 -0
- package/dist/application.js +50 -0
- package/dist/collection.js +3 -2
- package/dist/container.d.ts +1 -0
- package/dist/container.js +2 -1
- package/dist/environment-variables.js +1 -1
- package/dist/file-storage.js +0 -128
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.js +41 -4
- package/dist/icore.d.ts +68 -26
- package/dist/icore.js +235 -212
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/middleware.js +0 -8
- package/dist/multipart.js +4 -1
- package/dist/openapi.d.ts +37 -37
- package/dist/params.js +2 -2
- package/dist/response.js +6 -2
- package/dist/route-methods.js +1 -1
- package/dist/swagger-schema.js +4 -1
- package/dist/testing.js +5 -2
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +18 -0
- package/dist/utils/optional-require.d.ts +8 -0
- package/dist/utils/optional-require.js +70 -0
- package/dist/validation.d.ts +4 -1
- package/dist/validation.js +10 -5
- package/package.json +19 -11
- package/src/application.ts +96 -0
- package/src/authentication.ts +16 -0
- package/src/collection.ts +254 -0
- package/src/config.ts +39 -0
- package/src/constants.ts +1 -0
- package/src/container.ts +54 -0
- package/src/controller.ts +128 -0
- package/src/decorators.ts +27 -0
- package/src/environment-variables.ts +46 -0
- package/src/exceptions/http-exceptions.ts +86 -0
- package/src/exceptions/index.ts +1 -0
- package/src/exceptions/system-exception.ts +34 -0
- package/src/file-storage.ts +206 -0
- package/src/helpers.ts +328 -0
- package/src/icore.ts +1064 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +72 -0
- package/src/map-types.ts +159 -0
- package/src/middleware.ts +98 -0
- package/src/multipart.ts +116 -0
- package/src/openapi.ts +372 -0
- package/src/params.ts +111 -0
- package/src/queue.ts +126 -0
- package/src/response.ts +117 -0
- package/src/results.ts +30 -0
- package/src/route-methods.ts +186 -0
- package/src/swagger-schema.ts +213 -0
- package/src/testing.ts +220 -0
- package/src/types/app-builder.interface.ts +19 -0
- package/src/types/application.interface.ts +9 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/optional-require.ts +50 -0
- package/src/validation.ts +156 -0
- package/src/validator-extend.ts +25 -0
- package/dist/classToOpenapi.d.ts +0 -0
- package/dist/classToOpenapi.js +0 -1
- package/dist/render.d.ts +0 -1
- package/dist/render.js +0 -8
- package/jest.config.ts +0 -9
- package/tsconfig.json +0 -25
- /package/dist/{security.d.ts → utils/hash.d.ts} +0 -0
- /package/dist/{security.js → utils/hash.js} +0 -0
package/src/icore.ts
ADDED
|
@@ -0,0 +1,1064 @@
|
|
|
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
|
+
};
|
|
163
|
+
|
|
164
|
+
export interface AvleonTestAppliction {
|
|
165
|
+
addDataSource: (dataSourceOptions: DataSourceOptions) => void;
|
|
166
|
+
getApp: (options?: TestAppOptions) => any;
|
|
167
|
+
getController: <T>(controller: Constructor<T>) => T;
|
|
168
|
+
}
|
|
169
|
+
export interface IAvleonApplication {
|
|
170
|
+
isDevelopment(): boolean;
|
|
171
|
+
useCors(corsOptions?: FastifyCorsOptions): void;
|
|
172
|
+
useDataSource<
|
|
173
|
+
T extends IConfig<R>,
|
|
174
|
+
R = ReturnType<InstanceType<Constructable<T>>["config"]>,
|
|
175
|
+
>(
|
|
176
|
+
ConfigClass: Constructable<T>,
|
|
177
|
+
modifyConfig?: (config: R) => R,
|
|
178
|
+
): void;
|
|
179
|
+
useOpenApi<
|
|
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
|
+
useSwagger(options: OpenApiUiOptions): Promise<void>; // Deprecated
|
|
187
|
+
useMultipart(options: MultipartOptions): void;
|
|
188
|
+
|
|
189
|
+
useMiddlewares<T extends AppMiddleware>(mclasses: Constructor<T>[]): void;
|
|
190
|
+
useAuthoriztion<T extends any>(middleware: Constructor<T>): void;
|
|
191
|
+
mapRoute<T extends (...args: any[]) => any>(
|
|
192
|
+
method: "get" | "post" | "put" | "delete",
|
|
193
|
+
path: string,
|
|
194
|
+
fn: T,
|
|
195
|
+
): Promise<void>;
|
|
196
|
+
mapGet<T extends (...args: any[]) => any>(path: string, fn: T): any;
|
|
197
|
+
|
|
198
|
+
mapPost<T extends (...args: any[]) => any>(path: string, fn: T): any;
|
|
199
|
+
mapPut<T extends (...args: any[]) => any>(path: string, fn: T): any;
|
|
200
|
+
mapDelete<T extends (...args: any[]) => any>(path: string, fn: T): any;
|
|
201
|
+
mapControllers(controllers: any[]): any;
|
|
202
|
+
useStaticFiles(options?: StaticFileOptions): void;
|
|
203
|
+
run(port?: number): Promise<void>;
|
|
204
|
+
getTestApp(): TestApplication;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// InternalApplication
|
|
208
|
+
export class AvleonApplication {
|
|
209
|
+
private static instance: AvleonApplication;
|
|
210
|
+
private static buildOptions: any = {};
|
|
211
|
+
private app!: FastifyInstance;
|
|
212
|
+
private routeSet = new Set<string>(); // Use Set for fast duplicate detection
|
|
213
|
+
private alreadyRun = false;
|
|
214
|
+
private routes: Map<string, Function> = new Map();
|
|
215
|
+
private middlewares: Map<string, AppMiddleware> = new Map();
|
|
216
|
+
private rMap = new Map<string, FuncRoute>();
|
|
217
|
+
private hasSwagger = false;
|
|
218
|
+
private globalSwaggerOptions: any = {};
|
|
219
|
+
private dataSourceOptions?: DataSourceOptions = undefined;
|
|
220
|
+
private controllers: any[] = [];
|
|
221
|
+
private authorizeMiddleware?: any = undefined;
|
|
222
|
+
private appConfig: AppConfig;
|
|
223
|
+
private dataSource?: DataSource = undefined;
|
|
224
|
+
private isMapFeatures = false;
|
|
225
|
+
|
|
226
|
+
private metaCache = new Map<string, MethodParamMeta>();
|
|
227
|
+
private multipartOptions: FastifyMultipartOptions | undefined;
|
|
228
|
+
private constructor() {
|
|
229
|
+
this.app = fastify();
|
|
230
|
+
this.appConfig = new AppConfig();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private isTest() { }
|
|
234
|
+
|
|
235
|
+
static getApp(): AvleonApplication {
|
|
236
|
+
let isTestEnv = process.env.NODE_ENV == "test";
|
|
237
|
+
if (!AvleonApplication.instance) {
|
|
238
|
+
AvleonApplication.instance = new AvleonApplication();
|
|
239
|
+
}
|
|
240
|
+
return AvleonApplication.instance;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static getInternalApp(buildOptions: any): AvleonApplication {
|
|
244
|
+
let isTestEnv = process.env.NODE_ENV == "test";
|
|
245
|
+
if (!AvleonApplication.instance) {
|
|
246
|
+
AvleonApplication.instance = new AvleonApplication();
|
|
247
|
+
}
|
|
248
|
+
AvleonApplication.buildOptions = buildOptions;
|
|
249
|
+
if (buildOptions.dataSourceOptions) {
|
|
250
|
+
AvleonApplication.instance.dataSourceOptions =
|
|
251
|
+
buildOptions.dataSourceOptions;
|
|
252
|
+
const typeorm = require("typeorm");
|
|
253
|
+
const datasource = new typeorm.DataSource(
|
|
254
|
+
buildOptions.dataSourceOptions,
|
|
255
|
+
) as DataSource;
|
|
256
|
+
|
|
257
|
+
Container.set<DataSource>("idatasource",
|
|
258
|
+
datasource,
|
|
259
|
+
);
|
|
260
|
+
AvleonApplication.instance.dataSource = datasource;
|
|
261
|
+
}
|
|
262
|
+
return AvleonApplication.instance;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
isDevelopment() {
|
|
266
|
+
const env = container.get(Environment);
|
|
267
|
+
return env.get("NODE_ENV") == "development";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
private async initSwagger(options: OpenApiUiOptions) {
|
|
272
|
+
const { routePrefix, logo, ui, theme, configuration, ...restOptions } =
|
|
273
|
+
options;
|
|
274
|
+
|
|
275
|
+
this.app.register(swagger, {
|
|
276
|
+
openapi: {
|
|
277
|
+
openapi: "3.0.0",
|
|
278
|
+
...restOptions,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
const rPrefix = routePrefix ? routePrefix : "/docs";
|
|
282
|
+
|
|
283
|
+
if (options.ui && options.ui == "scalar") {
|
|
284
|
+
const scalarPlugin = optionalRequire("@scalar/fastify-api-reference", {
|
|
285
|
+
failOnMissing: true,
|
|
286
|
+
customMessage: 'Install "@scalar/fastify-api-reference" to enable API docs.\n\n npm install @scalar/fastify-api-reference',
|
|
287
|
+
});
|
|
288
|
+
await this.app.register(scalarPlugin, {
|
|
289
|
+
routePrefix: rPrefix as any,
|
|
290
|
+
configuration: configuration
|
|
291
|
+
? configuration
|
|
292
|
+
: {
|
|
293
|
+
metaData: {
|
|
294
|
+
title: "Avleon Api",
|
|
295
|
+
ogTitle: "Avleon",
|
|
296
|
+
},
|
|
297
|
+
theme: options.theme ? options.theme : "kepler",
|
|
298
|
+
favicon: "/static/favicon.png",
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
|
|
303
|
+
const fastifySwaggerUi = optionalRequire("@fastify/swagger-ui", {
|
|
304
|
+
failOnMissing: true,
|
|
305
|
+
customMessage: 'Install "@fastify/swagger-ui" to enable API docs.\n\n npm install @fastify/swagger-ui',
|
|
306
|
+
});
|
|
307
|
+
await this.app.register(fastifySwaggerUi, {
|
|
308
|
+
logo: logo ? logo : null,
|
|
309
|
+
theme: theme ? theme : {},
|
|
310
|
+
routePrefix: rPrefix as any,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
useCors(corsOptions: FastifyCorsOptions = {}) {
|
|
315
|
+
this.app.register(cors, corsOptions);
|
|
316
|
+
}
|
|
317
|
+
useOpenApi<
|
|
318
|
+
T extends IConfig<R>,
|
|
319
|
+
R = ReturnType<InstanceType<Constructable<T>>["config"]>,
|
|
320
|
+
>(ConfigClass: Constructable<T>, modifyConfig?: (config: R) => R) {
|
|
321
|
+
const openApiConfig: R = this.appConfig.get(ConfigClass);
|
|
322
|
+
if (modifyConfig) {
|
|
323
|
+
const modifiedConfig: R = modifyConfig(openApiConfig);
|
|
324
|
+
this.globalSwaggerOptions = modifiedConfig;
|
|
325
|
+
} else {
|
|
326
|
+
this.globalSwaggerOptions = openApiConfig;
|
|
327
|
+
}
|
|
328
|
+
this.hasSwagger = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Registers the fastify-multipart plugin with the Fastify instance.
|
|
335
|
+
* This enables handling of multipart/form-data requests, typically used for file uploads.
|
|
336
|
+
*
|
|
337
|
+
* @param {MultipartOptions} options - Options to configure the fastify-multipart plugin.
|
|
338
|
+
* @param {FastifyInstance} this.app - The Fastify instance to register the plugin with.
|
|
339
|
+
* @property {MultipartOptions} this.multipartOptions - Stores the provided multipart options.
|
|
340
|
+
* @see {@link https://github.com/fastify/fastify-multipart} for more details on available options.
|
|
341
|
+
*/
|
|
342
|
+
useMultipart(options: MultipartOptions) {
|
|
343
|
+
this.multipartOptions = options;
|
|
344
|
+
this.app.register(fastifyMultipart, {
|
|
345
|
+
...this.multipartOptions,
|
|
346
|
+
attachFieldsToBody: true,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Configures and initializes a TypeORM DataSource based on the provided configuration class.
|
|
353
|
+
* It retrieves the configuration from the application's configuration service and allows for optional modification.
|
|
354
|
+
* The initialized DataSource is then stored and registered within a dependency injection container.
|
|
355
|
+
*
|
|
356
|
+
* @template T - A generic type extending the `IConfig` interface, representing the configuration class.
|
|
357
|
+
* @template R - A generic type representing the return type of the configuration method of the `ConfigClass`.
|
|
358
|
+
* @param {Constructable<T>} ConfigClass - The constructor of the configuration class to be used for creating the DataSource.
|
|
359
|
+
* @param {(config: R) => R} [modifyConfig] - An optional function that takes the initial configuration and returns a modified configuration.
|
|
360
|
+
* @returns {void}
|
|
361
|
+
* @property {DataSourceOptions} this.dataSourceOptions - Stores the final DataSource options after potential modification.
|
|
362
|
+
* @property {DataSource} this.dataSource - Stores the initialized TypeORM DataSource instance.
|
|
363
|
+
* @see {@link https://typeorm.io/} for more information about TypeORM.
|
|
364
|
+
*/
|
|
365
|
+
useDataSource<
|
|
366
|
+
T extends IConfig<R>,
|
|
367
|
+
R = ReturnType<InstanceType<Constructable<T>>["config"]>,
|
|
368
|
+
>(ConfigClass: Constructable<T>, modifyConfig?: (config: R) => R) {
|
|
369
|
+
const dsConfig: R = this.appConfig.get(ConfigClass);
|
|
370
|
+
if (modifyConfig) {
|
|
371
|
+
const modifiedConfig: R = modifyConfig(dsConfig);
|
|
372
|
+
this.dataSourceOptions = modifiedConfig as unknown as DataSourceOptions;
|
|
373
|
+
} else {
|
|
374
|
+
this.dataSourceOptions = dsConfig as unknown as DataSourceOptions;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const typeorm = require("typeorm");
|
|
378
|
+
const datasource = new typeorm.DataSource(
|
|
379
|
+
dsConfig,
|
|
380
|
+
) as DataSource;
|
|
381
|
+
|
|
382
|
+
this.dataSource = datasource;
|
|
383
|
+
|
|
384
|
+
Container.set<DataSource>("idatasource",
|
|
385
|
+
datasource,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Registers an array of middleware classes to be executed before request handlers.
|
|
392
|
+
* It retrieves instances of the middleware classes from the dependency injection container
|
|
393
|
+
* and adds them as 'preHandler' hooks to the Fastify application.
|
|
394
|
+
*
|
|
395
|
+
* @template T - A generic type extending the `AppMiddleware` interface, representing the middleware class.
|
|
396
|
+
* @param {Constructor<T>[]} mclasses - An array of middleware class constructors to be registered.
|
|
397
|
+
* @returns {void}
|
|
398
|
+
* @property {Map<string, T>} this.middlewares - Stores the registered middleware instances, keyed by their class names.
|
|
399
|
+
* @see {@link https://www.fastify.io/docs/latest/Reference/Hooks/#prehandler} for more information about Fastify preHandler hooks.
|
|
400
|
+
*/
|
|
401
|
+
useMiddlewares<T extends AppMiddleware>(mclasses: Constructor<T>[]) {
|
|
402
|
+
for (const mclass of mclasses) {
|
|
403
|
+
const cls = Container.get<T>(mclass);
|
|
404
|
+
this.middlewares.set(mclass.name, cls);
|
|
405
|
+
this.app.addHook("preHandler", cls.invoke);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Registers a middleware constructor to be used for authorization purposes.
|
|
412
|
+
* The specific implementation and usage of this middleware will depend on the application's authorization logic.
|
|
413
|
+
*
|
|
414
|
+
* @template T - A generic type representing the constructor of the authorization middleware.
|
|
415
|
+
* @param {Constructor<T>} middleware - The constructor of the middleware to be used for authorization.
|
|
416
|
+
* @returns {void}
|
|
417
|
+
* @property {any} this.authorizeMiddleware - Stores the constructor of the authorization middleware.
|
|
418
|
+
*/
|
|
419
|
+
useAuthoriztion<T extends any>(middleware: Constructor<T>) {
|
|
420
|
+
this.authorizeMiddleware = middleware as any;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Registers the `@fastify/static` plugin to serve static files.
|
|
427
|
+
* It configures the root directory and URL prefix for serving static assets.
|
|
428
|
+
*
|
|
429
|
+
* @param {StaticFileOptions} [options={ path: undefined, prefix: undefined }] - Optional configuration for serving static files.
|
|
430
|
+
* @param {string} [options.path] - The absolute path to the static files directory. Defaults to 'process.cwd()/public'.
|
|
431
|
+
* @param {string} [options.prefix] - The URL prefix for serving static files. Defaults to '/static/'.
|
|
432
|
+
* @returns {void}
|
|
433
|
+
* @see {@link https://github.com/fastify/fastify-static} for more details on available options.
|
|
434
|
+
*/
|
|
435
|
+
useStaticFiles(
|
|
436
|
+
options: StaticFileOptions = { path: undefined, prefix: undefined },
|
|
437
|
+
) {
|
|
438
|
+
this.app.register(require("@fastify/static"), {
|
|
439
|
+
root: options.path ? options.path : path.join(process.cwd(), "public"),
|
|
440
|
+
prefix: options.prefix ? options.prefix : "/static/",
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
private handleMiddlewares<T extends AppMiddleware>(
|
|
449
|
+
mclasses: Constructor<T>[],
|
|
450
|
+
) {
|
|
451
|
+
for (const mclass of mclasses) {
|
|
452
|
+
const cls = Container.get<T>(mclass.constructor);
|
|
453
|
+
this.middlewares.set(mclass.name, cls);
|
|
454
|
+
this.app.addHook("preHandler", cls.invoke);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private executeMiddlewares(target: any, propertyKey?: string) {
|
|
459
|
+
const classMiddlewares =
|
|
460
|
+
Reflect.getMetadata("controller:middleware", target.constructor) || [];
|
|
461
|
+
const methodMiddlewares = propertyKey
|
|
462
|
+
? Reflect.getMetadata("route:middleware", target, propertyKey) || []
|
|
463
|
+
: [];
|
|
464
|
+
|
|
465
|
+
return [...classMiddlewares, ...methodMiddlewares];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* build controller
|
|
470
|
+
* @param controller
|
|
471
|
+
* @returns void
|
|
472
|
+
*/
|
|
473
|
+
private async buildController(controller: any) {
|
|
474
|
+
const ctrl: any = Container.get(controller);
|
|
475
|
+
const controllerMeta = Reflect.getMetadata(
|
|
476
|
+
CONTROLLER_META_KEY,
|
|
477
|
+
ctrl.constructor,
|
|
478
|
+
);
|
|
479
|
+
if (!controllerMeta) return;
|
|
480
|
+
const prototype = Object.getPrototypeOf(ctrl);
|
|
481
|
+
const methods = Object.getOwnPropertyNames(prototype).filter(
|
|
482
|
+
(name) => name !== "constructor",
|
|
483
|
+
);
|
|
484
|
+
const tag = ctrl.constructor.name.replace("Controller", "");
|
|
485
|
+
const swaggerControllerMeta =
|
|
486
|
+
Reflect.getMetadata("controller:openapi", ctrl.constructor) || {};
|
|
487
|
+
const authClsMeata = Reflect.getMetadata(
|
|
488
|
+
AUTHORIZATION_META_KEY,
|
|
489
|
+
ctrl.constructor,
|
|
490
|
+
) || { authorize: false, options: undefined };
|
|
491
|
+
|
|
492
|
+
for await (const method of methods) {
|
|
493
|
+
const methodMeta = Reflect.getMetadata(ROUTE_META_KEY, prototype, method);
|
|
494
|
+
if (!methodMeta) continue;
|
|
495
|
+
const methodmetaOptions = {
|
|
496
|
+
method: methodMeta.method.toLowerCase(),
|
|
497
|
+
path: formatUrl(controllerMeta.path + methodMeta.path),
|
|
498
|
+
};
|
|
499
|
+
const routeKey = `${methodmetaOptions.method}:${methodmetaOptions.path}`;
|
|
500
|
+
if (!this.routeSet.has(routeKey)) {
|
|
501
|
+
this.routeSet.add(routeKey);
|
|
502
|
+
}
|
|
503
|
+
const classMiddlewares = this.executeMiddlewares(ctrl, method);
|
|
504
|
+
|
|
505
|
+
// handle openapi data
|
|
506
|
+
const swaggerMeta =
|
|
507
|
+
Reflect.getMetadata("route:openapi", prototype, method) || {};
|
|
508
|
+
|
|
509
|
+
const authClsMethodMeata = Reflect.getMetadata(
|
|
510
|
+
AUTHORIZATION_META_KEY,
|
|
511
|
+
ctrl.constructor,
|
|
512
|
+
method,
|
|
513
|
+
) || { authorize: false, options: undefined };
|
|
514
|
+
const allMeta = this._processMeta(prototype, method);
|
|
515
|
+
|
|
516
|
+
let bodySchema: any = null;
|
|
517
|
+
allMeta.body.forEach((r) => {
|
|
518
|
+
if (r.schema) {
|
|
519
|
+
bodySchema = { ...r.schema };
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const routePath =
|
|
524
|
+
methodmetaOptions.path == "" ? "/" : methodmetaOptions.path;
|
|
525
|
+
|
|
526
|
+
let schema = { ...swaggerControllerMeta, ...swaggerMeta, tags: [tag] };
|
|
527
|
+
if (!swaggerMeta.body && bodySchema) {
|
|
528
|
+
schema = { ...schema, body: bodySchema };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.app.route({
|
|
532
|
+
url: routePath,
|
|
533
|
+
method: methodmetaOptions.method.toUpperCase(),
|
|
534
|
+
schema: { ...schema },
|
|
535
|
+
handler: async (req, res) => {
|
|
536
|
+
let reqClone = req as IRequest;
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
// class level authrization
|
|
540
|
+
if (authClsMeata.authorize && this.authorizeMiddleware) {
|
|
541
|
+
const cls = container.get(this.authorizeMiddleware) as any;
|
|
542
|
+
await cls.authorize(reqClone, authClsMeata.options);
|
|
543
|
+
if (res.sent) return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// method level authorization
|
|
547
|
+
if (authClsMethodMeata.authorize && this.authorizeMiddleware) {
|
|
548
|
+
const cls = container.get(this.authorizeMiddleware) as any;
|
|
549
|
+
await cls.authorize(reqClone, authClsMethodMeata.options);
|
|
550
|
+
if (res.sent) return;
|
|
551
|
+
}
|
|
552
|
+
if (classMiddlewares.length > 0) {
|
|
553
|
+
for (let m of classMiddlewares) {
|
|
554
|
+
const cls = Container.get<AppMiddleware>(m.constructor);
|
|
555
|
+
reqClone = (await cls.invoke(reqClone, res)) as IRequest;
|
|
556
|
+
if (res.sent) return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const args = await this._mapArgs(reqClone, allMeta);
|
|
560
|
+
|
|
561
|
+
for (let paramMeta of allMeta.params) {
|
|
562
|
+
if (paramMeta.required) {
|
|
563
|
+
validateOrThrow({ [paramMeta.key]: args[paramMeta.index] }, { [paramMeta.key]: { type: paramMeta.dataType } }, { location: 'param' })
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (let queryMeta of allMeta.query) {
|
|
568
|
+
if (queryMeta.validatorClass) {
|
|
569
|
+
const err = await validateObjectByInstance(
|
|
570
|
+
queryMeta.dataType,
|
|
571
|
+
args[queryMeta.index],
|
|
572
|
+
);
|
|
573
|
+
if (err) {
|
|
574
|
+
return await res.code(400).send({
|
|
575
|
+
code: 400,
|
|
576
|
+
error: "ValidationError",
|
|
577
|
+
errors: err,
|
|
578
|
+
message: err.message,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (queryMeta.required) {
|
|
583
|
+
validateOrThrow({ [queryMeta.key]: args[queryMeta.index] }, { [queryMeta.key]: { type: queryMeta.dataType } }, { location: 'queryparam' })
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (let bodyMeta of allMeta.body) {
|
|
588
|
+
if (bodyMeta.validatorClass) {
|
|
589
|
+
const err = await validateObjectByInstance(
|
|
590
|
+
bodyMeta.dataType,
|
|
591
|
+
args[bodyMeta.index],
|
|
592
|
+
);
|
|
593
|
+
if (err) {
|
|
594
|
+
return await res.code(400).send({
|
|
595
|
+
code: 400,
|
|
596
|
+
error: "ValidationError",
|
|
597
|
+
errors: err,
|
|
598
|
+
message: err.message,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const result = await prototype[method].apply(ctrl, args);
|
|
604
|
+
return result;
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* map all request parameters
|
|
612
|
+
* @param req
|
|
613
|
+
* @param meta
|
|
614
|
+
* @returns
|
|
615
|
+
*/
|
|
616
|
+
private async _mapArgs(req: IRequest, meta: MethodParamMeta): Promise<any[]> {
|
|
617
|
+
if (!req.hasOwnProperty("_argsCache")) {
|
|
618
|
+
Object.defineProperty(req, "_argsCache", {
|
|
619
|
+
value: new Map<string, any[]>(),
|
|
620
|
+
enumerable: false,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const cache: Map<string, any[]> = (req as any)._argsCache;
|
|
625
|
+
const cacheKey = JSON.stringify(meta); // Faster key-based lookup
|
|
626
|
+
|
|
627
|
+
if (cache.has(cacheKey)) {
|
|
628
|
+
return cache.get(cacheKey)!;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const args: any[] = meta.params.map((p) => req.params[p.key] || null);
|
|
632
|
+
meta.query.forEach(
|
|
633
|
+
(q) => (args[q.index] = q.key === "all" ? req.query : req.query[q.key]),
|
|
634
|
+
);
|
|
635
|
+
meta.body.forEach(
|
|
636
|
+
(body) => (args[body.index] = { ...req.body, ...req.formData }),
|
|
637
|
+
);
|
|
638
|
+
meta.currentUser.forEach((user) => (args[user.index] = req.user));
|
|
639
|
+
meta.headers.forEach(
|
|
640
|
+
(header) =>
|
|
641
|
+
(args[header.index] =
|
|
642
|
+
header.key === "all" ? req.headers : req.headers[header.key]),
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
if (meta.file) {
|
|
646
|
+
for await (let f of meta.file) {
|
|
647
|
+
args[f.index] = await req.file();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (
|
|
652
|
+
meta.files &&
|
|
653
|
+
req.headers["content-type"]?.startsWith("multipart/form-data") === true
|
|
654
|
+
) {
|
|
655
|
+
const files = await req.saveRequestFiles();
|
|
656
|
+
if (!files || files.length === 0) {
|
|
657
|
+
throw new BadRequestException({ error: "No files uploaded" });
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const fileInfo = files.map((file) => ({
|
|
661
|
+
type: file.type,
|
|
662
|
+
filepath: file.filepath,
|
|
663
|
+
fieldname: file.fieldname,
|
|
664
|
+
filename: file.filename,
|
|
665
|
+
encoding: file.encoding,
|
|
666
|
+
mimetype: file.mimetype,
|
|
667
|
+
fields: file.fields,
|
|
668
|
+
}));
|
|
669
|
+
for await (let f of meta.files) {
|
|
670
|
+
const findex = fileInfo.findIndex((x) => x.fieldname == f.fieldName);
|
|
671
|
+
if (f.fieldName != "all" && findex == -1) {
|
|
672
|
+
throw new BadRequestException(
|
|
673
|
+
`${f.fieldName} doesn't exists in request files tree.`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
args[f.index] =
|
|
677
|
+
f.fieldName == "all"
|
|
678
|
+
? fileInfo
|
|
679
|
+
: fileInfo.filter((x) => x.fieldname == f.fieldName);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
cache.set(cacheKey, args);
|
|
684
|
+
return args;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Process Meta for controlelr class methods
|
|
689
|
+
* @param prototype
|
|
690
|
+
* @param method
|
|
691
|
+
* @returns
|
|
692
|
+
*/
|
|
693
|
+
private _processMeta(prototype: any, method: string): MethodParamMeta {
|
|
694
|
+
const cacheKey = `${prototype.constructor.name}_${method}`;
|
|
695
|
+
if (this.metaCache.has(cacheKey)) {
|
|
696
|
+
return this.metaCache.get(cacheKey)!;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const meta: MethodParamMeta = {
|
|
700
|
+
params: Reflect.getMetadata(PARAM_META_KEY, prototype, method) || [],
|
|
701
|
+
query: Reflect.getMetadata(QUERY_META_KEY, prototype, method) || [],
|
|
702
|
+
body: Reflect.getMetadata(REQUEST_BODY_META_KEY, prototype, method) || [],
|
|
703
|
+
file: Reflect.getMetadata(REQUEST_BODY_FILE_KEY, prototype, method) || [],
|
|
704
|
+
files:
|
|
705
|
+
Reflect.getMetadata(REQUEST_BODY_FILES_KEY, prototype, method) || [],
|
|
706
|
+
headers:
|
|
707
|
+
Reflect.getMetadata(REQUEST_HEADER_META_KEY, prototype, method) || [],
|
|
708
|
+
currentUser:
|
|
709
|
+
Reflect.getMetadata(REQUEST_USER_META_KEY, prototype, method) || [],
|
|
710
|
+
// swagger: Reflect.getMetadata("route:openapi", prototype, method) || {}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
this.metaCache.set(cacheKey, meta);
|
|
714
|
+
return meta;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async autoControllers() {
|
|
718
|
+
const controllers: Function[] = [];
|
|
719
|
+
const files = await fs.readdir(controllerDir);
|
|
720
|
+
for (const file of files) {
|
|
721
|
+
if (isTsNode ? file.endsWith(".ts") : file.endsWith(".js")) {
|
|
722
|
+
const filePath = path.join(controllerDir, file);
|
|
723
|
+
const module = await import(filePath);
|
|
724
|
+
for (const exported of Object.values(module)) {
|
|
725
|
+
if (typeof exported === "function" && isApiController(exported)) {
|
|
726
|
+
this.buildController(exported);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
mapControllers(controllers: Function[]) {
|
|
734
|
+
this.controllers = controllers;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// addFeature(feature:{controllers:Function[]}){
|
|
738
|
+
// feature.controllers.forEach(c=> this.controllers.push(c))
|
|
739
|
+
// }
|
|
740
|
+
|
|
741
|
+
mapFeature(){
|
|
742
|
+
if(!this.isMapFeatures){
|
|
743
|
+
this.isMapFeatures = true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private async _mapControllers() {
|
|
748
|
+
if (this.controllers.length > 0) {
|
|
749
|
+
for (let controller of this.controllers) {
|
|
750
|
+
if (isApiController(controller)) {
|
|
751
|
+
this.buildController(controller);
|
|
752
|
+
} else {
|
|
753
|
+
throw new SystemUseError("Not a api controller.");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
mapControllersAuto() {
|
|
760
|
+
const isExists = existsSync(controllerDir);
|
|
761
|
+
if (isExists) {
|
|
762
|
+
this.autoControllers();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async handleRoute(args: any) { }
|
|
767
|
+
|
|
768
|
+
private async mapFn(fn: Function) {
|
|
769
|
+
const original = fn;
|
|
770
|
+
fn = function () { };
|
|
771
|
+
return fn;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
private _handleError(error: any): {
|
|
776
|
+
code: number;
|
|
777
|
+
error: string;
|
|
778
|
+
message: any;
|
|
779
|
+
} {
|
|
780
|
+
if (error instanceof BaseHttpException) {
|
|
781
|
+
return {
|
|
782
|
+
code: error.code,
|
|
783
|
+
error: error.name,
|
|
784
|
+
message: isValidJsonString(error.message)
|
|
785
|
+
? JSON.parse(error.message)
|
|
786
|
+
: error.message,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
code: 500,
|
|
791
|
+
error: "INTERNAL_ERROR",
|
|
792
|
+
message: error.message ? error.message : "Something going wrong.",
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async mapRoute<T extends (...args: any[]) => any>(
|
|
797
|
+
method: "get" | "post" | "put" | "delete",
|
|
798
|
+
path: string = "",
|
|
799
|
+
fn: T,
|
|
800
|
+
) {
|
|
801
|
+
await this.mapFn(fn); // Assuming mapFn is needed for all methods
|
|
802
|
+
|
|
803
|
+
this.app[method](path, async (req: any, res: any) => {
|
|
804
|
+
// Dynamic method call
|
|
805
|
+
try {
|
|
806
|
+
const result = await fn.apply(this, [req, res]);
|
|
807
|
+
if (typeof result === "object" && result !== null) {
|
|
808
|
+
res.json(result); // Use res.json for objects
|
|
809
|
+
} else {
|
|
810
|
+
res.send(result); // Fallback for other types
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.error(`Error in ${method} route handler:`, error);
|
|
814
|
+
const handledErr = this._handleError(error);
|
|
815
|
+
res.status(handledErr.code).send(handledErr);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
private _routeHandler<T extends (...args: any[]) => any>(
|
|
820
|
+
routePath: string,
|
|
821
|
+
method: string,
|
|
822
|
+
fn: T,
|
|
823
|
+
) {
|
|
824
|
+
const routeKey = method + ":" + routePath;
|
|
825
|
+
this.rMap.set(routeKey, {
|
|
826
|
+
handler: fn,
|
|
827
|
+
middlewares: [],
|
|
828
|
+
schema: {},
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
this.mapFn(fn);
|
|
832
|
+
|
|
833
|
+
const route = {
|
|
834
|
+
useMiddleware: <M extends AppMiddleware>(
|
|
835
|
+
middlewares: Constructor<AppMiddleware>[],
|
|
836
|
+
) => {
|
|
837
|
+
const midds = Array.isArray(middlewares) ? middlewares : [middlewares];
|
|
838
|
+
const ms: any[] = (midds as unknown as any[]).map((mclass) => {
|
|
839
|
+
const cls = Container.get<AppMiddleware>(mclass);
|
|
840
|
+
this.middlewares.set(mclass.name, cls);
|
|
841
|
+
return cls.invoke;
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const r = this.rMap.get(routeKey);
|
|
845
|
+
if (r) {
|
|
846
|
+
r.middlewares = ms;
|
|
847
|
+
}
|
|
848
|
+
return route;
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
useOpenApi: (options: OpenApiOptions) => {
|
|
852
|
+
const r = this.rMap.get(routeKey);
|
|
853
|
+
if (r) {
|
|
854
|
+
r.schema = options;
|
|
855
|
+
}
|
|
856
|
+
return route;
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
return route;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
mapGet<T extends (...args: any[]) => any>(path: string = "", fn: T) {
|
|
865
|
+
return this._routeHandler(path, "GET", fn);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
mapPost<T extends (...args: any[]) => any>(path: string = "", fn: T) {
|
|
869
|
+
return this._routeHandler(path, "POST", fn);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
mapPut<T extends (...args: any[]) => any>(path: string = "", fn: T) {
|
|
873
|
+
return this._routeHandler(path, "PUT", fn);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
mapDelete<T extends (...args: any[]) => any>(path: string = "", fn: T) {
|
|
877
|
+
return this._routeHandler(path, "DELETE", fn);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
private _mapFeatures(){
|
|
882
|
+
const features = Container.get('features');
|
|
883
|
+
console.log('Features', features);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async initializeDatabase() {
|
|
887
|
+
if (this.dataSourceOptions && this.dataSource) {
|
|
888
|
+
await this.dataSource.initialize();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async run(port: number = 4000, fn?:CallableFunction): Promise<void> {
|
|
893
|
+
if (this.alreadyRun) throw new SystemUseError("App already running");
|
|
894
|
+
this.alreadyRun = true;
|
|
895
|
+
|
|
896
|
+
if (this.hasSwagger) {
|
|
897
|
+
await this.initSwagger(this.globalSwaggerOptions);
|
|
898
|
+
}
|
|
899
|
+
await this.initializeDatabase();
|
|
900
|
+
if(this.isMapFeatures){
|
|
901
|
+
this._mapFeatures();
|
|
902
|
+
}
|
|
903
|
+
await this._mapControllers();
|
|
904
|
+
|
|
905
|
+
this.rMap.forEach((value, key) => {
|
|
906
|
+
const [m, r] = key.split(":");
|
|
907
|
+
this.app.route({
|
|
908
|
+
method: m,
|
|
909
|
+
url: r,
|
|
910
|
+
schema: value.schema || {},
|
|
911
|
+
preHandler: value.middlewares ? value.middlewares : [],
|
|
912
|
+
handler: async (req, res) => {
|
|
913
|
+
const result = await value.handler.apply(this, [req, res]);
|
|
914
|
+
return result;
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
this.app.setErrorHandler(async (error, req, res) => {
|
|
919
|
+
const handledErr = this._handleError(error);
|
|
920
|
+
if (error instanceof ValidationErrorException || error instanceof BadRequestException) {
|
|
921
|
+
return res.status(handledErr.code).send({
|
|
922
|
+
code: handledErr.code,
|
|
923
|
+
error: handledErr.error,
|
|
924
|
+
errors: handledErr.message,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return res.status(handledErr.code).send(handledErr);
|
|
928
|
+
});
|
|
929
|
+
await this.app.ready();
|
|
930
|
+
await this.app.listen({ port });
|
|
931
|
+
console.log(`Application running on http://127.0.0.1:${port}`);
|
|
932
|
+
}
|
|
933
|
+
getTestApp(buildOptions?: any): TestApplication {
|
|
934
|
+
try {
|
|
935
|
+
// }
|
|
936
|
+
// this.initializeDatabase();
|
|
937
|
+
this._mapControllers();
|
|
938
|
+
this.rMap.forEach((value, key) => {
|
|
939
|
+
const [m, r] = key.split(":");
|
|
940
|
+
this.app.route({
|
|
941
|
+
method: m,
|
|
942
|
+
url: r,
|
|
943
|
+
schema: value.schema || {},
|
|
944
|
+
preHandler: value.middlewares ? value.middlewares : [],
|
|
945
|
+
handler: async (req, res) => {
|
|
946
|
+
const result = await value.handler.apply(this, [req, res]);
|
|
947
|
+
return result;
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
this.app.setErrorHandler(async (error, req, res) => {
|
|
953
|
+
const handledErr = this._handleError(error);
|
|
954
|
+
if (error instanceof ValidationErrorException) {
|
|
955
|
+
return res.status(handledErr.code).send({
|
|
956
|
+
code: handledErr.code,
|
|
957
|
+
error: handledErr.error,
|
|
958
|
+
errors: handledErr.message,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return res.status(handledErr.code).send(handledErr);
|
|
962
|
+
});
|
|
963
|
+
// return this.app as any;
|
|
964
|
+
//
|
|
965
|
+
return {
|
|
966
|
+
get: async (url: string, options?: InjectOptions) =>
|
|
967
|
+
this.app.inject({ method: "GET", url, ...options }),
|
|
968
|
+
post: async (url: string, options?: InjectOptions) =>
|
|
969
|
+
this.app.inject({ method: "POST", url, ...options }),
|
|
970
|
+
put: async (url: string, options?: InjectOptions) =>
|
|
971
|
+
this.app.inject({ method: "PUT", url, ...options }),
|
|
972
|
+
patch: async (url: string, options?: InjectOptions) =>
|
|
973
|
+
this.app.inject({ method: "PATCH", url, ...options }),
|
|
974
|
+
delete: async (url: string, options?: InjectOptions) =>
|
|
975
|
+
this.app.inject({ method: "DELETE", url, ...options }),
|
|
976
|
+
options: async (url: string, options?: InjectOptions) =>
|
|
977
|
+
this.app.inject({ method: "OPTIONS", url, ...options }),
|
|
978
|
+
getController: <T>(controller: Constructor<T>) => {
|
|
979
|
+
return Container.get<T>(controller);
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
} catch (error) {
|
|
983
|
+
console.log(error);
|
|
984
|
+
throw new SystemUseError("Can't get test appliction");
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export type Application = typeof AvleonApplication;
|
|
990
|
+
|
|
991
|
+
// Applciation Builder
|
|
992
|
+
export interface ITestBuilder {
|
|
993
|
+
getTestApplication(): AvleonTestAppliction;
|
|
994
|
+
createTestApplication(options: any): AvleonTestAppliction;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export interface IAppBuilder {
|
|
998
|
+
registerPlugin<T extends Function, S extends {}>(
|
|
999
|
+
plugin: T,
|
|
1000
|
+
options: S,
|
|
1001
|
+
): Promise<void>;
|
|
1002
|
+
addDataSource<
|
|
1003
|
+
T extends IConfig<R>,
|
|
1004
|
+
R = ReturnType<InstanceType<Constructable<T>>["config"]>,
|
|
1005
|
+
>(
|
|
1006
|
+
ConfigClass: Constructable<T>,
|
|
1007
|
+
modifyConfig?: (config: R) => R,
|
|
1008
|
+
): void;
|
|
1009
|
+
build<T extends IAvleonApplication>(): T;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export class TestBuilder {
|
|
1013
|
+
private static instance: TestBuilder;
|
|
1014
|
+
private app: any;
|
|
1015
|
+
private dataSourceOptions?: DataSourceOptions | undefined;
|
|
1016
|
+
private constructor() {
|
|
1017
|
+
process.env.NODE_ENV = "test";
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
static createBuilder() {
|
|
1021
|
+
if (!TestBuilder.instance) {
|
|
1022
|
+
TestBuilder.instance = new TestBuilder();
|
|
1023
|
+
}
|
|
1024
|
+
return TestBuilder.instance;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
addDatasource(options: DataSourceOptions) {
|
|
1028
|
+
this.dataSourceOptions = options;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private getController<T>(controller: Constructor<T>) {
|
|
1032
|
+
return Container.get(controller);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private getService<T>(service: Constructor<T>) {
|
|
1036
|
+
return Container.get(service);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
getTestApplication(options: TestAppOptions) {
|
|
1040
|
+
const app = AvleonApplication.getInternalApp({
|
|
1041
|
+
dataSourceOptions: this.dataSourceOptions,
|
|
1042
|
+
});
|
|
1043
|
+
app.mapControllers([...options.controllers]);
|
|
1044
|
+
return app.getTestApp();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
build(app: IAvleonApplication) {
|
|
1048
|
+
return app.getTestApp();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
fromApplication(app: IAvleonApplication) {
|
|
1052
|
+
return app.getTestApp();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export class Builder {
|
|
1057
|
+
|
|
1058
|
+
static createApplication(){
|
|
1059
|
+
const app = AvleonApplication.getApp();
|
|
1060
|
+
return app
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
}
|