Files
iddaai-be/src/scripts/export-postman-collection.ts
T
2026-04-16 17:21:48 +03:00

642 lines
17 KiB
TypeScript

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<string, unknown>;
type SwaggerPaths = Record<string, Record<string, JsonRecord>>;
type SwaggerSchemas = Record<string, JsonRecord>;
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<string> = new Set<string>(),
): 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<JsonRecord>((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<JsonRecord> {
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<JsonRecord> {
const headers: Array<JsonRecord> = [
{
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<string, PostmanItem[]>();
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<void> {
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();