next-openapi-gen 0.7.11 → 0.8.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/README.md +123 -1
- package/dist/lib/drizzle-zod-processor.js +327 -0
- package/dist/lib/zod-converter.js +47 -1
- package/dist/types.js +1 -0
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@ Automatically generate OpenAPI 3.0 documentation from Next.js projects, with sup
|
|
|
6
6
|
|
|
7
7
|
- ✅ Automatic OpenAPI documentation generation from Next.js code
|
|
8
8
|
- ✅ Support for Next.js App Router (including `/api/users/[id]/route.ts` routes)
|
|
9
|
-
- ✅ Zod schemas support
|
|
10
9
|
- ✅ TypeScript types support
|
|
10
|
+
- ✅ Zod schemas support
|
|
11
|
+
- ✅ Drizzle-Zod support - Generate schemas from Drizzle ORM tables 🆕
|
|
11
12
|
- ✅ JSDoc comments support
|
|
12
13
|
- ✅ Multiple UI interfaces: `Scalar`, `Swagger`, `Redoc`, `Stoplight` and `Rapidoc` available at `/api-docs` url
|
|
13
14
|
- ✅ Path parameters detection (`/users/{id}`)
|
|
@@ -152,6 +153,44 @@ export async function GET(
|
|
|
152
153
|
}
|
|
153
154
|
```
|
|
154
155
|
|
|
156
|
+
### With Drizzle-Zod
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// src/db/schema.ts - Define your Drizzle table
|
|
160
|
+
import { pgTable, serial, varchar, text } from "drizzle-orm/pg-core";
|
|
161
|
+
|
|
162
|
+
export const posts = pgTable("posts", {
|
|
163
|
+
id: serial("id").primaryKey(),
|
|
164
|
+
title: varchar("title", { length: 255 }).notNull(),
|
|
165
|
+
content: text("content").notNull(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/schemas/post.ts - Generate Zod schema with drizzle-zod
|
|
169
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
170
|
+
import { posts } from "@/db/schema";
|
|
171
|
+
|
|
172
|
+
export const CreatePostSchema = createInsertSchema(posts, {
|
|
173
|
+
title: (schema) => schema.title.min(5).max(255).describe("Post title"),
|
|
174
|
+
content: (schema) => schema.content.min(10).describe("Post content"),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const PostResponseSchema = createSelectSchema(posts);
|
|
178
|
+
|
|
179
|
+
// src/app/api/posts/route.ts - Use in your API route
|
|
180
|
+
/**
|
|
181
|
+
* Create a new post
|
|
182
|
+
* @description Create a new blog post with Drizzle-Zod validation
|
|
183
|
+
* @body CreatePostSchema
|
|
184
|
+
* @response 201:PostResponseSchema
|
|
185
|
+
* @openapi
|
|
186
|
+
*/
|
|
187
|
+
export async function POST(request: NextRequest) {
|
|
188
|
+
const body = await request.json();
|
|
189
|
+
const validated = CreatePostSchema.parse(body);
|
|
190
|
+
// Implementation...
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
155
194
|
## JSDoc Documentation Tags
|
|
156
195
|
|
|
157
196
|
| Tag | Description |
|
|
@@ -694,6 +733,64 @@ type User = z.infer<typeof UserSchema>;
|
|
|
694
733
|
// The library will be able to recognize this schema by reference `UserSchema` or `User` type.
|
|
695
734
|
```
|
|
696
735
|
|
|
736
|
+
### Drizzle-Zod Support
|
|
737
|
+
|
|
738
|
+
The library fully supports **drizzle-zod** for generating Zod schemas from Drizzle ORM table definitions. This provides a single source of truth for your database schema, validation, and API documentation.
|
|
739
|
+
|
|
740
|
+
**Supported Functions:**
|
|
741
|
+
|
|
742
|
+
- `createInsertSchema()` - Generate schema for inserts
|
|
743
|
+
- `createSelectSchema()` - Generate schema for selects
|
|
744
|
+
- `createUpdateSchema()` - Generate schema for updates
|
|
745
|
+
|
|
746
|
+
**Features:**
|
|
747
|
+
|
|
748
|
+
- ✅ Automatic field extraction from refinements
|
|
749
|
+
- ✅ Validation method conversion (min, max, email, url, etc.)
|
|
750
|
+
- ✅ Optional/nullable field detection
|
|
751
|
+
- ✅ Intelligent type mapping based on field names
|
|
752
|
+
- ✅ Full OpenAPI schema generation
|
|
753
|
+
|
|
754
|
+
**Example:**
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
import { createInsertSchema } from "drizzle-zod";
|
|
758
|
+
import { posts } from "@/db/schema";
|
|
759
|
+
|
|
760
|
+
export const CreatePostSchema = createInsertSchema(posts, {
|
|
761
|
+
title: (schema) => schema.title.min(5).max(255),
|
|
762
|
+
content: (schema) => schema.content.min(10),
|
|
763
|
+
published: (schema) => schema.published.optional(),
|
|
764
|
+
});
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
See the [complete Drizzle-Zod example](./examples/next15-app-drizzle-zod) for a full working implementation with a blog API.
|
|
768
|
+
|
|
769
|
+
## Examples
|
|
770
|
+
|
|
771
|
+
This repository includes several complete example projects:
|
|
772
|
+
|
|
773
|
+
### 📦 Available Examples
|
|
774
|
+
|
|
775
|
+
| Example | Description | Features |
|
|
776
|
+
| --------------------------------------------------------------- | -------------------------- | ----------------------------------------------- |
|
|
777
|
+
| **[next15-app-zod](./examples/next15-app-zod)** | Zod schemas example | Users, Products, Orders API with Zod validation |
|
|
778
|
+
| **[next15-app-drizzle-zod](./examples/next15-app-drizzle-zod)** | Drizzle-Zod integration 🆕 | Blog API with Drizzle ORM + drizzle-zod |
|
|
779
|
+
| **[next15-app-typescript](./examples/next15-app-typescript)** | TypeScript types | API with pure TypeScript type definitions |
|
|
780
|
+
| **[next15-app-scalar](./examples/next15-app-scalar)** | Scalar UI | Modern API documentation interface |
|
|
781
|
+
| **[next15-app-swagger](./examples/next15-app-swagger)** | Swagger UI | Classic Swagger documentation |
|
|
782
|
+
|
|
783
|
+
### 🚀 Running Examples
|
|
784
|
+
|
|
785
|
+
```bash
|
|
786
|
+
cd examples/next15-app-drizzle-zod
|
|
787
|
+
npm install
|
|
788
|
+
npm run openapi:generate
|
|
789
|
+
npm run dev
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Visit `http://localhost:3000/api-docs` to see the generated documentation.
|
|
793
|
+
|
|
697
794
|
## Available UI providers
|
|
698
795
|
|
|
699
796
|
<div align="center">
|
|
@@ -727,6 +824,31 @@ type User = z.infer<typeof UserSchema>;
|
|
|
727
824
|
</table>
|
|
728
825
|
</div>
|
|
729
826
|
|
|
827
|
+
## Learn More
|
|
828
|
+
|
|
829
|
+
- **[Drizzle-Zod Example](./examples/next15-app-drizzle-zod)** - Complete example with Drizzle ORM integration
|
|
830
|
+
- **[Drizzle ORM](https://orm.drizzle.team/)** - TypeScript ORM for SQL databases
|
|
831
|
+
- **[drizzle-zod](https://orm.drizzle.team/docs/zod)** - Zod schema generator for Drizzle
|
|
832
|
+
- **[Zod Documentation](https://zod.dev/)** - TypeScript-first schema validation
|
|
833
|
+
- **[Next.js Documentation](https://nextjs.org/docs)** - React framework documentation
|
|
834
|
+
- **[OpenAPI Specification](https://swagger.io/specification/)** - OpenAPI 3.0 spec
|
|
835
|
+
- **[Scalar Documentation](https://docs.scalar.com/)** - Modern API documentation UI
|
|
836
|
+
|
|
837
|
+
## Contributing
|
|
838
|
+
|
|
839
|
+
We welcome contributions! 🎉
|
|
840
|
+
|
|
841
|
+
Please read our [Contributing Guide](CONTRIBUTING.md) for details on:
|
|
842
|
+
|
|
843
|
+
- 📝 Commit message guidelines (Conventional Commits)
|
|
844
|
+
- 🔧 Development setup
|
|
845
|
+
- ✅ Pull request process
|
|
846
|
+
- 🚀 Release workflow
|
|
847
|
+
|
|
848
|
+
## Changelog
|
|
849
|
+
|
|
850
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history and changes.
|
|
851
|
+
|
|
730
852
|
## License
|
|
731
853
|
|
|
732
854
|
MIT
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Processor for drizzle-zod schemas
|
|
5
|
+
*
|
|
6
|
+
* Drizzle-zod is a library that generates Zod schemas from Drizzle ORM table definitions.
|
|
7
|
+
* It provides helper functions like:
|
|
8
|
+
* - createInsertSchema(tableDefinition, refinements)
|
|
9
|
+
* - createSelectSchema(tableDefinition, refinements)
|
|
10
|
+
*
|
|
11
|
+
* This processor extracts field definitions and refinements to generate OpenAPI schemas.
|
|
12
|
+
*/
|
|
13
|
+
export class DrizzleZodProcessor {
|
|
14
|
+
/**
|
|
15
|
+
* Known drizzle-zod helper function names
|
|
16
|
+
*/
|
|
17
|
+
static DRIZZLE_ZOD_HELPERS = [
|
|
18
|
+
"createInsertSchema",
|
|
19
|
+
"createSelectSchema",
|
|
20
|
+
"createUpdateSchema",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Process a drizzle-zod schema node
|
|
24
|
+
*
|
|
25
|
+
* @param node - The CallExpression node representing a drizzle-zod function call
|
|
26
|
+
* @returns OpenAPI schema object
|
|
27
|
+
*/
|
|
28
|
+
static processSchema(node) {
|
|
29
|
+
const functionName = t.isIdentifier(node.callee)
|
|
30
|
+
? node.callee.name
|
|
31
|
+
: "unknown";
|
|
32
|
+
logger.debug(`Processing drizzle-zod schema: ${functionName}`);
|
|
33
|
+
const schema = {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {},
|
|
36
|
+
required: [],
|
|
37
|
+
};
|
|
38
|
+
// Check if there's a refinements object (second argument)
|
|
39
|
+
if (node.arguments.length > 1 && t.isObjectExpression(node.arguments[1])) {
|
|
40
|
+
const refinements = node.arguments[1];
|
|
41
|
+
// Process each property in the refinements object
|
|
42
|
+
refinements.properties.forEach((prop) => {
|
|
43
|
+
if (t.isObjectProperty(prop) || t.isObjectMethod(prop)) {
|
|
44
|
+
const key = this.extractPropertyKey(prop);
|
|
45
|
+
if (!key)
|
|
46
|
+
return;
|
|
47
|
+
// The value is typically an arrow function: (schema) => schema.field.method()
|
|
48
|
+
if (t.isObjectProperty(prop) &&
|
|
49
|
+
t.isArrowFunctionExpression(prop.value)) {
|
|
50
|
+
const arrowFunc = prop.value;
|
|
51
|
+
const fieldSchema = this.extractFieldSchema(arrowFunc.body);
|
|
52
|
+
if (fieldSchema) {
|
|
53
|
+
schema.properties[key] = fieldSchema;
|
|
54
|
+
// Determine if field is required based on schema modifiers
|
|
55
|
+
if (!this.isFieldOptional(arrowFunc.body)) {
|
|
56
|
+
schema.required.push(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// If no properties were extracted, return a generic object schema
|
|
64
|
+
if (Object.keys(schema.properties).length === 0) {
|
|
65
|
+
logger.debug("No properties extracted from drizzle-zod schema, returning generic object");
|
|
66
|
+
return { type: "object" };
|
|
67
|
+
}
|
|
68
|
+
return schema;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract property key from object property or method
|
|
72
|
+
*/
|
|
73
|
+
static extractPropertyKey(prop) {
|
|
74
|
+
if (t.isIdentifier(prop.key)) {
|
|
75
|
+
return prop.key.name;
|
|
76
|
+
}
|
|
77
|
+
if (t.isStringLiteral(prop.key)) {
|
|
78
|
+
return prop.key.value;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extract OpenAPI schema from a drizzle-zod field refinement
|
|
84
|
+
*
|
|
85
|
+
* Handles patterns like:
|
|
86
|
+
* - schema.field
|
|
87
|
+
* - schema.field.min(1)
|
|
88
|
+
* - schema.field.min(1).max(100).email()
|
|
89
|
+
*/
|
|
90
|
+
static extractFieldSchema(node) {
|
|
91
|
+
// Handle member expressions like: schema.field
|
|
92
|
+
if (t.isMemberExpression(node)) {
|
|
93
|
+
if (t.isIdentifier(node.property)) {
|
|
94
|
+
const fieldType = node.property.name;
|
|
95
|
+
return this.mapFieldTypeToOpenApi(fieldType);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Handle call expressions (chained methods like schema.field.min(1).max(100))
|
|
99
|
+
if (t.isCallExpression(node)) {
|
|
100
|
+
const baseSchema = this.extractFieldSchema(t.isMemberExpression(node.callee) ? node.callee.object : node);
|
|
101
|
+
if (baseSchema && t.isMemberExpression(node.callee)) {
|
|
102
|
+
const methodName = t.isIdentifier(node.callee.property)
|
|
103
|
+
? node.callee.property.name
|
|
104
|
+
: null;
|
|
105
|
+
if (methodName) {
|
|
106
|
+
return this.applyZodMethod(baseSchema, methodName, node.arguments);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return baseSchema;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if a drizzle-zod field is optional
|
|
115
|
+
*/
|
|
116
|
+
static isFieldOptional(node) {
|
|
117
|
+
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
|
|
118
|
+
const methodName = t.isIdentifier(node.callee.property)
|
|
119
|
+
? node.callee.property.name
|
|
120
|
+
: null;
|
|
121
|
+
if (methodName === "optional" ||
|
|
122
|
+
methodName === "nullable" ||
|
|
123
|
+
methodName === "nullish") {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Check parent chain recursively
|
|
127
|
+
return this.isFieldOptional(node.callee.object);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Map Drizzle field types to OpenAPI types
|
|
133
|
+
*
|
|
134
|
+
* This provides intelligent mapping based on common field naming patterns.
|
|
135
|
+
* For more accurate type detection, the drizzle table schema would need to be analyzed.
|
|
136
|
+
*/
|
|
137
|
+
static mapFieldTypeToOpenApi(fieldType) {
|
|
138
|
+
// Common mappings based on field naming conventions
|
|
139
|
+
const lowercaseField = fieldType.toLowerCase();
|
|
140
|
+
// String types
|
|
141
|
+
if (lowercaseField.includes("title") ||
|
|
142
|
+
lowercaseField.includes("name") ||
|
|
143
|
+
lowercaseField.includes("description") ||
|
|
144
|
+
lowercaseField.includes("content") ||
|
|
145
|
+
lowercaseField.includes("text") ||
|
|
146
|
+
lowercaseField.includes("slug") ||
|
|
147
|
+
lowercaseField.includes("email") ||
|
|
148
|
+
lowercaseField.includes("url") ||
|
|
149
|
+
lowercaseField.includes("phone")) {
|
|
150
|
+
const schema = { type: "string" };
|
|
151
|
+
// Add format hints
|
|
152
|
+
if (lowercaseField.includes("email")) {
|
|
153
|
+
schema.format = "email";
|
|
154
|
+
}
|
|
155
|
+
else if (lowercaseField.includes("url") ||
|
|
156
|
+
lowercaseField.includes("uri")) {
|
|
157
|
+
schema.format = "uri";
|
|
158
|
+
}
|
|
159
|
+
else if (lowercaseField.includes("uuid")) {
|
|
160
|
+
schema.format = "uuid";
|
|
161
|
+
}
|
|
162
|
+
return schema;
|
|
163
|
+
}
|
|
164
|
+
// Integer types
|
|
165
|
+
if (lowercaseField.includes("id") ||
|
|
166
|
+
lowercaseField.includes("count") ||
|
|
167
|
+
lowercaseField.includes("stock") ||
|
|
168
|
+
lowercaseField.includes("quantity") ||
|
|
169
|
+
lowercaseField.includes("age") ||
|
|
170
|
+
lowercaseField.includes("year")) {
|
|
171
|
+
return { type: "integer" };
|
|
172
|
+
}
|
|
173
|
+
// Number types
|
|
174
|
+
if (lowercaseField.includes("price") ||
|
|
175
|
+
lowercaseField.includes("amount") ||
|
|
176
|
+
lowercaseField.includes("rate") ||
|
|
177
|
+
lowercaseField.includes("percent")) {
|
|
178
|
+
return { type: "number" };
|
|
179
|
+
}
|
|
180
|
+
// Boolean types
|
|
181
|
+
if (lowercaseField.startsWith("is") ||
|
|
182
|
+
lowercaseField.startsWith("has") ||
|
|
183
|
+
lowercaseField.includes("active") ||
|
|
184
|
+
lowercaseField.includes("enabled") ||
|
|
185
|
+
lowercaseField.includes("published")) {
|
|
186
|
+
return { type: "boolean" };
|
|
187
|
+
}
|
|
188
|
+
// Date/time types
|
|
189
|
+
if (lowercaseField.includes("date") ||
|
|
190
|
+
lowercaseField.includes("time") ||
|
|
191
|
+
lowercaseField.includes("createdat") ||
|
|
192
|
+
lowercaseField.includes("updatedat") ||
|
|
193
|
+
lowercaseField.includes("deletedat")) {
|
|
194
|
+
return { type: "string", format: "date-time" };
|
|
195
|
+
}
|
|
196
|
+
// Default to string for unknown types
|
|
197
|
+
return { type: "string" };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Apply a Zod validation method to a schema
|
|
201
|
+
*
|
|
202
|
+
* Translates Zod validation methods to OpenAPI constraints:
|
|
203
|
+
* - min/max for strings become minLength/maxLength
|
|
204
|
+
* - min/max for numbers become minimum/maximum
|
|
205
|
+
* - email/url/uuid become format constraints
|
|
206
|
+
*/
|
|
207
|
+
static applyZodMethod(schema, methodName, args) {
|
|
208
|
+
const result = { ...schema };
|
|
209
|
+
switch (methodName) {
|
|
210
|
+
case "min":
|
|
211
|
+
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
212
|
+
if (schema.type === "string") {
|
|
213
|
+
result.minLength = args[0].value;
|
|
214
|
+
}
|
|
215
|
+
else if (schema.type === "number" || schema.type === "integer") {
|
|
216
|
+
result.minimum = args[0].value;
|
|
217
|
+
}
|
|
218
|
+
else if (schema.type === "array") {
|
|
219
|
+
result.minItems = args[0].value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case "max":
|
|
224
|
+
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
225
|
+
if (schema.type === "string") {
|
|
226
|
+
result.maxLength = args[0].value;
|
|
227
|
+
}
|
|
228
|
+
else if (schema.type === "number" || schema.type === "integer") {
|
|
229
|
+
result.maximum = args[0].value;
|
|
230
|
+
}
|
|
231
|
+
else if (schema.type === "array") {
|
|
232
|
+
result.maxItems = args[0].value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case "length":
|
|
237
|
+
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
238
|
+
if (schema.type === "string") {
|
|
239
|
+
result.minLength = args[0].value;
|
|
240
|
+
result.maxLength = args[0].value;
|
|
241
|
+
}
|
|
242
|
+
else if (schema.type === "array") {
|
|
243
|
+
result.minItems = args[0].value;
|
|
244
|
+
result.maxItems = args[0].value;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case "email":
|
|
249
|
+
result.format = "email";
|
|
250
|
+
break;
|
|
251
|
+
case "url":
|
|
252
|
+
result.format = "uri";
|
|
253
|
+
break;
|
|
254
|
+
case "uuid":
|
|
255
|
+
result.format = "uuid";
|
|
256
|
+
break;
|
|
257
|
+
case "datetime":
|
|
258
|
+
result.format = "date-time";
|
|
259
|
+
break;
|
|
260
|
+
case "regex":
|
|
261
|
+
if (args.length > 0) {
|
|
262
|
+
// Try to extract pattern from regex literal
|
|
263
|
+
if (t.isRegExpLiteral(args[0])) {
|
|
264
|
+
result.pattern = args[0].pattern;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
case "positive":
|
|
269
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
270
|
+
result.minimum = 0;
|
|
271
|
+
result.exclusiveMinimum = true;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case "nonnegative":
|
|
275
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
276
|
+
result.minimum = 0;
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case "negative":
|
|
280
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
281
|
+
result.maximum = 0;
|
|
282
|
+
result.exclusiveMaximum = true;
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
case "nonpositive":
|
|
286
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
287
|
+
result.maximum = 0;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
case "int":
|
|
291
|
+
result.type = "integer";
|
|
292
|
+
break;
|
|
293
|
+
case "optional":
|
|
294
|
+
case "nullable":
|
|
295
|
+
case "nullish":
|
|
296
|
+
// These are handled by the isFieldOptional check
|
|
297
|
+
// Don't modify the schema here
|
|
298
|
+
break;
|
|
299
|
+
case "describe":
|
|
300
|
+
if (args.length > 0 && t.isStringLiteral(args[0])) {
|
|
301
|
+
result.description = args[0].value;
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
case "default":
|
|
305
|
+
if (args.length > 0) {
|
|
306
|
+
// Extract default value
|
|
307
|
+
if (t.isStringLiteral(args[0])) {
|
|
308
|
+
result.default = args[0].value;
|
|
309
|
+
}
|
|
310
|
+
else if (t.isNumericLiteral(args[0])) {
|
|
311
|
+
result.default = args[0].value;
|
|
312
|
+
}
|
|
313
|
+
else if (t.isBooleanLiteral(args[0])) {
|
|
314
|
+
result.default = args[0].value;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if a function name is a drizzle-zod helper
|
|
323
|
+
*/
|
|
324
|
+
static isDrizzleZodHelper(name) {
|
|
325
|
+
return this.DRIZZLE_ZOD_HELPERS.includes(name);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -6,6 +6,7 @@ import * as t from "@babel/types";
|
|
|
6
6
|
const traverse = traverseModule.default || traverseModule;
|
|
7
7
|
import { parseTypeScriptFile } from "./utils.js";
|
|
8
8
|
import { logger } from "./logger.js";
|
|
9
|
+
import { DrizzleZodProcessor } from "./drizzle-zod-processor.js";
|
|
9
10
|
/**
|
|
10
11
|
* Class for converting Zod schemas to OpenAPI specifications
|
|
11
12
|
*/
|
|
@@ -15,6 +16,7 @@ export class ZodSchemaConverter {
|
|
|
15
16
|
processingSchemas = new Set();
|
|
16
17
|
processedModules = new Set();
|
|
17
18
|
typeToSchemaMapping = {};
|
|
19
|
+
drizzleZodImports = new Set();
|
|
18
20
|
constructor(schemaDir) {
|
|
19
21
|
this.schemaDir = path.resolve(schemaDir);
|
|
20
22
|
}
|
|
@@ -157,6 +159,15 @@ export class ZodSchemaConverter {
|
|
|
157
159
|
ImportDeclaration: (path) => {
|
|
158
160
|
// Keep track of imports to resolve external schemas
|
|
159
161
|
const source = path.node.source.value;
|
|
162
|
+
// Track drizzle-zod imports
|
|
163
|
+
if (source === "drizzle-zod") {
|
|
164
|
+
path.node.specifiers.forEach((specifier) => {
|
|
165
|
+
if (t.isImportSpecifier(specifier) ||
|
|
166
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
167
|
+
this.drizzleZodImports.add(specifier.local.name);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
160
171
|
// Process each import specifier
|
|
161
172
|
path.node.specifiers.forEach((specifier) => {
|
|
162
173
|
if (t.isImportSpecifier(specifier) ||
|
|
@@ -173,8 +184,17 @@ export class ZodSchemaConverter {
|
|
|
173
184
|
if (t.isIdentifier(declaration.id) &&
|
|
174
185
|
declaration.id.name === schemaName &&
|
|
175
186
|
declaration.init) {
|
|
176
|
-
// Check if this is a
|
|
187
|
+
// Check if this is a drizzle-zod helper function
|
|
177
188
|
if (t.isCallExpression(declaration.init) &&
|
|
189
|
+
t.isIdentifier(declaration.init.callee) &&
|
|
190
|
+
this.drizzleZodImports.has(declaration.init.callee.name)) {
|
|
191
|
+
const schema = this.processZodNode(declaration.init);
|
|
192
|
+
if (schema) {
|
|
193
|
+
this.zodSchemas[schemaName] = schema;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Check if this is a call expression with .extend()
|
|
197
|
+
else if (t.isCallExpression(declaration.init) &&
|
|
178
198
|
t.isMemberExpression(declaration.init.callee) &&
|
|
179
199
|
t.isIdentifier(declaration.init.callee.property) &&
|
|
180
200
|
declaration.init.callee.property.name === "extend") {
|
|
@@ -493,6 +513,12 @@ export class ZodSchemaConverter {
|
|
|
493
513
|
* Process a Zod node and convert it to OpenAPI schema
|
|
494
514
|
*/
|
|
495
515
|
processZodNode(node) {
|
|
516
|
+
// Handle drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
|
|
517
|
+
if (t.isCallExpression(node) &&
|
|
518
|
+
t.isIdentifier(node.callee) &&
|
|
519
|
+
this.drizzleZodImports.has(node.callee.name)) {
|
|
520
|
+
return DrizzleZodProcessor.processSchema(node);
|
|
521
|
+
}
|
|
496
522
|
// Handle reference to another schema (e.g. UserBaseSchema.extend)
|
|
497
523
|
if (t.isCallExpression(node) &&
|
|
498
524
|
t.isMemberExpression(node.callee) &&
|
|
@@ -1471,6 +1497,20 @@ export class ZodSchemaConverter {
|
|
|
1471
1497
|
try {
|
|
1472
1498
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
1473
1499
|
const ast = parseTypeScriptFile(content);
|
|
1500
|
+
// First, collect all drizzle-zod imports
|
|
1501
|
+
traverse(ast, {
|
|
1502
|
+
ImportDeclaration: (path) => {
|
|
1503
|
+
const source = path.node.source.value;
|
|
1504
|
+
if (source === "drizzle-zod") {
|
|
1505
|
+
path.node.specifiers.forEach((specifier) => {
|
|
1506
|
+
if (t.isImportSpecifier(specifier) ||
|
|
1507
|
+
t.isImportDefaultSpecifier(specifier)) {
|
|
1508
|
+
this.drizzleZodImports.add(specifier.local.name);
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
});
|
|
1474
1514
|
// Collect all exported Zod schemas
|
|
1475
1515
|
traverse(ast, {
|
|
1476
1516
|
ExportNamedDeclaration: (path) => {
|
|
@@ -1523,6 +1563,12 @@ export class ZodSchemaConverter {
|
|
|
1523
1563
|
*/
|
|
1524
1564
|
isZodSchema(node) {
|
|
1525
1565
|
if (t.isCallExpression(node)) {
|
|
1566
|
+
// Check for drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
|
|
1567
|
+
if (t.isIdentifier(node.callee) &&
|
|
1568
|
+
this.drizzleZodImports.has(node.callee.name)) {
|
|
1569
|
+
logger.debug(`[isZodSchema] Detected drizzle-zod function: ${node.callee.name}`);
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1526
1572
|
// Check direct z.method() calls
|
|
1527
1573
|
if (t.isMemberExpression(node.callee) &&
|
|
1528
1574
|
t.isIdentifier(node.callee.object) &&
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-openapi-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
21
|
"test:ui": "vitest --ui",
|
|
22
|
-
"test:coverage": "vitest run --coverage"
|
|
22
|
+
"test:coverage": "vitest run --coverage",
|
|
23
|
+
"release": "np --no-cleanup --no-tests",
|
|
24
|
+
"version": "conventional-changelog -p angular -i CHANGELOG.md -s -n .changelogrc.cjs && git add CHANGELOG.md"
|
|
23
25
|
},
|
|
24
26
|
"repository": {
|
|
25
27
|
"type": "git",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"docs",
|
|
37
39
|
"react",
|
|
38
40
|
"scalar",
|
|
39
|
-
"redoc"
|
|
41
|
+
"redoc",
|
|
42
|
+
"drizzle"
|
|
40
43
|
],
|
|
41
44
|
"publishConfig": {
|
|
42
45
|
"access": "public"
|
|
@@ -57,7 +60,18 @@
|
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@types/node": "^24.3.0",
|
|
59
62
|
"@vitest/ui": "^3.2.4",
|
|
63
|
+
"conventional-changelog-cli": "^5.0.0",
|
|
64
|
+
"np": "^10.2.0",
|
|
60
65
|
"typescript": "^5.9.2",
|
|
61
66
|
"vitest": "^3.2.4"
|
|
67
|
+
},
|
|
68
|
+
"np": {
|
|
69
|
+
"yarn": false,
|
|
70
|
+
"anyBranch": false,
|
|
71
|
+
"branch": "main",
|
|
72
|
+
"cleanup": false,
|
|
73
|
+
"tests": false,
|
|
74
|
+
"2fa": false,
|
|
75
|
+
"releaseDraft": false
|
|
62
76
|
}
|
|
63
77
|
}
|