@eggjs/typebox-validate 0.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 David Tse
4
+ Copyright (c) 2025-present eggjs-community and the contributors.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,465 @@
1
+ # @eggjs/typebox-validate
2
+
3
+ [![NPM version][npm-image]][npm-url]
4
+ [![Test coverage][codecov-image]][codecov-url]
5
+ [![Known Vulnerabilities][snyk-image]][snyk-url]
6
+ [![npm download][download-image]][download-url]
7
+
8
+ [npm-image]: https://img.shields.io/npm/v/@eggjs/typebox-validate.svg?style=flat-square
9
+ [npm-url]: https://npmjs.org/package/@eggjs/typebox-validate
10
+ [codecov-image]: https://img.shields.io/codecov/c/github/eggjs-community/@eggjs/typebox-validate.svg?style=flat-square
11
+ [codecov-url]: https://codecov.io/github/eggjs-community/@eggjs/typebox-validate?branch=master
12
+ [snyk-image]: https://snyk.io/test/npm/@eggjs/typebox-validate/badge.svg?style=flat-square
13
+ [snyk-url]: https://snyk.io/test/npm/@eggjs/typebox-validate
14
+ [download-image]: https://img.shields.io/npm/dm/@eggjs/typebox-validate.svg?style=flat-square
15
+ [download-url]: https://npmjs.org/package/@eggjs/typebox-validate
16
+
17
+ 基于 [typebox](https://github.com/sinclairzx81/typebox) 和 [ajv](https://github.com/ajv-validator/ajv) 封装的 egg validate 插件。
18
+
19
+ ## 为什么有这个项目
20
+
21
+ 一直以来,在 typescript 的 egg 项目里,对参数校验 ctx.validate 是比较难受的,比如:
22
+
23
+ ```js
24
+ class HomeController extends Controller {
25
+ async index() {
26
+ const { ctx } = this;
27
+ // 写一遍 js 的类型校验
28
+ ctx.validate({
29
+ id: 'string',
30
+ name: {
31
+ type: 'string',
32
+ required: false,
33
+ },
34
+ timestamp: {
35
+ type: 'number',
36
+ required: false,
37
+ },
38
+ }, ctx.params);
39
+
40
+ // 写一遍 ts 的类型定义,为了后面拿参数定义
41
+ const params: {
42
+ id: string;
43
+ name?: string;
44
+ timestamp: number;
45
+ } = ctx.params;
46
+ ...
47
+ ctx.body = params.id;
48
+ }
49
+ }
50
+
51
+ export default HomeController;
52
+ ```
53
+
54
+ 可以看到这里我们写了两遍的类型定义,一遍 js 的定义(用 [parameter](https://github.com/node-modules/parameter) 库的规则),另一遍用 ts 的方式来强转我们的参数类型,方便我们后面写代码的时候能得到 ts 的类型效果。
55
+ 对于简单的类型写起来还好,但是对于复杂点的参数定义,开发体验就不是那么好了。
56
+
57
+ 这就是这个库想要解决的问题,对于参数校验,写一遍类型就够了:
58
+
59
+ ```diff
60
+ + import { Static, Type } from 'egg-typebox-validate/typebox';
61
+
62
+ class HomeController extends Controller {
63
+ async index() {
64
+ const { ctx } = this;
65
+ // 写 js 类型定义
66
+ - ctx.validate({
67
+ - id: 'string',
68
+ - name: {
69
+ - type: 'string',
70
+ - required: false,
71
+ - },
72
+ - timestamp: {
73
+ - type: 'number',
74
+ - required: false,
75
+ - },
76
+ - }, ctx.params);
77
+
78
+ + const paramsSchema = Type.Object({
79
+ + id: Type.String(),
80
+ + name: Type.Optional(Type.String()),
81
+ + timestamp: Type.Optional(Type.Integer()),
82
+ + });
83
+ // 直接校验
84
+ + ctx.tValidate(paramsSchema, ctx.params);
85
+ // 不用写 js 类型定义
86
+ + const params: Static<typeof paramsSchema> = ctx.params;
87
+ - const params: {
88
+ - id: string;
89
+ - name?: string;
90
+ - timestamp: number;
91
+ - } = ctx.params;
92
+ ...
93
+ ctx.body = params.id;
94
+ }
95
+ }
96
+
97
+ export default HomeController;
98
+ ```
99
+
100
+ 用 `Static<typeof typebox>` 推导出的 ts 类型:
101
+
102
+ ![tpian](https://gw.alipayobjects.com/zos/antfincdn/XjH2W7lEB/ad5b628c-9ff9-456d-bb7b-2fb0ac418f1c.png)
103
+
104
+ ## 怎么使用
105
+
106
+ 1. 安装
107
+
108
+ 针对 `egg@4.x` 及以上版本,使用
109
+
110
+ ```js
111
+ npm i egg-typebox-validate -S
112
+ ```
113
+
114
+ 针对 `egg@3.x` 版本,使用
115
+
116
+ ```js
117
+ npm i egg-typebox-validate@3 -S
118
+ ```
119
+
120
+ 1. 在项目中配置
121
+
122
+ ```js
123
+ // config/plugin.ts
124
+ const plugin: EggPlugin = {
125
+ typeboxValidate: {
126
+ enable: true,
127
+ package: 'egg-typebox-validate',
128
+ },
129
+ };
130
+ ```
131
+
132
+ 3. 在业务代码中使用
133
+
134
+ ```diff
135
+ + import { Static, Type } from 'egg-typebox-validate/typebox';
136
+
137
+ // 写在 controller 外面,静态化,性能更好,下面有 benchmark
138
+ + const paramsSchema = Type.Object({
139
+ + id: Type.String(),
140
+ + name: Type.String(),
141
+ + timestamp: Type.Integer(),
142
+ + });
143
+
144
+ // 可以直接 export 出去,给下游 service 使用
145
+ + export type ParamsType = Static<typeof paramsSchema>;
146
+
147
+ class HomeController extends Controller {
148
+ async index() {
149
+ const { ctx } = this;
150
+
151
+ // 直接校验
152
+ + ctx.tValidate(paramsSchema, ctx.params);
153
+ // 不用写 js 类型定义
154
+ + const params: ParamsType = ctx.params;
155
+
156
+ ...
157
+ }
158
+ }
159
+
160
+ export default HomeController;
161
+ ```
162
+
163
+ ## 除了类型定义 write once 外,还有更多好处
164
+
165
+ 1. 类型组合方式特别香,能解决很多 DRY(Don't Repeat Yourself) 问题。比如有几张 db 表,都定义了 name 必填和 description 选填,那这个规则可以在各个实体类方法中被组合了。
166
+
167
+ Show me the code!
168
+
169
+ ```ts
170
+ export const TYPEBOX_NAME_DESC_OBJECT = Type.Object({
171
+ name: Type.String(),
172
+ description: Type.Optional(Type.String()),
173
+ });
174
+
175
+ // type NameAndDesc = { name: string; description?: string }
176
+ type NameAndDesc = Static<typeof TYPEBOX_NAME_DESC_OBJECT>;
177
+
178
+ // controller User
179
+ async create() {
180
+ const { ctx } = this;
181
+ const USER_TYPEBOX = Type.Intersect([
182
+ TYPEBOX_NAME_DESC_OBJECT,
183
+ Type.Object({ avatar: Type.String() }),
184
+ ])
185
+ ctx.tValidate(USER_TYPEBOX, ctx.request.body);
186
+
187
+ // 在编辑器都能正确得到提示
188
+ // type User = { name: string; description?: string } & { avatar: string }
189
+ const { name, description, avatar } = ctx.request.body as Static<typeof USER_TYPEBOX>;
190
+ ...
191
+ }
192
+
193
+ // controller Photo
194
+ async create() {
195
+ const { ctx } = this;
196
+ const PHOTO_TYPEBOX = Type.Intersect([
197
+ TYPEBOX_NAME_DESC_OBJECT,
198
+ Type.Object({ location: Type.String() }),
199
+ ])
200
+ ctx.tValidate(PHOTO_TYPEBOX, ctx.request.body);
201
+
202
+ // 在编辑器都能正确得到提示
203
+ // type Photo = { name: string; description?: string } & { location: string }
204
+ const { name, description, location } = ctx.request.body as Static<typeof PHOTO_TYPEBOX>;
205
+ ...
206
+ }
207
+ ```
208
+
209
+ 2. 校验规则使用的是业界标准的 [json-schema](https://json-schema.org/) 规范,内置很多[开箱即用的类型](https://github.com/ajv-validator/ajv-formats#formats)。
210
+
211
+ ```js
212
+ ('date-time',
213
+ 'time',
214
+ 'date',
215
+ 'email',
216
+ 'hostname',
217
+ 'ipv4',
218
+ 'ipv6',
219
+ 'uri',
220
+ 'uri-reference',
221
+ 'uuid',
222
+ 'uri-template',
223
+ 'json-pointer',
224
+ 'relative-json-pointer',
225
+ 'regex');
226
+ ```
227
+
228
+ 3. 写定义的时候写的是 js 对象(`Type.Number()`),有类型提示,语法也比较简单,有提示不容易写错;写 parameter 规范的时候,写字符串(`'nunber'`)有时候会不小心写错 😂,再加上它对于复杂嵌套对象的写法还是比较困难的,我每次都会查文档,官方的文档也不全。但是 typebox,就很容易举一反三了。
229
+
230
+ ## 与 egg-validate 性能比较
231
+
232
+ egg-typebox-validate 底层使用的是 [ajv](https://github.com/ajv-validator/ajv), 官网上宣称是 _**The fastest JSON validator for Node.js and browser.**_
233
+
234
+ 结论是在静态化的场景下,ajv 的性能要比 parameter 好得多,快不是一个数量级,详见[benchmark](./benchmark/ajv-vs-parameter.mjs)
235
+
236
+ ```js
237
+ suite
238
+ .add('#ajv', function () {
239
+ const rule = Type.Object({
240
+ name: Type.String(),
241
+ description: Type.Optional(Type.String()),
242
+ location: Type.Enum({ shanghai: 'shanghai', hangzhou: 'hangzhou' }),
243
+ });
244
+ ajv.validate(rule, DATA);
245
+ })
246
+ .add('#ajv define once', function () {
247
+ ajv.validate(typeboxRule, DATA);
248
+ })
249
+ .add('#parameter', function () {
250
+ const rule = {
251
+ name: 'string',
252
+ description: {
253
+ type: 'string',
254
+ required: false,
255
+ },
256
+ location: ['shanghai', 'hangzhou'],
257
+ };
258
+ p.validate(rule, DATA);
259
+ })
260
+ .add('#parameter define once', function () {
261
+ p.validate(parameterRule, DATA);
262
+ });
263
+ ```
264
+
265
+ 在 MacBook Pro(2.2 GHz 六核Intel Core i7)上,跑出来结果是:
266
+
267
+ ```bash
268
+ #ajv x 941 ops/sec ±3.97% (73 runs sampled)
269
+ #ajv define once x 17,188,370 ops/sec ±11.53% (73 runs sampled)
270
+ #parameter x 2,544,118 ops/sec ±4.68% (79 runs sampled)
271
+ #parameter define once x 2,541,590 ops/sec ±5.34% (77 runs sampled)
272
+ Fastest is #ajv define once
273
+ ```
274
+
275
+ ## 从 egg-validate 迁移到这个库的成本
276
+
277
+ 1. 把原来字符串式 js 对象写法迁移到 typebox 的对象写法。typebox 的写法还算简单和容易举一反三
278
+ 2. 把 `ctx.validate` 替换成 `ctx.tValidate`
279
+ 3. 建议渐进式迁移,先迁简单的,对业务影响不大的
280
+
281
+ ## 总结
282
+
283
+ 切换到 egg-typebox-validate 校验后:
284
+
285
+ 1. 可以解决 ts 项目中参数校验代码写两遍类型的问题,提升代码重用率,可维护性等问题
286
+ 2. 用标准 json-schema 来做参数校验,是更加标准的业界做法,内置更多业界标准模型
287
+
288
+ ## API
289
+
290
+ 1. `ctx.tValidate` 参数校验失败后,抛出错误,内部实现(错误码、错误标题等)逻辑和 `ctx.validate` 的保持一致
291
+
292
+ ```diff
293
+ + import { Static, Type } from 'egg-typebox-validate/typebox';
294
+
295
+ ctx.tValidate(Type.Object({
296
+ name: Type.String(),
297
+ }), ctx.request.body);
298
+ ```
299
+
300
+ 2. `ctx.tValidateWithoutThrow` 直接校验,不抛出错误
301
+
302
+ ```diff
303
+ + import { Static, Type } from 'egg-typebox-validate/typebox';
304
+
305
+ const valid = ctx.tValidateWithoutThrow(Type.Object({
306
+ name: Type.String(),
307
+ }), ctx.request.body);
308
+
309
+ if (valid) {
310
+ ...
311
+ } else {
312
+ const errors = this.app.ajv.errors
313
+ // handle errors
314
+ ...
315
+ }
316
+ ```
317
+
318
+ 3. ⭐⭐⭐ 装饰器 decorator `@Validate([ [rule1, ctx => ctx.xx1], [rule2, ctx => ctx.xx2] ])` 调用(写法更干净,推荐使用!️)
319
+
320
+ ```diff
321
+ + import { Validate, ValidateFactory } from 'egg-typebox-validate/decorator';
322
+
323
+ const ValidateWithRedirect = ValidateFactory(ctx => ctx.redirect('/422'));
324
+
325
+ class HomeController extends Controller {
326
+ + @Validate([
327
+ + [paramsSchema, ctx => ctx.params],
328
+ + [bodySchema, ctx => ctx.request.body, (ctx, errors) => 'MyErrorPrefix: ' + errors.map(e => e.message).join(', ')],
329
+ + ])
330
+ async index() {
331
+ const { ctx } = this;
332
+
333
+ // 直接校验
334
+ - ctx.tValidate(paramsSchema, ctx.params);
335
+ - ctx.tValidate(bodySchema, ctx.request.body);
336
+ // 不用写 js 类型定义
337
+ const params: ParamsType = ctx.params;
338
+
339
+ ...
340
+ }
341
+ + @ValidateWithRedirect([paramsSchema, ctx => ctx.params])
342
+ async post() {
343
+ // ...
344
+ }
345
+ }
346
+
347
+ export default HomeController;
348
+ ```
349
+
350
+ 目前装饰器只支持有 `this.ctx` 的 class 上使用,比如 controller,service 等。也可以通过内置的 `ValidateFactory` 自定义校验失败后的回调逻辑,更多使用案例可以看这个项目里写的测试用例。
351
+
352
+ ## 怎么写 typebox 定义
353
+
354
+ 参考 [https://github.com/sinclairzx81/typebox#types](https://github.com/sinclairzx81/typebox#types)
355
+
356
+ ## 支持 ajv 对 string 的 transform 校验
357
+
358
+ 比如:
359
+
360
+ ```js
361
+ const body = { name: ' david ' };
362
+
363
+ ctx.tValidate(
364
+ Type.Object({
365
+ name: Type.String({ minLength: 1, maxLength: 5, transform: ['trim'] }),
366
+ }),
367
+ body
368
+ );
369
+ ```
370
+
371
+ 1. 是可以通过校验的
372
+ 2. 会对 body 有副作用,改写 name 字段,trim name 字段,body 会变成 `{ name: 'david' }`
373
+
374
+ 更多 ajv 对 string 的 transform 操作,详见 [https://ajv.js.org/packages/ajv-keywords.html#transform](https://ajv.js.org/packages/ajv-keywords.html#transform)
375
+
376
+ ## 如何写自定义校验规则
377
+
378
+ 比如想校验上传的 string 是否是合法的 json string,我们可以对 Type.String 的 format 做 patch,针对 string 加一个 'json-string' 的 format
379
+
380
+ 1. 在 config.default.ts 里 patch 默认 ajv 实例的规则
381
+
382
+ ```ts
383
+ config.typeboxValidate = {
384
+ patchAjv: ajv => {
385
+ ajv.addFormat('json-string', {
386
+ type: 'string',
387
+ validate: x => {
388
+ try {
389
+ JSON.parse(x);
390
+ return true;
391
+ } catch (err) {
392
+ return false;
393
+ }
394
+ },
395
+ });
396
+ },
397
+ };
398
+ ```
399
+
400
+ 2. 使用
401
+
402
+ ```ts
403
+ async someFunc() {
404
+ const typebox = Type.Object({
405
+ jsonString: Type.Optional(Type.String({ format: 'json-string' })),
406
+ });
407
+
408
+ const res = ctx.tValidate(typebox, { a: '{"a":1}' }) // valid
409
+ const res = ctx.tValidate(typebox, { a: 'wrong{"a":1}' }) // invalid
410
+ }
411
+ ```
412
+
413
+ 当然也可以定义其他各种规则,比如我们常见的 semver 规范,那可以在我们的配置里继续 patch ajv string format
414
+
415
+ ```diff
416
+ + import { valid } from 'semver';
417
+
418
+ config.typeboxValidate = {
419
+ patchAjv: (ajv) => {
420
+ ajv.addFormat('json-string', {
421
+ type: 'string',
422
+ validate: (x) => {
423
+ try {
424
+ JSON.parse(x);
425
+ return true;
426
+ } catch (err) {
427
+ return false;
428
+ }
429
+ }
430
+ });
431
+
432
+ + ajv.addFormat("semver", {
433
+ + type: "string",
434
+ + validate: (x) => valid(x) != null,
435
+ + })
436
+ }
437
+ }
438
+ ```
439
+
440
+ 使用例子:
441
+
442
+ ```ts
443
+ async someFunc() {
444
+ const typebox = Type.Object({
445
+ version: Type.String({ format: 'semver' }),
446
+ });
447
+
448
+ const res = ctx.tValidate(typebox, { a: '1.0.0' }) // valid
449
+ const res = ctx.tValidate(typebox, { a: 'a.b.c' }) // invalid
450
+ }
451
+ ```
452
+
453
+ 上面例子是 string 的例子,当然也可以对其他类型做其他 patch,比如 number,array 等,限制你的只有想象力。
454
+
455
+ 全部 json-schema 支持的类型:[https://json-schema.org/understanding-json-schema/reference/type.html](https://json-schema.org/understanding-json-schema/reference/type.html)
456
+
457
+ ## License
458
+
459
+ [MIT](LICENSE)
460
+
461
+ ## Contributors
462
+
463
+ [![Contributors](https://contrib.rocks/image?repo=eggjs/egg)](https://github.com/eggjs/egg/graphs/contributors)
464
+
465
+ Made with [contributors-img](https://contrib.rocks).
@@ -0,0 +1,12 @@
1
+ import { Context } from 'egg';
2
+ import type { Schema } from 'ajv/dist/2019.js';
3
+ export default class AjvContext extends Context {
4
+ tValidate(schema: Schema, data: unknown): boolean;
5
+ tValidateWithoutThrow(schema: Schema, data: unknown): boolean;
6
+ }
7
+ declare module 'egg' {
8
+ interface Context {
9
+ tValidate(schema: Schema, data: unknown): boolean;
10
+ tValidateWithoutThrow(schema: Schema, data: unknown): boolean;
11
+ }
12
+ }
@@ -0,0 +1,21 @@
1
+ import { Context } from 'egg';
2
+ export default class AjvContext extends Context {
3
+ tValidate(schema, data) {
4
+ const ajv = this.app.ajv;
5
+ const res = ajv.validate(schema, data);
6
+ if (!res) {
7
+ this.throw(422, 'Validation Failed', {
8
+ code: 'invalid_param',
9
+ errorData: data,
10
+ currentSchema: JSON.stringify(schema),
11
+ errors: ajv.errors,
12
+ });
13
+ }
14
+ return res;
15
+ }
16
+ tValidateWithoutThrow(schema, data) {
17
+ const res = this.app.ajv.validate(schema, data);
18
+ return res;
19
+ }
20
+ }
21
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9hcHAvZXh0ZW5kL2NvbnRleHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLEtBQUssQ0FBQztBQUc5QixNQUFNLENBQUMsT0FBTyxPQUFPLFVBQVcsU0FBUSxPQUFPO0lBQzdDLFNBQVMsQ0FBQyxNQUFjLEVBQUUsSUFBYTtRQUNyQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsQ0FBQztRQUN2QyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDVCxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxtQkFBbUIsRUFBRTtnQkFDbkMsSUFBSSxFQUFFLGVBQWU7Z0JBQ3JCLFNBQVMsRUFBRSxJQUFJO2dCQUNmLGFBQWEsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQztnQkFDckMsTUFBTSxFQUFFLEdBQUcsQ0FBQyxNQUFNO2FBQ25CLENBQUMsQ0FBQztRQUNMLENBQUM7UUFDRCxPQUFPLEdBQUcsQ0FBQztJQUNiLENBQUM7SUFFRCxxQkFBcUIsQ0FBQyxNQUFjLEVBQUUsSUFBYTtRQUNqRCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ2hELE9BQU8sR0FBRyxDQUFDO0lBQ2IsQ0FBQztDQUNGIn0=
package/dist/app.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Application, ILifecycleBoot } from 'egg';
2
+ import { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
3
+ export default class AppBootHook implements ILifecycleBoot {
4
+ app: Application;
5
+ constructor(app: Application);
6
+ configDidLoad(): Promise<void>;
7
+ }
8
+ declare module 'egg' {
9
+ interface Application {
10
+ ajv: Ajv;
11
+ }
12
+ }
package/dist/app.js ADDED
@@ -0,0 +1,43 @@
1
+ import addFormats from 'ajv-formats';
2
+ import { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
3
+ import keyWords from 'ajv-keywords';
4
+ const getAjvInstance = () => {
5
+ const ajv = new Ajv();
6
+ // @ts-expect-error - keyWords types are not fully compatible
7
+ keyWords(ajv, 'transform');
8
+ // @ts-expect-error - addFormats types are not fully compatible
9
+ addFormats(ajv, [
10
+ 'date-time',
11
+ 'time',
12
+ 'date',
13
+ 'email',
14
+ 'hostname',
15
+ 'ipv4',
16
+ 'ipv6',
17
+ 'uri',
18
+ 'uri-reference',
19
+ 'uuid',
20
+ 'uri-template',
21
+ 'json-pointer',
22
+ 'relative-json-pointer',
23
+ 'regex',
24
+ ])
25
+ .addKeyword('kind')
26
+ .addKeyword('modifier');
27
+ return ajv;
28
+ };
29
+ export default class AppBootHook {
30
+ app;
31
+ constructor(app) {
32
+ this.app = app;
33
+ this.app.ajv = getAjvInstance();
34
+ }
35
+ async configDidLoad() {
36
+ const config = this.app.config;
37
+ const typeboxValidate = config.typeboxValidate;
38
+ if (typeboxValidate) {
39
+ typeboxValidate.patchAjv?.(this.app.ajv);
40
+ }
41
+ }
42
+ }
43
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLFVBQVUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxFQUFFLE9BQU8sSUFBSSxHQUFHLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNsRCxPQUFPLFFBQVEsTUFBTSxjQUFjLENBQUM7QUFFcEMsTUFBTSxjQUFjLEdBQUcsR0FBRyxFQUFFO0lBQzFCLE1BQU0sR0FBRyxHQUFHLElBQUksR0FBRyxFQUFFLENBQUM7SUFDdEIsNkRBQTZEO0lBQzdELFFBQVEsQ0FBQyxHQUFHLEVBQUUsV0FBVyxDQUFDLENBQUM7SUFDM0IsK0RBQStEO0lBQy9ELFVBQVUsQ0FBQyxHQUFHLEVBQUU7UUFDZCxXQUFXO1FBQ1gsTUFBTTtRQUNOLE1BQU07UUFDTixPQUFPO1FBQ1AsVUFBVTtRQUNWLE1BQU07UUFDTixNQUFNO1FBQ04sS0FBSztRQUNMLGVBQWU7UUFDZixNQUFNO1FBQ04sY0FBYztRQUNkLGNBQWM7UUFDZCx1QkFBdUI7UUFDdkIsT0FBTztLQUNSLENBQUM7U0FDQyxVQUFVLENBQUMsTUFBTSxDQUFDO1NBQ2xCLFVBQVUsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUMxQixPQUFPLEdBQUcsQ0FBQztBQUNiLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxPQUFPLE9BQU8sV0FBVztJQUN2QixHQUFHLENBQWM7SUFFeEIsWUFBWSxHQUFnQjtRQUMxQixJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxHQUFHLGNBQWMsRUFBRSxDQUFDO0lBQ2xDLENBQUM7SUFFRCxLQUFLLENBQUMsYUFBYTtRQUNqQixNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQztRQUMvQixNQUFNLGVBQWUsR0FBRyxNQUFNLENBQUMsZUFBZSxDQUFDO1FBQy9DLElBQUksZUFBZSxFQUFFLENBQUM7WUFDcEIsZUFBZSxDQUFDLFFBQVEsRUFBRSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDM0MsQ0FBQztJQUNILENBQUM7Q0FDRiJ9
@@ -0,0 +1,17 @@
1
+ import type { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
2
+ export interface TypeboxValidateConfig {
3
+ patchAjv?: (ajv: Ajv) => void;
4
+ }
5
+ declare const _default: {
6
+ typeboxValidate: TypeboxValidateConfig;
7
+ };
8
+ export default _default;
9
+ declare module 'egg' {
10
+ interface EggAppConfig {
11
+ /**
12
+ * typebox validate options
13
+ * @member Config#typeboxValidate
14
+ */
15
+ typeboxValidate: TypeboxValidateConfig;
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ typeboxValidate: {
3
+ patchAjv: undefined,
4
+ },
5
+ };
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmRlZmF1bHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvY29uZmlnL2NvbmZpZy5kZWZhdWx0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQU1BLGVBQWU7SUFDYixlQUFlLEVBQUU7UUFDZixRQUFRLEVBQUUsU0FBUztLQUNLO0NBQzNCLENBQUMifQ==
@@ -0,0 +1,9 @@
1
+ import type { Context } from 'egg';
2
+ import type { TSchema } from './typebox.ts';
3
+ import type { ErrorObject } from 'ajv/dist/2019.js';
4
+ type CustomErrorMessage = (ctx: Context, errors: ErrorObject[]) => string;
5
+ type GetData = (ctx: Context, args: unknown[]) => unknown;
6
+ export type ValidateRule = [TSchema, GetData, CustomErrorMessage?];
7
+ export declare function ValidateFactory(customHandler: (ctx: Context, data?: unknown, schema?: TSchema, customError?: CustomErrorMessage) => void): (rules: ValidateRule[]) => MethodDecorator;
8
+ export declare const Validate: (rules: ValidateRule[]) => MethodDecorator;
9
+ export {};
@@ -0,0 +1,31 @@
1
+ export function ValidateFactory(customHandler) {
2
+ return function Validate(rules) {
3
+ return (_, __, descriptor) => {
4
+ const fn = descriptor.value;
5
+ descriptor.value = async function (...args) {
6
+ const { ctx } = this;
7
+ for (const rule of rules) {
8
+ const [schema, getData, customError] = rule;
9
+ const data = getData(ctx, args);
10
+ const valid = ctx.tValidateWithoutThrow(schema, data);
11
+ if (!valid && customHandler) {
12
+ return customHandler(ctx, data, schema, customError);
13
+ }
14
+ }
15
+ return await fn.apply(this, args);
16
+ };
17
+ return descriptor;
18
+ };
19
+ };
20
+ }
21
+ export const Validate = ValidateFactory((ctx, data, schema, customError) => {
22
+ const app = ctx.app;
23
+ const message = customError ? customError(ctx, app.ajv.errors) : 'Validation Failed';
24
+ ctx.throw(422, message, {
25
+ code: 'invalid_param',
26
+ errorData: data,
27
+ currentSchema: JSON.stringify(schema),
28
+ errors: app.ajv.errors,
29
+ });
30
+ });
31
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVjb3JhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2RlY29yYXRvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLFVBQVUsZUFBZSxDQUM3QixhQUF5RztJQUV6RyxPQUFPLFNBQVMsUUFBUSxDQUFDLEtBQXFCO1FBQzVDLE9BQU8sQ0FBQyxDQUFDLEVBQUUsRUFBRSxFQUFFLFVBQThCLEVBQXNCLEVBQUU7WUFDbkUsTUFBTSxFQUFFLEdBQUcsVUFBVSxDQUFDLEtBQUssQ0FBQztZQUM1QixVQUFVLENBQUMsS0FBSyxHQUFHLEtBQUssV0FBVyxHQUFHLElBQWU7Z0JBQ25ELE1BQU0sRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUF3QixDQUFDO2dCQUN6QyxLQUFLLE1BQU0sSUFBSSxJQUFJLEtBQUssRUFBRSxDQUFDO29CQUN6QixNQUFNLENBQUMsTUFBTSxFQUFFLE9BQU8sRUFBRSxXQUFXLENBQUMsR0FBRyxJQUFJLENBQUM7b0JBQzVDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxHQUFHLEVBQUUsSUFBSSxDQUFDLENBQUM7b0JBQ2hDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLENBQUM7b0JBQ3RELElBQUksQ0FBQyxLQUFLLElBQUksYUFBYSxFQUFFLENBQUM7d0JBQzVCLE9BQU8sYUFBYSxDQUFDLEdBQUcsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLFdBQVcsQ0FBQyxDQUFDO29CQUN2RCxDQUFDO2dCQUNILENBQUM7Z0JBQ0QsT0FBTyxNQUFNLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDO1lBQ3BDLENBQUMsQ0FBQztZQUNGLE9BQU8sVUFBVSxDQUFDO1FBQ3BCLENBQUMsQ0FBQztJQUNKLENBQUMsQ0FBQztBQUNKLENBQUM7QUFFRCxNQUFNLENBQUMsTUFBTSxRQUFRLEdBQUcsZUFBZSxDQUFDLENBQUMsR0FBRyxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLEVBQUU7SUFDekUsTUFBTSxHQUFHLEdBQUcsR0FBRyxDQUFDLEdBQUcsQ0FBQztJQUNwQixNQUFNLE9BQU8sR0FBRyxXQUFXLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxDQUFDLEdBQUcsQ0FBQyxNQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsbUJBQW1CLENBQUM7SUFDdEYsR0FBRyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsT0FBTyxFQUFFO1FBQ3RCLElBQUksRUFBRSxlQUFlO1FBQ3JCLFNBQVMsRUFBRSxJQUFJO1FBQ2YsYUFBYSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDO1FBQ3JDLE1BQU0sRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLE1BQU07S0FDdkIsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMifQ==
@@ -0,0 +1,2 @@
1
+ import './types.ts';
2
+ export { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import "./types.js";
2
+ export { Ajv2019 as Ajv } from 'ajv/dist/2019.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxZQUFZLENBQUM7QUFFcEIsT0FBTyxFQUFFLE9BQU8sSUFBSSxHQUFHLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQyJ9
@@ -0,0 +1 @@
1
+ export * from '@sinclair/typebox';
@@ -0,0 +1,2 @@
1
+ export * from '@sinclair/typebox';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZWJveC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy90eXBlYm94LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsbUJBQW1CLENBQUMifQ==
@@ -0,0 +1,3 @@
1
+ import './config/config.default.ts';
2
+ import './app.ts';
3
+ import './app/extend/context.ts';
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ import "./config/config.default.js";
2
+ import "./app.js";
3
+ import "./app/extend/context.js";
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyw0QkFBNEIsQ0FBQztBQUNwQyxPQUFPLFVBQVUsQ0FBQztBQUNsQixPQUFPLHlCQUF5QixDQUFDIn0=
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@eggjs/typebox-validate",
3
+ "version": "0.0.0-beta.1",
4
+ "description": "another validate for typescript egg projects",
5
+ "type": "module",
6
+ "eggPlugin": {
7
+ "name": "typeboxValidate"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./app": "./dist/app.js",
12
+ "./app/extend/context": "./dist/app/extend/context.js",
13
+ "./config/config.default": "./dist/config/config.default.js",
14
+ "./decorator": "./dist/decorator.js",
15
+ "./typebox": "./dist/typebox.js",
16
+ "./types": "./dist/types.js",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "keywords": [
26
+ "egg",
27
+ "eggjs",
28
+ "validate",
29
+ "ajv",
30
+ "typebox",
31
+ "eggPlugin",
32
+ "egg-plugin"
33
+ ],
34
+ "license": "MIT",
35
+ "homepage": "https://github.com/eggjs/egg/tree/next/plugins/typebox-validate",
36
+ "bugs": {
37
+ "url": "https://github.com/eggjs/egg/issues"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/eggjs/egg.git",
42
+ "directory": "plugins/typebox-validate"
43
+ },
44
+ "author": "xiekw2010",
45
+ "engines": {
46
+ "node": ">= 22.18.0"
47
+ },
48
+ "dependencies": {
49
+ "@sinclair/typebox": "^0.23.0",
50
+ "ajv": "^8.8.2",
51
+ "ajv-formats": "^2.1.1",
52
+ "ajv-keywords": "^5.1.0"
53
+ },
54
+ "peerDependencies": {
55
+ "egg": "4.1.0-beta.22"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^24.6.2",
59
+ "oxlint": "^1.19.0",
60
+ "rimraf": "^6.0.1",
61
+ "semver": "^7.7.2",
62
+ "tsdown": "^0.15.4",
63
+ "typescript": "^5.9.3",
64
+ "vitest": "4.0.0-beta.16",
65
+ "@eggjs/tsconfig": "3.1.0-beta.22",
66
+ "@eggjs/mock": "7.0.0-beta.22",
67
+ "egg": "4.1.0-beta.22"
68
+ },
69
+ "main": "./dist/index.js",
70
+ "module": "./dist/index.js",
71
+ "types": "./dist/index.d.ts",
72
+ "scripts": {
73
+ "build": "tsdown && rimraf dist *.tsbuildinfo && tsc -p tsconfig.build.json",
74
+ "clean": "rimraf dist",
75
+ "typecheck": "tsc --noEmit",
76
+ "lint": "oxlint --type-aware",
77
+ "test": "vitest run"
78
+ }
79
+ }