From 0e6dec6c897fb8135ec4f060daeb7645b1d3ae0a Mon Sep 17 00:00:00 2001 From: PAMulligan Date: Wed, 10 Jun 2026 21:46:33 -0400 Subject: [PATCH] feat(scripts): add Postman collection generation to project setup setup-project.sh now generates postman/collection.json (Health, Auth, Resources folders, base_url/auth_token variables, collection-level pre-request script that injects the bearer token), a platform-aware postman/environment.json (port 3000 for Node, 8787 for Workers), and a project README with import and regeneration instructions. generate-openapi-docs.sh now tags the starter spec endpoints and, after exporting the spec, regenerates the Postman collection from it via openapi-to-postmanv2, then re-applies the base_url/auth_token variables and the auth pre-request script the converter cannot know about. Closes #59 Co-Authored-By: Claude Fable 5 --- scripts/generate-openapi-docs.sh | 66 ++++++++++ scripts/setup-project.sh | 207 +++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) diff --git a/scripts/generate-openapi-docs.sh b/scripts/generate-openapi-docs.sh index ac0e51f..a284867 100644 --- a/scripts/generate-openapi-docs.sh +++ b/scripts/generate-openapi-docs.sh @@ -3,6 +3,7 @@ set -euo pipefail # ============================================================================ # generate-openapi-docs.sh - Generate OpenAPI documentation from Hono routes +# and regenerate the Postman collection from it # Usage: ./scripts/generate-openapi-docs.sh [--serve] [--port 8080] # ============================================================================ @@ -79,6 +80,7 @@ const spec: Record = { '/health': { get: { summary: 'Health check', + tags: ['Health'], responses: { '200': { description: 'Service is healthy', @@ -101,6 +103,7 @@ const spec: Record = { '/': { get: { summary: 'API root', + tags: ['Health'], responses: { '200': { description: 'API information', @@ -178,12 +181,14 @@ paths: /health: get: summary: Health check + tags: [Health] responses: "200": description: Service is healthy /: get: summary: API root + tags: [Health] responses: "200": description: API information @@ -194,6 +199,67 @@ fi FILE_SIZE=$(wc -c < "$OUTPUT_FILE" | tr -d ' ') info "Spec size: ${FILE_SIZE} bytes" +# ---- Postman collection ---- +POSTMAN_DIR="$PROJECT_ROOT/postman" +POSTMAN_FILE="$POSTMAN_DIR/collection.json" + +# Re-applies Nerva conventions to the converted collection: the converter +# names its server variable baseUrl (Nerva standardizes on base_url), and +# the conversion has no knowledge of the auth_token injection script. +postman_merge() { + node -e ' +const fs = require("fs"); +const file = process.argv[1]; +let raw = fs.readFileSync(file, "utf8"); +raw = raw.split("{{baseUrl}}").join("{{base_url}}"); +const col = JSON.parse(raw); +const previous = (col.variable || []).find(function (v) { + return v.key === "baseUrl" || v.key === "base_url"; +}); +const vars = (col.variable || []).filter(function (v) { + return v.key !== "baseUrl" && v.key !== "base_url" && v.key !== "auth_token"; +}); +vars.push({ + key: "base_url", + value: (previous && previous.value) || "http://localhost:3000", + type: "string", +}); +vars.push({ key: "auth_token", value: "", type: "string" }); +col.variable = vars; +const events = (col.event || []).filter(function (e) { + return e.listen !== "prerequest"; +}); +events.push({ + listen: "prerequest", + script: { + type: "text/javascript", + exec: [ + "// Inject the bearer token when auth_token is set in the active scope", + "const token = pm.variables.get(\"auth_token\");", + "if (token) {", + " pm.request.headers.upsert({ key: \"Authorization\", value: \"Bearer \" + token });", + "}", + ], + }, +}); +col.event = events; +fs.writeFileSync(file, JSON.stringify(col, null, 2) + "\n"); +' "$1" +} + +echo "" +info "Regenerating Postman collection from the OpenAPI spec..." +mkdir -p "$POSTMAN_DIR" + +if npx --yes --package=openapi-to-postmanv2 openapi2postmanv2 \ + -s "$OUTPUT_FILE" -o "$POSTMAN_FILE" -p \ + -O folderStrategy=Tags > /dev/null \ + && postman_merge "$POSTMAN_FILE"; then + success "Postman collection written to: $POSTMAN_FILE" +else + warn "Could not regenerate the Postman collection (conversion or post-processing failed)." +fi + if [[ "$SERVE" == true ]]; then echo "" info "Starting Scalar API documentation UI on port $SERVE_PORT..." diff --git a/scripts/setup-project.sh b/scripts/setup-project.sh index 2f2869e..b06fc71 100644 --- a/scripts/setup-project.sh +++ b/scripts/setup-project.sh @@ -261,6 +261,209 @@ ENVEOF success "Node.js / Docker configured." fi +# ---- Postman collection ---- +step "Generating Postman collection..." + +if [[ "$PLATFORM" == "cloudflare" ]]; then + DEV_BASE_URL="http://localhost:8787" + PLATFORM_LABEL="Cloudflare Workers" + DEV_STEPS=$'npx wrangler dev # start the local dev server' +else + DEV_BASE_URL="http://localhost:3000" + PLATFORM_LABEL="Node.js / Docker" + DEV_STEPS=$'docker compose up -d # start PostgreSQL\npnpm dev # start the dev server' +fi + +make_dirs "$TARGET_DIR/postman" + +write_file "$TARGET_DIR/postman/collection.json" << COLEOF +{ + "info": { + "name": "$PROJECT_NAME", + "description": "Starter Postman collection for the $PROJECT_NAME API. Health holds the built-in endpoints and works immediately; Auth and Resources fill in as endpoints are generated. A collection-level pre-request script injects an Authorization header on every request once auth_token is set in the active environment.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Inject the bearer token when auth_token is set in the active scope", + "const token = pm.variables.get('auth_token');", + "if (token) {", + " pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer ' + token });", + "}" + ] + } + } + ], + "variable": [ + { "key": "base_url", "value": "$DEV_BASE_URL", "type": "string" }, + { "key": "auth_token", "value": "", "type": "string" } + ], + "item": [ + { + "name": "Health", + "description": "Built-in service endpoints available immediately after setup.", + "item": [ + { + "name": "Health check", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('status is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{base_url}}/health", + "description": "Returns service status, including database connectivity." + }, + "response": [] + }, + { + "name": "API root", + "request": { + "method": "GET", + "header": [], + "url": "{{base_url}}/", + "description": "Returns the API name and version." + }, + "response": [] + } + ] + }, + { + "name": "Auth", + "description": "Authentication endpoints. The Login request stores the returned token in the active environment as auth_token, which the collection pre-request script injects on subsequent requests.", + "item": [ + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Store the token so the collection pre-request script can inject it", + "if (pm.response.code === 200) {", + " const body = pm.response.json();", + " const token = body.token || (body.data && body.data.token);", + " if (token) {", + " pm.environment.set('auth_token', token);", + " }", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"user@example.com\",\n \"password\": \"change-me\"\n}", + "options": { "raw": { "language": "json" } } + }, + "url": "{{base_url}}/auth/login", + "description": "Available once auth endpoints are generated (api-authentication skill or /build-from-schema Phase 4)." + }, + "response": [] + } + ] + }, + { + "name": "Resources", + "description": "Generated CRUD endpoints land here. Regenerate this collection from the OpenAPI spec after running /build-from-schema -- see the README for the command.", + "item": [] + } + ] +} +COLEOF + +write_file "$TARGET_DIR/postman/environment.json" << PMENVEOF +{ + "name": "$PROJECT_NAME (local)", + "values": [ + { + "key": "base_url", + "value": "$DEV_BASE_URL", + "type": "default", + "enabled": true + }, + { + "key": "auth_token", + "value": "", + "type": "secret", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} +PMENVEOF + +success "Postman collection and environment created." + +# ---- Project README ---- +step "Generating project README..." + +{ + cat << READMEDYN +# $PROJECT_NAME + +A Nerva API project for the $PLATFORM_LABEL target. Service code lives in [api/](api/), documentation in [docs/](docs/), and a ready-to-import Postman collection in [postman/](postman/). + +## Quickstart + +\`\`\`bash +cd api +$DEV_STEPS +pnpm test # run the test suite +\`\`\` + +The dev server listens at $DEV_BASE_URL -- verify with a GET to /health. +READMEDYN + cat << 'READMESTATIC' + +## Postman + +`postman/collection.json` is a Postman Collection (v2.1 format) organized into three folders: **Health** (works immediately), **Auth**, and **Resources** (populated as endpoints are generated). `postman/environment.json` is the matching local environment defining `base_url` and `auth_token`. + +To import and run: + +1. In Postman, click **Import** and drop in `postman/collection.json` and `postman/environment.json`. +2. Pick the imported environment in the environment selector (top right). +3. With the dev server running, send **Health > Health check**. + +Authenticated requests are handled by the collection-level pre-request script: once `auth_token` has a value, every request gets an `Authorization: Bearer ` header. The **Auth > Login** request fills `auth_token` automatically from a successful login response; until auth endpoints exist, paste a token into the environment manually. + +### Regenerating the collection from an OpenAPI spec + +Once the API has an OpenAPI spec (the Nerva pipeline writes one to `docs/openapi.yaml`), regenerate the collection so it covers every endpoint: + +```bash +npx --yes --package=openapi-to-postmanv2 openapi2postmanv2 \ + -s docs/openapi.yaml -o postman/collection.json -p \ + -O folderStrategy=Tags +``` + +In a Nerva framework checkout, `./scripts/generate-openapi-docs.sh` runs this conversion automatically after exporting the spec, and additionally re-applies the `base_url`/`auth_token` variables and the auth pre-request script. Re-import the file in Postman to pick up changes; `postman/environment.json` is never overwritten, so your tokens and URLs survive regeneration. +READMESTATIC +} | write_file "$TARGET_DIR/README.md" + +success "README created." + # ---- .gitignore ---- write_file "$API_DIR/.gitignore" << 'GEOF' node_modules/ @@ -300,4 +503,8 @@ if ! $DRY_RUN; then fi echo " pnpm test # Run tests" echo "" + echo " Postman:" + echo " Import postman/collection.json and postman/environment.json" + echo " to start testing endpoints immediately." + echo "" fi