feat: Add automatic Postman collection generation and download endpoint Closes #199#356
feat: Add automatic Postman collection generation and download endpoint Closes #199#356mich-sys wants to merge 1 commit intoEDOHWARES:mainfrom
Conversation
|
@mich-sys Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Pull request overview
Adds automated Postman collection generation from the backend’s Swagger/OpenAPI (swagger-jsdoc) spec and exposes it via a download endpoint to improve third-party API testing (Issue #199).
Changes:
- Add
openapi-to-postmanv2dependency to the backend workspace. - Generate a cached Postman collection at server start from the existing
swaggerSpec. - Expose
/api/docs/postman.jsonto download the generated collection.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| package-lock.json | Updates workspace lockfile to include openapi-to-postmanv2 and its transitive dependencies. |
| backend/src/routes/docs.routes.js | Generates Postman collection from swaggerSpec at startup and adds the /postman.json download route. |
| backend/package.json | Adds openapi-to-postmanv2 to backend dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (err) { | ||
| console.error('Error generating Postman collection:', err); | ||
| } else if (!conversionResult.result) { | ||
| console.error('Could not convert to Postman format:', conversionResult.reason); |
There was a problem hiding this comment.
docs.routes.js uses console.error/console.log for runtime logging, but the backend consistently uses the shared config/logger (e.g., routes like src/routes/queue.routes.js). Please switch these to logger.error/logger.info (and import the logger) so logs are structured/consistent across environments.
|
|
||
| Converter.convert( | ||
| { type: 'json', data: swaggerSpec }, | ||
| { folderStrategy: 'Tags', includeWebhooks: true }, | ||
| (err, conversionResult) => { | ||
| if (err) { | ||
| console.error('Error generating Postman collection:', err); | ||
| } else if (!conversionResult.result) { | ||
| console.error('Could not convert to Postman format:', conversionResult.reason); | ||
| } else { | ||
| postmanCollection = conversionResult.output[0].data; | ||
| // Ensure pre-defined environment variables are included | ||
| postmanCollection.variable = postmanCollection.variable || []; | ||
|
|
||
| const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN'); | ||
| if (!hasAuth) { | ||
| postmanCollection.variable.push({ | ||
| key: 'AUTH_TOKEN', | ||
| value: '', | ||
| type: 'string', | ||
| description: 'Bearer token for authenticated requests' | ||
| }); | ||
| } | ||
| console.log('Postman collection successfully generated from Swagger annotations.'); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| router.get('/postman.json', (req, res) => { | ||
| if (!postmanCollection) { | ||
| return res.status(503).json({ error: 'Postman collection is still generating or failed to generate.' }); | ||
| } | ||
| res.attachment('EventHorizon_Postman_Collection.json'); | ||
| res.json(postmanCollection); |
There was a problem hiding this comment.
If Postman generation fails, the server only logs the error and leaves postmanCollection as null, so /postman.json will return a generic 503 forever with no way to recover or diagnose via the API. Consider storing the failure reason separately (e.g., last error + timestamp) and returning a 500 with a safe error message, and/or allowing regeneration (retry on next request or via an admin-only refresh endpoint).
| Converter.convert( | |
| { type: 'json', data: swaggerSpec }, | |
| { folderStrategy: 'Tags', includeWebhooks: true }, | |
| (err, conversionResult) => { | |
| if (err) { | |
| console.error('Error generating Postman collection:', err); | |
| } else if (!conversionResult.result) { | |
| console.error('Could not convert to Postman format:', conversionResult.reason); | |
| } else { | |
| postmanCollection = conversionResult.output[0].data; | |
| // Ensure pre-defined environment variables are included | |
| postmanCollection.variable = postmanCollection.variable || []; | |
| const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN'); | |
| if (!hasAuth) { | |
| postmanCollection.variable.push({ | |
| key: 'AUTH_TOKEN', | |
| value: '', | |
| type: 'string', | |
| description: 'Bearer token for authenticated requests' | |
| }); | |
| } | |
| console.log('Postman collection successfully generated from Swagger annotations.'); | |
| } | |
| } | |
| ); | |
| router.get('/postman.json', (req, res) => { | |
| if (!postmanCollection) { | |
| return res.status(503).json({ error: 'Postman collection is still generating or failed to generate.' }); | |
| } | |
| res.attachment('EventHorizon_Postman_Collection.json'); | |
| res.json(postmanCollection); | |
| let isGeneratingPostmanCollection = false; | |
| let lastPostmanGenerationError = null; | |
| let lastPostmanGenerationErrorAt = null; | |
| function generatePostmanCollection() { | |
| if (isGeneratingPostmanCollection) { | |
| return; | |
| } | |
| isGeneratingPostmanCollection = true; | |
| lastPostmanGenerationError = null; | |
| lastPostmanGenerationErrorAt = null; | |
| Converter.convert( | |
| { type: 'json', data: swaggerSpec }, | |
| { folderStrategy: 'Tags', includeWebhooks: true }, | |
| (err, conversionResult) => { | |
| isGeneratingPostmanCollection = false; | |
| if (err) { | |
| postmanCollection = null; | |
| lastPostmanGenerationError = 'Failed to generate Postman collection.'; | |
| lastPostmanGenerationErrorAt = new Date().toISOString(); | |
| console.error('Error generating Postman collection:', err); | |
| } else if (!conversionResult.result) { | |
| postmanCollection = null; | |
| lastPostmanGenerationError = 'Failed to convert OpenAPI spec to Postman collection.'; | |
| lastPostmanGenerationErrorAt = new Date().toISOString(); | |
| console.error('Could not convert to Postman format:', conversionResult.reason); | |
| } else { | |
| postmanCollection = conversionResult.output[0].data; | |
| lastPostmanGenerationError = null; | |
| lastPostmanGenerationErrorAt = null; | |
| // Ensure pre-defined environment variables are included | |
| postmanCollection.variable = postmanCollection.variable || []; | |
| const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN'); | |
| if (!hasAuth) { | |
| postmanCollection.variable.push({ | |
| key: 'AUTH_TOKEN', | |
| value: '', | |
| type: 'string', | |
| description: 'Bearer token for authenticated requests' | |
| }); | |
| } | |
| console.log('Postman collection successfully generated from Swagger annotations.'); | |
| } | |
| } | |
| ); | |
| } | |
| generatePostmanCollection(); | |
| router.get('/postman.json', (req, res) => { | |
| if (postmanCollection) { | |
| res.attachment('EventHorizon_Postman_Collection.json'); | |
| return res.json(postmanCollection); | |
| } | |
| if (isGeneratingPostmanCollection) { | |
| return res.status(503).json({ error: 'Postman collection is still generating.' }); | |
| } | |
| if (lastPostmanGenerationError) { | |
| generatePostmanCollection(); | |
| if (postmanCollection) { | |
| res.attachment('EventHorizon_Postman_Collection.json'); | |
| return res.json(postmanCollection); | |
| } | |
| if (isGeneratingPostmanCollection) { | |
| return res.status(503).json({ | |
| error: 'Postman collection regeneration is in progress.', | |
| lastFailure: { | |
| message: lastPostmanGenerationError, | |
| timestamp: lastPostmanGenerationErrorAt, | |
| }, | |
| }); | |
| } | |
| return res.status(500).json({ | |
| error: 'Postman collection is unavailable due to a generation error.', | |
| lastFailure: { | |
| message: lastPostmanGenerationError, | |
| timestamp: lastPostmanGenerationErrorAt, | |
| }, | |
| }); | |
| } | |
| generatePostmanCollection(); | |
| return res.status(503).json({ error: 'Postman collection is still generating.' }); |
| // Ensure pre-defined environment variables are included | ||
| postmanCollection.variable = postmanCollection.variable || []; | ||
|
|
||
| const hasAuth = postmanCollection.variable.find(v => v.key === 'AUTH_TOKEN'); | ||
| if (!hasAuth) { | ||
| postmanCollection.variable.push({ | ||
| key: 'AUTH_TOKEN', | ||
| value: '', | ||
| type: 'string', | ||
| description: 'Bearer token for authenticated requests' | ||
| }); |
There was a problem hiding this comment.
The collection variable AUTH_TOKEN is added, but the generated requests likely won’t automatically use it because the OpenAPI definition in this file doesn’t declare a bearer securityScheme (and the route JSDoc blocks generally don’t specify security). To make the exported Postman collection actually usable for authenticated endpoints, add a components.securitySchemes bearer auth definition to swaggerSpec.definition and apply it globally or per protected route so the converter generates Authorization headers that reference the token variable.
Closes #199