-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: impl ajv + typebox Validator (#201)
closes #200 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced `@eggjs/ajv-decorator` for enhanced type validation and transformation in TypeScript projects. - Added `@eggjs/tegg-ajv-plugin` for parameter validation and type definition in Egg.js applications, with complete TypeScript support. - Implemented custom error handling for validation failures in Ajv. - Provided installation, configuration, and usage guidelines for `tegg-dal-plugin` in both `egg` and `standalone` modes. - **Bug Fixes** - Removed unnecessary `await` in `Runner.ts` for lifecycle utilities, enhancing performance. - **Documentation** - Updated README files with detailed information on the newly introduced plugins and their usage in projects. - Provided comprehensive installation and configuration instructions for the `tegg-dal-plugin`. - **Tests** - Added new tests for AJV validation, ensuring robust error handling and successful data validation. - Updated test configurations and imports to reflect changes in the framework. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
Showing
66 changed files
with
764 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# `@eggjs/ajv-decorator` | ||
|
||
## Usage | ||
|
||
Please read [@eggjs/tegg-ajv-plugin](../../plugin/ajv-plugin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from '@sinclair/typebox'; | ||
export * from './src/enum/TransformEnum'; | ||
export * from './src/error/AjvInvalidParamError'; | ||
export * from './src/type/Ajv'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "[email protected]: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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { Schema } from 'ajv/dist/2019'; | ||
|
||
export interface Ajv { | ||
validate(schema: Schema, data: unknown): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "dist", | ||
"baseUrl": "./" | ||
}, | ||
"exclude": [ | ||
"dist", | ||
"node_modules", | ||
"test" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "dist", | ||
"baseUrl": "./" | ||
}, | ||
"exclude": [ | ||
"dist", | ||
"node_modules", | ||
"test" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@eggjs/ajv-decorator'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof SyncPackageTaskSchema>; | ||
``` | ||
|
||
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<typeof SyncPackageTaskSchema>; | ||
|
||
@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, | ||
}; | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!, | ||
}); | ||
} | ||
} | ||
} |
Oops, something went wrong.