import "reflect-metadata"; import { mkdirSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import { NestFactory } from "@nestjs/core"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "../app.module"; type JsonRecord = Record; type SwaggerPaths = Record>; type SwaggerSchemas = Record; interface PostmanResponse { name: string; originalRequest: JsonRecord; status: string; code: number; _postman_previewlanguage: "json"; header: Array<{ key: string; value: string }>; body: string; } interface PostmanItem { name: string; item?: PostmanItem[]; request?: JsonRecord; response?: PostmanResponse[]; } interface AiEndpointDefinition { name: string; method: "GET" | "POST"; path: string; description: string; query?: Array<{ key: string; value: string; description: string }>; body?: JsonRecord; response: JsonRecord; } function refName(ref: string | undefined): string | null { if (!ref) { return null; } const parts = ref.split("/"); return parts[parts.length - 1] ?? null; } function resolveSchema( schema: unknown, schemas: SwaggerSchemas, ): JsonRecord | null { if (!schema || typeof schema !== "object") { return null; } const schemaObject = schema as JsonRecord; const schemaRef = typeof schemaObject.$ref === "string" ? schemaObject.$ref : null; if (schemaRef) { const name = refName(schemaRef); return name ? (schemas[name] ?? null) : null; } return schemaObject; } function examplePrimitive(schema: JsonRecord): unknown { if (schema.example !== undefined) { return schema.example; } if (schema.default !== undefined) { return schema.default; } if (Array.isArray(schema.enum) && schema.enum.length > 0) { return schema.enum[0]; } const type = typeof schema.type === "string" ? schema.type : "string"; const format = typeof schema.format === "string" ? schema.format : ""; if (type === "string") { if (format === "email") { return "user@example.com"; } if (format === "date-time") { return "2026-04-14T00:00:00.000Z"; } if (format === "date") { return "2026-04-14"; } if (format === "uuid") { return "11111111-1111-1111-1111-111111111111"; } return "string"; } if (type === "integer" || type === "number") { return 1; } if (type === "boolean") { return true; } return "string"; } function buildExampleFromSchema( schema: unknown, schemas: SwaggerSchemas, visited: Set = new Set(), ): unknown { const resolved = resolveSchema(schema, schemas); if (!resolved) { return null; } const schemaRef = typeof resolved.$ref === "string" ? resolved.$ref : null; if (schemaRef) { const name = refName(schemaRef); if (!name || visited.has(name)) { return null; } const nextVisited = new Set(visited); nextVisited.add(name); return buildExampleFromSchema(schemas[name], schemas, nextVisited); } if (Array.isArray(resolved.allOf) && resolved.allOf.length > 0) { return resolved.allOf.reduce((accumulator, part) => { const partial = buildExampleFromSchema(part, schemas, visited); if (partial && typeof partial === "object" && !Array.isArray(partial)) { return { ...accumulator, ...(partial as JsonRecord) }; } return accumulator; }, {}); } if (Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) { return buildExampleFromSchema(resolved.oneOf[0], schemas, visited); } if (Array.isArray(resolved.anyOf) && resolved.anyOf.length > 0) { return buildExampleFromSchema(resolved.anyOf[0], schemas, visited); } const type = typeof resolved.type === "string" ? resolved.type : "object"; if (type === "array") { return [buildExampleFromSchema(resolved.items, schemas, visited)]; } if (type === "object" || resolved.properties) { const properties = (resolved.properties ?? {}) as JsonRecord; const output: JsonRecord = {}; for (const [key, value] of Object.entries(properties)) { output[key] = buildExampleFromSchema(value, schemas, visited); } if (Object.keys(output).length > 0) { return output; } } return examplePrimitive(resolved); } function swaggerSchemaFromContent(content: unknown): unknown { if (!content || typeof content !== "object") { return null; } const contentObject = content as JsonRecord; const jsonContent = contentObject["application/json"]; if (jsonContent && typeof jsonContent === "object") { return (jsonContent as JsonRecord).schema ?? null; } const firstContent = Object.values(contentObject)[0]; if (firstContent && typeof firstContent === "object") { return (firstContent as JsonRecord).schema ?? null; } return null; } function toPostmanPath(pathname: string): string { return pathname.replace(/\{([^}]+)\}/g, "{{$1}}"); } function buildRequestBody( operation: JsonRecord, schemas: SwaggerSchemas, ): string | null { const requestBody = operation.requestBody as JsonRecord | undefined; const schema = swaggerSchemaFromContent(requestBody?.content); if (!schema) { return null; } const example = buildExampleFromSchema(schema, schemas); return JSON.stringify(example ?? {}, null, 2); } function buildResponses( operation: JsonRecord, method: string, rawPath: string, baseUrlVariable: string, schemas: SwaggerSchemas, body: string | null, ): PostmanResponse[] { const responses = (operation.responses ?? {}) as JsonRecord; const entries = Object.entries(responses); return entries.map(([statusCode, responseObject]) => { const responseRecord = responseObject as JsonRecord; const schema = swaggerSchemaFromContent(responseRecord.content); const example = buildExampleFromSchema(schema, schemas); const numericStatus = Number(statusCode); return { name: `${method.toUpperCase()} ${rawPath} - ${statusCode}`, originalRequest: { method: method.toUpperCase(), header: [{ key: "Content-Type", value: "application/json" }], body: body ? { mode: "raw", raw: body, } : undefined, url: { raw: `{{${baseUrlVariable}}}${toPostmanPath(rawPath)}`, host: [`{{${baseUrlVariable}}}`], path: rawPath.split("/").filter(Boolean), }, }, status: typeof responseRecord.description === "string" ? responseRecord.description : `HTTP ${statusCode}`, code: Number.isFinite(numericStatus) ? numericStatus : 200, _postman_previewlanguage: "json", header: [{ key: "Content-Type", value: "application/json" }], body: JSON.stringify(example ?? {}, null, 2), }; }); } function buildQueryParams(operation: JsonRecord): Array { const parameters = Array.isArray(operation.parameters) ? (operation.parameters as JsonRecord[]) : []; return parameters .filter((parameter) => parameter.in === "query") .map((parameter) => ({ key: String(parameter.name ?? ""), value: parameter.schema && typeof parameter.schema === "object" ? String((parameter.schema as JsonRecord).default ?? "") : "", description: String(parameter.description ?? ""), disabled: parameter.required === true ? false : true, })); } function buildHeaders(operation: JsonRecord): Array { const headers: Array = [ { key: "Content-Type", value: "application/json", }, ]; const security = Array.isArray(operation.security) ? (operation.security as JsonRecord[]) : []; if (security.length > 0) { headers.push({ key: "Authorization", value: "Bearer {{accessToken}}", }); } return headers; } function createRequestItem( name: string, method: string, rawPath: string, baseUrlVariable: string, operation: JsonRecord, schemas: SwaggerSchemas, ): PostmanItem { const body = buildRequestBody(operation, schemas); const query = buildQueryParams(operation); const headers = buildHeaders(operation); const request: JsonRecord = { method: method.toUpperCase(), header: headers, description: typeof operation.description === "string" ? operation.description : typeof operation.summary === "string" ? operation.summary : "", url: { raw: `{{${baseUrlVariable}}}${toPostmanPath(rawPath)}`, host: [`{{${baseUrlVariable}}}`], path: rawPath.split("/").filter(Boolean), query, }, }; if (body) { request.body = { mode: "raw", raw: body, }; } return { name, request, response: buildResponses( operation, method, rawPath, baseUrlVariable, schemas, body, ), }; } function buildNestFolders(document: JsonRecord): PostmanItem[] { const paths = (document.paths ?? {}) as SwaggerPaths; const schemas = ((document.components ?? {}) as JsonRecord).schemas as | SwaggerSchemas | undefined; const safeSchemas = schemas ?? {}; const folders = new Map(); for (const [rawPath, pathItem] of Object.entries(paths)) { for (const [method, operationObject] of Object.entries(pathItem)) { if (!["get", "post", "put", "patch", "delete"].includes(method)) { continue; } const operation = operationObject; const tags = Array.isArray(operation.tags) ? operation.tags : []; const folderName = typeof tags[0] === "string" && tags[0].trim().length > 0 ? tags[0] : "Misc"; const requestName = typeof operation.summary === "string" && operation.summary.trim().length > 0 ? operation.summary : `${method.toUpperCase()} ${rawPath}`; const item = createRequestItem( requestName, method, rawPath, "beBaseUrl", operation, safeSchemas, ); const existing = folders.get(folderName) ?? []; existing.push(item); folders.set(folderName, existing); } } return [...folders.entries()] .sort((a, b) => a[0].localeCompare(b[0])) .map(([folderName, items]) => ({ name: folderName, item: items.sort((a, b) => a.name.localeCompare(b.name)), })); } function createAiRequest( endpoint: AiEndpointDefinition, folderName: string, ): PostmanItem { const url: JsonRecord = { raw: `{{aiBaseUrl}}${endpoint.path}`, host: ["{{aiBaseUrl}}"], path: endpoint.path.split("/").filter(Boolean), }; if (endpoint.query && endpoint.query.length > 0) { url.query = endpoint.query.map((queryItem) => ({ key: queryItem.key, value: queryItem.value, description: queryItem.description, })); } const request: JsonRecord = { method: endpoint.method, header: [{ key: "Content-Type", value: "application/json" }], description: endpoint.description, url, }; if (endpoint.body) { request.body = { mode: "raw", raw: JSON.stringify(endpoint.body, null, 2), }; } return { name: endpoint.name, request, response: [ { name: `${endpoint.method} ${endpoint.path}`, originalRequest: request, status: "OK", code: 200, _postman_previewlanguage: "json", header: [{ key: "Content-Type", value: "application/json" }], body: JSON.stringify(endpoint.response, null, 2), }, ], }; } function buildAiFolder(): PostmanItem { const v20Endpoints: AiEndpointDefinition[] = [ { name: "Root", method: "GET", path: "/", description: "AI engine root status endpoint", response: { status: "Suggest-Bet AI Engine v20+", engine: "V20 Plus Single Match Orchestrator", }, }, { name: "Health", method: "GET", path: "/health", description: "AI engine health endpoint", response: { status: "healthy", engine: "v20plus", ready: true }, }, { name: "Analyze Match", method: "POST", path: "/v20plus/analyze/{{match_id}}", description: "Full V20+ single match analysis", response: { model_version: "v30.0", match_info: { match_id: "{{match_id}}" }, main_pick: { market: "OU25", pick: "2.5 Üst" }, market_board: {}, }, }, { name: "Analyze HTMS", method: "GET", path: "/v20plus/analyze-htms/{{match_id}}", description: "Half-time result analysis endpoint", response: { match_id: "{{match_id}}", market: "HT" }, }, { name: "Analyze HTFT", method: "GET", path: "/v20plus/analyze-htft/{{match_id}}", description: "Half-time/full-time analysis endpoint", query: [ { key: "timeout_sec", value: "30", description: "Timeout between 3 and 120 seconds", }, ], response: { engine: "v20plus.1", match_info: { match_id: "{{match_id}}" }, ht_ft_probs: { "1/1": 0.25, "X/X": 0.18 }, }, }, { name: "Generate Coupon", method: "POST", path: "/v20plus/coupon", description: "Generate V20+ coupon from selected matches", body: { match_ids: ["match-1", "match-2"], strategy: "BALANCED", max_matches: 4, min_confidence: 55, }, response: { success: true, data: { strategy: "BALANCED", bets: [], }, }, }, { name: "Daily Banker", method: "GET", path: "/v20plus/daily-banker", description: "Get daily banker picks", query: [ { key: "count", value: "3", description: "Number of banker picks", }, ], response: { count: 3, bankers: [] }, }, { name: "Reversal Watchlist", method: "GET", path: "/v20plus/reversal-watchlist", description: "Reversal watchlist candidates", query: [ { key: "count", value: "20", description: "Result size" }, { key: "horizon_hours", value: "72", description: "Future horizon" }, { key: "min_score", value: "45", description: "Minimum score" }, { key: "top_leagues_only", value: "false", description: "Filter to top leagues", }, ], response: { count: 0, items: [] }, }, ]; const v2Endpoints: AiEndpointDefinition[] = [ { name: "V2 Health", method: "GET", path: "/v2/health", description: "V2 betting engine health", response: { status: "healthy", engine: "v2.betting_engine", models_loaded: true, }, }, { name: "V2 Analyze Match", method: "POST", path: "/v2/analyze/{{match_id}}", description: "V2 leakage-free match analysis", response: { model_version: "v2.betting_engine", match_info: { match_id: "{{match_id}}" }, main_pick: { market: "MS", pick: "1" }, market_board: { MS: { pick: "1", confidence: 58.4 }, }, }, }, ]; return { name: "AI Engine", item: [ { name: "V20+", item: v20Endpoints.map((endpoint) => createAiRequest(endpoint, "V20+")), }, { name: "V2", item: v2Endpoints.map((endpoint) => createAiRequest(endpoint, "V2")), }, ], }; } async function run(): Promise { const projectRoot = process.cwd(); const outputDir = path.join(projectRoot, "mds"); const outputFile = path.join( outputDir, "suggest-bet-platform.postman_collection.json", ); process.env.REDIS_ENABLED = "true"; const app = await NestFactory.create(AppModule, { logger: false }); app.setGlobalPrefix("api"); const swaggerConfig = new DocumentBuilder() .setTitle("Suggest Bet Backend API") .setDescription("Postman collection export source") .setVersion("1.0") .addBearerAuth() .build(); const document = SwaggerModule.createDocument( app, swaggerConfig, ) as unknown as JsonRecord; const collection: JsonRecord = { info: { name: "Suggest-Bet Platform API", description: "Auto-generated Postman collection for Nest backend and AI engine endpoints.", schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", }, variable: [ { key: "beBaseUrl", value: "http://localhost:3005" }, { key: "aiBaseUrl", value: "http://localhost:8000" }, { key: "accessToken", value: "" }, { key: "match_id", value: "sample-match-id" }, ], auth: { type: "bearer", bearer: [{ key: "token", value: "{{accessToken}}", type: "string" }], }, item: [ { name: "Nest API", item: buildNestFolders(document), }, buildAiFolder(), ], }; mkdirSync(outputDir, { recursive: true }); writeFileSync(outputFile, JSON.stringify(collection, null, 2), "utf8"); await app.close(); console.log(`✅ Postman collection exported: ${outputFile}`); } void run();