next-openapi-gen 0.7.11 → 0.8.0

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 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,16 @@ 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
+
730
837
  ## License
731
838
 
732
839
  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 call expression with .extend()
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.7.11",
3
+ "version": "0.8.0",
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",