Skip to content

Commit

Permalink
feat: impl ajv + typebox Validator
Browse files Browse the repository at this point in the history
closes #200
  • Loading branch information
fengmk2 committed Apr 2, 2024
1 parent 7637fc3 commit f6b213f
Show file tree
Hide file tree
Showing 67 changed files with 768 additions and 47 deletions.
5 changes: 5 additions & 0 deletions core/ajv-decorator/README.md
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)
4 changes: 4 additions & 0 deletions core/ajv-decorator/index.ts
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';
53 changes: 53 additions & 0 deletions core/ajv-decorator/package.json
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"
}
}
45 changes: 45 additions & 0 deletions core/ajv-decorator/src/enum/TransformEnum.ts
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',
}
21 changes: 21 additions & 0 deletions core/ajv-decorator/src/error/AjvInvalidParamError.ts
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;
}
}
5 changes: 5 additions & 0 deletions core/ajv-decorator/src/type/Ajv.ts
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;
}
8 changes: 8 additions & 0 deletions core/ajv-decorator/test/TransformEnum.test.ts
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');
});
});
12 changes: 12 additions & 0 deletions core/ajv-decorator/tsconfig.json
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"
]
}
12 changes: 12 additions & 0 deletions core/ajv-decorator/tsconfig.pub.json
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"
]
}
1 change: 0 additions & 1 deletion core/core-decorator/src/util/PrototypeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EggMultiInstancePrototypeInfo>(this.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz);
Expand Down
1 change: 1 addition & 0 deletions core/tegg/ajv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@eggjs/ajv-decorator';
1 change: 1 addition & 0 deletions core/tegg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
144 changes: 144 additions & 0 deletions plugin/ajv/README.md
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,
};
}
}
```
54 changes: 54 additions & 0 deletions plugin/ajv/lib/Ajv.ts
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!,
});
}
}
}
Loading

0 comments on commit f6b213f

Please sign in to comment.