From 233c7240a8e06d9a298c7debaa64036d6838c4a2 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 | 3 + core/ajv-decorator/package.json | 54 ++ core/ajv-decorator/src/enum/Transform.ts | 45 ++ core/ajv-decorator/src/type/Ajv.ts | 5 + core/ajv-decorator/tsconfig.json | 12 + core/ajv-decorator/tsconfig.pub.json | 12 + core/tegg/ajv.ts | 1 + core/tegg/package.json | 1 + plugin/ajv/README.md | 639 ++++++++++++++++++ plugin/ajv/app.ts | 14 + plugin/ajv/lib/Ajv.ts | 69 ++ plugin/ajv/package.json | 70 ++ plugin/ajv/test/ajv.test.ts | 34 + .../apps/ajv-app/config/config.default.js | 11 + .../fixtures/apps/ajv-app/config/module.json | 7 + .../fixtures/apps/ajv-app/config/plugin.js | 14 + .../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 | 13 + 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/app.ts | 4 +- plugin/dal/package.json | 2 +- plugin/dal/test/dal.test.ts | 2 +- .../apps/dal-app/config/config.default.js | 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 | 10 +- .../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 | 40 +- 64 files changed, 1222 insertions(+), 41 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/Transform.ts create mode 100644 core/ajv-decorator/src/type/Ajv.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/app.ts 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..c0fda1fb --- /dev/null +++ b/core/ajv-decorator/index.ts @@ -0,0 +1,3 @@ +export * from '@sinclair/typebox'; +export * from './src/type/Ajv'; +export * from './src/enum/Transform'; diff --git a/core/ajv-decorator/package.json b/core/ajv-decorator/package.json new file mode 100644 index 00000000..c8e445c5 --- /dev/null +++ b/core/ajv-decorator/package.json @@ -0,0 +1,54 @@ +{ + "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" + }, + "author": "killagu ", + "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": ">=14.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/Transform.ts b/core/ajv-decorator/src/enum/Transform.ts new file mode 100644 index 00000000..478f1b51 --- /dev/null +++ b/core/ajv-decorator/src/enum/Transform.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 Transform { + /** 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/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/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/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..05e33853 --- /dev/null +++ b/plugin/ajv/README.md @@ -0,0 +1,639 @@ +# @eggjs/tegg-dal-plugin + +@eggjs/tegg-dal-plugin 支持使用注解的方式来开发 egg 中的 dal。 + +## egg 模式 + +### Install +```shell +# tegg 注解 +npm i --save @eggjs/tegg +# tegg 插件 +npm i --save @eggjs/tegg-plugin +# tegg dal 插件 +npm i --save @eggjs/tegg-dal-plugin +``` + +### Prepare +```json +// tsconfig.json +{ + "extends": "@eggjs/tsconfig" +} +``` + +### Config + +```js +// config/plugin.js +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggDal = { + package: '@eggjs/tegg-dal-plugin', + enable: true, +}; +``` + +## standalone 模式 + +### Install +```shell +# tegg 注解 +npm i --save @eggjs/tegg +# tegg dal 插件 +npm i --save @eggjs/tegg-dal-plugin +``` + +### Prepare +```json +// tsconfig.json +{ + "extends": "@eggjs/tsconfig" +} +``` + +## Usage + +### module.yml +通过 module.yml 来配置 module 中的 mysql 数据源。 + +```yaml +dataSource: + # 数据源名称,可以在 @Table 注解中指定 + # 如果 module 中只有一个 dataSource,@Table 会默认使用这个数据源 + foo: + connectionLimit: 100 + database: 'test' + host: '127.0.0.1' + user: root + port: 3306 +``` + +### Table +`TableModel` 定义一个表结构,包括表配置、列、索引。 + +```ts +import { Table, Index, Column, ColumnType, IndexType } from '@eggjs/tegg/dal'; + +// 定义了一个表 +@Table({ + comment: 'foo table', +}) +// 定义了一个唯一索引,列是 name +@Index({ + keys: [ 'name' ], + type: IndexType.UNIQUE, +}) +export class Foo { + // 定义了主键,类型是 int + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + }) + id: number; + + // 定义了 name 列,类型是 varchar + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }) + name: string; +} +``` + +详细参数定义如下,具体参数值可以参考 https://dev.mysql.com/doc/refman/8.0/en/create-table.html + +建表参数,使用方式为 `@Table(parmas?: TableParams)` +```ts +export interface TableParams { + // 数据库表名 + name?: string; + // 数据源名称,如果 module 只有一个 dataSource 则默认使用这个 + dataSourceName?: string; + comment?: string; + autoExtendSize?: number; + autoIncrement?: number; + avgRowLength?: number; + characterSet?: string; + collate?: string; + compression?: CompressionType; + encryption?: boolean; + engine?: string; + engineAttribute?: string; + insertMethod?: InsertMethod; + keyBlockSize?: number; + maxRows?: number; + minRows?: number; + rowFormat?: RowFormat; + secondaryEngineAttribute?: string; +} +``` + +建索引参数,使用方式为 `@Index(parmas?: IndexParams)` +```ts +export interface IndexParams { + // 索引的列 + keys: string[]; + // 索引名称,如果未指定会用 列名拼接 + // 如 [column1, column2 ] + // 普通索引为 idx_column1_column2 + // 唯一索引为 uk_column1_column2 + name?: string; + type?: IndexType, + storeType?: IndexStoreType; + comment?: string; + engineAttribute?: string; + secondaryEngineAttribute?: string; + parser?: string; +} +``` + +建列参数,使用方式为 `@Column(type: ColumnTypeParams, parmas?: ColumnParams)` +```ts +export interface ColumnParams { + // 列名,默认转换规则 userName 至 user_name + name?: string; + // 默认值 + default?: string; + // 是否可控,默认为 false + canNull?: boolean; + comment?: string; + visible?: boolean; + autoIncrement?: boolean; + uniqueKey?: boolean; + primaryKey?: boolean; + collate?: string; + columnFormat?: ColumnFormat; + engineAttribute?: string; + secondaryEngineAttribute?: string; +} +``` + +支持的类型 +```ts +export enum ColumnType { + // Numeric + BIT = 'BIT', + TINYINT = 'TINYINT', + BOOL = 'BOOL', + SMALLINT = 'SMALLINT', + MEDIUMINT = 'MEDIUMINT', + INT = 'INT', + BIGINT = 'BIGINT', + DECIMAL = 'DECIMAL', + FLOAT = 'FLOAT', + DOUBLE = 'DOUBLE', + // Date + DATE = 'DATE', + DATETIME = 'DATETIME', + TIMESTAMP = 'TIMESTAMP', + TIME = 'TIME', + YEAR = 'YEAR', + // String + CHAR = 'CHAR', + VARCHAR = 'VARCHAR', + BINARY = 'BINARY', + VARBINARY = 'VARBINARY', + TINYBLOB = 'TINYBLOB', + TINYTEXT = 'TINYTEXT', + BLOB = 'BLOB', + TEXT = 'TEXT', + MEDIUMBLOB = 'MEDIUMBLOB', + MEDIUMTEXT = 'MEDIUMTEXT', + LONGBLOB = 'LONGBLOB', + LONGTEXT = 'LONGTEXT', + ENUM = 'ENUM', + SET = 'SET', + // JSON + JSON = 'JSON', + // Spatial + GEOMETRY = 'GEOMETRY', + POINT = 'POINT', + LINESTRING = 'LINESTRING', + POLYGON = 'POLYGON', + MULTIPOINT = 'MULTIPOINT', + MULTILINESTRING = 'MULTILINESTRING', + MULTIPOLYGON = 'MULTIPOLYGON', + GEOMETRYCOLLECTION = 'GEOMETRYCOLLECTION', +} +``` + +支持的类型参数,详细可参考 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'; + +// 定义了一个表 +@Table({ + comment: 'foo table', +}) +// 定义了一个唯一索引,列是 name +@Index({ + keys: [ 'name' ], + type: IndexType.UNIQUE, +}) +export class Foo { + // 定义了主键,类型是 int + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + }) + id: ColumnTsType['INT']; + + // 定义了 name 列,类型是 varchar + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }) + name: ColumnTsType['VARCHAR']; +} +``` + +```ts +// Bit 类型,对应 js 中的 Buffer +export interface BitParams { + type: ColumnType.BIT, + // Bit 长度 + length?: number; +} + +// Bool 类型,注意在 js 中需要使用 0 或者 1 +export interface BoolParams { + type: ColumnType.BOOL, +} + +// TinyInt 类型,对应 js 中的 number +export interface TinyIntParams { + type: ColumnType.TINYINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// SmallInt 类型,对应 js 中的 number +export interface SmallIntParams { + type: ColumnType.SMALLINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// MediumInt 类型,对应 js 中的 number +export interface MediumIntParams { + type: ColumnType.MEDIUMINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// MediumInt 类型,对应 js 中的 number +export interface IntParams { + type: ColumnType.INT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// BigInt 类型,对应 js 中的 string +export interface BigIntParams { + type: ColumnType.BIGINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Decimal 类型,对应 js 中的 string +export interface DecimalParams { + type: ColumnType.DECIMAL; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Float 类型,对应 js 中的 number +export interface FloatParams { + type: ColumnType.FLOAT; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Double 类型,对应 js 中的 number +export interface DoubleParams { + type: ColumnType.DOUBLE; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Date 类型,对应 js 中的 Date +export interface DateParams { + type: ColumnType.DATE; +} + +// DateTime 类型,对应 js 中的 Date +export interface DateTimeParams { + type: ColumnType.DATETIME; + precision?: number; +} + +// Timestamp 类型,对应 js 中的 Date +export interface TimestampParams { + type: ColumnType.TIMESTAMP; + precision?: number; +} + +// Times 类型,对应 js 中的 string +export interface TimeParams { + type: ColumnType.TIME; + precision?: number; +} + +// Year 类型,对应 js 中的 number +export interface YearParams { + type: ColumnType.YEAR; +} + +// Char 类型,对应 js 中的 string +export interface CharParams { + type: ColumnType.CHAR; + length?: number; + characterSet?: string; + collate?: string; +} + +// VarChar 类型,对应 js 中的 string +export interface VarCharParams { + type: ColumnType.VARCHAR; + length: number; + characterSet?: string; + collate?: string; +} + +// Binary 类型,对应 js 中的 Buffer +export interface BinaryParams { + type: ColumnType.BINARY; + length?: number; +} + +// VarBinary 类型,对应 js 中的 Buffer +export interface VarBinaryParams { + type: ColumnType.VARBINARY; + length: number; +} + +// TinyBlob 类型,对应 js 中的 Buffer +export interface TinyBlobParams { + type: ColumnType.TINYBLOB; +} + +// TinyText 类型,对应 js 中的 string +export interface TinyTextParams { + type: ColumnType.TINYTEXT; + characterSet?: string; + collate?: string; +} + +// Blob 类型,对应 js 中的 Buffer +export interface BlobParams { + type: ColumnType.BLOB; + length?: number; +} + +// Text 类型,对应 js 中的 string +export interface TextParams { + type: ColumnType.TEXT; + length?: number; + characterSet?: string; + collate?: string; +} + +// MediumBlob 类型,对应 js 中的 Buffer +export interface MediumBlobParams { + type: ColumnType.MEDIUMBLOB; +} + +// LongBlob 类型,对应 js 中的 Buffer +export interface LongBlobParams { + type: ColumnType.LONGBLOB; +} + +// MediumText 类型,对应 js 中的 string +export interface MediumTextParams { + type: ColumnType.MEDIUMTEXT; + characterSet?: string; + collate?: string; +} + +// LongText 类型,对应 js 中的 string +export interface LongTextParams { + type: ColumnType.LONGTEXT; + characterSet?: string; + collate?: string; +} + +// Enum 类型,对应 js 中的 string +export interface EnumParams { + type: ColumnType.ENUM; + enums: string[]; + characterSet?: string; + collate?: string; +} + +// Set 类型,对应 js 中的 string +export interface SetParams { + type: ColumnType.SET; + enums: string[]; + characterSet?: string; + collate?: string; +} + +// Json 类型,对应 js 中的 Object +export interface JsonParams { + type: ColumnType.JSON; +} + +// Gemotry 类型,对应 Point, Line, Polygon +export interface GeometryParams { + type: ColumnType.GEOMETRY; + SRID?: number; +} + +export interface PointParams { + type: ColumnType.POINT; + SRID?: number; +} + +export interface LinestringParams { + type: ColumnType.LINESTRING; + SRID?: number; +} + +export interface PolygonParams { + type: ColumnType.POLYGON; + SRID?: number; +} + +export interface MultiPointParams { + type: ColumnType.MULTIPOINT; + SRID?: number; +} + +export interface MultiLinestringParams { + type: ColumnType.MULTILINESTRING; + SRID?: number; +} + +export interface MultiPolygonParams { + type: ColumnType.MULTIPOLYGON; + SRID?: number; +} + +// GeometryCollection 对应 Array +export interface GeometryCollectionParams { + type: ColumnType.GEOMETRYCOLLECTION; + SRID?: number; +} +``` + +### 目录结构 +运行 `egg-bin dal gen` 即可生成 `dal` 相关目录,包括 dao、extension、structure + +```plain +dal +├── dao +│ ├── FooDAO.ts +│ └── base +│ └── BaseFooDAO.ts +├── extension +│ └── FooExtension.ts +└── structure + ├── Foo.json + └── Foo.sql +``` + +- dao: 表访问类,生成的 BaseDAO 请勿修改,其中包含了根据表结构生成的基础访问方法,如 insert/update/delete 以及根据索引信息生成的 find 方法 +- extension: 扩展文件,如果需要自定义 sql,需要在 extension 文件中定义 +- structure: 建表语句以及表结构 + +### DAO + +注入 DAO 即可实现对表的访问 + +```ts +import { SingletonProto, Inject } from '@eggjs/tegg'; + +@SingletonProto() +export class FooRepository { + @Inject() + private readonly fooDAO: FooDAO; + + async create(foo: Foo) { + await this.fooDAO.insert(foo); + } +} +``` + +#### 自定义 Sql +1. 在 extension 中定义自定义 Sql + +```ts +// dal/extension/FooExtension.ts +import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + +export default { + findByName: { + type: SqlType.SELECT, + sql: 'SELECT {{ allColumns }} FROM egg_foo WHERE name = {{ name }}', + }, +} as Record; +``` + +2. 在 dao 中定义自定义方法 +```ts +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseFooDAO } from './base/BaseFooDAO'; +import { Foo } from '../../Foo'; + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class FooDAO extends BaseFooDAO { + async findByName(name: string): Promise { + return this.dataSource.execute('findByName', { + name, + }); + } +} +``` + +支持的自定义 filter +``` +- toPoint +- toLine +- toPolygon +- toGeometry +- toMultiPoint +- toMultiLine +- toMultiPolygon +- toGeometryCollection +``` + +支持自定义 block 来简化 sql, 如内置的 allColumns +```ts +export default { + findByName: { + type: SqlType.BLOCK, + sql: 'id, name', + }, +} as Record; +``` + +### DataSource +DataSource 仅能在 DAO 中使用,可以将 mysql 返回的数据反序列化为类。支持的方法有 + +```ts +export interface DataSource { + // 将返回的行都转换为 T + execute(sqlName: string, data?: any): Promise>; + // 将返回的行都转换为 T, 仅返回第一条 + executeScalar(sqlName: string, data?: any): Promise; + // 直接返回 mysql 数据 + executeRaw(sqlName: string, data?: any): Promise>; + // 直接返回 mysql 数据, 仅返回第一条 + executeRawScalar(sqlName: string, data?: any): Promise; + // 返回分页数据 + paginate(sqlName: string, data: any, currentPage: number, perPageCount: number): Promise; + // 返回行数 + count(sqlName: string, data?: any): Promise; +} +``` + +### 时区问题 + +注意连接配置中的时区必须和数据库的时区完全一致,否则可能出现时间错误的问题。 + +```yaml +dataSource: + foo: + connectionLimit: 100 + database: 'test' + host: '127.0.0.1' + user: root + port: 3306 + timezone: '+08:00' +``` + +可以通过以下 SQL 来查看数据库时区 +```sql +SELECT @@GLOBAL.time_zone; +``` diff --git a/plugin/ajv/app.ts b/plugin/ajv/app.ts new file mode 100644 index 00000000..bced9bcc --- /dev/null +++ b/plugin/ajv/app.ts @@ -0,0 +1,14 @@ +import type { Application } from 'egg'; +import { Ajv } from './lib/Ajv'; + +export default class AopAppHook { + private readonly app: Application; + + constructor(app: Application) { + this.app = app; + } + + configDidLoad() { + this.app.ajv = new Ajv(); + } +} diff --git a/plugin/ajv/lib/Ajv.ts b/plugin/ajv/lib/Ajv.ts new file mode 100644 index 00000000..022743f4 --- /dev/null +++ b/plugin/ajv/lib/Ajv.ts @@ -0,0 +1,69 @@ +import { Ajv as IAjv } from '@eggjs/tegg/ajv'; +import Ajv2019, { type Schema, type ErrorObject } from 'ajv/dist/2019'; +import addFormats from 'ajv-formats'; +import keyWords from 'ajv-keywords'; + +export interface TEggAjvInvalidParamErrorOptions { + errorData: unknown; + currentSchema: string; + errors: ErrorObject[]; +} + +export class TEggAjvInvalidParamError extends Error { + errorData: unknown; + currentSchema: string; + errors: ErrorObject[]; + + constructor(message: string, options: TEggAjvInvalidParamErrorOptions) { + super(message); + this.name = this.constructor.name; + this.errorData = options.errorData; + this.currentSchema = options.currentSchema; + this.errors = options.errors; + } +} + +export class Ajv implements IAjv { + static InvalidParamErrorClass = TEggAjvInvalidParamError; + + readonly #ajvInstance: Ajv2019; + + constructor() { + 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..7f7709a5 --- /dev/null +++ b/plugin/ajv/package.json @@ -0,0 +1,70 @@ +{ + "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": ">=14.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", + "@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..0555afef --- /dev/null +++ b/plugin/ajv/test/ajv.test.ts @@ -0,0 +1,34 @@ +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 work', async () => { + const res = await app.httpRequest() + .get('/foo'); + assert.equal(res.status, 200); + console.log(res.body); + }); +}); 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..88cb83a9 --- /dev/null +++ b/plugin/ajv/test/fixtures/apps/ajv-app/config/plugin.js @@ -0,0 +1,14 @@ +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + 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..27761341 --- /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, Transform } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ Transform.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.GET, + 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..96ca4164 --- /dev/null +++ b/plugin/ajv/typings/index.d.ts @@ -0,0 +1,13 @@ +import 'egg'; +import '@eggjs/tegg-plugin'; +import '@eggjs/tegg-config'; +import type { Ajv } from '@eggjs/tegg/ajv'; + +declare module 'egg' { + interface TEggAjvApplication { + ajv: Ajv; + } + + interface Application extends TEggAjvApplication { + } +} 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/app.ts b/plugin/dal/app.ts index 0f86c014..17a9c08c 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/dal/test/fixtures/apps/dal-app/config/config.default.js b/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js index 586c9e0c..31ec8ab4 100644 --- a/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js +++ b/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js @@ -6,7 +6,7 @@ module.exports = function() { security: { csrf: { ignoreJSON: false, - } + }, }, }; return config; 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..bff45b60 100644 --- a/standalone/standalone/package.json +++ b/standalone/standalone/package.json @@ -44,6 +44,7 @@ "@eggjs/tegg-background-task": "^3.35.1", "@eggjs/tegg-common-util": "^3.35.1", "@eggjs/tegg-dal-plugin": "^3.35.1", + "@eggjs/tegg-ajv-plugin": "^3.35.1", "@eggjs/tegg-dynamic-inject-runtime": "^3.35.1", "@eggjs/tegg-lifecycle": "^3.35.1", "@eggjs/tegg-loader": "^3.35.1", diff --git a/standalone/standalone/src/Runner.ts b/standalone/standalone/src/Runner.ts index 6ff7aeb9..16961289 100644 --- a/standalone/standalone/src/Runner.ts +++ b/standalone/standalone/src/Runner.ts @@ -35,6 +35,7 @@ import { DalModuleLoadUnitHook } from '@eggjs/tegg-dal-plugin/lib/DalModuleLoadU import { MysqlDataSourceManager } from '@eggjs/tegg-dal-plugin/lib/MysqlDataSourceManager'; import { SqlMapManager } from '@eggjs/tegg-dal-plugin/lib/SqlMapManager'; import { TableModelManager } from '@eggjs/tegg-dal-plugin/lib/TableModelManager'; +import { Ajv } from '@eggjs/tegg-ajv-plugin/lib/Ajv'; export interface ModuleDependency extends ReadModuleReferenceOptions { baseDir: string; @@ -102,6 +103,11 @@ export class Runner { obj: runtimeConfig, }]; + // Inject ajv + this.innerObjects.ajv = [{ + obj: new Ajv(), + }]; + for (const reference of this.moduleReferences) { const absoluteRef = { path: ModuleConfigUtil.resolveModuleDir(reference.path, this.cwd), @@ -252,10 +258,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..9007437e --- /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, Transform } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ Transform.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..0773c8e6 --- /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, Transform } from '@eggjs/tegg/ajv'; + +const RequestBodySchema = Type.Object({ + fullname: Type.String({ + transform: [ Transform.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 8b3a3302..97cf65d0 100644 --- a/standalone/standalone/test/index.test.ts +++ b/standalone/standalone/test/index.test.ts @@ -1,15 +1,15 @@ import { strict as assert } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs/promises'; -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'; +import { ModuleConfig } from '@eggjs/tegg/helper'; import { MysqlDataSource, SqlGenerator } from '@eggjs/dal-runtime'; import { TableModel } from '@eggjs/dal-decorator'; +import { main, StandaloneContext, Runner } from '..'; +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')); @@ -249,4 +249,34 @@ describe('test/index.test.ts', () => { assert.equal(foo.col1, '2333'); }); }); + + describe('ajv runner', () => { + it('should throw TEggAjvInvalidParamError', async () => { + await assert.rejects(async () => { + await main(path.join(__dirname, './fixtures/ajv-module')); + }, (err: any) => { + assert.equal(err.name, 'TEggAjvInvalidParamError'); + 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')); + assert.equal(result, '{"body":{"fullname":"mock fullname","skipDependencies":true,"registryName":"ok"}}'); + }); + }); });