@avleon/core 0.0.20 → 0.0.25
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 +47 -0
- package/dist/application.js +50 -0
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +78 -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 +93 -42
- package/dist/icore.js +319 -251
- 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 +6 -3
- 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 +26 -9
- package/src/application.ts +125 -0
- package/src/authentication.ts +16 -0
- package/src/cache.ts +91 -0
- package/src/collection.ts +254 -0
- package/src/config.ts +42 -0
- package/src/constants.ts +1 -0
- package/src/container.ts +54 -0
- package/src/controller.ts +127 -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 +1140 -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/results.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type OkOptions = {
|
|
2
|
+
streamable: boolean;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export class Results {
|
|
6
|
+
static code = 500;
|
|
7
|
+
message: string = "Something going wrong";
|
|
8
|
+
|
|
9
|
+
static Ok<T>(data: T): Ok<T> {
|
|
10
|
+
return new Ok<T>(data);
|
|
11
|
+
}
|
|
12
|
+
static NoContent() {
|
|
13
|
+
this.code = 204;
|
|
14
|
+
}
|
|
15
|
+
static OkStream() {}
|
|
16
|
+
static NotFound<T>(message: T) {
|
|
17
|
+
return new NotFound<T>(message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Ok<T> {
|
|
22
|
+
constructor(public data: T) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class NotFound<T> {
|
|
26
|
+
constructor(public message: T) {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// This type ensures methods must return Results.Ok
|
|
30
|
+
export type RouteResult<T = any> = typeof Results.Ok<T>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2024
|
|
3
|
+
* @author Tareq Hossain
|
|
4
|
+
* @email xtrinsic96@gmail.com
|
|
5
|
+
* @url https://github.com/xtareq
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CONTROLLER_META_KEY, ROUTE_META_KEY } from "./container";
|
|
9
|
+
import { OpenApiOptions } from "./openapi";
|
|
10
|
+
import { HttpResponse } from "./response";
|
|
11
|
+
import { Results } from "./results";
|
|
12
|
+
export type RouteMethods =
|
|
13
|
+
| "GET"
|
|
14
|
+
| "POST"
|
|
15
|
+
| "PUT"
|
|
16
|
+
| "PATCH"
|
|
17
|
+
| "DELETE"
|
|
18
|
+
| "OPTIONS"
|
|
19
|
+
| "ALL";
|
|
20
|
+
|
|
21
|
+
const schema: OpenApiOptions = {
|
|
22
|
+
tags: ["hello"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for defining a route's method and metadata.
|
|
27
|
+
*/
|
|
28
|
+
export type RouteMethodOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* HTTP method for the route (e.g., GET, POST, PUT, DELETE).
|
|
31
|
+
*/
|
|
32
|
+
method?: RouteMethods;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The path or endpoint for the route (e.g., "/users/:id").
|
|
36
|
+
*/
|
|
37
|
+
path?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OpenAPI metadata for the route, including summary and description.
|
|
41
|
+
*/
|
|
42
|
+
openapi?: OpenApiOptions;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Name of the route.
|
|
46
|
+
*
|
|
47
|
+
* @description If Swagger is enabled in the project, this will appear as a tag in the generated documentation.
|
|
48
|
+
*/
|
|
49
|
+
name?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Overloads
|
|
53
|
+
|
|
54
|
+
// Implementation
|
|
55
|
+
export function createRouteDecorator(
|
|
56
|
+
method: RouteMethods = "GET",
|
|
57
|
+
): (
|
|
58
|
+
pathOrOptions: string | RouteMethodOptions,
|
|
59
|
+
maybeOptions?: RouteMethodOptions,
|
|
60
|
+
) => MethodDecorator {
|
|
61
|
+
return function (
|
|
62
|
+
pathOrOptions: string | RouteMethodOptions,
|
|
63
|
+
maybeOptions?: RouteMethodOptions,
|
|
64
|
+
): MethodDecorator {
|
|
65
|
+
return function (
|
|
66
|
+
target,
|
|
67
|
+
propertyKey: string | symbol,
|
|
68
|
+
descriptor: PropertyDescriptor,
|
|
69
|
+
) {
|
|
70
|
+
let path = "/";
|
|
71
|
+
let options: RouteMethodOptions = {};
|
|
72
|
+
|
|
73
|
+
if (typeof pathOrOptions === "string") {
|
|
74
|
+
path = pathOrOptions;
|
|
75
|
+
options = maybeOptions || {};
|
|
76
|
+
} else if (typeof pathOrOptions === "object") {
|
|
77
|
+
options = pathOrOptions;
|
|
78
|
+
path = options.name || "/";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Define metadata
|
|
82
|
+
Reflect.defineMetadata("route:path", path, target, propertyKey);
|
|
83
|
+
Reflect.defineMetadata(
|
|
84
|
+
"route:method",
|
|
85
|
+
method || "GET",
|
|
86
|
+
target,
|
|
87
|
+
propertyKey,
|
|
88
|
+
);
|
|
89
|
+
Reflect.getMetadata(CONTROLLER_META_KEY, target.constructor);
|
|
90
|
+
Reflect.defineMetadata(
|
|
91
|
+
ROUTE_META_KEY,
|
|
92
|
+
{ ...options, method, path, controller: target.constructor.name },
|
|
93
|
+
target,
|
|
94
|
+
propertyKey,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (options) {
|
|
98
|
+
Reflect.defineMetadata("route:options", options, target, propertyKey);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Usage Example
|
|
105
|
+
/**
|
|
106
|
+
* @description HTTP Get method
|
|
107
|
+
* @param {string} path
|
|
108
|
+
*/
|
|
109
|
+
export function Get(path?: string): MethodDecorator;
|
|
110
|
+
export function Get(path: string | RouteMethodOptions): MethodDecorator;
|
|
111
|
+
/**
|
|
112
|
+
* @description HTTP Get method
|
|
113
|
+
* @param {string} path
|
|
114
|
+
* @param {RouteMethodOptions} options
|
|
115
|
+
*/
|
|
116
|
+
export function Get(path: string, options: RouteMethodOptions): MethodDecorator;
|
|
117
|
+
export function Get(
|
|
118
|
+
path?: string | RouteMethodOptions,
|
|
119
|
+
options?: RouteMethodOptions,
|
|
120
|
+
) {
|
|
121
|
+
const parsedPath =
|
|
122
|
+
!path && !options ? "/" : (path as string | RouteMethodOptions);
|
|
123
|
+
if (options) {
|
|
124
|
+
return createRouteDecorator("GET")(parsedPath, options);
|
|
125
|
+
} else {
|
|
126
|
+
return createRouteDecorator("GET")(parsedPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function Post(path?: string): MethodDecorator;
|
|
131
|
+
export function Post(path: string | RouteMethodOptions): MethodDecorator;
|
|
132
|
+
export function Post(
|
|
133
|
+
path: string,
|
|
134
|
+
options: RouteMethodOptions,
|
|
135
|
+
): MethodDecorator;
|
|
136
|
+
export function Post(
|
|
137
|
+
path?: string | RouteMethodOptions,
|
|
138
|
+
options?: RouteMethodOptions,
|
|
139
|
+
) {
|
|
140
|
+
const parsedPath =
|
|
141
|
+
!path && !options ? "/" : (path as string | RouteMethodOptions);
|
|
142
|
+
if (options) {
|
|
143
|
+
return createRouteDecorator("POST")(parsedPath, options);
|
|
144
|
+
} else {
|
|
145
|
+
return createRouteDecorator("POST")(parsedPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function Put(path?: string): MethodDecorator;
|
|
150
|
+
export function Put(path: string | RouteMethodOptions): MethodDecorator;
|
|
151
|
+
export function Put(path: string, options: RouteMethodOptions): MethodDecorator;
|
|
152
|
+
export function Put(
|
|
153
|
+
path?: string | RouteMethodOptions,
|
|
154
|
+
options?: RouteMethodOptions,
|
|
155
|
+
) {
|
|
156
|
+
const parsedPath =
|
|
157
|
+
!path && !options ? "/" : (path as string | RouteMethodOptions);
|
|
158
|
+
if (options) {
|
|
159
|
+
return createRouteDecorator("PUT")(parsedPath, options);
|
|
160
|
+
} else {
|
|
161
|
+
return createRouteDecorator("PUT")(parsedPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function Delete(path?: string): MethodDecorator;
|
|
166
|
+
export function Delete(path: string | RouteMethodOptions): MethodDecorator;
|
|
167
|
+
export function Delete(
|
|
168
|
+
path: string,
|
|
169
|
+
options: RouteMethodOptions,
|
|
170
|
+
): MethodDecorator;
|
|
171
|
+
export function Delete(
|
|
172
|
+
path?: string | RouteMethodOptions,
|
|
173
|
+
options?: RouteMethodOptions,
|
|
174
|
+
) {
|
|
175
|
+
const parsedPath =
|
|
176
|
+
!path && !options ? "/" : (path as string | RouteMethodOptions);
|
|
177
|
+
if (options) {
|
|
178
|
+
return createRouteDecorator("DELETE")(parsedPath, options);
|
|
179
|
+
} else {
|
|
180
|
+
return createRouteDecorator("DELETE")(parsedPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const Patch = createRouteDecorator("PATCH");
|
|
185
|
+
export const Options = createRouteDecorator("OPTIONS");
|
|
186
|
+
export const All = createRouteDecorator("ALL");
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2024
|
|
3
|
+
* @author Tareq Hossain
|
|
4
|
+
* @email xtrinsic96@gmail.com
|
|
5
|
+
* @url https://github.com/xtareq
|
|
6
|
+
*/
|
|
7
|
+
import { getMetadataStorage } from "class-validator";
|
|
8
|
+
|
|
9
|
+
export function generateSwaggerSchema(classType: any): any {
|
|
10
|
+
const metadataStorage = getMetadataStorage();
|
|
11
|
+
const validationMetadata = metadataStorage.getTargetValidationMetadatas(
|
|
12
|
+
classType,
|
|
13
|
+
"",
|
|
14
|
+
true,
|
|
15
|
+
false
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const schema: any = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {},
|
|
21
|
+
required: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const prototype = classType.prototype;
|
|
25
|
+
|
|
26
|
+
const propertyKeys = new Set([
|
|
27
|
+
...Object.getOwnPropertyNames(prototype),
|
|
28
|
+
...validationMetadata.map((m: any) => m.propertyName),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
propertyKeys.forEach((propertyName) => {
|
|
32
|
+
if (!propertyName || propertyName === "constructor") return;
|
|
33
|
+
|
|
34
|
+
const openApiMeta: any = Reflect.getMetadata(
|
|
35
|
+
"property:openapi",
|
|
36
|
+
prototype,
|
|
37
|
+
propertyName
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (openApiMeta?.exclude) return;
|
|
41
|
+
|
|
42
|
+
const propertyType = Reflect.getMetadata(
|
|
43
|
+
"design:type",
|
|
44
|
+
prototype,
|
|
45
|
+
propertyName
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
let swaggerProperty: any = {};
|
|
49
|
+
|
|
50
|
+
switch (propertyType) {
|
|
51
|
+
case String:
|
|
52
|
+
swaggerProperty.type = "string";
|
|
53
|
+
break;
|
|
54
|
+
case Number:
|
|
55
|
+
swaggerProperty.type = "number";
|
|
56
|
+
break;
|
|
57
|
+
case Boolean:
|
|
58
|
+
swaggerProperty.type = "boolean";
|
|
59
|
+
break;
|
|
60
|
+
case Date:
|
|
61
|
+
swaggerProperty.type = "string";
|
|
62
|
+
swaggerProperty.format = "date-time";
|
|
63
|
+
break;
|
|
64
|
+
case Array:
|
|
65
|
+
swaggerProperty.type = "array";
|
|
66
|
+
swaggerProperty.items = { type: "string" }; // fallback
|
|
67
|
+
break;
|
|
68
|
+
case Object:
|
|
69
|
+
swaggerProperty = generateSwaggerSchema(propertyType);
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
swaggerProperty.type = propertyType?.name?.toLowerCase() || "string";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Apply OpenApi metadata if present
|
|
76
|
+
if (openApiMeta) {
|
|
77
|
+
swaggerProperty = {
|
|
78
|
+
...swaggerProperty,
|
|
79
|
+
...extractOpenApiFields(openApiMeta),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
schema.properties[propertyName] = swaggerProperty;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Handle validation rules
|
|
87
|
+
validationMetadata.forEach((meta: any) => {
|
|
88
|
+
const propertyName = meta.propertyName;
|
|
89
|
+
switch (meta.name) {
|
|
90
|
+
case "isNotEmpty":
|
|
91
|
+
if (!schema.required.includes(propertyName)) {
|
|
92
|
+
schema.required.push(propertyName);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "isDefined":
|
|
96
|
+
if (!schema.required.includes(propertyName)) {
|
|
97
|
+
schema.required.push(propertyName);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "isOptional":
|
|
101
|
+
schema.required = schema.required.filter(
|
|
102
|
+
(item: any) => item !== propertyName
|
|
103
|
+
);
|
|
104
|
+
break;
|
|
105
|
+
case "minLength":
|
|
106
|
+
schema.properties[propertyName].minLength = meta.constraints[0];
|
|
107
|
+
break;
|
|
108
|
+
case "maxLength":
|
|
109
|
+
schema.properties[propertyName].maxLength = meta.constraints[0];
|
|
110
|
+
break;
|
|
111
|
+
case "min":
|
|
112
|
+
schema.properties[propertyName].minimum = meta.constraints[0];
|
|
113
|
+
break;
|
|
114
|
+
case "max":
|
|
115
|
+
schema.properties[propertyName].maximum = meta.constraints[0];
|
|
116
|
+
break;
|
|
117
|
+
case "isEmail":
|
|
118
|
+
schema.properties[propertyName].format = "email";
|
|
119
|
+
break;
|
|
120
|
+
case "isDate":
|
|
121
|
+
schema.properties[propertyName].format = "date-time";
|
|
122
|
+
break;
|
|
123
|
+
case "isIn":
|
|
124
|
+
schema.properties[propertyName].enum = meta.constraints[0];
|
|
125
|
+
break;
|
|
126
|
+
case "isNumber":
|
|
127
|
+
schema.properties[propertyName].type = "number";
|
|
128
|
+
break;
|
|
129
|
+
case "isInt":
|
|
130
|
+
schema.properties[propertyName].type = "integer";
|
|
131
|
+
break;
|
|
132
|
+
case "isBoolean":
|
|
133
|
+
schema.properties[propertyName].type = "boolean";
|
|
134
|
+
break;
|
|
135
|
+
case "isString":
|
|
136
|
+
schema.properties[propertyName].type = "string";
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return schema;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractOpenApiFields(meta: any): any {
|
|
144
|
+
const result: any = {};
|
|
145
|
+
const fields = [
|
|
146
|
+
"description",
|
|
147
|
+
"summary",
|
|
148
|
+
"deprecated",
|
|
149
|
+
"example",
|
|
150
|
+
"enum",
|
|
151
|
+
"format",
|
|
152
|
+
"default",
|
|
153
|
+
"minimum",
|
|
154
|
+
"maximum",
|
|
155
|
+
"minLength",
|
|
156
|
+
"maxLength",
|
|
157
|
+
"pattern",
|
|
158
|
+
"oneOf",
|
|
159
|
+
"allOf",
|
|
160
|
+
"anyOf",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
fields.forEach((field) => {
|
|
164
|
+
if (meta[field] !== undefined) {
|
|
165
|
+
result[field] = meta[field];
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// export function generateSwaggerSchema(classType: any) {
|
|
173
|
+
// const { getMetadataStorage } = require("class-validator");
|
|
174
|
+
// const { plainToInstance } = require("class-transformer");
|
|
175
|
+
|
|
176
|
+
// const metadataStorage = getMetadataStorage();
|
|
177
|
+
// const validationMetadata = metadataStorage.getTargetValidationMetadatas(
|
|
178
|
+
// classType,
|
|
179
|
+
// "",
|
|
180
|
+
// true,
|
|
181
|
+
// );
|
|
182
|
+
|
|
183
|
+
// const schema: any = {
|
|
184
|
+
// type: "object",
|
|
185
|
+
// properties: {},
|
|
186
|
+
// required: [],
|
|
187
|
+
// };
|
|
188
|
+
|
|
189
|
+
// validationMetadata.forEach((meta: any) => {
|
|
190
|
+
// const propertyName = meta.propertyName;
|
|
191
|
+
|
|
192
|
+
// // Infer the type dynamically using Reflect metadata
|
|
193
|
+
// const propertyType = Reflect.getMetadata(
|
|
194
|
+
// "design:type",
|
|
195
|
+
// classType.prototype,
|
|
196
|
+
// propertyName,
|
|
197
|
+
// );
|
|
198
|
+
|
|
199
|
+
// schema.properties[propertyName] = {
|
|
200
|
+
// type: propertyType?.name.toLowerCase() || "string", // Default to string if type cannot be inferred
|
|
201
|
+
// };
|
|
202
|
+
|
|
203
|
+
// if (meta.name === "isNotEmpty") {
|
|
204
|
+
// schema.required.push(propertyName);
|
|
205
|
+
// }
|
|
206
|
+
|
|
207
|
+
// if (meta.name === "minLength") {
|
|
208
|
+
// schema.properties[propertyName].minLength = meta.constraints[0];
|
|
209
|
+
// }
|
|
210
|
+
// });
|
|
211
|
+
|
|
212
|
+
// return schema;
|
|
213
|
+
// }
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Container } from "typedi";
|
|
3
|
+
import { DataSource, DataSourceOptions } from "typeorm";
|
|
4
|
+
import { AvleonApplication } from "./icore";
|
|
5
|
+
|
|
6
|
+
// Enhanced Test Utilities
|
|
7
|
+
export class AvleonTestUtility {
|
|
8
|
+
private static testDataSource: DataSource | null = null;
|
|
9
|
+
private static testContainer: typeof Container;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize test environment
|
|
13
|
+
*/
|
|
14
|
+
static async init(options?: {
|
|
15
|
+
dataSourceOptions?: DataSourceOptions;
|
|
16
|
+
resetContainer?: boolean;
|
|
17
|
+
}) {
|
|
18
|
+
// Reset container if specified
|
|
19
|
+
if (options?.resetContainer) {
|
|
20
|
+
this.testContainer = Container;
|
|
21
|
+
this.testContainer.reset();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize test database if options provided
|
|
25
|
+
if (options?.dataSourceOptions) {
|
|
26
|
+
this.testDataSource = new DataSource({
|
|
27
|
+
...options.dataSourceOptions,
|
|
28
|
+
logging: false, // Disable logging during tests
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await this.testDataSource.initialize();
|
|
32
|
+
await this.testDataSource.synchronize(true); // Create schema
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mock a dependency for testing
|
|
40
|
+
* @param token Dependency token
|
|
41
|
+
* @param mockImplementation Mock implementation
|
|
42
|
+
*/
|
|
43
|
+
static mockDependency<T>(token: any, mockImplementation: T) {
|
|
44
|
+
Container.set(token, mockImplementation);
|
|
45
|
+
return mockImplementation;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create an isolated test instance of a class
|
|
50
|
+
* @param ClassType Class to instantiate
|
|
51
|
+
* @param overrides Optional property overrides
|
|
52
|
+
*/
|
|
53
|
+
static createTestInstance<T>(
|
|
54
|
+
ClassType: new (...args: any[]) => T,
|
|
55
|
+
overrides: Partial<T> = {},
|
|
56
|
+
): T {
|
|
57
|
+
const instance = Container.get(ClassType);
|
|
58
|
+
|
|
59
|
+
// Apply overrides
|
|
60
|
+
Object.keys(overrides).forEach((key) => {
|
|
61
|
+
(instance as any)[key] = (overrides as any)[key];
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return instance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup test environment
|
|
69
|
+
*/
|
|
70
|
+
static async cleanup() {
|
|
71
|
+
if (this.testDataSource) {
|
|
72
|
+
await this.testDataSource.dropDatabase();
|
|
73
|
+
await this.testDataSource.destroy();
|
|
74
|
+
this.testDataSource = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reset container
|
|
78
|
+
Container.reset();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Enhanced Test Builder
|
|
83
|
+
export class AvleonTestBuilder {
|
|
84
|
+
private controllers: any[] = [];
|
|
85
|
+
private testOptions: any = {};
|
|
86
|
+
private mocks: Map<any, any> = new Map();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Add controllers for testing
|
|
90
|
+
* @param controllers Controllers to add
|
|
91
|
+
*/
|
|
92
|
+
addControllers(...controllers: any[]) {
|
|
93
|
+
this.controllers.push(...controllers);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mock a dependency
|
|
99
|
+
* @param token Dependency token
|
|
100
|
+
* @param mockImplementation Mock implementation
|
|
101
|
+
*/
|
|
102
|
+
mockDependency(token: any, mockImplementation: any) {
|
|
103
|
+
this.mocks.set(token, mockImplementation);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set test options
|
|
109
|
+
* @param options Test configuration options
|
|
110
|
+
*/
|
|
111
|
+
setOptions(options: any) {
|
|
112
|
+
this.testOptions = { ...this.testOptions, ...options };
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build test application
|
|
118
|
+
*/
|
|
119
|
+
async build() {
|
|
120
|
+
// Apply mocks
|
|
121
|
+
this.mocks.forEach((mock, token) => {
|
|
122
|
+
Container.set(token, mock);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Initialize test utility
|
|
126
|
+
await AvleonTestUtility.init({
|
|
127
|
+
dataSourceOptions: this.testOptions.dataSourceOptions,
|
|
128
|
+
resetContainer: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Create test application
|
|
132
|
+
const app = AvleonApplication.getInternalApp({
|
|
133
|
+
dataSourceOptions: this.testOptions.dataSourceOptions,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Map controllers
|
|
137
|
+
app.useControllers(this.controllers);
|
|
138
|
+
|
|
139
|
+
// Get test application
|
|
140
|
+
return app.getTestApp();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Example Usage Decorator
|
|
145
|
+
export function UnitTest() {
|
|
146
|
+
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
|
147
|
+
const originalMethod = descriptor.value;
|
|
148
|
+
|
|
149
|
+
descriptor.value = async function (...args: any[]) {
|
|
150
|
+
try {
|
|
151
|
+
// Pre-test setup
|
|
152
|
+
await AvleonTestUtility.init();
|
|
153
|
+
|
|
154
|
+
// Execute test
|
|
155
|
+
const result = await originalMethod.apply(this, args);
|
|
156
|
+
|
|
157
|
+
// Post-test cleanup
|
|
158
|
+
await AvleonTestUtility.cleanup();
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Ensure cleanup even if test fails
|
|
163
|
+
await AvleonTestUtility.cleanup();
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return descriptor;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//
|
|
172
|
+
// // Example of Unit and Integration Test
|
|
173
|
+
// class UserServiceTest {
|
|
174
|
+
// @UnitTest()
|
|
175
|
+
// async testUserCreation() {
|
|
176
|
+
// // Mock UserRepository
|
|
177
|
+
// const mockRepo = AvleonTestUtility.mockDependency(
|
|
178
|
+
// UserRepository,
|
|
179
|
+
// { create: jest.fn() }
|
|
180
|
+
// );
|
|
181
|
+
//
|
|
182
|
+
// // Create test instance
|
|
183
|
+
// const userService = AvleonTestUtility.createTestInstance(UserService);
|
|
184
|
+
//
|
|
185
|
+
// // Perform test
|
|
186
|
+
// const result = await userService.createUser({
|
|
187
|
+
// name: 'Test User',
|
|
188
|
+
// email: 'test@example.com'
|
|
189
|
+
// });
|
|
190
|
+
//
|
|
191
|
+
// // Assertions
|
|
192
|
+
// expect(mockRepo.create).toHaveBeenCalledWith(expect.any(Object));
|
|
193
|
+
// }
|
|
194
|
+
// }
|
|
195
|
+
//
|
|
196
|
+
// // Enhanced E2E Testing Example
|
|
197
|
+
// class E2EUserControllerTest {
|
|
198
|
+
// async testUserRegistration() {
|
|
199
|
+
// // Build test application
|
|
200
|
+
// const testApp = await new AvleonTestBuilder()
|
|
201
|
+
// .addControllers(UserController)
|
|
202
|
+
// .mockDependency(AuthService, mockAuthService)
|
|
203
|
+
// .setOptions({
|
|
204
|
+
// dataSourceOptions: testDatabaseConfig
|
|
205
|
+
// })
|
|
206
|
+
// .build();
|
|
207
|
+
//
|
|
208
|
+
// // Perform HTTP request
|
|
209
|
+
// const response = await testApp.post('/users/register', {
|
|
210
|
+
// payload: {
|
|
211
|
+
// name: 'John Doe',
|
|
212
|
+
// email: 'john@example.com'
|
|
213
|
+
// }
|
|
214
|
+
// });
|
|
215
|
+
//
|
|
216
|
+
// // Assertions
|
|
217
|
+
// expect(response.statusCode).toBe(201);
|
|
218
|
+
// expect(response.json()).toHaveProperty('userId');
|
|
219
|
+
// }
|
|
220
|
+
// }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2024
|
|
3
|
+
* @author Tareq Hossain
|
|
4
|
+
* @email xtrinsic96@gmail.com
|
|
5
|
+
* @url https://github.com/xtareq
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import { IApplication } from "./application.interface";
|
|
10
|
+
|
|
11
|
+
export interface IAppBuilder {
|
|
12
|
+
createBuilder(): IAppBuilder;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @description will create a application instace
|
|
16
|
+
* @returns IApplication
|
|
17
|
+
*/
|
|
18
|
+
builder: () => IApplication;
|
|
19
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
export const hashPasswordSync = (password: string) => bcrypt.hashSync(password,12);
|
|
3
|
+
export const matchPasswordSync = (password: string, hash: string) => bcrypt.compareSync(password, hash);
|
|
4
|
+
export const hashPassword = (password: string) => bcrypt.hash(password,12);
|
|
5
|
+
export const matchPassword = (password: string, hash: string) => bcrypt.compare(password, hash);
|