mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
docs: add complete dev Postman API collection (#119)
* docs: add dev Postman starter collection * docs: generate complete dev Postman API collection
This commit is contained in:
parent
1a6edd255a
commit
13ce4b7e88
4 changed files with 19784 additions and 0 deletions
65
postman/README.md
Normal file
65
postman/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# SeaHaven Dev Postman collection
|
||||
|
||||
This directory contains the complete SeaHaven backend **dev** API collection,
|
||||
generated from the live OpenAPI contract. The generated collection currently
|
||||
contains every operation in the contract (368 operations across 330 paths),
|
||||
including all documented parameters, request bodies, schemas, and response
|
||||
codes.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `SeaHaven-Dev.postman_collection.json` | Complete OpenAPI-derived Postman v2.1 collection, grouped by API tag |
|
||||
| `SeaHaven-Dev.postman_environment.json` | Dev authentication environment with secret username, password, and reusable bearer token |
|
||||
| `../scripts/generate-postman-collection.mjs` | Deterministic generator and coverage checker |
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Import `SeaHaven-Dev.postman_collection.json` into Postman.
|
||||
2. Import `SeaHaven-Dev.postman_environment.json` and select **SeaHaven - Dev**.
|
||||
3. Enter the dev test account's `username` and `password` in the environment.
|
||||
They are empty and secret by default; never commit or share populated values.
|
||||
4. Run **Setup → Authentication - Login**. The pre-request script clears any
|
||||
stale token, skips the request when either credential is missing, and never
|
||||
logs credentials. A successful response stores only its non-empty `token`
|
||||
field in the environment's `token` variable.
|
||||
5. All generated API requests inherit collection-level `Bearer {{token}}`
|
||||
authentication. They reuse the captured token automatically.
|
||||
|
||||
## Generated requests and examples
|
||||
|
||||
Every OpenAPI operation is included under its first alphabetically sorted tag.
|
||||
Path, query, and header parameters retain their required/optional status,
|
||||
schema hint, and description. JSON request bodies use recursively generated
|
||||
illustrative placeholders from local OpenAPI schemas; multipart requests use
|
||||
form-data fields and mark binary fields as files. These examples are not live
|
||||
records: replace placeholder values with a valid dev fixture before sending.
|
||||
No response bodies, credentials, tokens, or other dev data are stored in this
|
||||
repository.
|
||||
|
||||
The generated test script only checks that the response status is one of the
|
||||
codes documented by OpenAPI. It does not assume every operation returns `200`.
|
||||
|
||||
## Regenerate or check coverage
|
||||
|
||||
From the backend repository root:
|
||||
|
||||
```bash
|
||||
node scripts/generate-postman-collection.mjs
|
||||
node scripts/generate-postman-collection.mjs --check
|
||||
```
|
||||
|
||||
The generator fetches `https://api.dev.seahaven.com/swagger/v1/swagger.json`,
|
||||
fails on a non-JSON/non-success response, and rewrites the collection and the
|
||||
empty-credential environment deterministically. `--check` compares the checked
|
||||
in collection with the current contract and verifies operation count, unique
|
||||
operation keys, request-body coverage, Bearer inheritance, and the explicit
|
||||
no-auth login exception.
|
||||
|
||||
## Safety
|
||||
|
||||
This collection is **dev-only**. Do not point it at staging or production. Keep
|
||||
`username`, `password`, and `token` local to your Postman workspace; never mark
|
||||
them shared, export them, or commit them. Review every illustrative body and
|
||||
parameter before running a mutating request.
|
||||
19314
postman/SeaHaven-Dev.postman_collection.json
Normal file
19314
postman/SeaHaven-Dev.postman_collection.json
Normal file
File diff suppressed because it is too large
Load diff
30
postman/SeaHaven-Dev.postman_environment.json
Normal file
30
postman/SeaHaven-Dev.postman_environment.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "SeaHaven - Dev",
|
||||
"values": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "https://api.dev.seahaven.com",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "token",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment"
|
||||
}
|
||||
375
scripts/generate-postman-collection.mjs
Normal file
375
scripts/generate-postman-collection.mjs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const DEFAULT_SOURCE = "https://api.dev.seahaven.com/swagger/v1/swagger.json";
|
||||
const ROOT = path.resolve(new URL("..", import.meta.url).pathname);
|
||||
const COLLECTION_PATH = path.join(ROOT, "postman", "SeaHaven-Dev.postman_collection.json");
|
||||
const ENVIRONMENT_PATH = path.join(ROOT, "postman", "SeaHaven-Dev.postman_environment.json");
|
||||
const METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
||||
|
||||
const sourceArg = process.argv.find((arg) => arg.startsWith("--source="));
|
||||
const inputArg = process.argv.find((arg) => arg.startsWith("--input="));
|
||||
const sourceUrl = sourceArg ? sourceArg.slice("--source=".length) : DEFAULT_SOURCE;
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
function operationEntries(document) {
|
||||
return Object.entries(document.paths ?? {}).flatMap(([route, pathItem]) =>
|
||||
METHODS.filter((method) => pathItem?.[method]).map((method) => ({
|
||||
route,
|
||||
method,
|
||||
operation: pathItem[method],
|
||||
pathParameters: pathItem.parameters ?? [],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function schemaRefName(ref) {
|
||||
return typeof ref === "string" ? ref.split("/").pop() : undefined;
|
||||
}
|
||||
|
||||
function resolveSchema(schema, document) {
|
||||
if (!schema) return undefined;
|
||||
if (schema.$ref) return document.components?.schemas?.[schemaRefName(schema.$ref)] ?? schema;
|
||||
return schema;
|
||||
}
|
||||
|
||||
function safeSample(schema, document, name = "value", depth = 0, seen = new Set()) {
|
||||
const resolved = resolveSchema(schema, document) ?? {};
|
||||
if (depth > 4) return "<nested value>";
|
||||
if (resolved.$ref) {
|
||||
if (seen.has(resolved.$ref)) return "<recursive value>";
|
||||
seen.add(resolved.$ref);
|
||||
}
|
||||
if (resolved.enum?.length) return resolved.enum[0];
|
||||
if (resolved.oneOf?.length) return safeSample(resolved.oneOf[0], document, name, depth + 1, seen);
|
||||
if (resolved.anyOf?.length) return safeSample(resolved.anyOf[0], document, name, depth + 1, seen);
|
||||
if (resolved.allOf?.length) {
|
||||
return resolved.allOf.reduce((acc, part) => {
|
||||
const value = safeSample(part, document, name, depth + 1, seen);
|
||||
return typeof value === "object" && value && !Array.isArray(value) ? { ...acc, ...value } : value;
|
||||
}, {});
|
||||
}
|
||||
if (resolved.type === "array" || resolved.items) {
|
||||
return [safeSample(resolved.items ?? {}, document, `${name}Item`, depth + 1, seen)];
|
||||
}
|
||||
if (resolved.type === "object" || resolved.properties || resolved.additionalProperties) {
|
||||
const object = {};
|
||||
for (const [property, propertySchema] of Object.entries(resolved.properties ?? {})) {
|
||||
object[property] = safeSample(propertySchema, document, property, depth + 1, seen);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
switch (resolved.format) {
|
||||
case "date-time": return "2026-01-01T00:00:00Z";
|
||||
case "date": return "2026-01-01";
|
||||
case "uuid": return "00000000-0000-0000-0000-000000000000";
|
||||
case "email": return "user@example.com";
|
||||
case "uri": return "https://example.com/resource";
|
||||
case "binary": return "<binary file>";
|
||||
default: break;
|
||||
}
|
||||
switch (resolved.type) {
|
||||
case "integer":
|
||||
case "number": return 0;
|
||||
case "boolean": return false;
|
||||
case "null": return null;
|
||||
default: return "<string>";
|
||||
}
|
||||
}
|
||||
|
||||
function schemaHint(schema) {
|
||||
if (!schema) return "schema unspecified";
|
||||
if (schema.$ref) return schema.$ref;
|
||||
const bits = [schema.type, schema.format].filter(Boolean);
|
||||
if (schema.enum?.length) bits.push(`enum: ${schema.enum.join(", ")}`);
|
||||
return bits.join("/") || "schema";
|
||||
}
|
||||
|
||||
function parameterValue(parameter, document) {
|
||||
const schema = parameter.schema ?? {};
|
||||
if (schema.enum?.length) return String(schema.enum[0]);
|
||||
if (parameter.example !== undefined) return String(parameter.example);
|
||||
if (schema.type === "boolean") return "false";
|
||||
if (["integer", "number"].includes(schema.type)) return "0";
|
||||
if (schema.type === "array") return "";
|
||||
return "";
|
||||
}
|
||||
|
||||
function mergeParameters(entry) {
|
||||
const parameters = [...entry.pathParameters, ...(entry.operation.parameters ?? [])];
|
||||
const seen = new Set();
|
||||
return parameters.filter((parameter) => {
|
||||
const key = `${parameter.in}:${parameter.name}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return ["path", "query", "header"].includes(parameter.in);
|
||||
});
|
||||
}
|
||||
|
||||
function urlFor(entry, parameters) {
|
||||
const pathParameters = parameters.filter((p) => p.in === "path");
|
||||
const queryParameters = parameters.filter((p) => p.in === "query");
|
||||
const headerParameters = parameters.filter((p) => p.in === "header");
|
||||
const postmanPath = entry.route.replaceAll(/\{([^}]+)\}/g, ":$1");
|
||||
const query = queryParameters.map((p) => ({
|
||||
key: p.name,
|
||||
value: parameterValue(p),
|
||||
description: `${p.required ? "Required" : "Optional"}; ${schemaHint(p.schema)}`,
|
||||
disabled: false,
|
||||
}));
|
||||
const raw = `{{baseUrl}}${postmanPath}${query.length ? `?${query.map((p) => `${p.key}=${p.value}`).join("&")}` : ""}`;
|
||||
return {
|
||||
raw,
|
||||
host: ["{{baseUrl}}"],
|
||||
path: postmanPath.split("/").filter(Boolean),
|
||||
...(query.length ? { query } : {}),
|
||||
...(pathParameters.length ? {
|
||||
variable: pathParameters.map((p) => ({
|
||||
key: p.name,
|
||||
value: parameterValue(p),
|
||||
description: `${p.required ? "Required" : "Optional"}; ${schemaHint(p.schema)}`,
|
||||
})),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function bodyFor(entry, document) {
|
||||
const content = entry.operation.requestBody?.content ?? {};
|
||||
const mediaTypes = Object.keys(content);
|
||||
const jsonType = mediaTypes.find((type) => type === "application/json" || type === "text/json" || type.includes("+json"));
|
||||
const type = jsonType ?? mediaTypes.find((value) => value === "multipart/form-data") ?? mediaTypes[0];
|
||||
if (!type) return undefined;
|
||||
const schema = content[type]?.schema;
|
||||
const description = `Illustrative placeholder generated from ${schemaHint(schema)}; replace placeholder values before sending.`;
|
||||
if (type === "multipart/form-data") {
|
||||
const resolved = resolveSchema(schema, document) ?? {};
|
||||
const formdata = Object.entries(resolved.properties ?? {}).map(([key, propertySchema]) => ({
|
||||
key,
|
||||
type: propertySchema.format === "binary" ? "file" : "text",
|
||||
...(propertySchema.format === "binary" ? { src: "" } : { value: String(safeSample(propertySchema, document, key)) }),
|
||||
description: `${resolved.required?.includes(key) ? "Required" : "Optional"}; ${schemaHint(propertySchema)}`,
|
||||
}));
|
||||
return { mode: "formdata", formdata, description };
|
||||
}
|
||||
if (type === "application/octet-stream") return { mode: "file", file: { src: "" }, description };
|
||||
if (type.startsWith("text/")) return { mode: "raw", raw: "<text payload>", options: { raw: { language: "text" } }, description };
|
||||
return { mode: "raw", raw: JSON.stringify(safeSample(schema, document), null, 2), options: { raw: { language: "json" } }, description };
|
||||
}
|
||||
|
||||
function operationDescription(entry, document, parameters, body) {
|
||||
const operation = entry.operation;
|
||||
const responses = Object.entries(operation.responses ?? {})
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([code, response]) => {
|
||||
const mediaTypes = Object.keys(response.content ?? {});
|
||||
return `${code}${mediaTypes.length ? ` (${mediaTypes.join(", ")})` : ""}`;
|
||||
})
|
||||
.join(", ") || "not documented";
|
||||
const requestMediaTypes = Object.keys(operation.requestBody?.content ?? {});
|
||||
const lines = [
|
||||
`Source: ${sourceUrl}`,
|
||||
`Tag(s): ${(operation.tags ?? ["Other"]).join(", ")}`,
|
||||
`Operation ID: ${operation.operationId ?? "not specified"}`,
|
||||
"",
|
||||
operation.summary ?? "",
|
||||
operation.description ?? "",
|
||||
"",
|
||||
`Parameters: ${parameters.length ? parameters.map((p) => `${p.in} ${p.name} (${p.required ? "required" : "optional"}, ${schemaHint(p.schema)})`).join("; ") : "none"}`,
|
||||
`Request body: ${body ? `${requestMediaTypes.join(", ") || "media type unspecified"}; ${body.description}` : "none documented"}`,
|
||||
`Documented response codes: ${responses}`,
|
||||
"",
|
||||
"Body values and parameter values are safe illustrative placeholders; review them against the target record before sending.",
|
||||
];
|
||||
return lines.filter((line, index) => !(line === "" && lines[index - 1] === "")).join("\n").trim();
|
||||
}
|
||||
|
||||
function testEvent(operation) {
|
||||
const codes = Object.keys(operation.responses ?? {}).filter((key) => /^\d{3}$/.test(key)).map(Number);
|
||||
if (!codes.length) return [];
|
||||
return [{
|
||||
listen: "test",
|
||||
script: {
|
||||
type: "text/javascript",
|
||||
exec: [
|
||||
`const documented = [${codes.join(", ")}];`,
|
||||
"pm.test(\"Response status is documented by OpenAPI\", function () {",
|
||||
" pm.expect(documented).to.include(pm.response.code);",
|
||||
"});",
|
||||
],
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
function requestItem(entry, document) {
|
||||
const parameters = mergeParameters(entry);
|
||||
const body = bodyFor(entry, document);
|
||||
const headers = parameters.filter((p) => p.in === "header").map((p) => ({
|
||||
key: p.name,
|
||||
value: parameterValue(p),
|
||||
description: `${p.required ? "Required" : "Optional"}; ${schemaHint(p.schema)}`,
|
||||
}));
|
||||
if (body?.mode === "raw" && body.options?.raw?.language === "json") headers.push({ key: "Content-Type", value: "application/json" });
|
||||
if (body?.mode === "raw" && body.options?.raw?.language === "text") headers.push({ key: "Content-Type", value: "text/plain" });
|
||||
const item = {
|
||||
name: `${entry.method.toUpperCase()} ${entry.route}`,
|
||||
request: {
|
||||
method: entry.method.toUpperCase(),
|
||||
header: headers,
|
||||
url: urlFor(entry, parameters),
|
||||
description: operationDescription(entry, document, parameters, body),
|
||||
...(body ? { body } : {}),
|
||||
},
|
||||
event: testEvent(entry.operation),
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
function setupItems(document) {
|
||||
const login = operationEntries(document).find((entry) => entry.route.toLowerCase() === "/api/authentication/login" && entry.method === "post");
|
||||
const loginSchema = login?.operation.requestBody?.content?.["application/json"]?.schema;
|
||||
return [
|
||||
{
|
||||
name: "Swagger - OpenAPI document",
|
||||
request: {
|
||||
auth: { type: "noauth" }, method: "GET", header: [],
|
||||
url: { raw: "{{baseUrl}}/swagger/v1/swagger.json", host: ["{{baseUrl}}"], path: ["swagger", "v1", "swagger.json"] },
|
||||
description: `Live OpenAPI contract for dev (${sourceUrl}). Import this URL when you need the latest server surface. No auth required.`,
|
||||
},
|
||||
event: [{ listen: "test", script: { type: "text/javascript", exec: [
|
||||
"pm.test(\"OpenAPI document responded 200\", function () { pm.response.to.have.status(200); });",
|
||||
"pm.test(\"OpenAPI document is JSON\", function () { pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\"); });",
|
||||
"pm.test(\"OpenAPI document has paths\", function () { pm.expect(pm.response.json()).to.have.property(\"paths\"); });",
|
||||
] } }],
|
||||
},
|
||||
{
|
||||
name: "Authentication - Login",
|
||||
request: {
|
||||
auth: { type: "noauth" }, method: "POST", header: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: { mode: "raw", raw: JSON.stringify({ username: "{{username}}", password: "{{password}}" }, null, 2), options: { raw: { language: "json" } }, description: "Credentials are read from the selected secret environment and are never logged." },
|
||||
url: { raw: "{{baseUrl}}/api/Authentication/login", host: ["{{baseUrl}}"], path: ["api", "Authentication", "login"] },
|
||||
description: `Authentication login from ${sourceUrl}. A successful response stores only the non-empty token in the selected environment. Schema: ${schemaHint(loginSchema)}.`,
|
||||
},
|
||||
event: [
|
||||
{ listen: "prerequest", script: { type: "text/javascript", exec: [
|
||||
"pm.environment.set(\"token\", \"\");",
|
||||
"const username = pm.environment.get(\"username\");",
|
||||
"const password = pm.environment.get(\"password\");",
|
||||
"if (!username || !password) {",
|
||||
" pm.execution.skipRequest();",
|
||||
" throw new Error(\"Select the SeaHaven - Dev environment and set username and password before logging in.\");",
|
||||
"}",
|
||||
] } },
|
||||
{ listen: "test", script: { type: "text/javascript", exec: [
|
||||
"pm.test(\"Login responded 200\", function () { pm.response.to.have.status(200); });",
|
||||
"let body;",
|
||||
"pm.test(\"Login responded with JSON\", function () { pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\"); body = pm.response.json(); });",
|
||||
"const token = body && body.token;",
|
||||
"pm.test(\"Login response contained a non-empty token\", function () { pm.expect(token, \"expected a non-empty token field\").to.be.a(\"string\").and.not.empty; });",
|
||||
"if (typeof token === \"string\" && token.length > 0) pm.environment.set(\"token\", token);",
|
||||
] } },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildCollection(document) {
|
||||
const entries = operationEntries(document);
|
||||
const loginEntry = entries.find((entry) => entry.route.toLowerCase() === "/api/authentication/login" && entry.method === "post");
|
||||
const folders = new Map();
|
||||
for (const entry of entries) {
|
||||
if (entry === loginEntry) continue;
|
||||
const tag = [...(entry.operation.tags ?? ["Other"])].sort()[0] ?? "Other";
|
||||
if (!folders.has(tag)) folders.set(tag, []);
|
||||
folders.get(tag).push(requestItem(entry, document));
|
||||
}
|
||||
const generatedFolders = [...folders.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, items]) => ({
|
||||
name,
|
||||
item: items.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}));
|
||||
return {
|
||||
info: {
|
||||
name: "SeaHaven - Dev API (OpenAPI generated)",
|
||||
_postman_id: "a1f0514a-5e9f-41bd-a145-7163671b94db",
|
||||
description: `Complete Postman collection generated from ${sourceUrl}. It contains every OpenAPI operation in the published dev contract. Dev only. Replace illustrative placeholders before sending requests.`,
|
||||
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
},
|
||||
auth: { type: "bearer", bearer: [{ key: "token", value: "{{token}}", type: "string" }] },
|
||||
variable: [{ key: "baseUrl", value: "https://api.dev.seahaven.com", type: "string" }],
|
||||
item: [{ name: "Setup", item: setupItems(document) }, ...generatedFolders],
|
||||
};
|
||||
}
|
||||
|
||||
function flattenCollectionItems(items, folder = "") {
|
||||
return items.flatMap((item) => item.item ? flattenCollectionItems(item.item, `${folder}${item.name}/`) : [{ ...item, folder }]);
|
||||
}
|
||||
|
||||
function validate(document, collection) {
|
||||
const sourceEntries = operationEntries(document);
|
||||
const allItems = flattenCollectionItems(collection.item);
|
||||
const generated = allItems.filter((item) => !item.folder.startsWith("Setup/"));
|
||||
const setupLogin = allItems.find((item) => item.name === "Authentication - Login");
|
||||
const sourceKeys = new Set(sourceEntries.map((entry) => `${entry.method.toUpperCase()} ${entry.route}`));
|
||||
const requestKey = (item) => `${item.request.method} ${item.request.url.raw.replace("{{baseUrl}}", "").replace(/\?.*$/, "").replaceAll(/:([^/]+)/g, "{$1}")}`;
|
||||
const representedItems = [...generated, setupLogin].filter(Boolean);
|
||||
const representedKeys = new Set(representedItems.map(requestKey));
|
||||
const bodyCount = sourceEntries.filter((entry) => entry.operation.requestBody).length;
|
||||
const generatedBodyCount = representedItems.filter((item) => item.request.body).length;
|
||||
const authFailures = generated.filter((item) => item.request.auth?.type === "noauth");
|
||||
const authConfig = collection.auth?.type === "bearer" && collection.auth.bearer?.some((entry) => entry.key === "token" && entry.value === "{{token}}");
|
||||
const missingKeys = [...sourceKeys].filter((key) => !representedKeys.has(key));
|
||||
const extraKeys = [...representedKeys].filter((key) => !sourceKeys.has(key));
|
||||
const checks = [
|
||||
["operation count", representedKeys.size, sourceEntries.length],
|
||||
["unique operation keys", missingKeys.length + extraKeys.length, 0],
|
||||
["request-body coverage", generatedBodyCount, bodyCount],
|
||||
["protected operations inherit collection auth", authFailures.length, 0],
|
||||
["collection bearer token auth", authConfig, true],
|
||||
["login is explicitly noauth", setupLogin?.request?.auth?.type, "noauth"],
|
||||
];
|
||||
const failures = checks.filter(([, actual, expected]) => actual !== expected);
|
||||
if (failures.length) {
|
||||
for (const [label, actual, expected] of failures) console.error(`FAIL ${label}: expected ${expected}, observed ${actual}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
for (const [label, actual, expected] of checks) console.log(`PASS ${label}: ${actual}`);
|
||||
if (missingKeys.length || extraKeys.length) {
|
||||
console.error(`Operation key mismatch. Missing: ${missingKeys.join(", ")}; extra: ${extraKeys.join(", ")}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadDocument() {
|
||||
if (inputArg) return JSON.parse(await fs.readFile(inputArg.slice("--input=".length), "utf8"));
|
||||
const response = await fetch(sourceUrl, { headers: { accept: "application/json" } });
|
||||
if (!response.ok) throw new Error(`OpenAPI fetch failed: HTTP ${response.status}`);
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.includes("json")) throw new Error(`OpenAPI fetch returned non-JSON content type: ${contentType}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const document = await loadDocument();
|
||||
const collection = buildCollection(document);
|
||||
if (checkOnly) {
|
||||
const existing = JSON.parse(await fs.readFile(COLLECTION_PATH, "utf8"));
|
||||
validate(document, existing);
|
||||
} else {
|
||||
await fs.writeFile(COLLECTION_PATH, `${JSON.stringify(collection, null, 2)}\n`);
|
||||
await fs.writeFile(ENVIRONMENT_PATH, `${JSON.stringify({
|
||||
name: "SeaHaven - Dev",
|
||||
values: [
|
||||
{ key: "baseUrl", value: "https://api.dev.seahaven.com", type: "default", enabled: true },
|
||||
{ key: "username", value: "", type: "secret", enabled: true },
|
||||
{ key: "password", value: "", type: "secret", enabled: true },
|
||||
{ key: "token", value: "", type: "secret", enabled: true },
|
||||
],
|
||||
_postman_variable_scope: "environment",
|
||||
}, null, 2)}\n`);
|
||||
console.log(`Generated ${operationEntries(document).length} operations at ${COLLECTION_PATH}`);
|
||||
validate(document, collection);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue