Skip to content

Commit 2e9d1c1

Browse files
authored
feat(core, serve): add logger, refactor validator loader, add built-in middlewares and middleware loader
- add "tslog" package and create "getLogger" by "LoggerFactory". - fix "transform" method in "PaginationTransformer". - add "defaultImport" function for loader used. - remove "default" for each validator classes and add "index.ts" in "data-type-validators" for export default. - add built-in middlewares for cors, request Id, audit logging, rate limit, and add test cases - add the middleware loader to load the middleware module. - set middleware and add test cases in the vulcan app server. - add ignore tag in "IPTypeValidator" class in test/ to prevent detecting unnecessary code.
1 parent 769f287 commit 2e9d1c1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1427
-136
lines changed

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
},
88
"private": true,
99
"dependencies": {
10+
"@koa/cors": "^3.3.0",
1011
"dayjs": "^1.11.2",
1112
"glob": "^8.0.1",
1213
"joi": "^17.6.0",
@@ -15,9 +16,11 @@
1516
"koa-bodyparser": "^4.3.0",
1617
"koa-compose": "^4.1.0",
1718
"koa-router": "^10.1.1",
19+
"koa2-ratelimit": "^1.1.1",
1820
"lodash": "^4.17.21",
1921
"nunjucks": "^3.2.3",
2022
"tslib": "^2.3.0",
23+
"tslog": "^3.3.3",
2124
"uuid": "^8.3.2"
2225
},
2326
"devDependencies": {
@@ -34,6 +37,8 @@
3437
"@types/koa": "^2.13.4",
3538
"@types/koa-compose": "^3.2.5",
3639
"@types/koa-router": "^7.4.4",
40+
"@types/koa2-ratelimit": "^0.9.3",
41+
"@types/koa__cors": "^3.3.0",
3742
"@types/lodash": "^4.14.182",
3843
"@types/node": "16.11.7",
3944
"@types/supertest": "^2.0.12",

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './lib/utils';
12
export * from './lib/validators';
23
// Export all other modules
34
export * from './models';

packages/core/src/lib/utils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './normalizedStringValue';
2+
export * from './logger';
3+
export * from './module';

packages/core/src/lib/utils/logger.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Logger } from 'tslog';
2+
import { AsyncLocalStorage } from 'async_hooks';
3+
export { Logger as ILogger };
4+
// The category according to package name
5+
export enum LoggingScope {
6+
CORE = 'CORE',
7+
BUILD = 'BUILD',
8+
SERVE = 'SERVE',
9+
AUDIT = 'AUDIT',
10+
}
11+
12+
type LoggingScopeTypes = keyof typeof LoggingScope;
13+
14+
export enum LoggingLevel {
15+
SILLY = 'silly',
16+
TRACE = 'trace',
17+
DEBUG = 'debug',
18+
INFO = 'info',
19+
WARN = 'warn',
20+
ERROR = 'error',
21+
FATAL = 'fatal',
22+
}
23+
24+
export interface LoggerOptions {
25+
level?: LoggingLevel;
26+
displayRequestId?: boolean;
27+
}
28+
29+
type LoggerMapConfig = {
30+
[scope in LoggingScope]: LoggerOptions;
31+
};
32+
33+
const defaultMapConfig: LoggerMapConfig = {
34+
[LoggingScope.CORE]: {
35+
level: LoggingLevel.DEBUG,
36+
displayRequestId: false,
37+
},
38+
[LoggingScope.BUILD]: {
39+
level: LoggingLevel.DEBUG,
40+
displayRequestId: false,
41+
},
42+
[LoggingScope.SERVE]: {
43+
level: LoggingLevel.DEBUG,
44+
displayRequestId: false,
45+
},
46+
[LoggingScope.AUDIT]: {
47+
level: LoggingLevel.DEBUG,
48+
displayRequestId: false,
49+
},
50+
};
51+
52+
export type AsyncRequestIdStorage = AsyncLocalStorage<{ requestId: string }>;
53+
class LoggerFactory {
54+
private loggerMap: { [scope: string]: Logger };
55+
public readonly asyncReqIdStorage: AsyncRequestIdStorage;
56+
57+
constructor() {
58+
this.asyncReqIdStorage = new AsyncLocalStorage();
59+
60+
this.loggerMap = {
61+
[LoggingScope.CORE]: this.createLogger(LoggingScope.CORE),
62+
[LoggingScope.BUILD]: this.createLogger(LoggingScope.BUILD),
63+
[LoggingScope.SERVE]: this.createLogger(LoggingScope.SERVE),
64+
};
65+
}
66+
67+
public getLogger({
68+
scopeName,
69+
options,
70+
}: {
71+
scopeName: LoggingScopeTypes;
72+
options?: LoggerOptions;
73+
}) {
74+
if (!(scopeName in LoggingScope))
75+
throw new Error(
76+
`The ${scopeName} does not belong to ${Object.keys(LoggingScope)}`
77+
);
78+
// if scope name exist in mapper and not update config
79+
if (scopeName in this.loggerMap) {
80+
if (!options) return this.loggerMap[scopeName];
81+
// if options existed, update settings.
82+
const logger = this.loggerMap[scopeName];
83+
this.updateSettings(logger, options);
84+
return logger;
85+
}
86+
// if scope name does not exist in map or exist but would like to update config
87+
const newLogger = this.createLogger(scopeName as LoggingScope, options);
88+
this.loggerMap[scopeName] = newLogger;
89+
return newLogger;
90+
}
91+
92+
private updateSettings(logger: Logger, options: LoggerOptions) {
93+
const prevSettings = logger.settings;
94+
logger.setSettings({
95+
minLevel: options.level || prevSettings.minLevel,
96+
displayRequestId:
97+
options.displayRequestId || prevSettings.displayRequestId,
98+
});
99+
}
100+
101+
private createLogger(name: LoggingScope, options?: LoggerOptions) {
102+
return new Logger({
103+
name,
104+
minLevel: options?.level || defaultMapConfig[name].level,
105+
// use function call for requestId, then when logger get requestId, it will get newest store again
106+
requestId: () => this.asyncReqIdStorage.getStore()?.requestId as string,
107+
displayRequestId:
108+
options?.displayRequestId || defaultMapConfig[name].displayRequestId,
109+
});
110+
}
111+
}
112+
113+
const factory = new LoggerFactory();
114+
export const getLogger = factory.getLogger.bind(factory);
115+
export const asyncReqIdStorage = factory.asyncReqIdStorage;

