@rx-ted/packages-honest-plugins 1.0.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/CHANGELOG.md +13 -0
- package/README.md +45 -0
- package/api-doc/.turbo/turbo-build.log +1 -0
- package/api-doc/.turbo/turbo-test.log +11 -0
- package/api-doc/.turbo/turbo-typecheck.log +1 -0
- package/api-doc/CHANGELOG.md +22 -0
- package/api-doc/README.md +80 -0
- package/api-doc/dist/api-doc.plugin.d.ts +53 -0
- package/api-doc/dist/api-doc.plugin.js +452 -0
- package/api-doc/dist/index.d.ts +5 -0
- package/api-doc/dist/index.js +3 -0
- package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
- package/api-doc/dist/interfaces/renderer.interface.js +1 -0
- package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/scalar.renderer.js +76 -0
- package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/swagger.renderer.js +83 -0
- package/api-doc/node_modules/.bin/esbuild +22 -0
- package/api-doc/node_modules/.bin/jiti +22 -0
- package/api-doc/node_modules/.bin/sass +22 -0
- package/api-doc/node_modules/.bin/tsx +22 -0
- package/api-doc/node_modules/.bin/vite +22 -0
- package/api-doc/node_modules/.bin/vitest +22 -0
- package/api-doc/node_modules/.bin/yaml +22 -0
- package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/api-doc/package.json +48 -0
- package/api-doc/src/api-doc.plugin.ts +554 -0
- package/api-doc/src/index.ts +5 -0
- package/api-doc/src/interfaces/renderer.interface.ts +5 -0
- package/api-doc/src/plugin.test.ts +740 -0
- package/api-doc/src/renderers/scalar.renderer.ts +82 -0
- package/api-doc/src/renderers/swagger.renderer.ts +89 -0
- package/api-doc/tsconfig.json +21 -0
- package/api-doc/vitest.config.ts +13 -0
- package/cache/.turbo/turbo-build.log +1 -0
- package/cache/.turbo/turbo-test.log +16 -0
- package/cache/CHANGELOG.md +23 -0
- package/cache/README.md +79 -0
- package/cache/dist/cache-service.d.ts +8 -0
- package/cache/dist/cache-service.js +21 -0
- package/cache/dist/cacheable.d.ts +2 -0
- package/cache/dist/cacheable.js +10 -0
- package/cache/dist/cloudflare/driver.d.ts +23 -0
- package/cache/dist/cloudflare/driver.js +102 -0
- package/cache/dist/cloudflare/index.d.ts +3 -0
- package/cache/dist/cloudflare/index.js +2 -0
- package/cache/dist/cloudflare/plugin.d.ts +19 -0
- package/cache/dist/cloudflare/plugin.js +58 -0
- package/cache/dist/index.d.ts +11 -0
- package/cache/dist/index.js +14 -0
- package/cache/dist/local/driver.d.ts +2 -0
- package/cache/dist/local/driver.js +98 -0
- package/cache/dist/local/index.d.ts +2 -0
- package/cache/dist/local/index.js +2 -0
- package/cache/dist/local/plugin.d.ts +14 -0
- package/cache/dist/local/plugin.js +24 -0
- package/cache/dist/redis/driver.d.ts +10 -0
- package/cache/dist/redis/driver.js +102 -0
- package/cache/dist/redis/index.d.ts +4 -0
- package/cache/dist/redis/index.js +2 -0
- package/cache/dist/redis/plugin.d.ts +25 -0
- package/cache/dist/redis/plugin.js +61 -0
- package/cache/dist/resolve.d.ts +24 -0
- package/cache/dist/resolve.js +114 -0
- package/cache/dist/types.d.ts +18 -0
- package/cache/dist/types.js +1 -0
- package/cache/node_modules/.bin/esbuild +22 -0
- package/cache/node_modules/.bin/jiti +22 -0
- package/cache/node_modules/.bin/sass +22 -0
- package/cache/node_modules/.bin/tsx +22 -0
- package/cache/node_modules/.bin/vite +22 -0
- package/cache/node_modules/.bin/vitest +22 -0
- package/cache/node_modules/.bin/yaml +22 -0
- package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/cache/package.json +46 -0
- package/cache/src/cache-service.test.ts +16 -0
- package/cache/src/cache-service.ts +15 -0
- package/cache/src/cacheable.test.ts +78 -0
- package/cache/src/cacheable.ts +16 -0
- package/cache/src/cloudflare/driver.test.ts +188 -0
- package/cache/src/cloudflare/driver.ts +118 -0
- package/cache/src/cloudflare/index.ts +3 -0
- package/cache/src/cloudflare/plugin.ts +71 -0
- package/cache/src/index.test.ts +68 -0
- package/cache/src/index.ts +20 -0
- package/cache/src/local/driver.ts +117 -0
- package/cache/src/local/index.ts +2 -0
- package/cache/src/local/plugin.ts +34 -0
- package/cache/src/redis/driver.test.ts +71 -0
- package/cache/src/redis/driver.ts +124 -0
- package/cache/src/redis/index.ts +4 -0
- package/cache/src/redis/plugin.ts +84 -0
- package/cache/src/resolve.test.ts +257 -0
- package/cache/src/resolve.ts +140 -0
- package/cache/src/types.ts +14 -0
- package/cache/tsconfig.json +21 -0
- package/cache/vitest.config.ts +13 -0
- package/db/.turbo/turbo-build.log +1 -0
- package/db/.turbo/turbo-test.log +21 -0
- package/db/CHANGELOG.md +26 -0
- package/db/README.md +99 -0
- package/db/dist/constants.d.ts +1 -0
- package/db/dist/constants.js +1 -0
- package/db/dist/d1/index.d.ts +3 -0
- package/db/dist/d1/index.js +2 -0
- package/db/dist/d1/plugin.d.ts +18 -0
- package/db/dist/d1/plugin.js +104 -0
- package/db/dist/db-service.d.ts +7 -0
- package/db/dist/db-service.js +21 -0
- package/db/dist/index.d.ts +15 -0
- package/db/dist/index.js +20 -0
- package/db/dist/mysql/client.d.ts +36 -0
- package/db/dist/mysql/client.js +84 -0
- package/db/dist/mysql/driver.d.ts +18 -0
- package/db/dist/mysql/driver.js +107 -0
- package/db/dist/mysql/index.d.ts +11 -0
- package/db/dist/mysql/index.js +11 -0
- package/db/dist/mysql/plugin.d.ts +51 -0
- package/db/dist/mysql/plugin.js +94 -0
- package/db/dist/mysql/types.d.ts +38 -0
- package/db/dist/mysql/types.js +1 -0
- package/db/dist/postgres/index.d.ts +2 -0
- package/db/dist/postgres/index.js +3 -0
- package/db/dist/repository.d.ts +14 -0
- package/db/dist/repository.js +79 -0
- package/db/dist/resolve.d.ts +6 -0
- package/db/dist/resolve.js +81 -0
- package/db/dist/schema-builder/column-builder.d.ts +26 -0
- package/db/dist/schema-builder/column-builder.js +53 -0
- package/db/dist/schema-builder/compile-d1.d.ts +5 -0
- package/db/dist/schema-builder/compile-d1.js +67 -0
- package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
- package/db/dist/schema-builder/compile-mysql.js +78 -0
- package/db/dist/schema-builder/index.d.ts +8 -0
- package/db/dist/schema-builder/index.js +22 -0
- package/db/dist/schema-builder/table-builder.d.ts +16 -0
- package/db/dist/schema-builder/table-builder.js +38 -0
- package/db/dist/schema-builder/types.d.ts +26 -0
- package/db/dist/schema-builder/types.js +1 -0
- package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
- package/db/dist/schema-builder/zod-bridge.js +53 -0
- package/db/dist/sqlite/index.d.ts +4 -0
- package/db/dist/sqlite/index.js +2 -0
- package/db/dist/sqlite/plugin.d.ts +17 -0
- package/db/dist/sqlite/plugin.js +33 -0
- package/db/dist/types.d.ts +42 -0
- package/db/dist/types.js +1 -0
- package/db/node_modules/.bin/esbuild +22 -0
- package/db/node_modules/.bin/jiti +22 -0
- package/db/node_modules/.bin/sass +22 -0
- package/db/node_modules/.bin/tsx +22 -0
- package/db/node_modules/.bin/vite +22 -0
- package/db/node_modules/.bin/vitest +22 -0
- package/db/node_modules/.bin/yaml +22 -0
- package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/db/package.json +59 -0
- package/db/src/constants.test.ts +8 -0
- package/db/src/constants.ts +1 -0
- package/db/src/d1/index.ts +13 -0
- package/db/src/d1/plugin.ts +135 -0
- package/db/src/db-service.ts +14 -0
- package/db/src/index.test.ts +63 -0
- package/db/src/index.ts +57 -0
- package/db/src/mysql/client.ts +112 -0
- package/db/src/mysql/driver.test.ts +190 -0
- package/db/src/mysql/driver.ts +116 -0
- package/db/src/mysql/index.ts +51 -0
- package/db/src/mysql/plugin.test.ts +228 -0
- package/db/src/mysql/plugin.ts +128 -0
- package/db/src/mysql/types.ts +32 -0
- package/db/src/postgres/index.ts +15 -0
- package/db/src/repository.test.ts +195 -0
- package/db/src/repository.ts +95 -0
- package/db/src/resolve.ts +105 -0
- package/db/src/schema-builder/column-builder.test.ts +55 -0
- package/db/src/schema-builder/column-builder.ts +87 -0
- package/db/src/schema-builder/compile-d1.test.ts +60 -0
- package/db/src/schema-builder/compile-d1.ts +70 -0
- package/db/src/schema-builder/compile-mysql.test.ts +64 -0
- package/db/src/schema-builder/compile-mysql.ts +102 -0
- package/db/src/schema-builder/index.test.ts +36 -0
- package/db/src/schema-builder/index.ts +41 -0
- package/db/src/schema-builder/table-builder.test.ts +55 -0
- package/db/src/schema-builder/table-builder.ts +50 -0
- package/db/src/schema-builder/types.test.ts +68 -0
- package/db/src/schema-builder/types.ts +30 -0
- package/db/src/schema-builder/zod-bridge.ts +89 -0
- package/db/src/sqlite/index.ts +14 -0
- package/db/src/sqlite/plugin.ts +46 -0
- package/db/src/types.ts +45 -0
- package/db/tsconfig.json +21 -0
- package/db/vitest.config.ts +13 -0
- package/mail/.turbo/turbo-build.log +1 -0
- package/mail/.turbo/turbo-test.log +12 -0
- package/mail/.turbo/turbo-typecheck.log +1 -0
- package/mail/CHANGELOG.md +25 -0
- package/mail/README.md +214 -0
- package/mail/dist/index.d.ts +4 -0
- package/mail/dist/index.js +2 -0
- package/mail/dist/plugin.d.ts +64 -0
- package/mail/dist/plugin.js +232 -0
- package/mail/dist/providers/brevo.provider.d.ts +15 -0
- package/mail/dist/providers/brevo.provider.js +75 -0
- package/mail/dist/providers/custom.provider.d.ts +9 -0
- package/mail/dist/providers/custom.provider.js +22 -0
- package/mail/dist/providers/resend.provider.d.ts +16 -0
- package/mail/dist/providers/resend.provider.js +77 -0
- package/mail/dist/providers/smtp.provider.d.ts +21 -0
- package/mail/dist/providers/smtp.provider.js +86 -0
- package/mail/dist/rate-limiter.d.ts +16 -0
- package/mail/dist/rate-limiter.js +49 -0
- package/mail/dist/types.d.ts +68 -0
- package/mail/dist/types.js +6 -0
- package/mail/node_modules/.bin/esbuild +22 -0
- package/mail/node_modules/.bin/jiti +22 -0
- package/mail/node_modules/.bin/sass +22 -0
- package/mail/node_modules/.bin/tsx +22 -0
- package/mail/node_modules/.bin/vite +22 -0
- package/mail/node_modules/.bin/vitest +22 -0
- package/mail/node_modules/.bin/yaml +22 -0
- package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/mail/package.json +52 -0
- package/mail/src/index.ts +4 -0
- package/mail/src/plugin.test.ts +219 -0
- package/mail/src/plugin.ts +313 -0
- package/mail/src/providers/brevo.provider.ts +78 -0
- package/mail/src/providers/custom.provider.ts +29 -0
- package/mail/src/providers/resend.provider.ts +74 -0
- package/mail/src/providers/smtp.provider.ts +85 -0
- package/mail/src/rate-limiter.test.ts +81 -0
- package/mail/src/rate-limiter.ts +54 -0
- package/mail/src/types.ts +81 -0
- package/mail/tsconfig.json +21 -0
- package/mail/vitest.config.ts +13 -0
- package/package.json +36 -0
- package/s3/.turbo/turbo-build.log +1 -0
- package/s3/.turbo/turbo-test.log +11 -0
- package/s3/CHANGELOG.md +20 -0
- package/s3/README.md +90 -0
- package/s3/dist/index.d.ts +6 -0
- package/s3/dist/index.js +4 -0
- package/s3/dist/s3-service.d.ts +7 -0
- package/s3/dist/s3-service.js +21 -0
- package/s3/dist/s3.driver.d.ts +6 -0
- package/s3/dist/s3.driver.js +221 -0
- package/s3/dist/s3.plugin.d.ts +24 -0
- package/s3/dist/s3.plugin.js +58 -0
- package/s3/dist/types.d.ts +30 -0
- package/s3/dist/types.js +1 -0
- package/s3/node_modules/.bin/esbuild +22 -0
- package/s3/node_modules/.bin/jiti +22 -0
- package/s3/node_modules/.bin/sass +22 -0
- package/s3/node_modules/.bin/tsx +22 -0
- package/s3/node_modules/.bin/vite +22 -0
- package/s3/node_modules/.bin/vitest +22 -0
- package/s3/node_modules/.bin/yaml +22 -0
- package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/s3/package.json +47 -0
- package/s3/src/index.test.ts +20 -0
- package/s3/src/index.ts +13 -0
- package/s3/src/s3-service.ts +14 -0
- package/s3/src/s3.driver.ts +259 -0
- package/s3/src/s3.plugin.ts +80 -0
- package/s3/src/types.ts +37 -0
- package/s3/tsconfig.json +21 -0
- package/s3/vitest.config.ts +13 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11bbae7: Remove defunct packages (web-admin, auth, http-client, search) and introduce barrel packages for consolidated exports. Migrate all internal imports from scoped plugin names (`@rx-ted/packages-honest-plugins-db`) to path-based barrel imports (`@rx-ted/packages-honest-plugins/db`). Reset all package versions to 1.0.0 with consolidated changelogs. Clean up obsolete documentation across the monorepo.
|
|
8
|
+
- Updated dependencies [11bbae7]
|
|
9
|
+
- @rx-ted/packages-honest-plugins-db@1.0.1
|
|
10
|
+
- @rx-ted/packages-honest-plugins-cache@1.0.1
|
|
11
|
+
- @rx-ted/packages-honest-plugins-mail@1.0.1
|
|
12
|
+
- @rx-ted/packages-honest-plugins-s3@1.0.1
|
|
13
|
+
- @rx-ted/packages-honest-plugins-api-doc@1.0.1
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins
|
|
2
|
+
|
|
3
|
+
HonestJS 插件集合 — db, cache, mail, s3, api-doc。
|
|
4
|
+
|
|
5
|
+
## 插件列表
|
|
6
|
+
|
|
7
|
+
| 插件 | 子包 | 用途 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `db` | `@rx-ted/packages-honest-plugins-db` | 数据库(Drizzle ORM + MySQL/SQLite/D1/PostgreSQL) |
|
|
10
|
+
| `cache` | `@rx-ted/packages-honest-plugins-cache` | 缓存(Redis/KV/Local) |
|
|
11
|
+
| `mail` | `@rx-ted/packages-honest-plugins-mail` | 邮件(Resend/Brevo/SMTP) |
|
|
12
|
+
| `s3` | `@rx-ted/packages-honest-plugins-s3` | 对象存储(AWS S3) |
|
|
13
|
+
| `api-doc` | `@rx-ted/packages-honest-plugins-api-doc` | OpenAPI 文档 + Scalar UI |
|
|
14
|
+
|
|
15
|
+
## 导入方式
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// 通过 barrel 导入
|
|
19
|
+
import { DbService } from '@rx-ted/packages-honest-plugins/db'
|
|
20
|
+
import { CacheService } from '@rx-ted/packages-honest-plugins/cache'
|
|
21
|
+
import { MailPlugin } from '@rx-ted/packages-honest-plugins/mail'
|
|
22
|
+
import { S3Plugin } from '@rx-ted/packages-honest-plugins/s3'
|
|
23
|
+
import { ApiDocPlugin } from '@rx-ted/packages-honest-plugins/api-doc'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 子包结构
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
packages/honest-plugins/
|
|
30
|
+
├── db/ @rx-ted/packages-honest-plugins-db (private)
|
|
31
|
+
├── cache/ @rx-ted/packages-honest-plugins-cache (private)
|
|
32
|
+
├── mail/ @rx-ted/packages-honest-plugins-mail (private)
|
|
33
|
+
├── s3/ @rx-ted/packages-honest-plugins-s3 (private)
|
|
34
|
+
└── api-doc/ @rx-ted/packages-honest-plugins-api-doc (private)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
子包为 `private: true`,仅通过 barrel 包 `@rx-ted/packages-honest-plugins` 发布。
|
|
38
|
+
|
|
39
|
+
## 开发
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm build # 构建所有插件
|
|
43
|
+
pnpm test # 运行所有测试
|
|
44
|
+
pnpm typecheck # 类型检查
|
|
45
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc -p tsconfig.json
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
$ vitest run
|
|
2
|
+
|
|
3
|
+
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.8 [39m[90m/Users/ben/projects/app/packages/honest-plugins/api-doc[39m
|
|
4
|
+
|
|
5
|
+
[32m✓[39m src/plugin.test.ts [2m([22m[2m45 tests[22m[2m)[22m[32m 296[2mms[22m[39m
|
|
6
|
+
|
|
7
|
+
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
8
|
+
[2m Tests [22m [1m[32m45 passed[39m[22m[90m (45)[39m
|
|
9
|
+
[2m Start at [22m 14:17:11
|
|
10
|
+
[2m Duration [22m 513ms[2m (transform 112ms, setup 0ms, import 159ms, tests 296ms, environment 0ms)[22m
|
|
11
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc -p tsconfig.json --noEmit
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-api-doc
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11bbae7: Remove defunct packages (web-admin, auth, http-client, search) and introduce barrel packages for consolidated exports. Migrate all internal imports from scoped plugin names (`@rx-ted/packages-honest-plugins-db`) to path-based barrel imports (`@rx-ted/packages-honest-plugins/db`). Reset all package versions to 1.0.0 with consolidated changelogs. Clean up obsolete documentation across the monorepo.
|
|
8
|
+
- Updated dependencies [11bbae7]
|
|
9
|
+
- @rx-ted/packages-core@1.0.1
|
|
10
|
+
- @rx-ted/packages-honest@1.0.1
|
|
11
|
+
|
|
12
|
+
## 1.0.0
|
|
13
|
+
|
|
14
|
+
Initial consolidated release.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- API documentation plugin for @rx-ted/packages-honest
|
|
19
|
+
- OpenAPI spec generation
|
|
20
|
+
- NestJS-compatible decorators: `@Headers`, `@Ip`, `@Session`, `@HostParam`, `@Next`
|
|
21
|
+
- `@Redirect(url, statusCode?)` method decorator
|
|
22
|
+
- DESIGN.md documentation
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-api-doc
|
|
2
|
+
|
|
3
|
+
OpenAPI documentation plugin for [@rx-ted/packages-honest](https://github.com/rx-ted/honest). Generates OpenAPI specs from Honest route metadata with Scalar and Swagger UI renderers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto-generate**: OpenAPI spec from Honest decorator metadata
|
|
8
|
+
- **Scalar UI**: modern interactive API documentation
|
|
9
|
+
- **Swagger UI**: classic Swagger documentation
|
|
10
|
+
- **Plugin-based**: integrates with Honest's plugin lifecycle
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @rx-ted/packages-honest-plugins-api-doc
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Peer dependencies: `@rx-ted/packages-honest`, `hono`, `reflect-metadata`
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Register as a plugin
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { ApiDocPlugin } from '@rx-ted/packages-honest-plugins/api-doc';
|
|
26
|
+
|
|
27
|
+
const plugin = new ApiDocPlugin({
|
|
28
|
+
title: 'My API',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
renderer: 'scalar', // 'scalar' | 'swagger'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { hono } = await Application.create(AppModule, {
|
|
34
|
+
plugins: [plugin],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Renderers
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { ScalarRenderer } from '@rx-ted/packages-honest-plugins/api-doc';
|
|
42
|
+
import { SwaggerRenderer } from '@rx-ted/packages-honest-plugins/api-doc';
|
|
43
|
+
|
|
44
|
+
// Scalar UI (default)
|
|
45
|
+
const scalar = new ScalarRenderer({ title: 'API Docs' });
|
|
46
|
+
|
|
47
|
+
// Swagger UI
|
|
48
|
+
const swagger = new SwaggerRenderer({ title: 'API Docs' });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Endpoints
|
|
52
|
+
|
|
53
|
+
Once registered, the plugin adds:
|
|
54
|
+
|
|
55
|
+
- `GET /openapi.json` — OpenAPI spec
|
|
56
|
+
- `GET /docs` — UI rendering (Scalar or Swagger)
|
|
57
|
+
|
|
58
|
+
## Types
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface ApiDocPluginOptions {
|
|
62
|
+
title?: string;
|
|
63
|
+
version?: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
renderer?: 'scalar' | 'swagger';
|
|
66
|
+
specPath?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface IDocRenderer {
|
|
70
|
+
render(spec: Record<string, unknown>): string;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Scripts
|
|
75
|
+
|
|
76
|
+
| Script | Description |
|
|
77
|
+
|--------|-------------|
|
|
78
|
+
| `pnpm test` | Run tests |
|
|
79
|
+
| `pnpm build` | Build |
|
|
80
|
+
| `pnpm typecheck` | Type check |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ILogger } from '@rx-ted/packages-core';
|
|
2
|
+
import type { ApiDocSecurityScheme, ApiDocSecurityRequirement, Application, HonestOptions, IPlugin } from '@rx-ted/packages-honest';
|
|
3
|
+
import type { Context, Hono } from 'hono';
|
|
4
|
+
import type { IDocRenderer } from './interfaces/renderer.interface';
|
|
5
|
+
type RendererName = 'scalar' | 'swagger';
|
|
6
|
+
export interface ApiDocPluginOptions {
|
|
7
|
+
specUrl?: string;
|
|
8
|
+
uiRoute?: string;
|
|
9
|
+
uiTitle?: string;
|
|
10
|
+
defaultRenderer?: RendererName;
|
|
11
|
+
renderers?: IDocRenderer[];
|
|
12
|
+
onRequest?: (c: Context) => void | Promise<void>;
|
|
13
|
+
securitySchemes?: Record<string, ApiDocSecurityScheme>;
|
|
14
|
+
security?: ApiDocSecurityRequirement[];
|
|
15
|
+
exclude?: string[];
|
|
16
|
+
excludeMethods?: string[];
|
|
17
|
+
excludeTags?: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function normalizeRoute(input: string): string;
|
|
20
|
+
export declare class ApiDocPlugin implements IPlugin {
|
|
21
|
+
readonly name = "api-doc-plugin";
|
|
22
|
+
readonly version = "1.0.0";
|
|
23
|
+
logger?: ILogger;
|
|
24
|
+
private readonly specUrl;
|
|
25
|
+
private readonly uiRoute;
|
|
26
|
+
private readonly uiTitle;
|
|
27
|
+
private readonly defaultRendererName;
|
|
28
|
+
private readonly onRequest?;
|
|
29
|
+
private readonly securitySchemes;
|
|
30
|
+
private readonly defaultSecurity?;
|
|
31
|
+
private readonly exclude?;
|
|
32
|
+
private readonly excludeMethods?;
|
|
33
|
+
private readonly excludeTags?;
|
|
34
|
+
private renderers;
|
|
35
|
+
private spec;
|
|
36
|
+
private routingPrefix?;
|
|
37
|
+
private routingVersion?;
|
|
38
|
+
private app?;
|
|
39
|
+
constructor(options?: ApiDocPluginOptions);
|
|
40
|
+
private applyPrefixToUrl;
|
|
41
|
+
setRoutingOptions(options?: HonestOptions['routing']): void;
|
|
42
|
+
registerRenderer(renderer: IDocRenderer): void;
|
|
43
|
+
getRenderer(name: string): IDocRenderer | undefined;
|
|
44
|
+
beforeModulesRegistered: (app: Application, hono: Hono) => Promise<void>;
|
|
45
|
+
afterModulesRegistered: (app: Application, hono: Hono) => Promise<void>;
|
|
46
|
+
private generateSpec;
|
|
47
|
+
private convertHonoPathToOpenAPI;
|
|
48
|
+
private isPathExcluded;
|
|
49
|
+
private toOpenAPISchema;
|
|
50
|
+
private zodToOpenAPI;
|
|
51
|
+
private runHook;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { ENV_SYMBOL } from '@rx-ted/packages-core';
|
|
2
|
+
import { ComponentManager, VERSION_NEUTRAL, normalizePath, resolvePluginLogger, } from '@rx-ted/packages-honest';
|
|
3
|
+
import { ScalarRenderer } from './renderers/scalar.renderer';
|
|
4
|
+
import { SwaggerRenderer } from './renderers/swagger.renderer';
|
|
5
|
+
export function normalizeRoute(input) {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
return '/';
|
|
9
|
+
let normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
10
|
+
if (normalized.length > 1) {
|
|
11
|
+
normalized = normalized.replace(/\/+$/g, '');
|
|
12
|
+
}
|
|
13
|
+
return normalized || '/';
|
|
14
|
+
}
|
|
15
|
+
const DEFAULT_SECURITY_SCHEMES = {
|
|
16
|
+
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
|
17
|
+
basicAuth: { type: 'http', scheme: 'basic' },
|
|
18
|
+
apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
|
|
19
|
+
cookieAuth: { type: 'apiKey', in: 'cookie', name: 'token' },
|
|
20
|
+
};
|
|
21
|
+
export class ApiDocPlugin {
|
|
22
|
+
name = 'api-doc-plugin';
|
|
23
|
+
version = '1.0.0';
|
|
24
|
+
logger;
|
|
25
|
+
specUrl = '/openapi.json';
|
|
26
|
+
uiRoute = '/docs';
|
|
27
|
+
uiTitle = 'API Documentation';
|
|
28
|
+
defaultRendererName = 'scalar';
|
|
29
|
+
onRequest;
|
|
30
|
+
securitySchemes = {
|
|
31
|
+
...DEFAULT_SECURITY_SCHEMES,
|
|
32
|
+
};
|
|
33
|
+
defaultSecurity;
|
|
34
|
+
exclude;
|
|
35
|
+
excludeMethods;
|
|
36
|
+
excludeTags;
|
|
37
|
+
renderers = new Map();
|
|
38
|
+
spec = null;
|
|
39
|
+
routingPrefix;
|
|
40
|
+
routingVersion;
|
|
41
|
+
app;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
if (options.specUrl !== undefined)
|
|
44
|
+
this.specUrl = options.specUrl;
|
|
45
|
+
if (options.uiRoute !== undefined)
|
|
46
|
+
this.uiRoute = normalizeRoute(options.uiRoute);
|
|
47
|
+
if (options.uiTitle !== undefined)
|
|
48
|
+
this.uiTitle = options.uiTitle;
|
|
49
|
+
if (options.defaultRenderer !== undefined)
|
|
50
|
+
this.defaultRendererName = options.defaultRenderer;
|
|
51
|
+
if (options.onRequest !== undefined)
|
|
52
|
+
this.onRequest = options.onRequest;
|
|
53
|
+
if (options.securitySchemes !== undefined)
|
|
54
|
+
this.securitySchemes = options.securitySchemes;
|
|
55
|
+
if (options.security !== undefined)
|
|
56
|
+
this.defaultSecurity = options.security;
|
|
57
|
+
if (options.exclude !== undefined)
|
|
58
|
+
this.exclude = options.exclude;
|
|
59
|
+
if (options.excludeMethods !== undefined)
|
|
60
|
+
this.excludeMethods = options.excludeMethods;
|
|
61
|
+
if (options.excludeTags !== undefined)
|
|
62
|
+
this.excludeTags = options.excludeTags;
|
|
63
|
+
if (options.renderers !== undefined) {
|
|
64
|
+
options.renderers.forEach((r) => this.renderers.set(r.name, r));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
[new ScalarRenderer(), new SwaggerRenderer()].forEach((r) => this.renderers.set(r.name, r));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
applyPrefixToUrl(url) {
|
|
71
|
+
if (!this.routingPrefix && !this.routingVersion) {
|
|
72
|
+
return url;
|
|
73
|
+
}
|
|
74
|
+
const prefix = this.routingPrefix ? normalizePath(`/${this.routingPrefix}`) : '';
|
|
75
|
+
let versionSegment = '';
|
|
76
|
+
if (this.routingVersion !== undefined &&
|
|
77
|
+
this.routingVersion !== null &&
|
|
78
|
+
this.routingVersion !== VERSION_NEUTRAL &&
|
|
79
|
+
!Array.isArray(this.routingVersion)) {
|
|
80
|
+
versionSegment = `/v${this.routingVersion}`;
|
|
81
|
+
}
|
|
82
|
+
const base = `${prefix}${versionSegment}`;
|
|
83
|
+
if (!base)
|
|
84
|
+
return url;
|
|
85
|
+
const urlPath = url.startsWith('/') ? url : `/${url}`;
|
|
86
|
+
return normalizePath(`${base}${urlPath}`);
|
|
87
|
+
}
|
|
88
|
+
setRoutingOptions(options) {
|
|
89
|
+
if (options) {
|
|
90
|
+
this.routingPrefix = options.prefix;
|
|
91
|
+
this.routingVersion = options.version;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
registerRenderer(renderer) {
|
|
95
|
+
this.renderers.set(renderer.name, renderer);
|
|
96
|
+
}
|
|
97
|
+
getRenderer(name) {
|
|
98
|
+
return this.renderers.get(name);
|
|
99
|
+
}
|
|
100
|
+
beforeModulesRegistered = async (app, hono) => {
|
|
101
|
+
this.logger ??= resolvePluginLogger(this.name);
|
|
102
|
+
if (!ComponentManager.hasPlugin(ENV_SYMBOL)) {
|
|
103
|
+
this.logger.warn('ApiDoc disabled (env not configured)');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const envInstance = ComponentManager.getPlugin(ENV_SYMBOL);
|
|
107
|
+
if (!envInstance.DEBUG) {
|
|
108
|
+
this.logger.warn('ApiDoc disabled in production');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const routingOptions = app.getRoutingOptions();
|
|
113
|
+
if (routingOptions) {
|
|
114
|
+
this.setRoutingOptions(routingOptions);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore if options are not accessible
|
|
119
|
+
}
|
|
120
|
+
const specUrlWithPrefix = this.applyPrefixToUrl(this.specUrl);
|
|
121
|
+
const uiRouteWithPrefix = this.applyPrefixToUrl(this.uiRoute);
|
|
122
|
+
// Register callback to print doc URLs when server starts
|
|
123
|
+
if (app.registerOnStartCallback) {
|
|
124
|
+
app.registerOnStartCallback((port) => {
|
|
125
|
+
const base = `http://localhost:${port}`;
|
|
126
|
+
this.logger?.info(`Docs: ${base}${uiRouteWithPrefix}`);
|
|
127
|
+
this.logger?.info(`OpenAPI JSON: ${base}${specUrlWithPrefix}`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
hono.get(specUrlWithPrefix, async (c) => {
|
|
131
|
+
await this.runHook(c);
|
|
132
|
+
if (!this.spec) {
|
|
133
|
+
return c.json({ status: 500, code: 'ERROR', data: null, error: 'API spec not generated yet' }, 500);
|
|
134
|
+
}
|
|
135
|
+
return c.json({ status: 200, code: 'OK', data: this.spec });
|
|
136
|
+
});
|
|
137
|
+
hono.get(uiRouteWithPrefix, async (c) => {
|
|
138
|
+
await this.runHook(c);
|
|
139
|
+
const renderer = this.renderers.get(this.defaultRendererName);
|
|
140
|
+
if (!renderer) {
|
|
141
|
+
return c.json({ error: `Default renderer '${this.defaultRendererName}' not found` }, 500);
|
|
142
|
+
}
|
|
143
|
+
return c.html(renderer.renderHtml({
|
|
144
|
+
specUrl: specUrlWithPrefix,
|
|
145
|
+
uiTitle: this.uiTitle,
|
|
146
|
+
}));
|
|
147
|
+
});
|
|
148
|
+
hono.get(`${uiRouteWithPrefix}/:renderer`, async (c) => {
|
|
149
|
+
await this.runHook(c);
|
|
150
|
+
const rendererName = c.req.param('renderer');
|
|
151
|
+
if (!rendererName)
|
|
152
|
+
return c.json({
|
|
153
|
+
error: `Renderer '${rendererName}' not found`,
|
|
154
|
+
available: Array.from(this.renderers.keys()),
|
|
155
|
+
}, 404);
|
|
156
|
+
const renderer = this.renderers.get(rendererName);
|
|
157
|
+
if (!renderer) {
|
|
158
|
+
return c.json({
|
|
159
|
+
error: `Renderer '${rendererName}' not found`,
|
|
160
|
+
available: Array.from(this.renderers.keys()),
|
|
161
|
+
}, 404);
|
|
162
|
+
}
|
|
163
|
+
return c.html(renderer.renderHtml({
|
|
164
|
+
specUrl: specUrlWithPrefix,
|
|
165
|
+
uiTitle: this.uiTitle,
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
afterModulesRegistered = async (app, hono) => {
|
|
170
|
+
this.app = app;
|
|
171
|
+
this.spec = this.generateSpec();
|
|
172
|
+
};
|
|
173
|
+
generateSpec() {
|
|
174
|
+
const paths = {};
|
|
175
|
+
const tags = [];
|
|
176
|
+
if (!this.app) {
|
|
177
|
+
return {
|
|
178
|
+
openapi: '3.0.0',
|
|
179
|
+
info: {
|
|
180
|
+
title: this.uiTitle,
|
|
181
|
+
version: '1.0.0',
|
|
182
|
+
description: 'API Documentation',
|
|
183
|
+
},
|
|
184
|
+
paths: {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const routes = this.app.getRoutes();
|
|
188
|
+
const controllerTags = new Map();
|
|
189
|
+
// Collect all controller tags first
|
|
190
|
+
for (const route of routes) {
|
|
191
|
+
if (route.controllerClass) {
|
|
192
|
+
const tag = Reflect.getMetadata('api:controller:tag', route.controllerClass);
|
|
193
|
+
if (tag && !controllerTags.has(route.controller)) {
|
|
194
|
+
controllerTags.set(route.controller, tag);
|
|
195
|
+
tags.push({
|
|
196
|
+
name: tag.name,
|
|
197
|
+
description: tag.description ?? `${tag.name} endpoints`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const route of routes) {
|
|
203
|
+
let fullPath = route.fullPath;
|
|
204
|
+
if (!fullPath)
|
|
205
|
+
continue;
|
|
206
|
+
// Skip excluded paths
|
|
207
|
+
if (this.exclude && this.isPathExcluded(fullPath, this.exclude)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Skip excluded methods
|
|
211
|
+
if (this.excludeMethods?.map((m) => m.toLowerCase()).includes(route.method.toLowerCase())) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Convert Hono :param syntax to OpenAPI {param} syntax
|
|
215
|
+
fullPath = this.convertHonoPathToOpenAPI(fullPath);
|
|
216
|
+
const pathItem = paths[fullPath] ?? {};
|
|
217
|
+
const apiDoc = route.controllerClass
|
|
218
|
+
? Reflect.getMetadata('api:method:apiDoc', route.controllerClass.prototype, route.handler)
|
|
219
|
+
: undefined;
|
|
220
|
+
const controllerTag = controllerTags.get(route.controller);
|
|
221
|
+
// Skip excluded tags
|
|
222
|
+
const tagName = apiDoc?.tags ?? (controllerTag ? [controllerTag.name] : ['Default']);
|
|
223
|
+
if (this.excludeTags && tagName.some((tag) => this.excludeTags.includes(tag))) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const operation = {
|
|
227
|
+
...(apiDoc?.summary && { summary: apiDoc.summary }),
|
|
228
|
+
...(apiDoc?.description && { description: apiDoc.description }),
|
|
229
|
+
tags: tagName,
|
|
230
|
+
// Add security requirements
|
|
231
|
+
...(apiDoc?.security
|
|
232
|
+
? { security: apiDoc.security }
|
|
233
|
+
: this.defaultSecurity
|
|
234
|
+
? { security: this.defaultSecurity }
|
|
235
|
+
: this.securitySchemes
|
|
236
|
+
? { security: Object.keys(this.securitySchemes).map((n) => ({ [n]: [] })) }
|
|
237
|
+
: {}),
|
|
238
|
+
};
|
|
239
|
+
// Handle parameters (path params from Zod schema + query params)
|
|
240
|
+
const parameters = [];
|
|
241
|
+
// Add path parameters from apiDoc.request.params
|
|
242
|
+
if (apiDoc?.request?.params) {
|
|
243
|
+
const paramsSchema = this.toOpenAPISchema(apiDoc.request.params);
|
|
244
|
+
if (paramsSchema.properties) {
|
|
245
|
+
for (const [name, schema] of Object.entries(paramsSchema.properties)) {
|
|
246
|
+
parameters.push({
|
|
247
|
+
name,
|
|
248
|
+
in: 'path',
|
|
249
|
+
required: true,
|
|
250
|
+
schema,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Add query parameters from apiDoc.request.query
|
|
256
|
+
if (apiDoc?.request?.query) {
|
|
257
|
+
const querySchema = this.toOpenAPISchema(apiDoc.request.query);
|
|
258
|
+
if (querySchema.properties) {
|
|
259
|
+
for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
260
|
+
parameters.push({
|
|
261
|
+
name,
|
|
262
|
+
in: 'query',
|
|
263
|
+
required: querySchema.required?.includes(name) ?? false,
|
|
264
|
+
schema,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (parameters.length > 0) {
|
|
270
|
+
operation.parameters = parameters;
|
|
271
|
+
}
|
|
272
|
+
if (apiDoc?.request?.body) {
|
|
273
|
+
operation.requestBody = {
|
|
274
|
+
required: true,
|
|
275
|
+
content: {
|
|
276
|
+
'application/json': {
|
|
277
|
+
schema: this.toOpenAPISchema(apiDoc.request.body),
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (apiDoc?.responses) {
|
|
283
|
+
operation.responses = {};
|
|
284
|
+
for (const [statusStr, response] of Object.entries(apiDoc.responses)) {
|
|
285
|
+
const status = parseInt(statusStr, 10);
|
|
286
|
+
operation.responses[status] = {
|
|
287
|
+
description: response.description ?? 'Success',
|
|
288
|
+
...(response.schema && {
|
|
289
|
+
content: {
|
|
290
|
+
'application/json': {
|
|
291
|
+
schema: this.toOpenAPISchema(response.schema),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
pathItem[route.method] = operation;
|
|
299
|
+
paths[fullPath] = pathItem;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
openapi: '3.0.0',
|
|
303
|
+
info: {
|
|
304
|
+
title: this.uiTitle,
|
|
305
|
+
version: '1.0.0',
|
|
306
|
+
description: 'API Documentation',
|
|
307
|
+
},
|
|
308
|
+
paths,
|
|
309
|
+
...(tags.length > 0 && { tags }),
|
|
310
|
+
components: {
|
|
311
|
+
securitySchemes: this.securitySchemes,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
convertHonoPathToOpenAPI(honoPath) {
|
|
316
|
+
// Convert Hono :param syntax to OpenAPI {param} syntax
|
|
317
|
+
return honoPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
318
|
+
}
|
|
319
|
+
isPathExcluded(path, excludePatterns) {
|
|
320
|
+
return excludePatterns.some((pattern) => {
|
|
321
|
+
// Convert pattern to regex (support wildcard *)
|
|
322
|
+
const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`);
|
|
323
|
+
return regex.test(path);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
toOpenAPISchema(schema) {
|
|
327
|
+
if (!schema)
|
|
328
|
+
return { type: 'object' };
|
|
329
|
+
if (schema._def) {
|
|
330
|
+
return this.zodToOpenAPI(schema);
|
|
331
|
+
}
|
|
332
|
+
if (schema.type || schema.properties) {
|
|
333
|
+
return schema;
|
|
334
|
+
}
|
|
335
|
+
return { type: 'object' };
|
|
336
|
+
}
|
|
337
|
+
zodToOpenAPI(schema) {
|
|
338
|
+
if (!schema?._def)
|
|
339
|
+
return { type: 'object' };
|
|
340
|
+
const def = schema._def;
|
|
341
|
+
// Handle Zod 4.x new structure (type, innerType, etc.)
|
|
342
|
+
// Zod 4.x uses def.type property instead of typeName
|
|
343
|
+
const zodType = def.type || def.typeName;
|
|
344
|
+
switch (zodType) {
|
|
345
|
+
case 'string':
|
|
346
|
+
case 'ZodString': {
|
|
347
|
+
const stringMeta = { type: 'string' };
|
|
348
|
+
// Zod 3.x: checks in def.checks
|
|
349
|
+
if (def.checks) {
|
|
350
|
+
for (const check of def.checks) {
|
|
351
|
+
if (check.kind === 'min')
|
|
352
|
+
stringMeta.minLength = check.value;
|
|
353
|
+
if (check.kind === 'max')
|
|
354
|
+
stringMeta.maxLength = check.value;
|
|
355
|
+
if (check.kind === 'regex')
|
|
356
|
+
stringMeta.pattern = check.regex.source;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Zod 4.x: instance properties
|
|
360
|
+
if (schema.minLength != null)
|
|
361
|
+
stringMeta.minLength = schema.minLength;
|
|
362
|
+
if (schema.maxLength != null)
|
|
363
|
+
stringMeta.maxLength = schema.maxLength;
|
|
364
|
+
return stringMeta;
|
|
365
|
+
}
|
|
366
|
+
case 'number':
|
|
367
|
+
case 'ZodNumber': {
|
|
368
|
+
const numberMeta = { type: 'number' };
|
|
369
|
+
// Zod 3.x: checks in def.checks
|
|
370
|
+
if (def.checks) {
|
|
371
|
+
for (const check of def.checks) {
|
|
372
|
+
if (check.kind === 'min')
|
|
373
|
+
numberMeta.minimum = check.value;
|
|
374
|
+
if (check.kind === 'max')
|
|
375
|
+
numberMeta.maximum = check.value;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Zod 4.x: instance properties
|
|
379
|
+
if (schema.minValue != null && Number.isFinite(schema.minValue))
|
|
380
|
+
numberMeta.minimum = schema.minValue;
|
|
381
|
+
if (schema.maxValue != null && Number.isFinite(schema.maxValue))
|
|
382
|
+
numberMeta.maximum = schema.maxValue;
|
|
383
|
+
return numberMeta;
|
|
384
|
+
}
|
|
385
|
+
case 'boolean':
|
|
386
|
+
case 'ZodBoolean':
|
|
387
|
+
return { type: 'boolean' };
|
|
388
|
+
case 'array':
|
|
389
|
+
case 'ZodArray': {
|
|
390
|
+
// Zod 4.x: def.element, Zod 3.x: def.type (the schema, not a string)
|
|
391
|
+
const elementSchema = def.element ||
|
|
392
|
+
(typeof def.type !== 'string' ? def.type : null) ||
|
|
393
|
+
(def.typeName ? def : null);
|
|
394
|
+
return {
|
|
395
|
+
type: 'array',
|
|
396
|
+
items: elementSchema ? this.zodToOpenAPI(elementSchema) : { type: 'object' },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
case 'object':
|
|
400
|
+
case 'ZodObject': {
|
|
401
|
+
const properties = {};
|
|
402
|
+
const required = [];
|
|
403
|
+
// Zod 4.x: def.shape() or def.shape
|
|
404
|
+
// Zod 3.x: def.shape()
|
|
405
|
+
const shape = typeof def.shape === 'function' ? def.shape() : def.shape || {};
|
|
406
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
407
|
+
const field = value;
|
|
408
|
+
properties[key] = this.zodToOpenAPI(field);
|
|
409
|
+
// Check if field is required (not optional)
|
|
410
|
+
const fieldDef = field?._def;
|
|
411
|
+
const fieldType = fieldDef?.type || fieldDef?.typeName;
|
|
412
|
+
if (fieldType !== 'optional' && fieldType !== 'ZodOptional') {
|
|
413
|
+
required.push(key);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
type: 'object',
|
|
418
|
+
properties,
|
|
419
|
+
...(required.length > 0 && { required }),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case 'optional':
|
|
423
|
+
case 'ZodOptional':
|
|
424
|
+
// Zod 4.x: def.innerType, Zod 3.x: def.innerType or def.type
|
|
425
|
+
return this.zodToOpenAPI(def.innerType ?? def.type);
|
|
426
|
+
case 'enum':
|
|
427
|
+
case 'ZodEnum':
|
|
428
|
+
return {
|
|
429
|
+
type: 'string',
|
|
430
|
+
enum: def.values || (def.entries ? Object.values(def.entries) : []),
|
|
431
|
+
};
|
|
432
|
+
case 'null':
|
|
433
|
+
case 'ZodNull':
|
|
434
|
+
return { type: 'null' };
|
|
435
|
+
case 'union':
|
|
436
|
+
case 'ZodUnion':
|
|
437
|
+
if (def.options) {
|
|
438
|
+
return {
|
|
439
|
+
oneOf: def.options.map((opt) => this.zodToOpenAPI(opt)),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return { type: 'object' };
|
|
443
|
+
default:
|
|
444
|
+
return { type: 'object' };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async runHook(c) {
|
|
448
|
+
if (this.onRequest) {
|
|
449
|
+
await this.onRequest(c);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|