diff --git a/README.md b/README.md index 2b057ed..ad8b283 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ Explore the detailed documentation at [DummyJSON/Docs](https://dummyjson.com/doc **New**: Now you can generate your own [custom responses](https://dummyjson.com/custom-response) from DummyJSON, [try it now!](https://dummyjson.com/custom-response) +**New**: We now have an OpenAPI 3.1.0 specification available for DummyJSON! +You can find the `openapi.json` [here](https://dummyjson.com/api-docs/openapi.json). +Additionally, we’ve launched a fully interactive API UI powered by [Scalar](https://github.com/scalar/scalar) — making it even easier to explore and integrate DummyJSON into your projects. Check it out [here](https://dummyjson.com/api-docs)! + + ## Why DummyJSON? Ever felt bogged down by the complexities of setting up a backend just to fetch dummy data for your front-end project? Or perhaps you dreamt of a learning app where obtaining realistic data didn't involve navigating through convoluted public APIs and cumbersome registration processes. Well, say hello to DummyJSON! diff --git a/package.json b/package.json index ece19ad..15c011c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.485.0", + "@scalar/express-api-reference": "^0.7.1", "axios": "^1.7.7", "color-convert": "^2.0.1", "compression": "^1.7.4", @@ -34,6 +35,7 @@ "on-finished": "^2.3.0", "on-headers": "^1.0.2", "sharp": "0.31.1", + "swagger-jsdoc": "^6.2.8", "uuid": "^10.0.0", "winston": "^3.14.2" }, diff --git a/public/img/icons/code.svg b/public/img/icons/code.svg new file mode 100644 index 0000000..425b0a8 --- /dev/null +++ b/public/img/icons/code.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/img/icons/doc.svg b/public/img/icons/doc.svg new file mode 100644 index 0000000..8522909 --- /dev/null +++ b/public/img/icons/doc.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/middleware/openapi.js b/src/middleware/openapi.js new file mode 100644 index 0000000..b96e523 --- /dev/null +++ b/src/middleware/openapi.js @@ -0,0 +1,55 @@ +const openapiSpec = require('../utils/openapi'); +const { log, logError } = require('../helpers/logger'); +const { version } = require('../../package.json'); + +// We'll set up a middleware that will dynamically load the ESM module +let apiReferenceMiddleware = null; + +const initializeApiReference = async () => { + try { + const scalarModule = await import('@scalar/express-api-reference'); + const { apiReference } = scalarModule; + + apiReferenceMiddleware = apiReference({ + // URL to the OpenAPI specification + url: '/api-docs/openapi.json', + // URL to the API reference + cdn: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference', + // Set theme, using the default theme + theme: 'default', + // Title for the API reference + title: 'DummyJSON API Reference', + // Version of the API + version, + }); + + log('Scalar API Reference initialized successfully'); + } catch (error) { + logError('Failed to initialize Scalar API Reference:', { error }); + // Provide a fallback middleware + apiReferenceMiddleware = (req, res) => { + res.status(500).json({ message: 'API documentation is currently unavailable' }); + }; + } +}; + +initializeApiReference(); + +const serveOpenAPISpec = (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(openapiSpec); +}; + +// Middleware that checks if the API reference is loaded and uses it +const serveApiReference = (req, res, next) => { + if (!apiReferenceMiddleware) { + return res.status(503).json({ message: 'API documentation is loading, please try again shortly' }); + } + + return apiReferenceMiddleware(req, res, next); +}; + +module.exports = { + serveOpenAPISpec, + serveApiReference, +}; diff --git a/src/models/openapi-schemas/auth.js b/src/models/openapi-schemas/auth.js new file mode 100644 index 0000000..0dacc22 --- /dev/null +++ b/src/models/openapi-schemas/auth.js @@ -0,0 +1,119 @@ +/** + * @openapi + * components: + * securitySchemes: + * bearerAuth: + * type: http + * scheme: bearer + * bearerFormat: JWT + * description: JWT token for authentication + * + * schemas: + * LoginRequest: + * type: object + * properties: + * username: + * type: string + * description: Username for authentication + * example: "emilys" + * password: + * type: string + * description: Password for authentication + * example: "emilyspass" + * expiresInMins: + * type: integer + * description: Token expiration time in minutes + * default: 60 + * example: 60 + * required: + * - username + * - password + * + * RefreshTokenRequest: + * type: object + * properties: + * refreshToken: + * type: string + * description: Refresh token obtained from login or previous token refresh + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * expiresInMins: + * type: integer + * description: Token expiration time in minutes + * default: 60 + * example: 60 + * required: + * - refreshToken + * + * UserAuth: + * type: object + * properties: + * id: + * type: integer + * description: User ID + * example: 1 + * username: + * type: string + * description: Username of authenticated user + * example: "emilys" + * email: + * type: string + * description: Email of authenticated user + * example: "emily.johnson@x.dummyjson.com" + * firstName: + * type: string + * description: First name of user + * example: "Emily" + * lastName: + * type: string + * description: Last name of user + * example: "Johnson" + * gender: + * type: string + * description: Gender of user + * example: "female" + * image: + * type: string + * description: Profile image URL + * example: "https://dummyjson.com/icon/emilys/128" + * required: + * - id + * - username + * - email + * - firstName + * - lastName + * + * AuthResponse: + * allOf: + * - $ref: '#/components/schemas/UserAuth' + * - type: object + * properties: + * accessToken: + * type: string + * description: JWT Access Token + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * description: JWT Refresh Token for obtaining a new access token + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * required: + * - accessToken + * - refreshToken + * + * TokenResponse: + * type: object + * properties: + * accessToken: + * type: string + * description: New JWT access token + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * description: New JWT refresh token + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * required: + * - accessToken + * - refreshToken + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/cart.js b/src/models/openapi-schemas/cart.js new file mode 100644 index 0000000..8c87093 --- /dev/null +++ b/src/models/openapi-schemas/cart.js @@ -0,0 +1,193 @@ +/** + * @openapi + * components: + * schemas: + * CartProduct: + * type: object + * properties: + * id: + * type: integer + * description: Product ID in the cart + * example: 168 + * title: + * type: string + * description: Product title + * example: "Charger SXT RWD" + * price: + * type: number + * description: Product price + * example: 32999.99 + * quantity: + * type: integer + * description: Quantity of this product in the cart + * example: 3 + * total: + * type: number + * description: Total price for this product (price * quantity) + * example: 98999.97 + * discountPercentage: + * type: number + * description: Discount percentage for this product + * example: 13.39 + * discountedTotal: + * type: number + * description: Discounted total for this product + * example: 85743.87 + * thumbnail: + * type: string + * description: Product thumbnail URL + * example: "https://cdn.dummyjson.com/products/images/vehicle/Charger%20SXT%20RWD/thumbnail.png" + * required: + * - id + * - title + * - price + * - quantity + * - total + * - discountPercentage + * - discountedTotal + * - thumbnail + * + * Cart: + * type: object + * properties: + * id: + * type: integer + * description: Cart ID + * example: 1 + * products: + * type: array + * description: List of products in the cart + * items: + * $ref: '#/components/schemas/CartProduct' + * total: + * type: number + * description: Total price of all products in the cart + * example: 103774.85 + * discountedTotal: + * type: number + * description: Total price after discounts + * example: 89686.65 + * userId: + * type: integer + * description: User ID who owns the cart + * example: 33 + * totalProducts: + * type: integer + * description: Number of different products in the cart + * example: 4 + * totalQuantity: + * type: integer + * description: Total quantity of all products in the cart + * example: 15 + * required: + * - id + * - products + * - total + * - discountedTotal + * - userId + * - totalProducts + * - totalQuantity + * + * CartsResponse: + * type: object + * properties: + * carts: + * type: array + * items: + * $ref: '#/components/schemas/Cart' + * total: + * type: integer + * description: Total number of carts + * example: 50 + * skip: + * type: integer + * description: Number of carts skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of carts returned + * example: 30 + * + * SingleCartResponse: + * $ref: '#/components/schemas/Cart' + * + * AddCartRequest: + * type: object + * properties: + * userId: + * type: integer + * description: User ID for the new cart + * example: 1 + * products: + * type: array + * description: Array of products to add to the cart + * items: + * type: object + * properties: + * id: + * type: integer + * description: Product ID + * example: 144 + * quantity: + * type: integer + * description: Quantity of the product + * example: 4 + * example: + * - id: 144 + * quantity: 4 + * - id: 98 + * quantity: 1 + * required: + * - userId + * - products + * + * UpdateCartRequest: + * type: object + * properties: + * merge: + * type: boolean + * description: Whether to merge with existing cart products + * example: true + * userId: + * type: integer + * description: User ID for the cart + * example: 1 + * products: + * type: array + * description: Array of products to update in the cart + * items: + * type: object + * properties: + * id: + * type: integer + * description: Product ID + * example: 1 + * quantity: + * type: integer + * description: Quantity of the product + * example: 1 + * example: + * - id: 1 + * quantity: 1 + * + * DeletedCartResponse: + * allOf: + * - $ref: '#/components/schemas/Cart' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the cart is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the cart was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/comment.js b/src/models/openapi-schemas/comment.js new file mode 100644 index 0000000..fb452bc --- /dev/null +++ b/src/models/openapi-schemas/comment.js @@ -0,0 +1,131 @@ +/** + * @openapi + * components: + * schemas: + * CommentUser: + * type: object + * properties: + * id: + * type: integer + * description: User ID + * example: 105 + * username: + * type: string + * description: Username of the commenter + * example: "emmac" + * fullName: + * type: string + * description: Full name of the commenter + * example: "Emma Wilson" + * required: + * - id + * - username + * - fullName + * + * Comment: + * type: object + * properties: + * id: + * type: integer + * description: Comment ID + * example: 1 + * body: + * type: string + * description: Comment text + * example: "This is some awesome thinking!" + * postId: + * type: integer + * description: ID of the post the comment belongs to + * example: 242 + * likes: + * type: integer + * description: Number of likes for the comment + * example: 3 + * user: + * $ref: '#/components/schemas/CommentUser' + * required: + * - id + * - body + * - postId + * - user + * + * CommentsResponse: + * type: object + * properties: + * comments: + * type: array + * items: + * $ref: '#/components/schemas/Comment' + * total: + * type: integer + * description: Total number of comments + * example: 340 + * skip: + * type: integer + * description: Number of comments skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of comments returned + * example: 30 + * + * SingleCommentResponse: + * $ref: '#/components/schemas/Comment' + * + * AddCommentRequest: + * type: object + * properties: + * body: + * type: string + * description: Comment text + * example: "This makes all sense to me!" + * postId: + * type: integer + * description: ID of the post to comment on + * example: 3 + * userId: + * type: integer + * description: ID of the user making the comment + * example: 5 + * required: + * - body + * - postId + * - userId + * + * UpdateCommentRequest: + * type: object + * properties: + * body: + * type: string + * description: Updated comment text + * example: "I think I should shift to the moon" + * postId: + * type: integer + * description: Updated post ID + * example: 242 + * userId: + * type: integer + * description: Updated user ID + * example: 105 + * + * DeletedCommentResponse: + * allOf: + * - $ref: '#/components/schemas/Comment' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the comment is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the comment was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/custom-response.js b/src/models/openapi-schemas/custom-response.js new file mode 100644 index 0000000..9928df1 --- /dev/null +++ b/src/models/openapi-schemas/custom-response.js @@ -0,0 +1,50 @@ +/** + * @openapi + * components: + * schemas: + * CustomResponseGenerateRequest: + * type: object + * properties: + * json: + * type: object + * description: The custom JSON data to be served by the generated endpoint + * example: + * foo: "bar" + * method: + * type: string + * description: HTTP method for the custom endpoint + * enum: [GET, POST, PUT, PATCH, DELETE] + * example: "GET" + * required: + * - json + * - method + * + * CustomResponseGenerateResponse: + * type: object + * properties: + * url: + * type: string + * description: The generated URL for the custom response + * example: "https://dummyjson.com/c/abcd-1234" + * required: + * - url + * + * CustomResponseData: + * type: object + * description: The custom JSON data returned from the generated endpoint + * example: + * foo: "bar" + * + * CustomResponseError: + * type: object + * properties: + * message: + * type: string + * description: Error message + * example: "Missing JSON" + * required: + * - message + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/http.js b/src/models/openapi-schemas/http.js new file mode 100644 index 0000000..b8121f8 --- /dev/null +++ b/src/models/openapi-schemas/http.js @@ -0,0 +1,33 @@ +/** + * @openapi + * components: + * schemas: + * HttpMockResponse: + * type: object + * properties: + * status: + * type: integer + * description: HTTP status code + * example: 200 + * message: + * type: string + * description: Message for the status code + * example: OK + * title: + * type: string + * description: Title for error responses (4xx/5xx) + * example: Not Found + * type: + * type: string + * description: Error type (for error responses) + * example: about:blank + * detail: + * type: string + * description: Error detail (for error responses) + * example: Not Found + * required: + * - status + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/post.js b/src/models/openapi-schemas/post.js new file mode 100644 index 0000000..be361ee --- /dev/null +++ b/src/models/openapi-schemas/post.js @@ -0,0 +1,146 @@ +/** + * @openapi + * components: + * schemas: + * PostReactions: + * type: object + * properties: + * likes: + * type: integer + * description: Number of likes + * example: 192 + * dislikes: + * type: integer + * description: Number of dislikes + * example: 25 + * required: + * - likes + * - dislikes + * + * Post: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the post + * example: 1 + * title: + * type: string + * description: Title of the post + * example: "His mother had always taught him" + * body: + * type: string + * description: Content of the post + * example: "His mother had always taught him not to ever think of himself as better than others..." + * tags: + * type: array + * description: Tags associated with the post + * items: + * type: string + * example: ["history", "american", "crime"] + * reactions: + * $ref: '#/components/schemas/PostReactions' + * views: + * type: integer + * description: Number of views + * example: 305 + * userId: + * type: integer + * description: ID of the user who created the post + * example: 121 + * required: + * - id + * - title + * - body + * - tags + * - reactions + * - views + * - userId + * + * PostsResponse: + * type: object + * properties: + * posts: + * type: array + * items: + * $ref: '#/components/schemas/Post' + * total: + * type: integer + * description: Total number of posts + * example: 100 + * skip: + * type: integer + * description: Number of posts skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of posts returned + * example: 30 + * + * AddPostRequest: + * type: object + * properties: + * title: + * type: string + * example: "His mother had always taught him" + * body: + * type: string + * example: "His mother had always taught him not to ever think of himself as better than others..." + * userId: + * type: integer + * example: 121 + * tags: + * type: array + * items: + * type: string + * example: ["history", "american", "crime"] + * reactions: + * $ref: '#/components/schemas/PostReactions' + * required: + * - title + * - body + * - userId + * - tags + * - reactions + * + * UpdatePostRequest: + * type: object + * properties: + * title: + * type: string + * example: "Updated title" + * body: + * type: string + * example: "Updated body content." + * userId: + * type: integer + * example: 121 + * tags: + * type: array + * items: + * type: string + * example: ["history", "american", "crime"] + * reactions: + * $ref: '#/components/schemas/PostReactions' + * + * DeletedPostResponse: + * allOf: + * - $ref: '#/components/schemas/Post' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the post is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the post was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/product.js b/src/models/openapi-schemas/product.js new file mode 100644 index 0000000..fb66b45 --- /dev/null +++ b/src/models/openapi-schemas/product.js @@ -0,0 +1,222 @@ +/** + * @openapi + * components: + * schemas: + * ProductReview: + * type: object + * properties: + * id: + * type: integer + * description: Review ID + * example: 1 + * userId: + * type: integer + * description: User ID of the reviewer + * example: 5 + * username: + * type: string + * description: Username of the reviewer + * example: johnsmith + * rating: + * type: number + * format: float + * description: Rating given by the reviewer (0-5) + * example: 4.8 + * comment: + * type: string + * description: Review comment + * example: "Great product, exactly as described!" + * date: + * type: string + * format: date-time + * description: Date when the review was posted + * example: "2023-01-15T10:30:00Z" + * required: + * - id + * - userId + * - rating + * + * Product: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the product + * example: 1 + * title: + * type: string + * description: Name of the product + * example: "Essence Mascara Lash Princess" + * description: + * type: string + * description: Detailed description of the product + * example: "The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects. Achieve dramatic lashes with this long-lasting and cruelty-free formula." + * price: + * type: number + * description: Current price of the product + * example: 9.99 + * discountPercentage: + * type: number + * description: Discount percentage for the product + * example: 7.17 + * rating: + * type: number + * description: Average product rating (0-5) + * example: 4.94 + * stock: + * type: integer + * description: Available stock quantity + * example: 5 + * brand: + * type: string + * description: Brand name of the product + * example: "Essence" + * category: + * type: string + * description: Product category + * example: "beauty" + * thumbnail: + * type: string + * description: URL to the product thumbnail image + * example: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png" + * images: + * type: array + * description: URLs to product images + * items: + * type: string + * example: ["https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/1.png"] + * tags: + * type: array + * description: Tags associated with the product + * items: + * type: string + * example: ["mascara"] + * sku: + * type: string + * description: Stock keeping unit identifier + * example: "RCH45Q1A" + * weight: + * type: integer + * description: Weight of the product + * example: 2 + * dimensions: + * type: object + * description: Product dimensions + * properties: + * depth: + * type: number + * example: 28.01 + * warrantyInformation: + * type: string + * description: Warranty information + * example: "1 month warranty" + * shippingInformation: + * type: string + * description: Shipping information + * example: "Ships in 1 month" + * availabilityStatus: + * type: string + * description: Product availability status + * example: "Low Stock" + * returnPolicy: + * type: string + * description: Return policy information + * example: "30 days return policy" + * minimumOrderQuantity: + * type: integer + * description: Minimum order quantity + * example: 24 + * meta: + * type: object + * description: Additional metadata + * properties: + * qrCode: + * type: string + * example: "https://assets.dummyjson.com/public/qr-code.png" + * required: + * - id + * - title + * - description + * - price + * - category + * - thumbnail + * + * ProductsResponse: + * type: object + * properties: + * products: + * type: array + * items: + * $ref: '#/components/schemas/Product' + * total: + * type: integer + * description: Total number of products + * example: 100 + * skip: + * type: integer + * description: Number of products skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of products returned + * example: 30 + * + * SingleProductResponse: + * $ref: '#/components/schemas/Product' + * + * AddProductRequest: + * type: object + * properties: + * title: + * type: string + * example: "Essence Mascara Lash Princess" + * description: + * type: string + * example: "The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects." + * price: + * type: number + * example: 9.99 + * discountPercentage: + * type: number + * example: 7.17 + * rating: + * type: number + * example: 4.94 + * stock: + * type: integer + * example: 5 + * brand: + * type: string + * example: "Essence" + * category: + * type: string + * example: "beauty" + * thumbnail: + * type: string + * example: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png" + * images: + * type: array + * items: + * type: string + * example: ["https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/1.png"] + * required: + * - title + * - price + * - category + * + * UpdateProductRequest: + * type: object + * properties: + * title: + * type: string + * example: "Updated Essence Mascara" + * price: + * type: number + * example: 12.99 + * stock: + * type: integer + * example: 10 + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/quote.js b/src/models/openapi-schemas/quote.js new file mode 100644 index 0000000..0e4d8e4 --- /dev/null +++ b/src/models/openapi-schemas/quote.js @@ -0,0 +1,50 @@ +/** + * @openapi + * components: + * schemas: + * Quote: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the quote + * example: 1 + * quote: + * type: string + * description: The quote text + * example: "Your heart is the size of an ocean. Go find yourself in its hidden depths." + * author: + * type: string + * description: Author of the quote + * example: "Rumi" + * required: + * - id + * - quote + * - author + * + * QuotesResponse: + * type: object + * properties: + * quotes: + * type: array + * items: + * $ref: '#/components/schemas/Quote' + * total: + * type: integer + * description: Total number of quotes + * example: 100 + * skip: + * type: integer + * description: Number of quotes skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of quotes returned + * example: 30 + * + * SingleQuoteResponse: + * $ref: '#/components/schemas/Quote' + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/recipe.js b/src/models/openapi-schemas/recipe.js new file mode 100644 index 0000000..780cb53 --- /dev/null +++ b/src/models/openapi-schemas/recipe.js @@ -0,0 +1,237 @@ +/** + * @openapi + * components: + * schemas: + * Recipe: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the recipe + * example: 1 + * name: + * type: string + * description: Name of the recipe + * example: "Classic Margherita Pizza" + * ingredients: + * type: array + * description: List of ingredients + * items: + * type: string + * example: ["Pizza dough", "Tomato sauce", "Fresh mozzarella cheese"] + * instructions: + * type: array + * description: Step-by-step instructions + * items: + * type: string + * example: ["Preheat the oven to 475°F (245°C).", "Roll out the pizza dough and spread tomato sauce evenly."] + * prepTimeMinutes: + * type: integer + * description: Preparation time in minutes + * example: 20 + * cookTimeMinutes: + * type: integer + * description: Cooking time in minutes + * example: 15 + * servings: + * type: integer + * description: Number of servings + * example: 4 + * difficulty: + * type: string + * description: Difficulty level + * example: "Easy" + * cuisine: + * type: string + * description: Cuisine type + * example: "Italian" + * caloriesPerServing: + * type: integer + * description: Calories per serving + * example: 300 + * tags: + * type: array + * description: Tags associated with the recipe + * items: + * type: string + * example: ["Pizza", "Italian"] + * userId: + * type: integer + * description: User ID of the recipe creator + * example: 45 + * image: + * type: string + * description: URL to the recipe image + * example: "https://cdn.dummyjson.com/recipe-images/1.webp" + * rating: + * type: number + * description: Average rating (0-5) + * example: 4.6 + * reviewCount: + * type: integer + * description: Number of reviews + * example: 3 + * mealType: + * type: array + * description: Meal types (e.g., Breakfast, Lunch, Dinner) + * items: + * type: string + * example: ["Dinner"] + * required: + * - id + * - name + * - ingredients + * - instructions + * - prepTimeMinutes + * - cookTimeMinutes + * - servings + * - difficulty + * - cuisine + * - caloriesPerServing + * - tags + * - userId + * - image + * - rating + * - mealType + * + * RecipesResponse: + * type: object + * properties: + * recipes: + * type: array + * items: + * $ref: '#/components/schemas/Recipe' + * total: + * type: integer + * description: Total number of recipes + * example: 100 + * skip: + * type: integer + * description: Number of recipes skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of recipes returned + * example: 30 + * + * AddRecipeRequest: + * type: object + * properties: + * name: + * type: string + * example: "Tasty Pizza" + * ingredients: + * type: array + * items: + * type: string + * instructions: + * type: array + * items: + * type: string + * prepTimeMinutes: + * type: integer + * cookTimeMinutes: + * type: integer + * servings: + * type: integer + * difficulty: + * type: string + * cuisine: + * type: string + * caloriesPerServing: + * type: integer + * tags: + * type: array + * items: + * type: string + * userId: + * type: integer + * image: + * type: string + * rating: + * type: number + * reviewCount: + * type: integer + * mealType: + * type: array + * items: + * type: string + * required: + * - name + * - ingredients + * - instructions + * - prepTimeMinutes + * - cookTimeMinutes + * - servings + * - difficulty + * - cuisine + * - caloriesPerServing + * - tags + * - userId + * - image + * - rating + * - mealType + * + * UpdateRecipeRequest: + * type: object + * properties: + * name: + * type: string + * ingredients: + * type: array + * items: + * type: string + * instructions: + * type: array + * items: + * type: string + * prepTimeMinutes: + * type: integer + * cookTimeMinutes: + * type: integer + * servings: + * type: integer + * difficulty: + * type: string + * cuisine: + * type: string + * caloriesPerServing: + * type: integer + * tags: + * type: array + * items: + * type: string + * userId: + * type: integer + * image: + * type: string + * rating: + * type: number + * reviewCount: + * type: integer + * mealType: + * type: array + * items: + * type: string + * + * DeletedRecipeResponse: + * allOf: + * - $ref: '#/components/schemas/Recipe' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the recipe is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the recipe was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/test.js b/src/models/openapi-schemas/test.js new file mode 100644 index 0000000..e96e27e --- /dev/null +++ b/src/models/openapi-schemas/test.js @@ -0,0 +1,22 @@ +/** + * @openapi + * components: + * schemas: + * TestStatusResponse: + * type: object + * properties: + * status: + * type: string + * description: Status of the server + * example: ok + * method: + * type: string + * description: HTTP method used for the request + * example: GET + * required: + * - status + * - method + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/todo.js b/src/models/openapi-schemas/todo.js new file mode 100644 index 0000000..faa5ab7 --- /dev/null +++ b/src/models/openapi-schemas/todo.js @@ -0,0 +1,137 @@ +/** + * @openapi + * components: + * schemas: + * Todo: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the todo + * example: 1 + * todo: + * type: string + * description: The todo task description + * example: Do something nice for someone you care about + * completed: + * type: boolean + * description: Whether the todo is completed + * example: false + * userId: + * type: integer + * description: ID of the user who owns the todo + * example: 152 + * required: + * - id + * - todo + * - completed + * - userId + * example: + * id: 1 + * todo: Do something nice for someone you care about + * completed: false + * userId: 152 + * + * TodosResponse: + * type: object + * properties: + * todos: + * type: array + * items: + * $ref: '#/components/schemas/Todo' + * total: + * type: integer + * description: Total number of todos + * example: 150 + * skip: + * type: integer + * description: Number of todos skipped for pagination + * example: 0 + * limit: + * type: integer + * description: Maximum number of todos returned + * example: 30 + * example: + * todos: + * - id: 1 + * todo: Do something nice for someone you care about + * completed: false + * userId: 152 + * - id: 2 + * todo: Memorize a poem + * completed: true + * userId: 13 + * - id: 3 + * todo: Watch a classic movie + * completed: true + * userId: 68 + * total: 150 + * skip: 0 + * limit: 30 + * + * AddTodoRequest: + * type: object + * properties: + * todo: + * type: string + * example: Use DummyJSON in the project + * completed: + * type: boolean + * example: false + * userId: + * type: integer + * example: 5 + * required: + * - todo + * - completed + * - userId + * example: + * todo: Use DummyJSON in the project + * completed: false + * userId: 5 + * + * UpdateTodoRequest: + * type: object + * properties: + * todo: + * type: string + * example: Bake pastries for yourself and neighbor + * completed: + * type: boolean + * example: true + * userId: + * type: integer + * example: 198 + * example: + * todo: Bake pastries for yourself and neighbor + * completed: true + * userId: 198 + * + * DeletedTodoResponse: + * allOf: + * - $ref: '#/components/schemas/Todo' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the todo is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the todo was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + * example: + * id: 1 + * todo: Do something nice for someone you care about + * completed: false + * userId: 152 + * isDeleted: true + * deletedOn: "2024-05-01T12:34:56.789Z" + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/models/openapi-schemas/user.js b/src/models/openapi-schemas/user.js new file mode 100644 index 0000000..d31623c --- /dev/null +++ b/src/models/openapi-schemas/user.js @@ -0,0 +1,502 @@ +/** + * @openapi + * components: + * schemas: + * User: + * type: object + * properties: + * id: + * type: integer + * description: Unique identifier for the user + * example: 1 + * firstName: + * type: string + * example: "Terry" + * lastName: + * type: string + * example: "Medhurst" + * maidenName: + * type: string + * example: "Smitham" + * age: + * type: integer + * example: 50 + * gender: + * type: string + * example: "male" + * email: + * type: string + * example: "atuny0@sohu.com" + * phone: + * type: string + * example: "+63 791 675 8914" + * username: + * type: string + * example: "atuny0" + * password: + * type: string + * example: "9uQFF1Lh" + * birthDate: + * type: string + * example: "2000-12-25" + * image: + * type: string + * example: "https://robohash.org/hicveldicta.png" + * bloodGroup: + * type: string + * example: "A−" + * height: + * type: integer + * example: 189 + * weight: + * type: number + * example: 75.4 + * eyeColor: + * type: string + * example: "Green" + * hair: + * type: object + * properties: + * color: + * type: string + * example: "Black" + * type: + * type: string + * example: "Strands" + * domain: + * type: string + * example: "slashdot.org" + * ip: + * type: string + * example: "117.29.86.254" + * address: + * type: object + * properties: + * address: + * type: string + * example: "1745 T Street Southeast" + * city: + * type: string + * example: "Washington" + * coordinates: + * type: object + * properties: + * lat: + * type: number + * example: 38.867033 + * lng: + * type: number + * example: -76.979235 + * postalCode: + * type: string + * example: "20020" + * state: + * type: string + * example: "DC" + * macAddress: + * type: string + * example: "13:69:BA:56:A3:74" + * university: + * type: string + * example: "Capitol University" + * bank: + * type: object + * properties: + * cardExpire: + * type: string + * example: "06/22" + * cardNumber: + * type: string + * example: "50380955204220685" + * cardType: + * type: string + * example: "maestro" + * currency: + * type: string + * example: "Peso" + * iban: + * type: string + * example: "NO17 0695 2754 967" + * company: + * type: object + * properties: + * address: + * type: object + * properties: + * address: + * type: string + * example: "629 Debbie Drive" + * city: + * type: string + * example: "Nashville" + * coordinates: + * type: object + * properties: + * lat: + * type: number + * example: 36.208114 + * lng: + * type: number + * example: -86.586211 + * postalCode: + * type: string + * example: "37076" + * state: + * type: string + * example: "TN" + * department: + * type: string + * example: "Marketing" + * name: + * type: string + * example: "Blanda-O'Keefe" + * title: + * type: string + * example: "Help Desk Operator" + * ein: + * type: string + * example: "20-9487066" + * ssn: + * type: string + * example: "661-64-2976" + * userAgent: + * type: string + * example: "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" + * required: + * - id + * - firstName + * - lastName + * - age + * - gender + * - email + * - phone + * - username + * - password + * - birthDate + * - image + * - bloodGroup + * - height + * - weight + * - eyeColor + * - hair + * - domain + * - ip + * - address + * - macAddress + * - university + * - bank + * - company + * - ein + * - ssn + * - userAgent + * example: + * id: 1 + * firstName: "Terry" + * lastName: "Medhurst" + * maidenName: "Smitham" + * age: 50 + * gender: "male" + * email: "atuny0@sohu.com" + * phone: "+63 791 675 8914" + * username: "atuny0" + * password: "9uQFF1Lh" + * birthDate: "2000-12-25" + * image: "https://robohash.org/hicveldicta.png" + * bloodGroup: "A−" + * height: 189 + * weight: 75.4 + * eyeColor: "Green" + * hair: + * color: "Black" + * type: "Strands" + * domain: "slashdot.org" + * ip: "117.29.86.254" + * address: + * address: "1745 T Street Southeast" + * city: "Washington" + * coordinates: + * lat: 38.867033 + * lng: -76.979235 + * postalCode: "20020" + * state: "DC" + * macAddress: "13:69:BA:56:A3:74" + * university: "Capitol University" + * bank: + * cardExpire: "06/22" + * cardNumber: "50380955204220685" + * cardType: "maestro" + * currency: "Peso" + * iban: "NO17 0695 2754 967" + * company: + * address: + * address: "629 Debbie Drive" + * city: "Nashville" + * coordinates: + * lat: 36.208114 + * lng: -86.586211 + * postalCode: "37076" + * state: "TN" + * department: "Marketing" + * name: "Blanda-O'Keefe" + * title: "Help Desk Operator" + * ein: "20-9487066" + * ssn: "661-64-2976" + * userAgent: "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" + * + * UsersResponse: + * type: object + * properties: + * users: + * type: array + * items: + * $ref: '#/components/schemas/User' + * total: + * type: integer + * example: 100 + * skip: + * type: integer + * example: 0 + * limit: + * type: integer + * example: 30 + * example: + * users: + * - id: 1 + * firstName: "Terry" + * lastName: "Medhurst" + * maidenName: "Smitham" + * age: 50 + * gender: "male" + * email: "atuny0@sohu.com" + * phone: "+63 791 675 8914" + * username: "atuny0" + * password: "9uQFF1Lh" + * birthDate: "2000-12-25" + * image: "https://robohash.org/hicveldicta.png" + * bloodGroup: "A−" + * height: 189 + * weight: 75.4 + * eyeColor: "Green" + * hair: + * color: "Black" + * type: "Strands" + * domain: "slashdot.org" + * ip: "117.29.86.254" + * address: + * address: "1745 T Street Southeast" + * city: "Washington" + * coordinates: + * lat: 38.867033 + * lng: -76.979235 + * postalCode: "20020" + * state: "DC" + * macAddress: "13:69:BA:56:A3:74" + * university: "Capitol University" + * bank: + * cardExpire: "06/22" + * cardNumber: "50380955204220685" + * cardType: "maestro" + * currency: "Peso" + * iban: "NO17 0695 2754 967" + * company: + * address: + * address: "629 Debbie Drive" + * city: "Nashville" + * coordinates: + * lat: 36.208114 + * lng: -86.586211 + * postalCode: "37076" + * state: "TN" + * department: "Marketing" + * name: "Blanda-O'Keefe" + * title: "Help Desk Operator" + * ein: "20-9487066" + * ssn: "661-64-2976" + * userAgent: "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" + * total: 100 + * skip: 0 + * limit: 30 + * + * AddUserRequest: + * type: object + * properties: + * firstName: + * type: string + * example: "Terry" + * lastName: + * type: string + * example: "Medhurst" + * email: + * type: string + * example: "atuny0@sohu.com" + * username: + * type: string + * example: "atuny0" + * password: + * type: string + * example: "9uQFF1Lh" + * age: + * type: integer + * example: 50 + * gender: + * type: string + * example: "male" + * required: + * - firstName + * - lastName + * - email + * - username + * - password + * - age + * - gender + * example: + * firstName: "Terry" + * lastName: "Medhurst" + * email: "atuny0@sohu.com" + * username: "atuny0" + * password: "9uQFF1Lh" + * age: 50 + * gender: "male" + * + * UpdateUserRequest: + * type: object + * properties: + * firstName: + * type: string + * example: "Terry" + * lastName: + * type: string + * example: "Medhurst" + * email: + * type: string + * example: "atuny0@sohu.com" + * username: + * type: string + * example: "atuny0" + * password: + * type: string + * example: "9uQFF1Lh" + * age: + * type: integer + * example: 50 + * gender: + * type: string + * example: "male" + * example: + * firstName: "Terry" + * lastName: "Medhurst" + * email: "atuny0@sohu.com" + * username: "atuny0" + * password: "9uQFF1Lh" + * age: 50 + * gender: "male" + * + * DeletedUserResponse: + * allOf: + * - $ref: '#/components/schemas/User' + * - type: object + * properties: + * isDeleted: + * type: boolean + * description: Whether the user is deleted + * example: true + * deletedOn: + * type: string + * format: date-time + * description: ISO timestamp when the user was deleted + * example: "2024-05-01T12:34:56.789Z" + * required: + * - isDeleted + * - deletedOn + * example: + * id: 1 + * firstName: "Terry" + * lastName: "Medhurst" + * maidenName: "Smitham" + * age: 50 + * gender: "male" + * email: "atuny0@sohu.com" + * phone: "+63 791 675 8914" + * username: "atuny0" + * password: "9uQFF1Lh" + * birthDate: "2000-12-25" + * image: "https://robohash.org/hicveldicta.png" + * bloodGroup: "A−" + * height: 189 + * weight: 75.4 + * eyeColor: "Green" + * hair: + * color: "Black" + * type: "Strands" + * domain: "slashdot.org" + * ip: "117.29.86.254" + * address: + * address: "1745 T Street Southeast" + * city: "Washington" + * coordinates: + * lat: 38.867033 + * lng: -76.979235 + * postalCode: "20020" + * state: "DC" + * macAddress: "13:69:BA:56:A3:74" + * university: "Capitol University" + * bank: + * cardExpire: "06/22" + * cardNumber: "50380955204220685" + * cardType: "maestro" + * currency: "Peso" + * iban: "NO17 0695 2754 967" + * company: + * address: + * address: "629 Debbie Drive" + * city: "Nashville" + * coordinates: + * lat: 36.208114 + * lng: -86.586211 + * postalCode: "37076" + * state: "TN" + * department: "Marketing" + * name: "Blanda-O'Keefe" + * title: "Help Desk Operator" + * ein: "20-9487066" + * ssn: "661-64-2976" + * userAgent: "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" + * isDeleted: true + * deletedOn: "2024-05-01T12:34:56.789Z" + * + * UserLoginResponse: + * type: object + * properties: + * accessToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: + * type: string + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * id: + * type: integer + * example: 1 + * firstName: + * type: string + * example: "Terry" + * lastName: + * type: string + * example: "Medhurst" + * email: + * type: string + * example: "atuny0@sohu.com" + * username: + * type: string + * example: "atuny0" + * example: + * accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * id: 1 + * firstName: "Terry" + * lastName: "Medhurst" + * email: "atuny0@sohu.com" + * username: "atuny0" + */ + +// Export the model schema (empty object since we're just using the JSDoc for OpenAPI) +module.exports = {}; diff --git a/src/routes/api-docs.js b/src/routes/api-docs.js new file mode 100644 index 0000000..b2041f0 --- /dev/null +++ b/src/routes/api-docs.js @@ -0,0 +1,10 @@ +const router = require('express').Router(); +const { serveOpenAPISpec, serveApiReference } = require('../middleware/openapi'); + +// Serve OpenAPI specification at /api-docs/openapi.json +router.get('/openapi.json', serveOpenAPISpec); + +// Serve Scalar API Reference UI at /api-docs +router.use('/', serveApiReference); + +module.exports = router; diff --git a/src/routes/auth.js b/src/routes/auth.js index 2ea3e82..723c350 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,8 +1,112 @@ +/** + * @openapi + * tags: + * name: Auth + * description: API endpoints for authentication and authorization + */ + const router = require('express').Router(); const { loginByUsernamePassword, getNewRefreshToken } = require('../controllers/auth'); const authUser = require('../middleware/auth'); -// login user +/** + * @openapi + * components: + * schemas: + * LoginRequest: + * type: object + * properties: + * username: + * type: string + * description: Username for authentication + * example: "emilys" + * password: + * type: string + * description: Password for authentication + * example: "0lelplR" + * expiresInMins: + * type: integer + * description: Token expiration time in minutes + * default: 60 + * example: 60 + * required: + * - username + * - password + * + * RefreshTokenRequest: + * type: object + * properties: + * refreshToken: + * type: string + * description: Refresh token obtained from login or previous token refresh + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * expiresInMins: + * type: integer + * description: Token expiration time in minutes + * default: 60 + * example: 60 + * required: + * - refreshToken + * + * AuthResponse: + * type: object + * properties: + * id: + * type: integer + * description: User ID + * example: 15 + * username: + * type: string + * description: Username of authenticated user + * example: "kminchelle" + * email: + * type: string + * description: Email of authenticated user + * example: "emily.johnson@x.dummyjson.com" + * firstName: + * type: string + * description: First name of user + * example: "Jeanne" + * lastName: + * type: string + * description: Last name of user + * example: "Halvorson" + * gender: + * type: string + * description: Gender of user + * example: "female" + * image: + * type: string + * description: Profile image URL + * example: "https://robohash.org/autquiaut.png" + * token: + * type: string + * description: JWT Access Token + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ + +/** + * @openapi + * /auth/login: + * post: + * summary: Authenticate user and get token + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequest' + * responses: + * 200: + * description: Successfully authenticated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AuthResponse' + * 400: + * description: Invalid credentials or missing required fields + */ router.post('/login', async (req, res, next) => { try { const payload = await loginByUsernamePassword(req.body); @@ -17,7 +121,24 @@ router.post('/login', async (req, res, next) => { } }); -// logout user +/** + * @openapi + * /auth/logout: + * post: + * summary: Log out user by clearing auth cookies + * tags: [Auth] + * responses: + * 200: + * description: Successfully logged out + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Logged out successfully + */ router.post('/logout', (req, res) => { res.clearCookie('auth', { httpOnly: true, @@ -27,12 +148,52 @@ router.post('/logout', (req, res) => { res.status(200).json({ message: 'Logged out successfully' }); }); -// get current authenticated user +/** + * @openapi + * /auth/me: + * get: + * summary: Get current authenticated user information + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Current user information + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserAuth' + * 401: + * description: Unauthorized - Access token is missing or invalid + */ router.get('/me', authUser, (req, res) => { res.send(req.user); }); -// get new refresh token +/** + * @openapi + * /auth/refresh: + * post: + * summary: Get new access token using refresh token + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RefreshTokenRequest' + * responses: + * 200: + * description: New access and refresh tokens + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TokenResponse' + * 401: + * description: Refresh token required + * 403: + * description: Invalid or expired refresh token + */ router.post('/refresh', async (req, res, next) => { try { const { expiresInMins = 60 } = req.body; diff --git a/src/routes/cart.js b/src/routes/cart.js index edd424f..00e8d62 100644 --- a/src/routes/cart.js +++ b/src/routes/cart.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * - name: Cart + * description: Cart management and shopping cart operations + */ + const router = require('express').Router(); const { getAllCarts, @@ -8,12 +15,66 @@ const { deleteCartById, } = require('../controllers/cart'); -// get all carts +/** + * @openapi + * /carts: + * get: + * summary: Get all carts + * tags: [Cart] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of carts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of carts to skip + * responses: + * 200: + * description: List of carts + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CartsResponse' + */ router.get('/', (req, res) => { res.send(getAllCarts({ ...req._options })); }); -// get cart by user +/** + * @openapi + * /carts/user/{userId}: + * get: + * summary: Get carts by user ID + * tags: [Cart] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: ID of the user + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of carts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of carts to skip + * responses: + * 200: + * description: List of carts for the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CartsResponse' + */ router.get('/user/:userId', (req, res) => { const { userId } = req.params; const { limit, skip } = req._options; @@ -21,31 +82,152 @@ router.get('/user/:userId', (req, res) => { res.send(getCartsByUserId({ userId, limit, skip })); }); -// get cart by id +/** + * @openapi + * /carts/{id}: + * get: + * summary: Get a single cart by ID + * tags: [Cart] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Cart ID + * responses: + * 200: + * description: Cart object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCartResponse' + * 404: + * description: Cart not found + */ router.get('/:id', (req, res) => { res.send(getCartById({ ...req.params })); }); -// add new cart +/** + * @openapi + * /carts/add: + * post: + * summary: Add a new cart + * tags: [Cart] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddCartRequest' + * responses: + * 201: + * description: New cart created (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCartResponse' + * 400: + * description: Invalid input + */ router.post('/add', (req, res) => { res.status(201).send(addNewCart({ ...req.body })); }); -// update cart by id (PUT) +/** + * @openapi + * /carts/{id}: + * put: + * summary: Update a cart by ID (replace) + * tags: [Cart] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Cart ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateCartRequest' + * responses: + * 200: + * description: Updated cart (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCartResponse' + * 400: + * description: Invalid input + * 404: + * description: Cart not found + * patch: + * summary: Update a cart by ID (partial) + * tags: [Cart] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Cart ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateCartRequest' + * responses: + * 200: + * description: Updated cart (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCartResponse' + * 400: + * description: Invalid input + * 404: + * description: Cart not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updateCartById({ id, ...req.body })); }); -// update cart by id (PATCH) router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updateCartById({ id, ...req.body })); }); -// delete cart +/** + * @openapi + * /carts/{id}: + * delete: + * summary: Delete a cart by ID + * tags: [Cart] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Cart ID + * responses: + * 200: + * description: Deleted cart (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedCartResponse' + * 404: + * description: Cart not found + */ router.delete('/:id', (req, res) => { res.send(deleteCartById({ ...req.params })); }); diff --git a/src/routes/comment.js b/src/routes/comment.js index e2916ba..070809a 100644 --- a/src/routes/comment.js +++ b/src/routes/comment.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * - name: Comment + * description: Comment management and user feedback operations + */ + const router = require('express').Router(); const { getAllComments, @@ -8,45 +15,220 @@ const { deleteCommentById, } = require('../controllers/comment'); -// get all comments +/** + * @openapi + * /comments: + * get: + * summary: Get all comments + * tags: [Comment] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of comments to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of comments to skip + * responses: + * 200: + * description: List of comments + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CommentsResponse' + */ router.get('/', (req, res) => { res.send(getAllComments({ ...req._options })); }); -// get comment by id +/** + * @openapi + * /comments/{id}: + * get: + * summary: Get a single comment by ID + * tags: [Comment] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Comment ID + * responses: + * 200: + * description: Comment object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCommentResponse' + * 404: + * description: Comment not found + */ router.get('/:id', (req, res) => { const { id } = req.params; res.send(getCommentById({ id })); }); -// get comments by postId +/** + * @openapi + * /comments/post/{postId}: + * get: + * summary: Get all comments by post ID + * tags: [Comment] + * parameters: + * - in: path + * name: postId + * required: true + * schema: + * type: integer + * description: ID of the post + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of comments to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of comments to skip + * responses: + * 200: + * description: List of comments for the post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CommentsResponse' + */ router.get('/post/:postId', (req, res) => { const { postId } = req.params; res.send(getAllCommentsByPostId({ postId, ...req._options })); }); -// add new comment +/** + * @openapi + * /comments/add: + * post: + * summary: Add a new comment + * tags: [Comment] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddCommentRequest' + * responses: + * 201: + * description: New comment created (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCommentResponse' + * 400: + * description: Invalid input + */ router.post('/add', (req, res) => { res.status(201).send(addNewComment({ ...req.body })); }); -// update comment by id (PUT) +/** + * @openapi + * /comments/{id}: + * put: + * summary: Update a comment by ID (replace) + * tags: [Comment] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Comment ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateCommentRequest' + * responses: + * 200: + * description: Updated comment (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCommentResponse' + * 400: + * description: Invalid input + * 404: + * description: Comment not found + * patch: + * summary: Update a comment by ID (partial) + * tags: [Comment] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Comment ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateCommentRequest' + * responses: + * 200: + * description: Updated comment (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleCommentResponse' + * 400: + * description: Invalid input + * 404: + * description: Comment not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updateCommentById({ id, ...req.body })); }); -// update comment by id (PATCH) router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updateCommentById({ id, ...req.body })); }); -// delete comment by id +/** + * @openapi + * /comments/{id}: + * delete: + * summary: Delete a comment by ID + * tags: [Comment] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Comment ID + * responses: + * 200: + * description: Deleted comment (simulated) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedCommentResponse' + * 404: + * description: Comment not found + */ router.delete('/:id', (req, res) => { res.send(deleteCommentById({ ...req.params })); }); diff --git a/src/routes/custom-response.js b/src/routes/custom-response.js index f30619b..96aab37 100644 --- a/src/routes/custom-response.js +++ b/src/routes/custom-response.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * - name: CustomResponse + * description: Generate and serve custom JSON responses via unique URLs + */ + const router = require('express').Router(); const CustomResponse = require('../models/custom-response'); const { isDbConnected } = require('../utils/db'); @@ -5,6 +12,44 @@ const { generateUniqueIdentifier, generateHash } = require('../utils/custom-resp const { customResponseExpiresInDays } = require('../constants'); const cacheMiddleware = require('../middleware/cache'); +/** + * @openapi + * /c/generate: + * post: + * summary: Generate a custom JSON response endpoint + * tags: [CustomResponse] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseGenerateRequest' + * responses: + * 200: + * description: Successfully generated a custom response URL + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseGenerateResponse' + * 400: + * description: Missing or invalid JSON + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseError' + * 413: + * description: Payload Too Large + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseError' + * 500: + * description: Failed to generate unique identifier + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseError' + */ router.post('/generate', async (req, res, next) => { if (!isDbConnected()) { next(); @@ -49,6 +94,38 @@ router.post('/generate', async (req, res, next) => { } }); +/** + * @openapi + * /c/{identifier}: + * get: + * summary: Get the custom JSON response by identifier + * tags: [CustomResponse] + * parameters: + * - in: path + * name: identifier + * required: true + * schema: + * type: string + * description: Unique identifier for the custom response + * responses: + * 200: + * description: The custom JSON data + * headers: + * x-expires-on: + * description: Expiry date of the custom response (ISO string) + * schema: + * type: string + * x-expires-in-days: + * description: Number of days until expiry + * schema: + * type: integer + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CustomResponseData' + * 404: + * description: Not found + */ router.use('/:identifier', cacheMiddleware, async (req, res, next) => { if (!isDbConnected()) { next(); diff --git a/src/routes/http.js b/src/routes/http.js index 2d3fd65..8ed9ae9 100644 --- a/src/routes/http.js +++ b/src/routes/http.js @@ -1,6 +1,45 @@ +/** + * @openapi + * tags: + * - name: HttpMock + * description: Mock and test HTTP responses for any status code + */ + const router = require('express').Router(); const { getHttpStatus } = require('../controllers/http'); +/** + * @openapi + * /http/{httpCode}/{message}: + * get: + * summary: Mock an HTTP response with a specific status code and optional message + * tags: [HttpMock] + * parameters: + * - in: path + * name: httpCode + * required: true + * schema: + * type: integer + * description: HTTP status code to mock (e.g., 200, 404, 500) + * example: 200 + * - in: path + * name: message + * required: false + * schema: + * type: string + * description: Optional message to include in the response + * example: Hello_Peter + * responses: + * 200: + * description: Mocked HTTP response (success or error) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HttpMockResponse' + * application/problem+json: + * schema: + * $ref: '#/components/schemas/HttpMockResponse' + */ router.use('/:httpCode/:message?', (req, res) => { const data = getHttpStatus(req.params); diff --git a/src/routes/icon.js b/src/routes/icon.js index e9ecbb4..2481e82 100644 --- a/src/routes/icon.js +++ b/src/routes/icon.js @@ -1,7 +1,58 @@ +/** + * @openapi + * tags: + * - name: Icon + * description: Generate consistent, unique identicon images based on input hash values + */ + const router = require('express').Router(); const { generateIcon } = require('../controllers/icon'); -// generate icon +/** + * @openapi + * /icon/{hash}/{size}: + * get: + * summary: Generate a unique identicon based on a hash value + * description: Creates deterministic identicon images that are unique to the provided hash value, useful for default user avatars or visual identification. + * tags: + * - Icon + * parameters: + * - name: hash + * in: path + * required: false + * description: Hash or string value to generate the identicon from (defaults to 'DummyJSON') + * schema: + * type: string + * - name: size + * in: path + * required: false + * description: Size of the square identicon in pixels (defaults to 100, max 1000) + * schema: + * type: integer + * minimum: 1 + * maximum: 1000 + * default: 100 + * - name: type + * in: query + * required: false + * description: Output format of the identicon + * schema: + * type: string + * enum: [png, svg] + * default: png + * responses: + * 200: + * description: Identicon generated successfully + * content: + * image/png: + * schema: + * type: string + * format: binary + * image/svg: + * schema: + * type: string + * format: binary + */ router.get('/:hash?/:size?', (req, res) => { const { hash = 'DummyJSON', size = '100' } = req.params; const { type } = req.query; diff --git a/src/routes/image.js b/src/routes/image.js index 112778d..5c2d30a 100644 --- a/src/routes/image.js +++ b/src/routes/image.js @@ -1,9 +1,113 @@ +/** + * @openapi + * tags: + * - name: Image + * description: Generate customizable placeholder images with various sizes, colors, and text options + */ + const router = require('express').Router(); const { imageComposer } = require('../controllers/image'); const { logError } = require('../helpers/logger'); const APIError = require('../utils/error'); const { generateProperData } = require('../utils/image'); +/** + * @openapi + * /image/{size}/{backgroundParam}/{colorParam}/{typeParam}: + * get: + * summary: Generate a placeholder image + * description: Creates a customizable placeholder image with options for size, background color, text color, and content. + * tags: + * - Image + * parameters: + * - name: size + * in: path + * required: true + * description: Size of the image. Can be a single number for square (e.g., 150) or width x height (e.g., 200x100) + * schema: + * type: string + * - name: backgroundParam + * in: path + * required: false + * description: Background color in hex (without #) or color name + * schema: + * type: string + * - name: colorParam + * in: path + * required: false + * description: Text color in hex (without #) or color name + * schema: + * type: string + * - name: typeParam + * in: path + * required: false + * description: Image format (png, jpg/jpeg, webp) + * schema: + * type: string + * enum: [png, jpg, jpeg, webp] + * - name: background + * in: query + * required: false + * description: Background color in hex (without #) or color name (alternative to path parameter) + * schema: + * type: string + * - name: color + * in: query + * required: false + * description: Text color in hex (without #) or color name (alternative to path parameter) + * schema: + * type: string + * - name: type + * in: query + * required: false + * description: Image format (alternative to path parameter) + * schema: + * type: string + * enum: [png, jpg, jpeg, webp] + * - name: text + * in: query + * required: false + * description: Custom text to display on the image (defaults to image dimensions) + * schema: + * type: string + * - name: fontFamily + * in: query + * required: false + * description: Font family for the text + * schema: + * type: string + * enum: [bitter, cairo, comfortaa, cookie, dosis, gotham, lobster, marhey, pacifico, poppins, quicksand, qwigley, satisfy, ubuntu] + * default: bitter + * - name: fontSize + * in: query + * required: false + * description: Font size in pixels (defaults to dynamic sizing based on text and image width) + * schema: + * type: integer + * minimum: 1 + * responses: + * 200: + * description: Image generated successfully + * content: + * image/png: + * schema: + * type: string + * format: binary + * image/jpeg: + * schema: + * type: string + * format: binary + * image/webp: + * schema: + * type: string + * format: binary + * 302: + * description: Redirect to cached image + * 400: + * description: Invalid parameters + * 500: + * description: Server error while generating image + */ const endpoint = '/:size/:backgroundParam?/:colorParam?/:typeParam?'; router.get(endpoint, async (req, res, next) => { try { diff --git a/src/routes/index.js b/src/routes/index.js index 86e473a..b46ab14 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -5,6 +5,9 @@ const forceHTTPS = require('../middleware/force-https'); // static page routes router.use('/', forceHTTPS, require('./static')); +// API documentation routes +router.use('/api-docs', require('./api-docs')); + // static resource routes router.use('/auth', require('./auth')); router.use(['/cart', '/carts'], require('./cart')); diff --git a/src/routes/post.js b/src/routes/post.js index d57b616..26327fb 100644 --- a/src/routes/post.js +++ b/src/routes/post.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * name: Posts + * description: API endpoints for managing posts + */ + const router = require('express').Router(); const { getAllCommentsByPostId } = require('../controllers/comment'); const { @@ -13,27 +20,170 @@ const { getPostsByTag, } = require('../controllers/post'); -// get all posts +/** + * @openapi + * /posts: + * get: + * summary: Get all posts + * tags: [Posts] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of posts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of posts to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of posts + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponse' + */ router.get('/', (req, res) => { res.send(getAllPosts({ ...req._options })); }); -// search post +/** + * @openapi + * /posts/search: + * get: + * summary: Search posts + * tags: [Posts] + * parameters: + * - in: query + * name: q + * schema: + * type: string + * required: true + * description: Search query string + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of posts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of posts to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of posts matching the search query + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponse' + */ router.get('/search', (req, res) => { res.send(searchPosts({ ...req._options })); }); -// get post tag list +/** + * @openapi + * /posts/tag-list: + * get: + * summary: Get all available post tags (flat list) + * tags: [Posts] + * responses: + * 200: + * description: List of all available tags + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + */ router.get('/tag-list', (req, res) => { res.send(getPostTagList()); }); -// get post tags +/** + * @openapi + * /posts/tags: + * get: + * summary: Get all available post tags (grouped) + * tags: [Posts] + * responses: + * 200: + * description: List of all available tags grouped + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + */ router.get('/tags', (req, res) => { res.send(getPostTags()); }); -// get post by id +/** + * @openapi + * /posts/{id}: + * get: + * summary: Get a post by ID + * tags: [Posts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The post ID + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * responses: + * 200: + * description: The post object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 404: + * description: Post not found + */ router.get('/:id', (req, res) => { const { id } = req.params; const { select } = req._options; @@ -41,47 +191,256 @@ router.get('/:id', (req, res) => { res.send(getPostById({ id, select })); }); -// get posts by tag +/** + * @openapi + * /posts/tag/{tag}: + * get: + * summary: Get posts by tag + * tags: [Posts] + * parameters: + * - in: path + * name: tag + * required: true + * schema: + * type: string + * description: Tag to filter posts by + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of posts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of posts to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of posts with the given tag + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponse' + */ router.get('/tag/:tag', (req, res) => { const { tag } = req.params; res.send(getPostsByTag({ tag, ...req._options })); }); -// get posts by userId +/** + * @openapi + * /posts/user/{userId}: + * get: + * summary: Get posts by user ID + * tags: [Posts] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: User ID to filter posts by + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of posts to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of posts to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of posts by the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponse' + */ router.get('/user/:userId', (req, res) => { const { userId } = req.params; res.send(getPostsByUserId({ userId, ...req._options })); }); -// get comments by postId +/** + * @openapi + * /posts/{postId}/comments: + * get: + * summary: Get comments for a post + * tags: [Posts] + * parameters: + * - in: path + * name: postId + * required: true + * schema: + * type: integer + * description: The post ID + * responses: + * 200: + * description: List of comments for the post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CommentsResponse' + */ router.get('/:postId/comments', (req, res) => { const { postId } = req.params; res.send(getAllCommentsByPostId({ postId, ...req._options })); }); -// add new post +/** + * @openapi + * /posts/add: + * post: + * summary: Add a new post + * tags: [Posts] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddPostRequest' + * responses: + * 201: + * description: The created post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + */ router.post('/add', (req, res) => { res.status(201).send(addNewPost({ ...req.body })); }); -// update post by id (PUT) +/** + * @openapi + * /posts/{id}: + * put: + * summary: Update a post by ID (replace) + * tags: [Posts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The post ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdatePostRequest' + * responses: + * 200: + * description: The updated post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 404: + * description: Post not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updatePost({ id, ...req.body })); }); -// update post by id (PATCH) +/** + * @openapi + * /posts/{id}: + * patch: + * summary: Update a post by ID (partial) + * tags: [Posts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The post ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdatePostRequest' + * responses: + * 200: + * description: The updated post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 404: + * description: Post not found + */ router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updatePost({ id, ...req.body })); }); -// delete post by id +/** + * @openapi + * /posts/{id}: + * delete: + * summary: Delete a post by ID + * tags: [Posts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The post ID + * responses: + * 200: + * description: The deleted post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedPostResponse' + * 404: + * description: Post not found + */ router.delete('/:id', (req, res) => { res.send(deletePostById({ ...req.params })); }); diff --git a/src/routes/product.js b/src/routes/product.js index 7052fa9..3533ce6 100644 --- a/src/routes/product.js +++ b/src/routes/product.js @@ -1,3 +1,9 @@ +/** + * @openapi + * tags: + * name: Products + * description: API endpoints for managing products + */ const router = require('express').Router(); const { getAllProducts, @@ -11,27 +17,222 @@ const { deleteProductById, } = require('../controllers/product'); -// get all products +/** + * @openapi + * /products: + * get: + * summary: Get all products + * tags: [Products] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 30 + * description: Maximum number of products to return + * - in: query + * name: skip + * schema: + * type: integer + * default: 0 + * description: Number of products to skip + * - in: query + * name: select + * schema: + * type: string + * description: Fields to include in the response (comma separated) + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * default: asc + * description: Sort order + * responses: + * 200: + * description: A list of products + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductsResponse' + * example: + * products: + * - id: 1 + * title: "Essence Mascara Lash Princess" + * description: "The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects. Achieve dramatic lashes with this long-lasting and cruelty-free formula." + * price: 9.99 + * discountPercentage: 7.17 + * rating: 4.94 + * stock: 5 + * brand: "Essence" + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png" + * images: ["https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/1.png"] + * - id: 2 + * title: "Eyeshadow Palette with Mirror" + * description: "The Eyeshadow Palette with Mirror offers a versatile range of eyeshadow shades for creating stunning eye looks. With a built-in mirror, it's convenient for on-the-go makeup application." + * price: 19.99 + * discountPercentage: 5.5 + * rating: 3.28 + * stock: 44 + * brand: "Glamour Beauty" + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png" + * images: ["https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/1.png"] + * total: 100 + * skip: 0 + * limit: 30 + */ router.get('/', (req, res) => { res.send(getAllProducts({ ...req._options })); }); -// search product +/** + * @openapi + * /products/search: + * get: + * summary: Search for products + * tags: [Products] + * parameters: + * - in: query + * name: q + * required: true + * schema: + * type: string + * description: Search query + * - in: query + * name: limit + * schema: + * type: integer + * default: 30 + * description: Maximum number of products to return + * - in: query + * name: skip + * schema: + * type: integer + * default: 0 + * description: Number of products to skip + * responses: + * 200: + * description: Search results + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductsResponse' + * example: + * products: + * - id: 1 + * title: "Essence Mascara Lash Princess" + * description: "The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects. Achieve dramatic lashes with this long-lasting and cruelty-free formula." + * price: 9.99 + * discountPercentage: 7.17 + * rating: 4.94 + * stock: 5 + * brand: "Essence" + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png" + * total: 1 + * skip: 0 + * limit: 1 + */ router.get('/search', (req, res) => { res.send(searchProducts({ ...req._options })); }); -// get product category list +/** + * @openapi + * /products/category-list: + * get: + * summary: Get product category list + * tags: [Products] + * responses: + * 200: + * description: List of product categories + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + */ router.get('/category-list', (req, res) => { res.send(getProductCategoryList()); }); -// get product categories +/** + * @openapi + * /products/categories: + * get: + * summary: Get product categories + * tags: [Products] + * responses: + * 200: + * description: List of product categories + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: ["beauty", "fragrances", "furniture", "groceries", "home-decoration", "kitchen-accessories"] + */ router.get('/categories', (req, res) => { res.send(getProductCategories()); }); -// get product by id +/** + * @openapi + * /products/{id}: + * get: + * summary: Get product by ID + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Product ID + * - in: query + * name: select + * schema: + * type: string + * description: Fields to include in the response (comma separated) + * responses: + * 200: + * description: Product details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SingleProductResponse' + * example: + * id: 6 + * title: "Calvin Klein CK One" + * description: "CK One by Calvin Klein is a classic unisex fragrance, known for its fresh and clean scent. It's a versatile fragrance suitable for everyday wear." + * price: 49.99 + * discountPercentage: 0.32 + * rating: 4.85 + * stock: 17 + * brand: "Calvin Klein" + * category: "fragrances" + * thumbnail: "https://cdn.dummyjson.com/products/images/fragrances/Calvin%20Klein%20CK%20One/thumbnail.png" + * images: ["https://cdn.dummyjson.com/products/images/fragrances/Calvin%20Klein%20CK%20One/3.png"] + * tags: ["perfumes"] + * sku: "DZM2JQZE" + * dimensions: + * depth: 6.81 + * warrantyInformation: "5 year warranty" + * shippingInformation: "Ships overnight" + * availabilityStatus: "In Stock" + * minimumOrderQuantity: 20 + * 404: + * description: Product not found + */ router.get('/:id', (req, res) => { const { id } = req.params; const { select } = req._options; @@ -39,33 +240,213 @@ router.get('/:id', (req, res) => { res.send(getProductById({ id, select })); }); -// get products by categoryName +/** + * @openapi + * /products/category/{categoryName}: + * get: + * summary: Get products by category name + * tags: [Products] + * parameters: + * - in: path + * name: categoryName + * required: true + * schema: + * type: string + * description: Category name + * - in: query + * name: limit + * schema: + * type: integer + * default: 30 + * description: Maximum number of products to return + * - in: query + * name: skip + * schema: + * type: integer + * default: 0 + * description: Number of products to skip + * - in: query + * name: select + * schema: + * type: string + * description: Fields to include in the response (comma separated) + * responses: + * 200: + * description: A list of products by category + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductsResponse' + * example: + * products: + * - id: 1 + * title: "Essence Mascara Lash Princess" + * price: 9.99 + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png" + * - id: 2 + * title: "Eyeshadow Palette with Mirror" + * price: 19.99 + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png" + * total: 5 + * skip: 0 + * limit: 30 + */ router.get('/category/:categoryName', (req, res) => { const { categoryName } = req.params; res.send(getProductsByCategoryName({ categoryName, ...req._options })); }); -// add new product +/** + * @openapi + * /products/add: + * post: + * summary: Add a new product + * tags: [Products] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddProductRequest' + * example: + * title: "New Beauty Product" + * description: "High quality beauty product with amazing results" + * price: 29.99 + * discountPercentage: 5.0 + * rating: 0 + * stock: 50 + * brand: "Beauty Co" + * category: "beauty" + * thumbnail: "https://cdn.dummyjson.com/products/images/beauty/NewProduct/thumbnail.png" + * images: ["https://cdn.dummyjson.com/products/images/beauty/NewProduct/1.png"] + * responses: + * 201: + * description: The created product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + */ router.post('/add', (req, res) => { res.status(201).send(addNewProduct({ ...req.body })); }); -// update product by id (PUT) +/** + * @openapi + * /products/{id}: + * put: + * summary: Update product by ID + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Product ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateProductRequest' + * example: + * title: "Updated Red Lipstick" + * price: 15.99 + * stock: 75 + * description: "Premium long-lasting red lipstick with moisturizing formula" + * responses: + * 200: + * description: The updated product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * 404: + * description: Product not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updateProductById({ id, ...req.body })); }); -// update product by id (PATCH) +/** + * @openapi + * /products/{id}: + * patch: + * summary: Partially update product by ID + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Product ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateProductRequest' + * example: + * stock: 25 + * price: 11.99 + * responses: + * 200: + * description: The updated product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * 404: + * description: Product not found + */ router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updateProductById({ id, ...req.body })); }); -// delete product by id +/** + * @openapi + * /products/{id}: + * delete: + * summary: Delete product by ID + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Product ID + * responses: + * 200: + * description: The deleted product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Product' + * example: + * id: 4 + * title: "Red Lipstick" + * description: "The Red Lipstick is a classic and bold choice for adding a pop of color to your lips. With a creamy and pigmented formula, it provides a vibrant and long-lasting finish." + * price: 12.99 + * discountPercentage: 19.03 + * rating: 2.51 + * stock: 68 + * brand: "Chic Cosmetics" + * category: "beauty" + * isDeleted: true + * deletedOn: "2025-04-26T12:00:00.000Z" + * 404: + * description: Product not found + */ router.delete('/:id', (req, res) => { res.send(deleteProductById({ ...req.params })); }); diff --git a/src/routes/quote.js b/src/routes/quote.js index fb8f007..4f4339a 100644 --- a/src/routes/quote.js +++ b/src/routes/quote.js @@ -1,17 +1,96 @@ +/** + * @openapi + * tags: + * - name: Quote + * description: Famous quotes from various authors + */ + const router = require('express').Router(); const { getAllQuotes, getRandomQuote, getQuoteById } = require('../controllers/quote'); -// get all quotes +/** + * @openapi + * /quotes: + * get: + * summary: Get all quotes + * tags: [Quote] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of quotes to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of quotes to skip + * responses: + * 200: + * description: List of quotes + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/QuotesResponse' + */ router.get('/', (req, res) => { res.send(getAllQuotes({ ...req._options })); }); -// get random quote(s) +/** + * @openapi + * /quotes/random/{length}: + * get: + * summary: Get random quote(s) + * tags: [Quote] + * parameters: + * - in: path + * name: length + * required: false + * schema: + * type: integer + * minimum: 1 + * maximum: 10 + * description: Number of random quotes to return (1-10, optional) + * responses: + * 200: + * description: A random quote or array of random quotes + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/Quote' + * - type: array + * items: + * $ref: '#/components/schemas/Quote' + */ router.get('/random/:length?', (req, res) => { res.send(getRandomQuote({ ...req.params })); }); -// get quote by id +/** + * @openapi + * /quotes/{id}: + * get: + * summary: Get a quote by ID + * tags: [Quote] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The quote ID + * responses: + * 200: + * description: The quote object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Quote' + * 404: + * description: Quote not found + */ router.get('/:id', (req, res) => { res.send(getQuoteById({ ...req.params })); }); diff --git a/src/routes/recipe.js b/src/routes/recipe.js index 7a0a40e..def5d95 100644 --- a/src/routes/recipe.js +++ b/src/routes/recipe.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * name: Recipes + * description: API endpoints for managing recipes + */ + const router = require('express').Router(); const { getAllRecipes, @@ -11,22 +18,150 @@ const { deleteRecipeById, } = require('../controllers/recipes'); -// get all recipes +/** + * @openapi + * /recipes: + * get: + * summary: Get all recipes + * tags: [Recipes] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of recipes to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of recipes to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of recipes + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecipesResponse' + */ router.get('/', (req, res) => { res.send(getAllRecipes({ ...req._options })); }); -// search recipe +/** + * @openapi + * /recipes/search: + * get: + * summary: Search recipes + * tags: [Recipes] + * parameters: + * - in: query + * name: q + * schema: + * type: string + * required: true + * description: Search query string + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of recipes to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of recipes to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of recipes matching the search query + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecipesResponse' + */ router.get('/search', (req, res) => { res.send(searchRecipes({ ...req._options })); }); -// get recipe tags +/** + * @openapi + * /recipes/tags: + * get: + * summary: Get all recipe tags + * tags: [Recipes] + * responses: + * 200: + * description: List of unique tags + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + */ router.get('/tags', (req, res) => { res.send(getRecipeTags()); }); -// get recipe by id +/** + * @openapi + * /recipes/{id}: + * get: + * summary: Get a recipe by ID + * tags: [Recipes] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The recipe ID + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * responses: + * 200: + * description: The recipe object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Recipe' + * 404: + * description: Recipe not found + */ router.get('/:id', (req, res) => { const { id } = req.params; const { select } = req._options; @@ -34,36 +169,229 @@ router.get('/:id', (req, res) => { res.send(getRecipeById({ id, select })); }); -// get recipes by tag +/** + * @openapi + * /recipes/tag/{tag}: + * get: + * summary: Get recipes by tag + * tags: [Recipes] + * parameters: + * - in: path + * name: tag + * required: true + * schema: + * type: string + * description: Tag to filter recipes by + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of recipes to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of recipes to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of recipes with the given tag + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecipesResponse' + */ router.get('/tag/:tag', (req, res) => { const { tag } = req.params; res.send(getRecipesByTag({ tag, ...req._options })); }); -// get recipes by meal type +/** + * @openapi + * /recipes/meal-type/{mealType}: + * get: + * summary: Get recipes by meal type + * tags: [Recipes] + * parameters: + * - in: path + * name: mealType + * required: true + * schema: + * type: string + * description: Meal type to filter recipes by + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of recipes to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of recipes to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of recipes for the given meal type + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RecipesResponse' + */ router.get('/meal-type/:mealType', (req, res) => { const { mealType } = req.params; res.send(getRecipesByMealType({ mealType, ...req._options })); }); +/** + * @openapi + * /recipes/add: + * post: + * summary: Add a new recipe + * tags: [Recipes] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddRecipeRequest' + * responses: + * 201: + * description: The created recipe + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Recipe' + */ router.post('/add', (req, res) => { - res.send(addNewRecipe({ ...req.body })); + res.status(201).send(addNewRecipe({ ...req.body })); }); +/** + * @openapi + * /recipes/{id}: + * put: + * summary: Update a recipe by ID (replace) + * tags: [Recipes] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The recipe ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateRecipeRequest' + * responses: + * 200: + * description: The updated recipe + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Recipe' + * 404: + * description: Recipe not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updateRecipeById({ id, ...req.body })); }); +/** + * @openapi + * /recipes/{id}: + * patch: + * summary: Update a recipe by ID (partial) + * tags: [Recipes] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The recipe ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateRecipeRequest' + * responses: + * 200: + * description: The updated recipe + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Recipe' + * 404: + * description: Recipe not found + */ router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updateRecipeById({ id, ...req.body })); }); +/** + * @openapi + * /recipes/{id}: + * delete: + * summary: Delete a recipe by ID + * tags: [Recipes] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The recipe ID + * responses: + * 200: + * description: The deleted recipe + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedRecipeResponse' + * 404: + * description: Recipe not found + */ router.delete('/:id', (req, res) => { const { id } = req.params; diff --git a/src/routes/test.js b/src/routes/test.js index 252110d..4833e56 100644 --- a/src/routes/test.js +++ b/src/routes/test.js @@ -1,5 +1,56 @@ +/** + * @openapi + * tags: + * name: Tests + * description: API endpoints for testing server status + */ + const router = require('express').Router(); +/** + * @openapi + * /test: + * get: + * summary: Test server status (GET) + * tags: [Tests] + * responses: + * 200: + * description: Server is up and responding to GET + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TestStatusResponse' + * post: + * summary: Test server status (POST) + * tags: [Tests] + * responses: + * 200: + * description: Server is up and responding to POST + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TestStatusResponse' + * put: + * summary: Test server status (PUT) + * tags: [Tests] + * responses: + * 200: + * description: Server is up and responding to PUT + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TestStatusResponse' + * delete: + * summary: Test server status (DELETE) + * tags: [Tests] + * responses: + * 200: + * description: Server is up and responding to DELETE + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TestStatusResponse' + */ router.use('/', (req, res) => { res.status(200).send({ status: 'ok', method: req.method }); }); diff --git a/src/routes/todo.js b/src/routes/todo.js index c1e3410..9674b90 100644 --- a/src/routes/todo.js +++ b/src/routes/todo.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * name: Todos + * description: API endpoints for managing todos + */ + const router = require('express').Router(); const { getAllTodos, @@ -9,49 +16,250 @@ const { deleteTodoById, } = require('../controllers/todo'); -// get all todos +/** + * @openapi + * /todos: + * get: + * summary: Get all todos + * tags: [Todos] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of todos to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of todos to skip + * responses: + * 200: + * description: List of todos + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TodosResponse' + */ router.get('/', (req, res) => { res.send(getAllTodos({ ...req._options })); }); -// get random todo(s) +/** + * @openapi + * /todos/random/{length}: + * get: + * summary: Get random todo(s) + * tags: [Todos] + * parameters: + * - in: path + * name: length + * required: false + * schema: + * type: integer + * minimum: 1 + * maximum: 10 + * description: Number of random todos to return (max 10) + * responses: + * 200: + * description: Random todo(s) + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/Todo' + * - type: array + * items: + * $ref: '#/components/schemas/Todo' + */ router.get('/random/:length?', (req, res) => { res.send(getRandomTodo({ ...req.params })); }); -// get todo by id +/** + * @openapi + * /todos/{id}: + * get: + * summary: Get a todo by ID + * tags: [Todos] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The todo ID + * responses: + * 200: + * description: The todo object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Todo' + * 404: + * description: Todo not found + */ router.get('/:id', (req, res) => { res.send(getTodoById(req.params)); }); -// get todo by userId +/** + * @openapi + * /todos/user/{userId}: + * get: + * summary: Get todos by user ID + * tags: [Todos] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: User ID to filter todos by + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of todos to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of todos to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * responses: + * 200: + * description: List of todos for the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TodosResponse' + */ router.get('/user/:userId', (req, res) => { const { userId } = req.params; const { limit, skip, select } = req._options; - res.send(getTodosByUserId({ userId, limit, skip, select })); }); -// add new todo +/** + * @openapi + * /todos/add: + * post: + * summary: Add a new todo + * tags: [Todos] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddTodoRequest' + * responses: + * 201: + * description: The created todo + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Todo' + */ router.post('/add', (req, res) => { res.status(201).send(addNewTodo(req.body)); }); -// update todo by id (PUT) +/** + * @openapi + * /todos/{id}: + * put: + * summary: Update a todo by ID (replace) + * tags: [Todos] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The todo ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateTodoRequest' + * responses: + * 200: + * description: The updated todo + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Todo' + * 404: + * description: Todo not found + */ router.put('/:id', (req, res) => { const { id } = req.params; - res.send(updateTodoById({ id, ...req.body })); }); -// update todo by id (PATCH) +/** + * @openapi + * /todos/{id}: + * patch: + * summary: Update a todo by ID (partial) + * tags: [Todos] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The todo ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateTodoRequest' + * responses: + * 200: + * description: The updated todo + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Todo' + * 404: + * description: Todo not found + */ router.patch('/:id', (req, res) => { const { id } = req.params; - res.send(updateTodoById({ id, ...req.body })); }); -// delete todo +/** + * @openapi + * /todos/{id}: + * delete: + * summary: Delete a todo by ID + * tags: [Todos] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The todo ID + * responses: + * 200: + * description: The deleted todo + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedTodoResponse' + * 404: + * description: Todo not found + */ router.delete('/:id', (req, res) => { res.send(deleteTodoById({ ...req.params })); }); diff --git a/src/routes/user.js b/src/routes/user.js index 59b1827..b716c16 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,3 +1,10 @@ +/** + * @openapi + * tags: + * name: Users + * description: API endpoints for managing users + */ + const router = require('express').Router(); const { getCartsByUserId } = require('../controllers/cart'); const { getPostsByUserId } = require('../controllers/post'); @@ -15,12 +22,78 @@ const authUser = require('../middleware/auth'); const { verifyUserHandler } = require('../helpers'); const { loginByUsernamePassword } = require('../controllers/auth'); -// get all users +/** + * @openapi + * /users: + * get: + * summary: Get all users + * tags: [Users] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * description: Maximum number of users to return + * - in: query + * name: skip + * schema: + * type: integer + * description: Number of users to skip + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * - in: query + * name: sortBy + * schema: + * type: string + * description: Field to sort by + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: Sort order + * responses: + * 200: + * description: List of users + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UsersResponse' + */ router.get('/', (req, res) => { res.send(getAllUsers({ ...req._options })); }); -// login user +/** + * @openapi + * /users/login: + * post: + * summary: Login user + * tags: [Users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * example: kminchelle + * password: + * type: string + * example: 0lelplR + * responses: + * 200: + * description: Login successful + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserLoginResponse' + */ router.post('/login', async (req, res, next) => { try { const payload = await loginByUsernamePassword(req.body); @@ -35,22 +108,152 @@ router.post('/login', async (req, res, next) => { } }); -// get current authenticated user +/** + * @openapi + * /users/me: + * get: + * summary: Get current authenticated user + * tags: [Users] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: The authenticated user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + */ router.get('/me', authUser, (req, res) => { res.send(req.user); }); -// search users +/** + * @openapi + * /users/search: + * get: + * summary: Search users + * tags: [Users] + * parameters: + * - in: query + * name: q + * schema: + * type: string + * required: true + * description: Search query string + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: skip + * schema: + * type: integer + * - in: query + * name: select + * schema: + * type: string + * - in: query + * name: sortBy + * schema: + * type: string + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * responses: + * 200: + * description: Search results + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UsersResponse' + */ router.get('/search', (req, res) => { res.send(searchUsers({ ...req._options })); }); -// filter users +/** + * @openapi + * /users/filter: + * get: + * summary: Filter users + * tags: [Users] + * parameters: + * - in: query + * name: key + * schema: + * type: string + * required: true + * description: Field to filter by (e.g. gender) + * - in: query + * name: value + * schema: + * type: string + * required: true + * description: Value to filter by (e.g. male) + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: skip + * schema: + * type: integer + * - in: query + * name: select + * schema: + * type: string + * - in: query + * name: sortBy + * schema: + * type: string + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * responses: + * 200: + * description: Filtered users + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UsersResponse' + */ router.get('/filter', (req, res) => { res.send(filterUsers({ ...req._options })); }); -// get user by id +/** + * @openapi + * /users/{id}: + * get: + * summary: Get a user by ID + * tags: [Users] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The user ID + * - in: query + * name: select + * schema: + * type: string + * description: Comma-separated list of fields to include + * responses: + * 200: + * description: The user object + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 404: + * description: User not found + */ router.get('/:id', (req, res) => { const { id } = req.params; const { select } = req._options; @@ -58,7 +261,35 @@ router.get('/:id', (req, res) => { res.send(getUserById({ id, select })); }); -// get carts by userId +/** + * @openapi + * /users/{userId}/carts: + * get: + * summary: Get carts by user ID + * tags: [Users] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: The user ID + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: skip + * schema: + * type: integer + * responses: + * 200: + * description: List of carts for the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CartsResponse' + */ router.get('/:userId/carts', (req, res) => { const { userId } = req.params; const { limit, skip } = req._options; @@ -68,7 +299,39 @@ router.get('/:userId/carts', (req, res) => { res.send(getCartsByUserId({ userId, limit, skip })); }); -// get posts by userId +/** + * @openapi + * /users/{userId}/posts: + * get: + * summary: Get posts by user ID + * tags: [Users] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: The user ID + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: skip + * schema: + * type: integer + * - in: query + * name: select + * schema: + * type: string + * responses: + * 200: + * description: List of posts for the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponse' + */ router.get('/:userId/posts', (req, res) => { const { userId } = req.params; const { limit, skip, select } = req._options; @@ -81,7 +344,39 @@ router.get('/:userId/posts', (req, res) => { // get products by userId // * products are independent from users -// get todos by userId +/** + * @openapi + * /users/{userId}/todos: + * get: + * summary: Get todos by user ID + * tags: [Users] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: The user ID + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: skip + * schema: + * type: integer + * - in: query + * name: select + * schema: + * type: string + * responses: + * 200: + * description: List of todos for the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TodosResponse' + */ router.get('/:userId/todos', (req, res) => { const { userId } = req.params; const { limit, skip, select } = req._options; @@ -91,26 +386,123 @@ router.get('/:userId/todos', (req, res) => { res.send(getTodosByUserId({ userId, limit, skip, select })); }); -// add new user +/** + * @openapi + * /users/add: + * post: + * summary: Add a new user + * tags: [Users] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddUserRequest' + * responses: + * 201: + * description: The created user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + */ router.post('/add', (req, res) => { res.status(201).send(addNewUser({ ...req.body })); }); -// update user by id (PUT) +/** + * @openapi + * /users/{id}: + * put: + * summary: Update a user by ID (replace) + * tags: [Users] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The user ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateUserRequest' + * responses: + * 200: + * description: The updated user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 404: + * description: User not found + */ router.put('/:id', (req, res) => { const { id } = req.params; res.send(updateUserById({ id, ...req.body })); }); -// update user by id (PATCH) +/** + * @openapi + * /users/{id}: + * patch: + * summary: Update a user by ID (partial) + * tags: [Users] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The user ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateUserRequest' + * responses: + * 200: + * description: The updated user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 404: + * description: User not found + */ router.patch('/:id', (req, res) => { const { id } = req.params; res.send(updateUserById({ id, ...req.body })); }); -// delete user by id +/** + * @openapi + * /users/{id}: + * delete: + * summary: Delete a user by ID + * tags: [Users] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The user ID + * responses: + * 200: + * description: The deleted user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DeletedUserResponse' + * 404: + * description: User not found + */ router.delete('/:id', (req, res) => { res.send(deleteUserById({ ...req.params })); }); diff --git a/src/utils/openapi.js b/src/utils/openapi.js new file mode 100644 index 0000000..bae5c47 --- /dev/null +++ b/src/utils/openapi.js @@ -0,0 +1,45 @@ +const swaggerJsDoc = require('swagger-jsdoc'); +const { version } = require('../../package.json'); + +// Swagger definition +const swaggerDefinition = { + openapi: '3.1.0', + info: { + title: 'DummyJSON API Documentation', + version, + description: 'Documentation for the DummyJSON REST API', + author: 'Muhammad Ovi ', + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT', + }, + }, + servers: [ + { + url: 'https://dummyjson.com', + description: 'DummyJSON API Server', + }, + ], + externalDocs: { + description: 'scalar-api-docs', + url: '/api-docs', + }, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, +}; + +const options = { + swaggerDefinition, + apis: ['./src/routes/*.js', './src/controllers/*.js', './src/models/openapi-schemas/*.js'], +}; + +const swaggerSpec = swaggerJsDoc(options); + +module.exports = swaggerSpec; diff --git a/views/docs.ejs b/views/docs.ejs index aeaf0e7..5cb5f20 100644 --- a/views/docs.ejs +++ b/views/docs.ejs @@ -38,6 +38,30 @@ +
+ API Documentation +

Access our interactive OpenAPI documentation to explore all endpoints.

+

+ DummyJSON provides OpenAPI-compliant documentation with a modern Scalar UI. You can also download the OpenAPI specification to use with your own tools. +

+ +

+          // Fetch the OpenAPI specification
+          fetch('https://dummyjson.com/api-docs/openapi.json')
+            .then(res => res.json())
+            .then(spec => console.log('API has', Object.keys(spec.paths).length, 'endpoints'));
+        
+
+
Limiting Resources