packages/core/src/lib/utils/module.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// The type for class T
2+
export interface ClassType<T> extends Function {
3+
new (...args: any[]): T;
4+
}
5+
6+
/**
7+
* dynamic import default module.
8+
* @param folderOrFile The folder / file path
9+
* @returns default module
10+
*/
11+
export const defaultImport = async <T = any>(folderOrFile: string) => {
12+
const module = await import(folderOrFile);
13+
return module.default as T;
14+
};

packages/core/src/lib/validators/data-type-validators/dateTypeValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as Joi from 'joi';
22
import { isUndefined } from 'lodash';
33
import * as dayjs from 'dayjs';
44
import customParseFormat = require('dayjs/plugin/customParseFormat');
5-
import IValidator from '../validator';
5+
import { IValidator } from '../validator';
66

77
// Support custom date format -> dayjs.format(...)
88
dayjs.extend(customParseFormat);
@@ -13,7 +13,7 @@ export interface DateInputArgs {
1313
format?: string;
1414
}
1515

16-
export default class DateTypeValidator implements IValidator {
16+
export class DateTypeValidator implements IValidator {
1717
public readonly name = 'date';
1818
// Validator for arguments schema in schema.yaml, should match DateInputArgs
1919
private argsValidator = Joi.object({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// export all other non-default objects of validators module
2+
export * from './dateTypeValidator';
3+
export * from './integerTypeValidator';
4+
export * from './stringTypeValidator';
5+
export * from './uuidTypeValidator';
6+
7+
// import default objects and export
8+
import { DateTypeValidator } from './dateTypeValidator';
9+
import { IntegerTypeValidator } from './integerTypeValidator';
10+
import { StringTypeValidator } from './stringTypeValidator';
11+
import { UUIDTypeValidator } from './uuidTypeValidator';
12+
13+
export default [
14+
DateTypeValidator,
15+
IntegerTypeValidator,
16+
StringTypeValidator,
17+
UUIDTypeValidator,
18+
];

packages/core/src/lib/validators/data-type-validators/integerTypeValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Joi from 'joi';
22
import { isUndefined } from 'lodash';
3-
import IValidator from '../validator';
3+
import { IValidator } from '../validator';
44

55
export interface IntInputArgs {
66
// The integer minimum value
@@ -13,7 +13,7 @@ export interface IntInputArgs {
1313
less?: number;
1414
}
1515

16-
export default class IntegerTypeValidator implements IValidator {
16+
export class IntegerTypeValidator implements IValidator {
1717
public readonly name = 'integer';
1818
// Validator for arguments schema in schema.yaml, should match IntInputArgs
1919
private argsValidator = Joi.object({

packages/core/src/lib/validators/data-type-validators/stringTypeValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Joi from 'joi';
22
import { isUndefined } from 'lodash';
3-
import IValidator from '../validator';
3+
import { IValidator } from '../validator';
44

55
export interface StringInputArgs {
66
// The string regex format pattern
@@ -13,7 +13,7 @@ export interface StringInputArgs {
1313
max?: number;
1414
}
1515

16-
export default class StringTypeValidator implements IValidator {
16+
export class StringTypeValidator implements IValidator {
1717
public readonly name = 'string';
1818
// Validator for arguments schema in schema.yaml, should match StringInputArgs
1919
private argsValidator = Joi.object({

packages/core/src/lib/validators/data-type-validators/uuidTypeValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Joi from 'joi';
22
import { GuidVersions } from 'joi';
33
import { isUndefined } from 'lodash';
4-
import IValidator from '../validator';
4+
import { IValidator } from '../validator';
55

66
type UUIDVersion = 'uuid_v1' | 'uuid_v4' | 'uuid_v5';
77

@@ -10,7 +10,7 @@ export interface UUIDInputArgs {
1010
version?: UUIDVersion;
1111
}
1212

13-
export default class UUIDTypeValidator implements IValidator {
13+
export class UUIDTypeValidator implements IValidator {
1414
public readonly name = 'uuid';
1515
// Validator for arguments schema in schema.yaml, should match UUIDInputArgs
1616
private argsValidator = Joi.object({
+2-20
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,3 @@
1-
// export all other non-default objects of validators module
2-
export * from './data-type-validators/dateTypeValidator';
3-
export * from './data-type-validators/integerTypeValidator';
4-
export * from './data-type-validators/stringTypeValidator';
5-
export * from './data-type-validators/uuidTypeValidator';
1+
export * from './data-type-validators';
62
export * from './validatorLoader';
7-
8-
// import default objects and export
9-
import IValidator from './validator';
10-
import DateTypeValidator from './data-type-validators/dateTypeValidator';
11-
import IntegerTypeValidator from './data-type-validators/integerTypeValidator';
12-
import StringTypeValidator from './data-type-validators/stringTypeValidator';
13-
import UUIDTypeValidator from './data-type-validators/uuidTypeValidator';
14-
15-
export {
16-
IValidator,
17-
DateTypeValidator,
18-
IntegerTypeValidator,
19-
StringTypeValidator,
20-
UUIDTypeValidator,
21-
};
3+
export * from './validator';

packages/core/src/lib/validators/validator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default interface IValidator<T = any> {
1+
export interface IValidator<T = any> {
22
// validator name
33
readonly name: string;
44
// validate Schema format
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
1-
import IValidator from './validator';
2-
import * as glob from 'glob';
1+
import { IValidator } from './validator';
2+
33
import * as path from 'path';
4+
import { defaultImport, ClassType } from '../utils';
5+
6+
// The extension module interface
7+
export interface ExtensionModule {
8+
validators?: ClassType<IValidator>[];
9+
}
410

511
export interface IValidatorLoader {
612
load(validatorName: string): Promise<IValidator>;
713
}
814

915
export class ValidatorLoader implements IValidatorLoader {
1016
// only found built-in validators in sub folders
11-
private builtInFolderPath: string = path.resolve(__dirname, '*', '*.ts');
12-
private userDefinedFolderPath?: string;
17+
private builtInFolder: string = path.join(__dirname, 'data-type-validators');
18+
private extensionPath?: string;
1319

1420
constructor(folderPath?: string) {
15-
this.userDefinedFolderPath = folderPath;
21+
this.extensionPath = folderPath;
1622
}
1723
public async load(validatorName: string) {
18-
let validatorFiles = [
19-
...(await this.getValidatorFilePaths(this.builtInFolderPath)),
20-
];
21-
if (this.userDefinedFolderPath) {
22-
// include sub-folder or non sub-folders
23-
const userDefinedValidatorFiles = await this.getValidatorFilePaths(
24-
path.resolve(this.userDefinedFolderPath, '**', '*.ts')
25-
);
26-
validatorFiles = validatorFiles.concat(userDefinedValidatorFiles);
24+
// read built-in validators in index.ts, the content is an array middleware class
25+
const builtInClass = await defaultImport<ClassType<IValidator>[]>(
26+
this.builtInFolder
27+
);
28+
29+
// if extension path setup, load extension middlewares classes
30+
let extensionClass: ClassType<IValidator>[] = [];
31+
if (this.extensionPath) {
32+
// import extension which user customized
33+
const module = await defaultImport<ExtensionModule>(this.extensionPath);
34+
extensionClass = module.validators || [];
2735
}
2836

29-
for (const file of validatorFiles) {
30-
// import validator files to module
31-
const validatorModule = await import(file);
32-
// get validator class by getting default.
33-
if (validatorModule && validatorModule.default) {
34-
const validatorClass = validatorModule.default;
35-
const validator = new validatorClass() as IValidator;
36-
if (validator.name === validatorName) return validator;
37-
}
37+
// create all middlewares by new it.
38+
for (const validatorClass of [...builtInClass, ...extensionClass]) {
39+
const validator = new validatorClass() as IValidator;
40+
if (validator.name === validatorName) return validator;
3841
}
42+
3943
// throw error if not found
4044
throw new Error(
4145
`The name "${validatorName}" of validator not defined in built-in validators and passed folder path, or the defined validator not export as default.`
4246
);
4347
}
44-
45-
private async getValidatorFilePaths(sourcePath: string): Promise<string[]> {
46-
return new Promise((resolve, reject) => {
47-
glob(sourcePath, { nodir: true }, (err, files) => {
48-
if (err) return reject(err);
49-
else return resolve(files);
50-
});
51-
});
52-
}
5348
}

packages/core/test/validators/data-type-validators/dataTypeValidator.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ describe('Test "date" type validator', () => {
1313
const args = JSON.parse(inputArgs);
1414
// Act
1515
const validator = new DateTypeValidator();
16-
const result = validator.validateSchema(args);
1716
// Assert
1817
expect(() => validator.validateSchema(args)).not.toThrow();
1918
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IPTypeValidator } from './ip-type-validator';
2+
3+
// Imitate extension for testing
4+
export default {
5+
validators: [IPTypeValidator],
6+
middlewares: [],
7+
};

0 commit comments

Comments
 (0)