diff --git a/README.md b/README.md index 21a5d02..a5d359d 100644 --- a/README.md +++ b/README.md @@ -690,6 +690,50 @@ export async function GET() { } ``` +### TypeScript Utility Types Support + +The library supports common TypeScript utility types including `Awaited` and `ReturnType`, allowing you to derive types from function implementations: + +```typescript +// src/app/api/users/route.utils.ts + +// Define your implementation with explicit return type +export const getUserData = async (id: string): Promise<{ + id: string; + name: string; + email: string; +}> => { + // Your implementation here + return { + id, + name: "John Doe", + email: "john@example.com", + }; +}; +``` + +```typescript +// src/app/api/users/[id]/route.ts + +import { getUserData } from "../route.utils"; + +// Derive the unwrapped response type from the async function +type UserResponse = Awaited>; + +/** + * Get user by ID + * @description Retrieve user information + * @response UserResponse + * @openapi + */ +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + const user = await getUserData(params.id); + return NextResponse.json(user); +} +``` + +**Note**: The function must have an explicit return type annotation for `ReturnType<>` to work correctly. + ### Intelligent Examples The library generates intelligent examples for parameters based on their name: @@ -847,6 +891,7 @@ Explore complete demo projects in the **[examples](./examples/)** directory, cov ### 🚀 Run an Example ```bash +npm run build cd examples/next15-app-zod npm install npx next-openapi-gen generate diff --git a/examples/next15-app-scalar/public/openapi.json b/examples/next15-app-scalar/public/openapi.json index eae6f93..b66a05c 100644 --- a/examples/next15-app-scalar/public/openapi.json +++ b/examples/next15-app-scalar/public/openapi.json @@ -99,43 +99,188 @@ } } }, - "CommentsResponse": { + "User": { "type": "object", "properties": { - "task": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - } - } + "id": { + "type": "string", + "description": "User ID" }, - "project": { + "name": { + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" + } + } + }, + "Attachment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Attachment ID" + }, + "fileName": { + "type": "string", + "description": "Original file name" + }, + "fileSize": { + "type": "number", + "description": "Size in bytes" + }, + "fileType": { + "type": "string", + "description": "MIME type" + }, + "url": { + "type": "string", + "description": "Download URL" + }, + "thumbnailUrl": { + "type": "string", + "description": "Thumbnail URL for images" + }, + "uploadedAt": { + "type": "string", + "format": "date-time", + "description": "When the file was uploaded" + } + } + }, + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Comment ID" + }, + "content": { + "type": "string", + "description": "Comment content" + }, + "author": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "User ID" }, "name": { - "type": "string" + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" } - } + }, + "description": "User who created the comment" }, - "organization": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Attachment ID" + }, + "fileName": { + "type": "string", + "description": "Original file name" + }, + "fileSize": { + "type": "number", + "description": "Size in bytes" + }, + "fileType": { + "type": "string", + "description": "MIME type" + }, + "url": { + "type": "string", + "description": "Download URL" + }, + "thumbnailUrl": { + "type": "string", + "description": "Thumbnail URL for images" + }, + "uploadedAt": { + "type": "string", + "format": "date-time", + "description": "When the file was uploaded" + } } - } + }, + "description": "Attached files" }, - "comments": { + "mentions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" + } + } + }, + "description": "Users mentioned in the comment" + }, + "likes": { + "type": "number", + "description": "Number of likes" + }, + "likedBy": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs who liked the comment" + }, + "replyTo": { + "type": "string", + "description": "Parent comment ID if this is a reply" + }, + "replies": { "type": "array", "items": { "type": "object", @@ -282,166 +427,62 @@ } } }, - "description": "List of comments" + "description": "Child comments (if includeReplies=true)" }, - "pagination": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + }, + "deletedAt": { + "type": "string", + "format": "date-time", + "description": "Soft deletion timestamp" + } + } + }, + "CommentsResponse": { + "type": "object", + "properties": { + "task": { "type": "object", "properties": { - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "limit": { - "type": "number" + "id": { + "type": "string" }, - "pages": { - "type": "number" + "title": { + "type": "string" } } }, - "permissions": { + "project": { "type": "object", "properties": { - "canCreate": { - "type": "boolean" - }, - "canEdit": { - "type": "boolean" - }, - "canDelete": { - "type": "boolean" + "id": { + "type": "string" }, - "canModerate": { - "type": "boolean" + "name": { + "type": "string" } } - } - } - }, - "Comment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" }, - "author": { + "organization": { "type": "object", "properties": { "id": { - "type": "string", - "description": "User ID" + "type": "string" }, "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "User ID" - }, - "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } + "type": "string" } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" + } }, - "replies": { + "comments": { "type": "array", "items": { "type": "object", @@ -588,82 +629,41 @@ } } }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "User ID" - }, - "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } - } - }, - "Attachment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" + "description": "List of comments" }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "limit": { + "type": "number" + }, + "pages": { + "type": "number" + } + } }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" + "permissions": { + "type": "object", + "properties": { + "canCreate": { + "type": "boolean" + }, + "canEdit": { + "type": "boolean" + }, + "canDelete": { + "type": "boolean" + }, + "canModerate": { + "type": "boolean" + } + } } } }, @@ -1067,6 +1067,23 @@ } } }, + "UserAddress": { + "type": "object", + "properties": { + "line1": { + "type": "string" + }, + "line2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "postalCode": { + "type": "string" + } + } + }, "UserResponse": { "type": "object", "properties": { @@ -1104,23 +1121,6 @@ } } } - }, - "UserAddress": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } } }, "responses": { diff --git a/examples/next15-app-swagger/public/openapi.json b/examples/next15-app-swagger/public/openapi.json index eae6f93..b66a05c 100644 --- a/examples/next15-app-swagger/public/openapi.json +++ b/examples/next15-app-swagger/public/openapi.json @@ -99,43 +99,188 @@ } } }, - "CommentsResponse": { + "User": { "type": "object", "properties": { - "task": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - } - } + "id": { + "type": "string", + "description": "User ID" }, - "project": { + "name": { + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" + } + } + }, + "Attachment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Attachment ID" + }, + "fileName": { + "type": "string", + "description": "Original file name" + }, + "fileSize": { + "type": "number", + "description": "Size in bytes" + }, + "fileType": { + "type": "string", + "description": "MIME type" + }, + "url": { + "type": "string", + "description": "Download URL" + }, + "thumbnailUrl": { + "type": "string", + "description": "Thumbnail URL for images" + }, + "uploadedAt": { + "type": "string", + "format": "date-time", + "description": "When the file was uploaded" + } + } + }, + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Comment ID" + }, + "content": { + "type": "string", + "description": "Comment content" + }, + "author": { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "User ID" }, "name": { - "type": "string" + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" } - } + }, + "description": "User who created the comment" }, - "organization": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Attachment ID" + }, + "fileName": { + "type": "string", + "description": "Original file name" + }, + "fileSize": { + "type": "number", + "description": "Size in bytes" + }, + "fileType": { + "type": "string", + "description": "MIME type" + }, + "url": { + "type": "string", + "description": "Download URL" + }, + "thumbnailUrl": { + "type": "string", + "description": "Thumbnail URL for images" + }, + "uploadedAt": { + "type": "string", + "format": "date-time", + "description": "When the file was uploaded" + } } - } + }, + "description": "Attached files" }, - "comments": { + "mentions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "description": "User's full name" + }, + "avatar": { + "type": "string", + "description": "URL to user's avatar" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "member", + "guest" + ], + "description": "User's role in the organization" + } + } + }, + "description": "Users mentioned in the comment" + }, + "likes": { + "type": "number", + "description": "Number of likes" + }, + "likedBy": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs who liked the comment" + }, + "replyTo": { + "type": "string", + "description": "Parent comment ID if this is a reply" + }, + "replies": { "type": "array", "items": { "type": "object", @@ -282,166 +427,62 @@ } } }, - "description": "List of comments" + "description": "Child comments (if includeReplies=true)" }, - "pagination": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + }, + "deletedAt": { + "type": "string", + "format": "date-time", + "description": "Soft deletion timestamp" + } + } + }, + "CommentsResponse": { + "type": "object", + "properties": { + "task": { "type": "object", "properties": { - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "limit": { - "type": "number" + "id": { + "type": "string" }, - "pages": { - "type": "number" + "title": { + "type": "string" } } }, - "permissions": { + "project": { "type": "object", "properties": { - "canCreate": { - "type": "boolean" - }, - "canEdit": { - "type": "boolean" - }, - "canDelete": { - "type": "boolean" + "id": { + "type": "string" }, - "canModerate": { - "type": "boolean" + "name": { + "type": "string" } } - } - } - }, - "Comment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" }, - "author": { + "organization": { "type": "object", "properties": { "id": { - "type": "string", - "description": "User ID" + "type": "string" }, "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "User ID" - }, - "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } + "type": "string" } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" + } }, - "replies": { + "comments": { "type": "array", "items": { "type": "object", @@ -588,82 +629,41 @@ } } }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "User ID" - }, - "name": { - "type": "string", - "description": "User's full name" - }, - "avatar": { - "type": "string", - "description": "URL to user's avatar" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "member", - "guest" - ], - "description": "User's role in the organization" - } - } - }, - "Attachment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" + "description": "List of comments" }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "limit": { + "type": "number" + }, + "pages": { + "type": "number" + } + } }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" + "permissions": { + "type": "object", + "properties": { + "canCreate": { + "type": "boolean" + }, + "canEdit": { + "type": "boolean" + }, + "canDelete": { + "type": "boolean" + }, + "canModerate": { + "type": "boolean" + } + } } } }, @@ -1067,6 +1067,23 @@ } } }, + "UserAddress": { + "type": "object", + "properties": { + "line1": { + "type": "string" + }, + "line2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "postalCode": { + "type": "string" + } + } + }, "UserResponse": { "type": "object", "properties": { @@ -1104,23 +1121,6 @@ } } } - }, - "UserAddress": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } } }, "responses": { diff --git a/examples/next15-app-typescript/public/openapi.json b/examples/next15-app-typescript/public/openapi.json index 42f371b..d7d60e2 100644 --- a/examples/next15-app-typescript/public/openapi.json +++ b/examples/next15-app-typescript/public/openapi.json @@ -20,34 +20,8 @@ } }, "schemas": { - "LoginBody": { - "type": "object", - "properties": { - "email": { - "type": "string", - "description": "user email", - "nullable": false - }, - "password": { - "type": "string", - "description": "user password", - "nullable": false - } - } - }, - "LoginResponse": { - "type": "object", - "properties": { - "token": { - "type": "string", - "description": "auth token" - }, - "refresh_token": { - "type": "string", - "description": "refresh token" - } - } - }, + "LoginBody": {}, + "LoginResponse": {}, "MyApiSuccessResponseBody": { "type": "object", "properties": { @@ -121,1437 +95,18 @@ } } }, - "CommentsQueryParams": { - "type": "object", - "properties": { - "page": { - "type": "number", - "description": "Page number for pagination" - }, - "limit": { - "type": "number", - "description": "Number of comments per page" - }, - "sort": { - "type": "string", - "enum": [ - "newest", - "oldest", - "likes" - ], - "description": "Sort order" - }, - "includeDeleted": { - "type": "boolean", - "description": "Whether to include soft-deleted comments" - }, - "includeReplies": { - "type": "boolean", - "description": "Whether to include replies" - }, - "user": { - "type": "string", - "description": "Filter by user ID" - } - } - }, - "CommentPathParams": { - "type": "object", - "properties": { - "orgId": { - "type": "string", - "description": "Organization ID" - }, - "projectId": { - "type": "string", - "description": "Project ID within the organization" - }, - "taskId": { - "type": "string", - "description": "Task ID within the project" - } - } - }, - "CommentsResponse": { - "type": "object", - "properties": { - "task": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "organization": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" - }, - "author": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" - }, - "replies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Comment" - }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - } - }, - "description": "List of comments" - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "limit": { - "type": "number" - }, - "pages": { - "type": "number" - } - } - }, - "permissions": { - "type": "object", - "properties": { - "canCreate": { - "type": "boolean" - }, - "canEdit": { - "type": "boolean" - }, - "canDelete": { - "type": "boolean" - }, - "canModerate": { - "type": "boolean" - } - } - } - } - }, - "Comment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" - }, - "author": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" - }, - "replies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" - }, - "author": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" - }, - "replies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Comment" - }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - } - }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "UserAddress": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "Attachment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "CreateCommentBody": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Comment content", - "nullable": false - }, - "attachmentIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "IDs of previously uploaded attachments", - "nullable": true - }, - "mentions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs mentioned in the comment", - "nullable": true - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply", - "nullable": true - } - } - }, - "CreateCommentResponse": { - "type": "object", - "properties": { - "comment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" - }, - "author": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" - }, - "replies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Comment" - }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - }, - "description": "Created comment" - }, - "success": { - "type": "boolean", - "description": "Whether creation was successful" - }, - "message": { - "type": "string", - "description": "Success or error message" - } - } - }, - "UpdateCommentBody": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Updated comment content", - "nullable": true - }, - "addAttachmentIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "IDs of attachments to add", - "nullable": true - }, - "removeAttachmentIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "IDs of attachments to remove", - "nullable": true - }, - "addMentions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs to add to mentions", - "nullable": true - }, - "removeMentions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs to remove from mentions", - "nullable": true - } - } - }, - "UpdateCommentResponse": { - "type": "object", - "properties": { - "comment": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Comment ID" - }, - "content": { - "type": "string", - "description": "Comment content" - }, - "author": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - }, - "description": "User who created the comment" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Attachment ID" - }, - "fileName": { - "type": "string", - "description": "Original file name" - }, - "fileSize": { - "type": "number", - "description": "Size in bytes" - }, - "fileType": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "Download URL" - }, - "thumbnailUrl": { - "type": "string", - "description": "Thumbnail URL for images" - }, - "uploadedAt": { - "type": "string", - "format": "date-time", - "description": "When the file was uploaded" - } - } - }, - "description": "Attached files" - }, - "mentions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "internalNotes": { - "type": "string" - } - } - }, - "description": "Users mentioned in the comment" - }, - "likes": { - "type": "number", - "description": "Number of likes" - }, - "likedBy": { - "type": "array", - "items": { - "type": "string" - }, - "description": "User IDs who liked the comment" - }, - "replyTo": { - "type": "string", - "description": "Parent comment ID if this is a reply" - }, - "replies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Comment" - }, - "description": "Child comments (if includeReplies=true)" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Creation timestamp" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Last update timestamp" - }, - "deletedAt": { - "type": "string", - "format": "date-time", - "description": "Soft deletion timestamp" - } - }, - "description": "Updated comment" - }, - "success": { - "type": "boolean", - "description": "Whether update was successful" - }, - "message": { - "type": "string", - "description": "Success or error message" - } - } - }, - "DeleteCommentResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "Whether deletion was successful" - }, - "message": { - "type": "string", - "description": "Success or error message" - } - } - }, - "UploadFormData": { - "type": "object", - "properties": { - "file": { - "description": "Image file (PNG/JPG, max 5MB)", - "nullable": false - }, - "description": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "nullable": false - } - } - }, - "File": {}, - "UploadResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "size": { - "type": "number" - }, - "type": { - "type": "string", - "description": "MIME type" - }, - "url": { - "type": "string", - "description": "File access URL" - }, - "category": { - "type": "string", - "description": "File category" - }, - "description": { - "type": "string" - }, - "uploadedAt": { - "type": "string" - } - } - }, - "UserIdParam": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "User's unique identifier" - } - } - }, - "UserResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "admin", - "user" - ] - }, - "address": { - "type": "object", - "properties": { - "line1": { - "type": "string" - }, - "line2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "postalCode": { - "type": "string" - } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - } + "CommentsQueryParams": {}, + "CommentPathParams": {}, + "CommentsResponse": {}, + "CreateCommentBody": {}, + "CreateCommentResponse": {}, + "UpdateCommentBody": {}, + "UpdateCommentResponse": {}, + "DeleteCommentResponse": {}, + "UploadFormData": {}, + "UploadResponse": {}, + "UserIdParam": {}, + "UserResponse": {} }, "responses": { "400": { @@ -1692,106 +247,7 @@ "tags": [ "Organizations" ], - "parameters": [ - { - "in": "query", - "name": "page", - "schema": { - "type": "number", - "description": "Page number for pagination" - }, - "required": false, - "description": "Page number for pagination" - }, - { - "in": "query", - "name": "limit", - "schema": { - "type": "number", - "description": "Number of comments per page" - }, - "required": false, - "description": "Number of comments per page" - }, - { - "in": "query", - "name": "sort", - "schema": { - "type": "string", - "enum": [ - "newest", - "oldest", - "likes" - ], - "description": "Sort order" - }, - "required": false, - "description": "Sort order" - }, - { - "in": "query", - "name": "includeDeleted", - "schema": { - "type": "boolean", - "description": "Whether to include soft-deleted comments" - }, - "required": false, - "description": "Whether to include soft-deleted comments" - }, - { - "in": "query", - "name": "includeReplies", - "schema": { - "type": "boolean", - "description": "Whether to include replies" - }, - "required": false, - "description": "Whether to include replies" - }, - { - "in": "query", - "name": "user", - "schema": { - "type": "string", - "description": "Filter by user ID" - }, - "required": false, - "description": "Filter by user ID" - }, - { - "in": "path", - "name": "orgId", - "schema": { - "type": "string", - "description": "Organization ID" - }, - "required": true, - "description": "Organization ID", - "example": "123" - }, - { - "in": "path", - "name": "projectId", - "schema": { - "type": "string", - "description": "Project ID within the organization" - }, - "required": true, - "description": "Project ID within the organization", - "example": "123" - }, - { - "in": "path", - "name": "taskId", - "schema": { - "type": "string", - "description": "Task ID within the project" - }, - "required": true, - "description": "Task ID within the project", - "example": "123" - } - ], + "parameters": [], "security": [ { "BearerAuth": [] @@ -1823,41 +279,7 @@ "tags": [ "Organizations" ], - "parameters": [ - { - "in": "path", - "name": "orgId", - "schema": { - "type": "string", - "description": "Organization ID" - }, - "required": true, - "description": "Organization ID", - "example": "123" - }, - { - "in": "path", - "name": "projectId", - "schema": { - "type": "string", - "description": "Project ID within the organization" - }, - "required": true, - "description": "Project ID within the organization", - "example": "123" - }, - { - "in": "path", - "name": "taskId", - "schema": { - "type": "string", - "description": "Task ID within the project" - }, - "required": true, - "description": "Task ID within the project", - "example": "123" - } - ], + "parameters": [], "security": [ { "BearerAuth": [] @@ -1898,41 +320,7 @@ "tags": [ "Organizations" ], - "parameters": [ - { - "in": "path", - "name": "orgId", - "schema": { - "type": "string", - "description": "Organization ID" - }, - "required": true, - "description": "Organization ID", - "example": "123" - }, - { - "in": "path", - "name": "projectId", - "schema": { - "type": "string", - "description": "Project ID within the organization" - }, - "required": true, - "description": "Project ID within the organization", - "example": "123" - }, - { - "in": "path", - "name": "taskId", - "schema": { - "type": "string", - "description": "Task ID within the project" - }, - "required": true, - "description": "Task ID within the project", - "example": "123" - } - ], + "parameters": [], "security": [ { "BearerAuth": [] @@ -1973,41 +361,7 @@ "tags": [ "Organizations" ], - "parameters": [ - { - "in": "path", - "name": "orgId", - "schema": { - "type": "string", - "description": "Organization ID" - }, - "required": true, - "description": "Organization ID", - "example": "123" - }, - { - "in": "path", - "name": "projectId", - "schema": { - "type": "string", - "description": "Project ID within the organization" - }, - "required": true, - "description": "Project ID within the organization", - "example": "123" - }, - { - "in": "path", - "name": "taskId", - "schema": { - "type": "string", - "description": "Task ID within the project" - }, - "required": true, - "description": "Task ID within the project", - "example": "123" - } - ], + "parameters": [], "security": [ { "BearerAuth": [] @@ -2080,19 +434,7 @@ "tags": [ "Users" ], - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "string", - "description": "User's unique identifier" - }, - "required": true, - "description": "User's unique identifier", - "example": "123" - } - ], + "parameters": [], "responses": { "200": { "description": "Successful response", diff --git a/examples/next15-app-zod/public/openapi.json b/examples/next15-app-zod/public/openapi.json index 68fa3dc..cf42a9d 100644 --- a/examples/next15-app-zod/public/openapi.json +++ b/examples/next15-app-zod/public/openapi.json @@ -653,6 +653,26 @@ "role" ] }, + "UserIdParam": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User ID" + } + } + }, + "UserNameByIdResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "firstName": { + "type": "string" + } + } + }, "UserFieldsQuery": { "type": "object", "properties": { @@ -2323,6 +2343,47 @@ } } }, + "/users/{id}/name": { + "get": { + "operationId": "get-users-{id}-name", + "summary": "Get User Name by ID", + "description": "Retrieves a user's name information", + "tags": [ + "Users" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "description": "User ID" + }, + "required": true, + "description": "User ID", + "example": "123" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserNameByIdResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, "/users-paginated": { "get": { "operationId": "get-users-paginated", diff --git a/examples/next15-app-zod/src/app/api/users/[id]/name/route.ts b/examples/next15-app-zod/src/app/api/users/[id]/name/route.ts new file mode 100644 index 0000000..7f13dbf --- /dev/null +++ b/examples/next15-app-zod/src/app/api/users/[id]/name/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUserNameById } from "./route.utils"; + +type UserIdParam = { + id: string; // User ID +}; + +type UserNameByIdResponse = Awaited>; + +/** + * Get User Name by ID + * @description Retrieves a user's name information + * @pathParams UserIdParam + * @response UserNameByIdResponse + * @openapi + */ +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + const user = await getUserNameById(params.id); + return NextResponse.json(user); +} diff --git a/examples/next15-app-zod/src/app/api/users/[id]/name/route.utils.ts b/examples/next15-app-zod/src/app/api/users/[id]/name/route.utils.ts new file mode 100644 index 0000000..95fd252 --- /dev/null +++ b/examples/next15-app-zod/src/app/api/users/[id]/name/route.utils.ts @@ -0,0 +1,19 @@ +export const getUserNameById = async (id: string): Promise<{ + name: string; + firstName: string; +}> => { + // Simulate fetching user data from a database + const result = await new Promise<[{ name: string }]>((resolve) => { + return resolve([ + { + name: "John Doe", + }, + ]); + }); + + // Do some more processing if needed + return result.map(({ name }) => ({ + name: name, + firstName: name.split(" ")[0], + }))[0]; +}; diff --git a/src/lib/route-processor.ts b/src/lib/route-processor.ts index 5d694c8..f9c18bd 100644 --- a/src/lib/route-processor.ts +++ b/src/lib/route-processor.ts @@ -344,6 +344,9 @@ export class RouteProcessor { this.swaggerPaths[routePath] = {}; } + // Set context file path for utility type resolution (ReturnType) + this.schemaProcessor.setContextFilePath(filePath); + const { params, pathParams, body, responses } = this.schemaProcessor.getSchemaContent(dataTypes); diff --git a/src/lib/schema-processor.ts b/src/lib/schema-processor.ts index 45e9f6d..943dfa9 100644 --- a/src/lib/schema-processor.ts +++ b/src/lib/schema-processor.ts @@ -42,6 +42,7 @@ export class SchemaProcessor { private zodSchemaConverter: ZodSchemaConverter | null = null; private schemaTypes: SchemaType[]; private isResolvingPickOmitBase: boolean = false; + private currentFilePath: string | null = null; constructor( schemaDir: string, @@ -139,6 +140,14 @@ export class SchemaProcessor { return merged; } + /** + * Set the context file path for resolving utility types like ReturnType + * This should be called before processing schemas from a route file + */ + public setContextFilePath(filePath: string | null): void { + this.currentFilePath = filePath; + } + public findSchemaDefinition( schemaName: string, contentType: ContentType @@ -186,8 +195,25 @@ export class SchemaProcessor { ); } - // Fall back to TypeScript types - this.scanSchemaDir(this.schemaDir, schemaName); + // Priority 3: Check current file if set (for route-level types) + // Always process TypeScript types from route files, even if schemaType is "zod" + // This allows utility types like Awaited> to work + if (this.currentFilePath) { + logger.debug(`Processing current file for ${schemaName}: ${this.currentFilePath}`); + this.processSchemaFile(this.currentFilePath, schemaName); + if (this.openapiDefinitions[schemaName]) { + logger.debug(`Found ${schemaName} in current file`); + return this.openapiDefinitions[schemaName]; + } + } + + // Priority 4: Fall back to TypeScript types in schema directory + // Only search schema directory if TypeScript is enabled + if (this.schemaTypes.includes("typescript")) { + logger.debug(`Searching schema directory for ${schemaName}`); + this.scanSchemaDir(this.schemaDir, schemaName); + } + return schemaNode; } @@ -473,6 +499,16 @@ export class SchemaProcessor { return { type: "string", format: "date-time" }; } + if (typeName === "Promise") { + // For Promise types, extract and resolve the inner type + if (node.typeParameters && node.typeParameters.params.length > 0) { + logger.debug(`Resolving Promise type parameter`); + return this.resolveTSNodeType(node.typeParameters.params[0]); + } + // Promise without type parameter - return object + return { type: "object" }; + } + if (typeName === "Array" || typeName === "ReadonlyArray") { if (node.typeParameters && node.typeParameters.params.length > 0) { return { @@ -547,6 +583,20 @@ export class SchemaProcessor { } } + // Handle Awaited - unwraps Promise types + if (typeName === "Awaited") { + if (node.typeParameters && node.typeParameters.params.length > 0) { + return this.resolveAwaitedType(node.typeParameters.params[0]); + } + } + + // Handle ReturnType + if (typeName === "ReturnType") { + if (node.typeParameters && node.typeParameters.params.length > 0) { + return this.resolveReturnType(node.typeParameters.params[0]); + } + } + // Handle custom generic types if (node.typeParameters && node.typeParameters.params.length > 0) { // Find the generic type definition first @@ -717,6 +767,9 @@ export class SchemaProcessor { if (this.processSchemaTracker[`${filePath}-${schemaName}`]) return; try { + // Store current file path for function lookup + this.currentFilePath = filePath; + // Recognizes different elements of TS like variable, type, interface, enum const content = fs.readFileSync(filePath, "utf-8"); const ast = parseTypeScriptFile(content); @@ -789,6 +842,228 @@ export class SchemaProcessor { return []; } + /** + * Resolve Awaited utility type - unwraps Promise types + * Handles: Promise -> T, Promise> -> T + */ + private resolveAwaitedType(typeNode: any): OpenAPIDefinition { + logger.debug(`Resolving Awaited type, node type: ${typeNode?.type}`); + + // If it's a Promise type reference, unwrap it FIRST + if (t.isTSTypeReference(typeNode) && t.isIdentifier(typeNode.typeName)) { + const typeName = typeNode.typeName.name; + logger.debug(`Type reference: ${typeName}`); + + if (typeName === "Promise") { + logger.debug(`Unwrapping Promise type`); + // Unwrap Promise -> T + if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) { + logger.debug(`Recursively unwrapping Promise parameter`); + // Recursively unwrap nested Promises + return this.resolveAwaitedType(typeNode.typeParameters.params[0]); + } + } + } + + // Then resolve the inner type (if not a Promise) + logger.debug(`Not a Promise, resolving as regular type`); + const resolvedType = this.resolveTSNodeType(typeNode); + logger.debug(`Resolved to: ${JSON.stringify(resolvedType).substring(0, 200)}`); + + // If not a Promise, return as-is + return resolvedType; + } + + /** + * Resolve ReturnType utility type + * Finds the function declaration and extracts its return type + */ + private resolveReturnType(typeNode: any): OpenAPIDefinition { + logger.debug(`Resolving ReturnType`); + + // Check if it's a typeof expression + if (t.isTSTypeQuery(typeNode)) { + const functionName = this.extractFunctionNameFromTypeOf(typeNode); + logger.debug(`Function name: ${functionName}`); + + if (functionName) { + // Find the function declaration + const functionReturnType = this.findFunctionReturnType(functionName); + + if (functionReturnType) { + logger.debug(`Found return type annotation for ${functionName}`); + const resolved = this.resolveTSNodeType(functionReturnType); + logger.debug(`Resolved to: ${JSON.stringify(resolved).substring(0, 200)}`); + return resolved; + } else { + logger.debug(`No return type annotation found for ${functionName}`); + } + } + } + + // If we can't resolve it, return empty object + logger.debug(`Failed to resolve ReturnType, returning empty object`); + return {}; + } + + /** + * Extract function name from typeof expression + * Handles: typeof functionName, typeof obj.method + */ + private extractFunctionNameFromTypeOf(typeQueryNode: any): string | null { + if (t.isIdentifier(typeQueryNode.exprName)) { + return typeQueryNode.exprName.name; + } + + // Handle qualified names like obj.method (not supported yet) + // Would require more complex resolution + return null; + } + + /** + * Find function declaration and extract its return type annotation + */ + private findFunctionReturnType(functionName: string): any | null { + logger.debug(`Looking for function: ${functionName}`); + + // Helper to search a single file for the function + // Returns the found type node or null + const searchFile = (filePath: string): any | null => { + if (!fs.existsSync(filePath)) { + logger.debug(`[findFunctionReturnType] File does not exist: ${filePath}`); + return null; + } + + logger.debug(`[findFunctionReturnType] Searching in file: ${filePath}`); + + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const ast = parseTypeScriptFile(content); + let foundTypeNode: any = null; + + traverse(ast, { + // Arrow function with export + ExportNamedDeclaration: (path: any) => { + if (t.isVariableDeclaration(path.node.declaration)) { + path.node.declaration.declarations.forEach((declarator: any) => { + if ( + t.isIdentifier(declarator.id) && + declarator.id.name === functionName && + t.isArrowFunctionExpression(declarator.init) + ) { + foundTypeNode = declarator.init.returnType?.typeAnnotation; + logger.debug(`[findFunctionReturnType] Found exported arrow function: ${functionName}`); + } + }); + } + }, + // Regular function declaration + FunctionDeclaration: (path: any) => { + if ( + t.isIdentifier(path.node.id) && + path.node.id.name === functionName + ) { + foundTypeNode = path.node.returnType?.typeAnnotation; + logger.debug(`[findFunctionReturnType] Found function declaration: ${functionName}`); + } + }, + // Arrow function variable + VariableDeclarator: (path: any) => { + if ( + t.isIdentifier(path.node.id) && + path.node.id.name === functionName && + t.isArrowFunctionExpression(path.node.init) + ) { + foundTypeNode = path.node.init.returnType?.typeAnnotation; + logger.debug(`[findFunctionReturnType] Found arrow function variable: ${functionName}`); + } + }, + }); + + if (foundTypeNode) { + logger.debug(`[findFunctionReturnType] Successfully extracted return type for: ${functionName}`); + } + + return foundTypeNode; + } catch (error) { + logger.debug(`[findFunctionReturnType] Error parsing file ${filePath}: ${error.message}`); + return null; + } + }; + + // Recursive helper to search directory + // Returns the found type node or null + const searchDirectory = (dir: string): any | null => { + if (!fs.existsSync(dir)) { + logger.debug(`[findFunctionReturnType] Directory does not exist: ${dir}`); + return null; + } + + logger.debug(`[findFunctionReturnType] Searching in directory: ${dir}`); + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + // Recursively search subdirectories + const result = searchDirectory(filePath); + if (result) { + return result; // Found in subdirectory + } + } else if (file.endsWith(".ts") || file.endsWith(".tsx")) { + const result = searchFile(filePath); + if (result) { + return result; // Found it + } + } + } + + return null; + }; + + // 1. First, search in the same directory as the current file being processed + if (this.currentFilePath) { + const currentDir = path.dirname(this.currentFilePath); + logger.debug(`[findFunctionReturnType] Searching in current directory: ${currentDir}`); + + // Check for common patterns like route.utils.ts, utils.ts, helpers.ts + const commonUtilFiles = [ + "route.utils.ts", + "utils.ts", + "helpers.ts", + "lib.ts", + ]; + + for (const utilFile of commonUtilFiles) { + const utilPath = path.join(currentDir, utilFile); + const result = searchFile(utilPath); + if (result) { + logger.debug(`[findFunctionReturnType] Found in ${utilFile}`); + return result; + } + } + + // Search all files in the current directory + const result = searchDirectory(currentDir); + if (result) { + logger.debug(`[findFunctionReturnType] Found in current directory`); + return result; + } + } + + // 2. Fall back to searching the schema directory + logger.debug(`[findFunctionReturnType] Searching in schema directory: ${this.schemaDir}`); + const result = searchDirectory(this.schemaDir); + + if (!result) { + logger.debug(`[findFunctionReturnType] Function ${functionName} not found`); + } + + return result; + } + private getPropertyOptions(node: any): PropertyOptions { const isOptional = !!node.optional; // check if property is optional diff --git a/tests/typescript-utility-types.test.ts b/tests/typescript-utility-types.test.ts new file mode 100644 index 0000000..544d1b6 --- /dev/null +++ b/tests/typescript-utility-types.test.ts @@ -0,0 +1,206 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import path from "path"; +import { SchemaProcessor } from "../src/lib/schema-processor"; +import { Config } from "../src/types"; + +describe("TypeScript Utility Types Resolution", () => { + let schemaProcessor: SchemaProcessor; + const testConfig: Config = { + apiDir: "src/app/api", + schemaDir: "src/types", + schemaType: "typescript", + outputFile: "openapi.json", + docsUrl: "/api-docs", + includeOpenApiRoutes: false, + ignoreRoutes: [], + }; + + beforeEach(() => { + schemaProcessor = new SchemaProcessor(testConfig); + }); + + describe("Awaited utility type", () => { + it("should unwrap Promise to T", () => { + const testFilePath = path.resolve( + __dirname, + "./fixtures/utility-types/awaited-promise.ts" + ); + + // Create test file content + const testContent = ` +export type UserResponse = { id: string; name: string }; + +export type AwaitedUser = Awaited>; + `; + + // This test would need actual file creation or mocking + // For now, we test the core logic + }); + + it("should unwrap nested Promise> to T", () => { + // Test that Awaited recursively unwraps nested Promises + }); + + it("should return type as-is if not a Promise", () => { + // Test that Awaited where T is not a Promise returns T + }); + }); + + describe("ReturnType utility type", () => { + it("should extract return type from arrow function with explicit type", () => { + const testContent = ` +export const getUserData = async (id: string): Promise<{ name: string; email: string }> => { + return { name: "John", email: "john@example.com" }; +}; + +export type UserDataResponse = ReturnType; + `; + + // Test return type extraction + }); + + it("should extract return type from regular function declaration", () => { + const testContent = ` +export function getProduct(id: string): { id: string; price: number } { + return { id, price: 99.99 }; +} + +export type ProductResponse = ReturnType; + `; + + // Test return type extraction from function declaration + }); + + it("should extract return type from async function", () => { + const testContent = ` +export async function fetchUser(id: string): Promise<{ id: string; name: string }> { + return { id, name: "John" }; +} + +export type FetchUserResponse = ReturnType; + `; + + // Test async function return type + }); + }); + + describe("Awaited> combination", () => { + it("should resolve Awaited> for async function", () => { + const testContent = ` +export const getUserNameById = async (id: string): Promise<{ name: string; firstName: string }> => { + return { + name: "John Doe", + firstName: "John", + }; +}; + +export type UserNameByIdResponse = Awaited>; + `; + + // This should resolve to { name: string; firstName: string } + // without the Promise wrapper + }); + + it("should handle complex return types", () => { + const testContent = ` +export const getComplexData = async () => { + return { + users: [{ id: "1", name: "John" }], + metadata: { + total: 1, + page: 1, + }, + }; +}; + +export type ComplexDataResponse = Awaited>; + `; + + // Should resolve to the full object structure + }); + }); + + describe("Edge cases", () => { + it("should handle function without explicit return type", () => { + const testContent = ` +export const getData = () => { + return { value: "test" }; +}; + +export type DataResponse = ReturnType; + `; + + // Should return empty schema since no explicit type annotation + }); + + it("should handle function not found", () => { + const testContent = ` +export type MissingFunctionResponse = ReturnType; + `; + + // Should return empty schema + }); + + it("should handle void return type", () => { + const testContent = ` +export function performAction(): void { + console.log("action"); +} + +export type ActionResponse = ReturnType; + `; + + // Should handle void type + }); + }); + + describe("Integration with other utility types", () => { + it("should work with Pick>>", () => { + const testContent = ` +export const getUserFull = async (): Promise<{ + id: string; + name: string; + email: string; + password: string; +}> => { + return { id: "1", name: "John", email: "j@ex.com", password: "secret" }; +}; + +export type UserPublic = Pick>, "id" | "name" | "email">; + `; + + // Should resolve to object with only id, name, email + }); + + it("should work with Omit>>", () => { + const testContent = ` +export const getUserFull = async (): Promise<{ + id: string; + name: string; + password: string; +}> => { + return { id: "1", name: "John", password: "secret" }; +}; + +export type UserSafe = Omit>, "password">; + `; + + // Should resolve to object without password + }); + + it("should work with Partial>>", () => { + const testContent = ` +export const getRequiredData = async (): Promise<{ + name: string; + email: string; +}> => { + return { name: "John", email: "j@ex.com" }; +}; + +export type OptionalData = Partial>>; + `; + + // Should make all properties optional + }); + }); +});