This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
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 as JsonRecord;
|
||||
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();
|
||||
Reference in New Issue
Block a user