From 3698ac7946c5ad0de4889933f7693aea69cc8635 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 1 Apr 2024 16:32:26 +0800 Subject: [PATCH] feat: impl ajv + typebox Validator closes https://github.com/eggjs/tegg/issues/200 --- core/ajv-decorator/README.md | 5 + core/ajv-decorator/index.ts | 4 + core/ajv-decorator/package.json | 53 +++++++ core/ajv-decorator/src/enum/TransformEnum.ts | 45 ++++++ .../src/error/AjvInvalidParamError.ts | 21 +++ core/ajv-decorator/src/type/Ajv.ts | 5 + core/ajv-decorator/test/TransformEnum.test.ts | 8 + core/ajv-decorator/tsconfig.json | 12 ++ core/ajv-decorator/tsconfig.pub.json | 12 ++ core/core-decorator/src/util/PrototypeUtil.ts | 1 - core/tegg/ajv.ts | 1 + core/tegg/package.json | 1 + plugin/ajv/README.md | 144 ++++++++++++++++++ plugin/ajv/lib/Ajv.ts | 54 +++++++ plugin/ajv/package.json | 71 +++++++++ plugin/ajv/test/ajv.test.ts | 53 +++++++ .../apps/ajv-app/config/config.default.js | 11 ++ .../fixtures/apps/ajv-app/config/module.json | 7 + .../fixtures/apps/ajv-app/config/plugin.js | 19 +++ .../ajv-app/modules/demo/FooController.ts | 33 ++++ .../apps/ajv-app/modules/demo/module.yml | 0 .../apps/ajv-app/modules/demo/package.json | 6 + .../test/fixtures/apps/ajv-app/package.json | 3 + plugin/ajv/tsconfig.json | 9 ++ plugin/ajv/tsconfig.pub.json | 10 ++ plugin/ajv/typings/index.d.ts | 4 + plugin/aop/app.ts | 2 +- plugin/aop/test/aop.test.ts | 2 +- .../test/DuplicateOptionalModule.test.ts | 2 +- plugin/config/test/ReadModule.test.ts | 2 +- plugin/controller/test/http/acl.test.ts | 2 +- plugin/controller/test/http/edgecase.test.ts | 2 +- plugin/controller/test/http/host.test.ts | 2 +- .../controller/test/http/middleware.test.ts | 2 +- plugin/controller/test/http/module.test.ts | 2 +- plugin/controller/test/http/params.test.ts | 2 +- plugin/controller/test/http/priority.test.ts | 2 +- plugin/controller/test/http/request.test.ts | 2 +- .../test/lib/ControllerMetaManager.test.ts | 2 +- .../test/lib/EggControllerLoader.test.ts | 2 +- .../test/lib/HTTPMethodRegister.test.ts | 2 +- plugin/dal/README.md | 28 +++- plugin/dal/app.ts | 4 +- plugin/dal/package.json | 2 +- plugin/dal/test/dal.test.ts | 2 +- plugin/eventbus/test/eventbus.test.ts | 2 +- plugin/orm/test/index.test.ts | 2 +- plugin/schedule/test/schedule.test.ts | 2 +- plugin/tegg/test/AccessLevelCheck.test.ts | 2 +- plugin/tegg/test/BackgroundTask.test.ts | 2 +- plugin/tegg/test/DynamicInject.test.ts | 2 +- plugin/tegg/test/EggCompatible.test.ts | 2 +- plugin/tegg/test/ModuleConfig.test.ts | 2 +- plugin/tegg/test/NoModuleJson.test.ts | 2 +- plugin/tegg/test/OptionalModule.test.ts | 2 +- plugin/tegg/test/OptionalPluginModule.test.ts | 2 +- plugin/tegg/test/SameProtoName.test.ts | 2 +- plugin/tegg/test/Subscription.test.ts | 2 +- plugin/tegg/test/close.test.ts | 2 +- standalone/standalone/package.json | 1 + standalone/standalone/src/Runner.ts | 4 +- .../test/fixtures/ajv-module-pass/foo.ts | 33 ++++ .../fixtures/ajv-module-pass/package.json | 6 + .../test/fixtures/ajv-module/foo.ts | 29 ++++ .../test/fixtures/ajv-module/package.json | 6 + standalone/standalone/test/index.test.ts | 43 +++++- 66 files changed, 764 insertions(+), 44 deletions(-) create mode 100644 core/ajv-decorator/README.md create mode 100644 core/ajv-decorator/index.ts create mode 100644 core/ajv-decorator/package.json create mode 100644 core/ajv-decorator/src/enum/TransformEnum.ts create mode 100644 core/ajv-decorator/src/error/AjvInvalidParamError.ts create mode 100644 core/ajv-decorator/src/type/Ajv.ts create mode 100644 core/ajv-decorator/test/TransformEnum.test.ts create mode 100644 core/ajv-decorator/tsconfig.json create mode 100644 core/ajv-decorator/tsconfig.pub.json create mode 100644 core/tegg/ajv.ts create mode 100644 plugin/ajv/README.md create mode 100644 plugin/ajv/lib/Ajv.ts create mode 100644 plugin/ajv/package.json create mode 100644 plugin/ajv/test/ajv.test.ts create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/config/config.default.js create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/config/module.json create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/config/plugin.js create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/FooController.ts create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/module.yml create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/package.json create mode 100644 plugin/ajv/test/fixtures/apps/ajv-app/package.json create mode 100644 plugin/ajv/tsconfig.json create mode 100644 plugin/ajv/tsconfig.pub.json create mode 100644 plugin/ajv/typings/index.d.ts create mode 100644 standalone/standalone/test/fixtures/ajv-module-pass/foo.ts create mode 100644 standalone/standalone/test/fixtures/ajv-module-pass/package.json create mode 100644 standalone/standalone/test/fixtures/ajv-module/foo.ts create mode 100644 standalone/standalone/test/fixtures/ajv-module/package.json diff --git a/core/ajv-decorator/README.md b/core/ajv-decorator/README.md new file mode 100644 index 00000000..a359c0b4 --- /dev/null +++ b/core/ajv-decorator/README.md @@ -0,0 +1,5 @@ +# `@eggjs/ajv-decorator` + +## Usage + +Please read [@eggjs/tegg-ajv-plugin](../../plugin/ajv-plugin) diff --git a/core/ajv-decorator/index.ts b/core/ajv-decorator/index.ts new file mode 100644 index 00000000..6cdb73e9 --- /dev/null +++ b/core/ajv-decorator/index.ts @@ -0,0 +1,4 @@ +export * from '@sinclair/typebox'; +export * from './src/enum/TransformEnum'; +export * from './src/error/AjvInvalidParamError'; +export * from './src/type/Ajv'; diff --git a/core/ajv-decorator/package.json b/core/ajv-decorator/package.json new file mode 100644 index 00000000..ae024ba6 --- /dev/null +++ b/core/ajv-decorator/package.json @@ -0,0 +1,53 @@ +{ + "name": "@eggjs/ajv-decorator", + "version": "3.35.1", + "description": "tegg ajv decorator", + "keywords": [ + "egg", + "typescript", + "decorator", + "tegg", + "ajv" + ], + "main": "dist/index.js", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "typings": "dist/index.d.ts", + "scripts": { + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--no-deprecation' mocha", + "clean": "tsc -b --clean", + "tsc": "npm run clean && tsc -p ./tsconfig.json", + "tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json", + "prepublishOnly": "npm run tsc:pub" + }, + "license": "MIT", + "homepage": "https://github.com/eggjs/tegg", + "bugs": { + "url": "https://github.com/eggjs/tegg/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/tegg.git", + "directory": "core/ajv-decorator" + }, + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "@sinclair/typebox": "^0.32.20", + "ajv": "^8.12.0" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.4", + "cross-env": "^7.0.3", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } +} diff --git a/core/ajv-decorator/src/enum/TransformEnum.ts b/core/ajv-decorator/src/enum/TransformEnum.ts new file mode 100644 index 00000000..aa1dab02 --- /dev/null +++ b/core/ajv-decorator/src/enum/TransformEnum.ts @@ -0,0 +1,45 @@ +/** + * This keyword allows a string to be modified during validation. + * This keyword applies only to strings. If the data is not a string, the transform keyword is ignored. + * @see https://github.com/ajv-validator/ajv-keywords?tab=readme-ov-file#transform + */ +export enum TransformEnum { + /** remove whitespace from start and end */ + trim = 'trim', + /** remove whitespace from start */ + trimStart = 'trimStart', + /** + * @alias trimStart + */ + trimLeft = 'trimLeft', + /** remove whitespace from end */ + trimEnd = 'trimEnd', + /** + * @alias trimEnd + */ + trimRight = 'trimRight', + /** convert to lower case */ + toLowerCase = 'toLowerCase', + /** convert to upper case */ + toUpperCase = 'toUpperCase', + /** + * change string case to be equal to one of `enum` values in the schema + * + * **NOTE**: requires that all allowed values are unique when case insensitive + * ```ts + * const schema = { + * type: "array", + * items: { + * type: "string", + * transform: ["trim", Transform.toEnumCase], + * enum: ["pH"], + * }, + * }; + * + * const data = ["ph", " Ph", "PH", "pH "]; + * ajv.validate(schema, data); + * console.log(data) // ['pH','pH','pH','pH']; + * ``` + */ + toEnumCase = 'toEnumCase', +} diff --git a/core/ajv-decorator/src/error/AjvInvalidParamError.ts b/core/ajv-decorator/src/error/AjvInvalidParamError.ts new file mode 100644 index 00000000..7a6b790e --- /dev/null +++ b/core/ajv-decorator/src/error/AjvInvalidParamError.ts @@ -0,0 +1,21 @@ +import { type ErrorObject } from 'ajv/dist/2019'; + +export interface AjvInvalidParamErrorOptions { + errorData: unknown; + currentSchema: string; + errors: ErrorObject[]; +} + +export class AjvInvalidParamError extends Error { + errorData: unknown; + currentSchema: string; + errors: ErrorObject[]; + + constructor(message: string, options: AjvInvalidParamErrorOptions) { + super(message); + this.name = this.constructor.name; + this.errorData = options.errorData; + this.currentSchema = options.currentSchema; + this.errors = options.errors; + } +} diff --git a/core/ajv-decorator/src/type/Ajv.ts b/core/ajv-decorator/src/type/Ajv.ts new file mode 100644 index 00000000..05f21683 --- /dev/null +++ b/core/ajv-decorator/src/type/Ajv.ts @@ -0,0 +1,5 @@ +import type { Schema } from 'ajv/dist/2019'; + +export interface Ajv { + validate(schema: Schema, data: unknown): void; +} diff --git a/core/ajv-decorator/test/TransformEnum.test.ts b/core/ajv-decorator/test/TransformEnum.test.ts new file mode 100644 index 00000000..5f86d113 --- /dev/null +++ b/core/ajv-decorator/test/TransformEnum.test.ts @@ -0,0 +1,8 @@ +import { strict as assert } from 'node:assert'; +import { TransformEnum } from '..'; + +describe('core/ajv-decorator/test/TransformEnum.test.ts', () => { + it('should get TransformEnum', () => { + assert.equal(TransformEnum.trim, 'trim'); + }); +}); diff --git a/core/ajv-decorator/tsconfig.json b/core/ajv-decorator/tsconfig.json new file mode 100644 index 00000000..64b22405 --- /dev/null +++ b/core/ajv-decorator/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/ajv-decorator/tsconfig.pub.json b/core/ajv-decorator/tsconfig.pub.json new file mode 100644 index 00000000..64b22405 --- /dev/null +++ b/core/ajv-decorator/tsconfig.pub.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/core-decorator/src/util/PrototypeUtil.ts b/core/core-decorator/src/util/PrototypeUtil.ts index c6d02385..79519e36 100644 --- a/core/core-decorator/src/util/PrototypeUtil.ts +++ b/core/core-decorator/src/util/PrototypeUtil.ts @@ -124,7 +124,6 @@ export class PrototypeUtil { * get class property * @param {EggProtoImplClass} clazz - * @param {MultiInstancePrototypeGetObjectsContext} ctx - - * @return {EggPrototypeInfo} - */ static getMultiInstanceProperty(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): EggMultiInstancePrototypeInfo | undefined { const metadata = MetadataUtil.getMetaData(this.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz); diff --git a/core/tegg/ajv.ts b/core/tegg/ajv.ts new file mode 100644 index 00000000..227cd9f7 --- /dev/null +++ b/core/tegg/ajv.ts @@ -0,0 +1 @@ +export * from '@eggjs/ajv-decorator'; diff --git a/core/tegg/package.json b/core/tegg/package.json index 03945a74..ae1b3bee 100644 --- a/core/tegg/package.json +++ b/core/tegg/package.json @@ -35,6 +35,7 @@ "node": ">=14.0.0" }, "dependencies": { + "@eggjs/ajv-decorator": "^3.35.1", "@eggjs/aop-decorator": "^3.35.1", "@eggjs/controller-decorator": "^3.35.1", "@eggjs/core-decorator": "^3.35.1", diff --git a/plugin/ajv/README.md b/plugin/ajv/README.md new file mode 100644 index 00000000..993bb019 --- /dev/null +++ b/plugin/ajv/README.md @@ -0,0 +1,144 @@ +# @eggjs/tegg-ajv-plugin + +参考 [egg-typebox-validate](https://github.com/xiekw2010/egg-typebox-validate) 的最佳实践,结合 ajv + typebox,只需要定义一次参数类型和规则,就能同时拥有参数校验和类型定义(完整的 ts 类型提示)。 + +## egg 模式 + +### Install + +```shell +# tegg 注解 +npm i --save @eggjs/tegg +# tegg 插件 +npm i --save @eggjs/tegg-plugin +# tegg ajv 插件 +npm i --save @eggjs/tegg-ajv-plugin +``` + +### Prepare + +```json +// tsconfig.json +{ + "extends": "@eggjs/tsconfig" +} +``` + +### Config + +```js +// config/plugin.js +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggAjv = { + package: '@eggjs/tegg-ajv-plugin', + enable: true, +}; +``` + +## standalone 模式 + +### Install + +```shell +# tegg 注解 +npm i --save @eggjs/tegg +# tegg ajv 插件 +npm i --save @eggjs/tegg-ajv-plugin +``` + +### Prepare + +```json +// tsconfig.json +{ + "extends": "@eggjs/tsconfig" +} +``` + +## Usage + +1、定义入参校验 Schema + +使用 typebox 定义,会内置到 tegg 导出 + +```ts +import { Type, TransformEnum } from '@eggjs/tegg/ajv'; + +const SyncPackageTaskSchema = Type.Object({ + fullname: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 100, + }), + tips: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 1024, + }), + skipDependencies: Type.Boolean(), + syncDownloadData: Type.Boolean(), + // force sync immediately, only allow by admin + force: Type.Boolean(), + // sync history version + forceSyncHistory: Type.Boolean(), + // source registry + registryName: Type.Optional(Type.String()), +}); +``` + +2、从校验 Schema 生成静态的入参类型 + +```ts +import { Static } from '@eggjs/tegg/ajv'; + +type SyncPackageTaskType = Static; +``` + +3、在 Controller 中使用入参类型和校验 Schema + +注入全局单例 ajv,调用 `ajv.validate(XxxSchema, params)` 进行参数校验,参数校验失败会直接抛出 `AjvInvalidParamError` 异常, +tegg 会自动返回相应的错误响应给客户端。 + +```ts +import { Inject, HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody } from '@eggjs/tegg'; +import { Ajv, Type, Static, TransformEnum } from '@eggjs/tegg/ajv'; + +const SyncPackageTaskSchema = Type.Object({ + fullname: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 100, + }), + tips: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 1024, + }), + skipDependencies: Type.Boolean(), + syncDownloadData: Type.Boolean(), + // force sync immediately, only allow by admin + force: Type.Boolean(), + // sync history version + forceSyncHistory: Type.Boolean(), + // source registry + registryName: Type.Optional(Type.String()), +}); + +type SyncPackageTaskType = Static; + +@HTTPController() +export class HelloController { + private readonly ajv: Ajv; + + @HTTPMethod({ + method: HTTPMethodEnum.POST, + path: '/sync', + }) + async sync(@HTTPBody() task: SyncPackageTaskType) { + this.ajv.validate(SyncPackageTaskSchema, task); + return { + task, + }; + } +} +``` diff --git a/plugin/ajv/lib/Ajv.ts b/plugin/ajv/lib/Ajv.ts new file mode 100644 index 00000000..beca399d --- /dev/null +++ b/plugin/ajv/lib/Ajv.ts @@ -0,0 +1,54 @@ +import Ajv2019, { type Schema } from 'ajv/dist/2019'; +import addFormats from 'ajv-formats'; +import keyWords from 'ajv-keywords'; +import { type Ajv as IAjv, AjvInvalidParamError } from '@eggjs/tegg/ajv'; +import { SingletonProto, AccessLevel, LifecycleInit } from '@eggjs/tegg'; + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class Ajv implements IAjv { + static InvalidParamErrorClass = AjvInvalidParamError; + + #ajvInstance: Ajv2019; + + @LifecycleInit() + protected _init() { + this.#ajvInstance = new Ajv2019(); + keyWords(this.#ajvInstance, 'transform'); + addFormats(this.#ajvInstance, [ + 'date-time', + 'time', + 'date', + 'email', + 'hostname', + 'ipv4', + 'ipv6', + 'uri', + 'uri-reference', + 'uuid', + 'uri-template', + 'json-pointer', + 'relative-json-pointer', + 'regex', + ]) + .addKeyword('kind') + .addKeyword('modifier'); + } + + /** + * Validate data with typebox Schema. + * + * If validate fail, with throw `Ajv.InvalidParamErrorClass` + */ + validate(schema: Schema, data: unknown): void { + const result = this.#ajvInstance.validate(schema, data); + if (!result) { + throw new Ajv.InvalidParamErrorClass('Validation Failed', { + errorData: data, + currentSchema: JSON.stringify(schema), + errors: this.#ajvInstance.errors!, + }); + } + } +} diff --git a/plugin/ajv/package.json b/plugin/ajv/package.json new file mode 100644 index 00000000..5ff7f4ef --- /dev/null +++ b/plugin/ajv/package.json @@ -0,0 +1,71 @@ +{ + "name": "@eggjs/tegg-ajv-plugin", + "eggPlugin": { + "name": "teggAjv", + "strict": false, + "dependencies": [ + "tegg" + ] + }, + "eggModule": { + "name": "teggAjv" + }, + "version": "3.35.1", + "description": "ajv plugin for egg and tegg", + "keywords": [ + "egg", + "plugin", + "typescript", + "module", + "tegg", + "ajv" + ], + "files": [ + "lib/**/*.js", + "lib/**/*.d.ts", + "typings/*.d.ts" + ], + "types": "typings/index.d.ts", + "scripts": { + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--no-deprecation' mocha", + "clean": "tsc -b --clean", + "tsc": "npm run clean && tsc -p ./tsconfig.json", + "tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json", + "prepublishOnly": "npm run tsc:pub" + }, + "homepage": "https://github.com/eggjs/tegg", + "bugs": { + "url": "https://github.com/eggjs/tegg/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/tegg.git", + "directory": "plugin/ajv" + }, + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "@eggjs/tegg": "^3.35.1", + "@sinclair/typebox": "^0.32.20", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "devDependencies": { + "@eggjs/tegg-config": "^3.35.1", + "@eggjs/tegg-plugin": "^3.35.1", + "@eggjs/tegg-controller-plugin": "^3.35.1", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.4", + "cross-env": "^7.0.3", + "egg": "^3.9.1", + "egg-mock": "^5.5.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugin/ajv/test/ajv.test.ts b/plugin/ajv/test/ajv.test.ts new file mode 100644 index 00000000..38352afb --- /dev/null +++ b/plugin/ajv/test/ajv.test.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert'; +import path from 'node:path'; +import mm, { MockApplication } from 'egg-mock'; + +describe('plugin/ajv/test/ajv.test.ts', () => { + let app: MockApplication; + + afterEach(async () => { + return mm.restore(); + }); + + before(async () => { + mm(process.env, 'EGG_TYPESCRIPT', true); + mm(process, 'cwd', () => { + return path.join(__dirname, '../'); + }); + app = mm.app({ + baseDir: path.join(__dirname, './fixtures/apps/ajv-app'), + framework: require.resolve('egg'), + }); + await app.ready(); + }); + + after(() => { + return app.close(); + }); + + it('should throw AjvInvalidParamError', async () => { + app.mockCsrf(); + const res = await app.httpRequest() + .post('/foo') + .send({}); + assert.equal(res.status, 500); + assert.match(res.text, /AjvInvalidParamError: Validation Failed/); + }); + + it('should pass', async () => { + app.mockCsrf(); + const res = await app.httpRequest() + .post('/foo') + .send({ + fullname: 'fullname ', + skipDependencies: false, + }); + assert.equal(res.status, 200); + assert.deepEqual(res.body, { + body: { + fullname: 'fullname', + skipDependencies: false, + }, + }); + }); +}); diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/config/config.default.js b/plugin/ajv/test/fixtures/apps/ajv-app/config/config.default.js new file mode 100644 index 00000000..1e9c3ed7 --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/config/config.default.js @@ -0,0 +1,11 @@ +module.exports = () => { + const config = { + keys: 'test key', + security: { + csrf: { + ignoreJSON: false, + }, + }, + }; + return config; +}; diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/config/module.json b/plugin/ajv/test/fixtures/apps/ajv-app/config/module.json new file mode 100644 index 00000000..9774bd7c --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/config/module.json @@ -0,0 +1,7 @@ +[ + { + "path": "../modules/demo" + }, { + "package": "../../../../" + } +] diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/config/plugin.js b/plugin/ajv/test/fixtures/apps/ajv-app/config/plugin.js new file mode 100644 index 00000000..3b812de0 --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/config/plugin.js @@ -0,0 +1,19 @@ +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + enable: true, +}; + +exports.teggController = { + package: '@eggjs/tegg-controller-plugin', + enable: true, +}; diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/FooController.ts b/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/FooController.ts new file mode 100644 index 00000000..0f5cee28 --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/FooController.ts @@ -0,0 +1,33 @@ +import { + HTTPController, HTTPMethod, HTTPMethodEnum, Inject, + HTTPBody, +} from '@eggjs/tegg'; +import { Ajv, Static, Type, TransformEnum } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 100, + }), + skipDependencies: Type.Boolean(), + registryName: Type.Optional(Type.String()), +}); + +type RequestBody = Static; + +@HTTPController() +export class FooController { + @Inject() + private readonly ajv: Ajv; + + @HTTPMethod({ + method: HTTPMethodEnum.POST, + path: '/foo', + }) + async echo(@HTTPBody() body: RequestBody) { + this.ajv.validate(RequestBodySchema, body); + return { + body, + }; + } +} diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/module.yml b/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/module.yml new file mode 100644 index 00000000..e69de29b diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/package.json b/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/package.json new file mode 100644 index 00000000..2ee6d081 --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/modules/demo/package.json @@ -0,0 +1,6 @@ +{ + "name": "demo", + "eggModule": { + "name": "demo" + } +} diff --git a/plugin/ajv/test/fixtures/apps/ajv-app/package.json b/plugin/ajv/test/fixtures/apps/ajv-app/package.json new file mode 100644 index 00000000..9025389b --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "ajv-app" +} diff --git a/plugin/ajv/tsconfig.json b/plugin/ajv/tsconfig.json new file mode 100644 index 00000000..74935f4b --- /dev/null +++ b/plugin/ajv/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/plugin/ajv/tsconfig.pub.json b/plugin/ajv/tsconfig.pub.json new file mode 100644 index 00000000..8205bd19 --- /dev/null +++ b/plugin/ajv/tsconfig.pub.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./" + }, + "exclude": [ + "node_modules", + "test" + ] +} diff --git a/plugin/ajv/typings/index.d.ts b/plugin/ajv/typings/index.d.ts new file mode 100644 index 00000000..b0e53ed8 --- /dev/null +++ b/plugin/ajv/typings/index.d.ts @@ -0,0 +1,4 @@ +import 'egg'; +import '@eggjs/tegg-plugin'; +import '@eggjs/tegg-config'; +import '@eggjs/tegg-controller-plugin'; diff --git a/plugin/aop/app.ts b/plugin/aop/app.ts index d342300d..c2623769 100644 --- a/plugin/aop/app.ts +++ b/plugin/aop/app.ts @@ -12,7 +12,7 @@ export default class AopAppHook { private readonly eggObjectAopHook: EggObjectAopHook; private aopContextHook: AopContextHook; - constructor(app) { + constructor(app: Application) { this.app = app; this.crosscutAdviceFactory = new CrosscutAdviceFactory(); this.loadUnitAopHook = new LoadUnitAopHook(this.crosscutAdviceFactory); diff --git a/plugin/aop/test/aop.test.ts b/plugin/aop/test/aop.test.ts index dca10c46..f5623858 100644 --- a/plugin/aop/test/aop.test.ts +++ b/plugin/aop/test/aop.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/aop.test.ts', () => { +describe('plugin/aop/test/aop.test.ts', () => { let app; after(async () => { diff --git a/plugin/config/test/DuplicateOptionalModule.test.ts b/plugin/config/test/DuplicateOptionalModule.test.ts index 34a19394..abe59a23 100644 --- a/plugin/config/test/DuplicateOptionalModule.test.ts +++ b/plugin/config/test/DuplicateOptionalModule.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import assert from 'assert'; import path from 'path'; -describe('test/DuplicateOptionalModule.test.ts', () => { +describe('plugin/config/test/DuplicateOptionalModule.test.ts', () => { let app; const fixturesPath = path.join(__dirname, './fixtures/apps/duplicate-optional-module'); diff --git a/plugin/config/test/ReadModule.test.ts b/plugin/config/test/ReadModule.test.ts index da2b2a3b..bae80d79 100644 --- a/plugin/config/test/ReadModule.test.ts +++ b/plugin/config/test/ReadModule.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import assert from 'assert'; import path from 'path'; -describe('test/ReadModule.test.ts', () => { +describe('plugin/config/test/ReadModule.test.ts', () => { let app; const fixturesPath = path.join(__dirname, './fixtures/apps/app-with-modules'); diff --git a/plugin/controller/test/http/acl.test.ts b/plugin/controller/test/http/acl.test.ts index 95c07bb1..8d07fdc5 100644 --- a/plugin/controller/test/http/acl.test.ts +++ b/plugin/controller/test/http/acl.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/edgecase.test.ts', () => { +describe('plugin/controller/test/http/acl.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/edgecase.test.ts b/plugin/controller/test/http/edgecase.test.ts index 743c4175..021f0f9d 100644 --- a/plugin/controller/test/http/edgecase.test.ts +++ b/plugin/controller/test/http/edgecase.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/edgecase.test.ts', () => { +describe('plugin/controller/test/http/edgecase.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/host.test.ts b/plugin/controller/test/http/host.test.ts index 8f574d67..3855879e 100644 --- a/plugin/controller/test/http/host.test.ts +++ b/plugin/controller/test/http/host.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/host.test.ts', () => { +describe('plugin/controller/test/http/host.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/middleware.test.ts b/plugin/controller/test/http/middleware.test.ts index 42b482c5..3c0d87d0 100644 --- a/plugin/controller/test/http/middleware.test.ts +++ b/plugin/controller/test/http/middleware.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/middleware.test.ts', () => { +describe('plugin/controller/test/http/middleware.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/module.test.ts b/plugin/controller/test/http/module.test.ts index 61eab8ac..db217638 100644 --- a/plugin/controller/test/http/module.test.ts +++ b/plugin/controller/test/http/module.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/module.test.ts', () => { +describe('plugin/controller/test/http/module.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/params.test.ts b/plugin/controller/test/http/params.test.ts index c80d7273..42553543 100644 --- a/plugin/controller/test/http/params.test.ts +++ b/plugin/controller/test/http/params.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/params.test.ts', () => { +describe('plugin/controller/test/http/params.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/priority.test.ts b/plugin/controller/test/http/priority.test.ts index edd8180e..2ff67fe2 100644 --- a/plugin/controller/test/http/priority.test.ts +++ b/plugin/controller/test/http/priority.test.ts @@ -1,7 +1,7 @@ import mm from 'egg-mock'; import path from 'path'; -describe('test/priority.test.ts', () => { +describe('plugin/controller/test/http/priority.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/http/request.test.ts b/plugin/controller/test/http/request.test.ts index d65859bb..6077a581 100644 --- a/plugin/controller/test/http/request.test.ts +++ b/plugin/controller/test/http/request.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/params.test.ts', () => { +describe('plugin/controller/test/http/request.test.ts', () => { let app; beforeEach(() => { diff --git a/plugin/controller/test/lib/ControllerMetaManager.test.ts b/plugin/controller/test/lib/ControllerMetaManager.test.ts index 1ba545d5..3dd7fa2f 100644 --- a/plugin/controller/test/lib/ControllerMetaManager.test.ts +++ b/plugin/controller/test/lib/ControllerMetaManager.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/ControllerMetaManager.test.ts', () => { +describe('plugin/controller/test/lib/ControllerMetaManager.test.ts', () => { beforeEach(() => { mm(process.env, 'EGG_TYPESCRIPT', true); }); diff --git a/plugin/controller/test/lib/EggControllerLoader.test.ts b/plugin/controller/test/lib/EggControllerLoader.test.ts index 810a1f21..23af6a09 100644 --- a/plugin/controller/test/lib/EggControllerLoader.test.ts +++ b/plugin/controller/test/lib/EggControllerLoader.test.ts @@ -4,7 +4,7 @@ import path from 'path'; import { EggControllerLoader } from '../../lib/EggControllerLoader'; import { ControllerMetadataUtil } from '@eggjs/tegg'; -describe('test/lib/EggModuleLoader.test.ts', () => { +describe('plugin/controller/test/lib/EggModuleLoader.test.ts', () => { beforeEach(() => { mm(process.env, 'EGG_TYPESCRIPT', true); }); diff --git a/plugin/controller/test/lib/HTTPMethodRegister.test.ts b/plugin/controller/test/lib/HTTPMethodRegister.test.ts index 96c6ba79..bde0307e 100644 --- a/plugin/controller/test/lib/HTTPMethodRegister.test.ts +++ b/plugin/controller/test/lib/HTTPMethodRegister.test.ts @@ -15,7 +15,7 @@ import { EggControllerPrototypeHook } from '../../lib/EggControllerPrototypeHook import { HTTPMethodRegister } from '../../lib/impl/http/HTTPMethodRegister'; import { EggContainerFactory } from '@eggjs/tegg-runtime'; -describe('test/lib/HTTPControllerRegister.test.ts', () => { +describe('plugin/controller/test/lib/HTTPControllerRegister.test.ts', () => { describe('method/path is registered', () => { const router = new KoaRouter(); diff --git a/plugin/dal/README.md b/plugin/dal/README.md index 54089080..7b2ac030 100644 --- a/plugin/dal/README.md +++ b/plugin/dal/README.md @@ -5,6 +5,7 @@ ## egg 模式 ### Install + ```shell # tegg 注解 npm i --save @eggjs/tegg @@ -15,6 +16,7 @@ npm i --save @eggjs/tegg-dal-plugin ``` ### Prepare + ```json // tsconfig.json { @@ -40,6 +42,7 @@ exports.teggDal = { ## standalone 模式 ### Install + ```shell # tegg 注解 npm i --save @eggjs/tegg @@ -48,6 +51,7 @@ npm i --save @eggjs/tegg-dal-plugin ``` ### Prepare + ```json // tsconfig.json { @@ -58,6 +62,7 @@ npm i --save @eggjs/tegg-dal-plugin ## Usage ### module.yml + 通过 module.yml 来配置 module 中的 mysql 数据源。 ```yaml @@ -73,6 +78,7 @@ dataSource: ``` ### Table + `TableModel` 定义一个表结构,包括表配置、列、索引。 ```ts @@ -108,6 +114,7 @@ export class Foo { 详细参数定义如下,具体参数值可以参考 https://dev.mysql.com/doc/refman/8.0/en/create-table.html 建表参数,使用方式为 `@Table(parmas?: TableParams)` + ```ts export interface TableParams { // 数据库表名 @@ -134,6 +141,7 @@ export interface TableParams { ``` 建索引参数,使用方式为 `@Index(parmas?: IndexParams)` + ```ts export interface IndexParams { // 索引的列 @@ -153,6 +161,7 @@ export interface IndexParams { ``` 建列参数,使用方式为 `@Column(type: ColumnTypeParams, parmas?: ColumnParams)` + ```ts export interface ColumnParams { // 列名,默认转换规则 userName 至 user_name @@ -174,6 +183,7 @@ export interface ColumnParams { ``` 支持的类型 + ```ts export enum ColumnType { // Numeric @@ -225,6 +235,7 @@ export enum ColumnType { 支持的类型参数,详细可参考 https://dev.mysql.com/doc/refman/8.0/en/data-types.html 如果 mysql 类型和 ts 类型对应关系不确定可直接使用 `ColumnTsType` 类型,如 + ```ts import { Table, Index, Column, ColumnType, IndexType, ColumnTsType } from '@eggjs/tegg/dal'; @@ -506,7 +517,8 @@ export interface GeometryCollectionParams { ``` ### 目录结构 -运行 `egg-bin dal gen` 即可生成 `dal` 相关目录,包括 dao、extension、structure + +运行 `egg-bin dal gen` 即可生成 `dal` 相关目录,包括 dao、extension、structure ```plain dal @@ -519,7 +531,7 @@ dal └── structure ├── Foo.json └── Foo.sql -``` +``` - dao: 表访问类,生成的 BaseDAO 请勿修改,其中包含了根据表结构生成的基础访问方法,如 insert/update/delete 以及根据索引信息生成的 find 方法 - extension: 扩展文件,如果需要自定义 sql,需要在 extension 文件中定义 @@ -543,8 +555,9 @@ export class FooRepository { } ``` -#### 自定义 Sql -1. 在 extension 中定义自定义 Sql +#### 自定义 SQL + +1. 在 extension 中定义自定义 SQL ```ts // dal/extension/FooExtension.ts @@ -559,6 +572,7 @@ export default { ``` 2. 在 dao 中定义自定义方法 + ```ts import { SingletonProto, AccessLevel } from '@eggjs/tegg'; import { BaseFooDAO } from './base/BaseFooDAO'; @@ -577,6 +591,7 @@ export default class FooDAO extends BaseFooDAO { ``` 支持的自定义 filter + ``` - toPoint - toLine @@ -589,6 +604,7 @@ export default class FooDAO extends BaseFooDAO { ``` 支持自定义 block 来简化 sql, 如内置的 allColumns + ```ts export default { findByName: { @@ -599,7 +615,8 @@ export default { ``` ### DataSource -DataSource 仅能在 DAO 中使用,可以将 mysql 返回的数据反序列化为类。支持的方法有 + +DataSource 仅能在 DAO 中使用,可以将 MySQL 返回的数据反序列化为类。支持的方法有 ```ts export interface DataSource { @@ -634,6 +651,7 @@ dataSource: ``` 可以通过以下 SQL 来查看数据库时区 + ```sql SELECT @@GLOBAL.time_zone; ``` diff --git a/plugin/dal/app.ts b/plugin/dal/app.ts index adb6b42b..c7b0e07b 100644 --- a/plugin/dal/app.ts +++ b/plugin/dal/app.ts @@ -25,10 +25,10 @@ export default class ControllerAppBootHook { async beforeClose() { if (this.dalTableEggPrototypeHook) { - await this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.dalTableEggPrototypeHook); + this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.dalTableEggPrototypeHook); } if (this.dalModuleLoadUnitHook) { - await this.app.loadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook); + this.app.loadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook); } MysqlDataSourceManager.instance.clear(); SqlMapManager.instance.clear(); diff --git a/plugin/dal/package.json b/plugin/dal/package.json index 5f02f1bb..c4fe8f19 100644 --- a/plugin/dal/package.json +++ b/plugin/dal/package.json @@ -44,7 +44,7 @@ "repository": { "type": "git", "url": "git@github.com:eggjs/tegg.git", - "directory": "plugin/controller" + "directory": "plugin/dal" }, "engines": { "node": ">=14.0.0" diff --git a/plugin/dal/test/dal.test.ts b/plugin/dal/test/dal.test.ts index e838e053..401e1594 100644 --- a/plugin/dal/test/dal.test.ts +++ b/plugin/dal/test/dal.test.ts @@ -4,7 +4,7 @@ import mm, { MockApplication } from 'egg-mock'; import FooDAO from './fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO'; import { Foo } from './fixtures/apps/dal-app/modules/dal/Foo'; -describe('test/dal.test.ts', () => { +describe('plugin/dal/test/dal.test.ts', () => { let app: MockApplication; afterEach(async () => { diff --git a/plugin/eventbus/test/eventbus.test.ts b/plugin/eventbus/test/eventbus.test.ts index 07228bc2..3a6ee7e5 100644 --- a/plugin/eventbus/test/eventbus.test.ts +++ b/plugin/eventbus/test/eventbus.test.ts @@ -6,7 +6,7 @@ import { HelloService } from './fixtures/apps/event-app/app/event-module/HelloSe import { HelloLogger } from './fixtures/apps/event-app/app/event-module/HelloLogger'; import { MultiEventHandler } from './fixtures/apps/event-app/app/event-module/MultiEventHandler'; -describe('test/eventbus.test.ts', () => { +describe('plugin/eventbus/test/eventbus.test.ts', () => { let app: MockApplication; afterEach(async () => { diff --git a/plugin/orm/test/index.test.ts b/plugin/orm/test/index.test.ts index c029ec28..2454d776 100644 --- a/plugin/orm/test/index.test.ts +++ b/plugin/orm/test/index.test.ts @@ -11,7 +11,7 @@ import { App } from './fixtures/apps/orm-app/modules/orm-module/model/App'; import { CtxService } from './fixtures/apps/orm-app/modules/orm-module/CtxService'; import { EggContext } from '@eggjs/tegg'; -describe('test/orm.test.ts', () => { +describe('plugin/orm/test/orm.test.ts', () => { // TODO win32 ci not support mysql if (os.platform() === 'win32') { return; diff --git a/plugin/schedule/test/schedule.test.ts b/plugin/schedule/test/schedule.test.ts index 4cbe7577..1ef1d180 100644 --- a/plugin/schedule/test/schedule.test.ts +++ b/plugin/schedule/test/schedule.test.ts @@ -4,7 +4,7 @@ import assert from 'assert'; import mm from 'egg-mock'; import { TimerUtil } from '@eggjs/tegg-common-util'; -describe('test/schedule.test.ts', () => { +describe('plugin/schedule/test/schedule.test.ts', () => { let app; afterEach(async () => { diff --git a/plugin/tegg/test/AccessLevelCheck.test.ts b/plugin/tegg/test/AccessLevelCheck.test.ts index 4a5790ae..36fb0830 100644 --- a/plugin/tegg/test/AccessLevelCheck.test.ts +++ b/plugin/tegg/test/AccessLevelCheck.test.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import path from 'path'; import MainService from './fixtures/apps/access-level-check/modules/module-main/MainService'; -describe('test/AccessLevelCheck.test.ts', () => { +describe('plugin/tegg/test/AccessLevelCheck.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/access-level-check'); diff --git a/plugin/tegg/test/BackgroundTask.test.ts b/plugin/tegg/test/BackgroundTask.test.ts index 6ac6d48c..c2cc547a 100644 --- a/plugin/tegg/test/BackgroundTask.test.ts +++ b/plugin/tegg/test/BackgroundTask.test.ts @@ -8,7 +8,7 @@ import { BackgroundTaskHelper } from '@eggjs/tegg'; import { EggContext, EggContextLifecycleUtil } from '@eggjs/tegg-runtime'; import { CountService } from './fixtures/apps/background-app/modules/multi-module-background/CountService'; -describe('test/BackgroundTask.test.ts', () => { +describe('plugin/tegg/test/BackgroundTask.test.ts', () => { const appDir = path.join(__dirname, 'fixtures/apps/background-app'); let app; diff --git a/plugin/tegg/test/DynamicInject.test.ts b/plugin/tegg/test/DynamicInject.test.ts index 375610a8..b4585b5f 100644 --- a/plugin/tegg/test/DynamicInject.test.ts +++ b/plugin/tegg/test/DynamicInject.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import path from 'path'; import assert from 'assert'; -describe('test/DynamicInject.test.ts', () => { +describe('plugin/tegg/test/DynamicInject.test.ts', () => { let app; after(async () => { diff --git a/plugin/tegg/test/EggCompatible.test.ts b/plugin/tegg/test/EggCompatible.test.ts index f3bbf359..25d8db8b 100644 --- a/plugin/tegg/test/EggCompatible.test.ts +++ b/plugin/tegg/test/EggCompatible.test.ts @@ -4,7 +4,7 @@ import path from 'path'; import EggTypeService from './fixtures/apps/egg-app/modules/multi-module-service/EggTypeService'; import TraceService from './fixtures/apps/egg-app/modules/multi-module-service/TraceService'; -describe('test/EggCompatible.test.ts', () => { +describe('plugin/tegg/test/EggCompatible.test.ts', () => { let app; after(async () => { diff --git a/plugin/tegg/test/ModuleConfig.test.ts b/plugin/tegg/test/ModuleConfig.test.ts index caabba68..11ad55eb 100644 --- a/plugin/tegg/test/ModuleConfig.test.ts +++ b/plugin/tegg/test/ModuleConfig.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import assert from 'assert'; import path from 'path'; -describe('test/ModuleConfig.test.ts', () => { +describe('plugin/tegg/test/ModuleConfig.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/inject-module-config'); diff --git a/plugin/tegg/test/NoModuleJson.test.ts b/plugin/tegg/test/NoModuleJson.test.ts index b7f3b221..ff0138d3 100644 --- a/plugin/tegg/test/NoModuleJson.test.ts +++ b/plugin/tegg/test/NoModuleJson.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import assert from 'assert'; import path from 'path'; -describe('test/NoModuleJson.test.ts', () => { +describe('plugin/tegg/test/NoModuleJson.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/app-with-no-module-json'); diff --git a/plugin/tegg/test/OptionalModule.test.ts b/plugin/tegg/test/OptionalModule.test.ts index f9d4eea5..f1d1b6b0 100644 --- a/plugin/tegg/test/OptionalModule.test.ts +++ b/plugin/tegg/test/OptionalModule.test.ts @@ -5,7 +5,7 @@ import { RootProto } from './fixtures/apps/optional-module/app/modules/root/Root import { UsedProto } from './fixtures/apps/optional-module/node_modules/used/Used'; import { UnusedProto } from './fixtures/apps/optional-module/node_modules/unused/Unused'; -describe('test/OptionalModule.test.ts', () => { +describe('plugin/tegg/test/OptionalModule.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/optional-module'); diff --git a/plugin/tegg/test/OptionalPluginModule.test.ts b/plugin/tegg/test/OptionalPluginModule.test.ts index 2fb0a3c3..bcfa5eaa 100644 --- a/plugin/tegg/test/OptionalPluginModule.test.ts +++ b/plugin/tegg/test/OptionalPluginModule.test.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import path from 'path'; import { UsedProto } from './fixtures/apps/plugin-module/node_modules/foo-plugin/Used'; -describe('test/OptionalPluginModule.test.ts', () => { +describe('plugin/tegg/test/OptionalPluginModule.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/plugin-module'); diff --git a/plugin/tegg/test/SameProtoName.test.ts b/plugin/tegg/test/SameProtoName.test.ts index 6507a035..c89c565a 100644 --- a/plugin/tegg/test/SameProtoName.test.ts +++ b/plugin/tegg/test/SameProtoName.test.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import path from 'path'; import { BarService } from './fixtures/apps/same-name-protos/app/modules/module-a/BarService'; -describe('test/SameProtoName.test.ts', () => { +describe('plugin/tegg/test/SameProtoName.test.ts', () => { let app; const fixtureDir = path.join(__dirname, 'fixtures/apps/same-name-protos'); diff --git a/plugin/tegg/test/Subscription.test.ts b/plugin/tegg/test/Subscription.test.ts index d565f7f3..4ddc7902 100644 --- a/plugin/tegg/test/Subscription.test.ts +++ b/plugin/tegg/test/Subscription.test.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import path from 'path'; import AppService from './fixtures/apps/schedule-app/modules/multi-module-service/AppService'; -describe('test/Subscription.test.ts', () => { +describe('plugin/tegg/test/Subscription.test.ts', () => { let app; after(async () => { diff --git a/plugin/tegg/test/close.test.ts b/plugin/tegg/test/close.test.ts index 028b7662..7d1784d4 100644 --- a/plugin/tegg/test/close.test.ts +++ b/plugin/tegg/test/close.test.ts @@ -2,7 +2,7 @@ import mm from 'egg-mock'; import assert from 'assert'; import path from 'path'; -describe('test/close.test.ts', () => { +describe('plugin/tegg/test/close.test.ts', () => { it('should clean lifecycle hooks', async () => { mm(process.env, 'EGG_TYPESCRIPT', true); mm(process, 'cwd', () => { diff --git a/standalone/standalone/package.json b/standalone/standalone/package.json index e5aed0bf..403f98af 100644 --- a/standalone/standalone/package.json +++ b/standalone/standalone/package.json @@ -54,6 +54,7 @@ "access": "public" }, "devDependencies": { + "@eggjs/tegg-ajv-plugin": "^3.35.1", "@types/mocha": "^10.0.1", "@types/node": "^20.2.4", "cross-env": "^7.0.3", diff --git a/standalone/standalone/src/Runner.ts b/standalone/standalone/src/Runner.ts index bdc0cef5..fa6b2df0 100644 --- a/standalone/standalone/src/Runner.ts +++ b/standalone/standalone/src/Runner.ts @@ -252,10 +252,10 @@ export class Runner { } if (this.dalTableEggPrototypeHook) { - await EggPrototypeLifecycleUtil.deleteLifecycle(this.dalTableEggPrototypeHook); + EggPrototypeLifecycleUtil.deleteLifecycle(this.dalTableEggPrototypeHook); } if (this.dalModuleLoadUnitHook) { - await LoadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook); + LoadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook); } MysqlDataSourceManager.instance.clear(); SqlMapManager.instance.clear(); diff --git a/standalone/standalone/test/fixtures/ajv-module-pass/foo.ts b/standalone/standalone/test/fixtures/ajv-module-pass/foo.ts new file mode 100644 index 00000000..554e5e67 --- /dev/null +++ b/standalone/standalone/test/fixtures/ajv-module-pass/foo.ts @@ -0,0 +1,33 @@ +import { ContextProto, Inject } from '@eggjs/tegg'; +import { Runner, MainRunner } from '@eggjs/tegg/standalone'; +import { Ajv, Static, Type, TransformEnum } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 100, + }), + skipDependencies: Type.Boolean(), + registryName: Type.Optional(Type.String()), +}); + +type RequestBody = Static; + +@ContextProto() +@Runner() +export class Foo implements MainRunner { + @Inject() + private readonly ajv: Ajv; + + async main(): Promise { + const body: RequestBody = { + fullname: 'mock fullname', + skipDependencies: true, + registryName: 'ok', + }; + this.ajv.validate(RequestBodySchema, body); + return JSON.stringify({ + body, + }); + } +} diff --git a/standalone/standalone/test/fixtures/ajv-module-pass/package.json b/standalone/standalone/test/fixtures/ajv-module-pass/package.json new file mode 100644 index 00000000..1c4562cc --- /dev/null +++ b/standalone/standalone/test/fixtures/ajv-module-pass/package.json @@ -0,0 +1,6 @@ +{ + "name": "simple", + "eggModule": { + "name": "simple" + } +} diff --git a/standalone/standalone/test/fixtures/ajv-module/foo.ts b/standalone/standalone/test/fixtures/ajv-module/foo.ts new file mode 100644 index 00000000..2ec62879 --- /dev/null +++ b/standalone/standalone/test/fixtures/ajv-module/foo.ts @@ -0,0 +1,29 @@ +import { ContextProto, Inject } from '@eggjs/tegg'; +import { Runner, MainRunner } from '@eggjs/tegg/standalone'; +import { Ajv, Static, Type, TransformEnum } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ TransformEnum.trim ], + maxLength: 100, + }), + skipDependencies: Type.Boolean(), + registryName: Type.Optional(Type.String()), +}); + +type RequestBody = Static; + +@ContextProto() +@Runner() +export class Foo implements MainRunner { + @Inject() + private readonly ajv: Ajv; + + async main(): Promise { + const body: RequestBody = {} as any; + this.ajv.validate(RequestBodySchema, body); + return JSON.stringify({ + body, + }); + } +} diff --git a/standalone/standalone/test/fixtures/ajv-module/package.json b/standalone/standalone/test/fixtures/ajv-module/package.json new file mode 100644 index 00000000..1c4562cc --- /dev/null +++ b/standalone/standalone/test/fixtures/ajv-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "simple", + "eggModule": { + "name": "simple" + } +} diff --git a/standalone/standalone/test/index.test.ts b/standalone/standalone/test/index.test.ts index e1b21db9..60e82daa 100644 --- a/standalone/standalone/test/index.test.ts +++ b/standalone/standalone/test/index.test.ts @@ -1,13 +1,12 @@ import { strict as assert } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs/promises'; +import { ModuleConfig, ModuleConfigs } from '@eggjs/tegg/helper'; import { main, StandaloneContext, Runner } from '..'; -import { ModuleConfigs } from '@eggjs/tegg'; -import { ModuleConfig } from 'egg'; import { crosscutAdviceParams, pointcutAdviceParams } from './fixtures/aop-module/Hello'; import { Foo } from './fixtures/dal-module/Foo'; -describe('test/index.test.ts', () => { +describe('standalone/standalone/test/index.test.ts', () => { describe('simple runner', () => { it('should work', async () => { const msg: string = await main(path.join(__dirname, './fixtures/simple')); @@ -223,4 +222,42 @@ describe('test/index.test.ts', () => { assert.equal(foo.col1, '2333'); }); }); + + describe('ajv runner', () => { + it('should throw AjvInvalidParamError', async () => { + await assert.rejects(async () => { + await main(path.join(__dirname, './fixtures/ajv-module'), { + dependencies: [ + path.dirname(require.resolve('@eggjs/tegg-ajv-plugin/package.json')), + ], + }); + }, (err: any) => { + assert.equal(err.name, 'AjvInvalidParamError'); + assert.equal(err.message, 'Validation Failed'); + assert.deepEqual(err.errorData, {}); + assert.equal(err.currentSchema, '{"type":"object","properties":{"fullname":{"transform":["trim"],"maxLength":100,"type":"string"},"skipDependencies":{"type":"boolean"},"registryName":{"type":"string"}},"required":["fullname","skipDependencies"]}'); + assert.deepEqual(err.errors, [ + { + instancePath: '', + schemaPath: '#/required', + keyword: 'required', + params: { + missingProperty: 'fullname', + }, + message: "must have required property 'fullname'", + }, + ]); + return true; + }); + }); + + it('should pass', async () => { + const result = await main(path.join(__dirname, './fixtures/ajv-module-pass'), { + dependencies: [ + path.dirname(require.resolve('@eggjs/tegg-ajv-plugin/package.json')), + ], + }); + assert.equal(result, '{"body":{"fullname":"mock fullname","skipDependencies":true,"registryName":"ok"}}'); + }); + }); });