website cleanup
This commit is contained in:
@@ -18,6 +18,7 @@ interface OpenAPISchema {
|
||||
$ref?: string;
|
||||
items?: OpenAPISchema;
|
||||
properties?: Record<string, OpenAPISchema>;
|
||||
additionalProperties?: OpenAPISchema;
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
nullable?: boolean;
|
||||
@@ -78,13 +79,50 @@ async function generateSpec() {
|
||||
async function processDTOFile(filePath: string, schemas: Record<string, OpenAPISchema>) {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// Find class definitions with DTO suffix
|
||||
const classRegex = /export\s+class\s+(\w+(?:DTO|Dto))\s*(?:extends\s+\w+)?\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs;
|
||||
let classMatch;
|
||||
// Find exported class definitions with DTO suffix.
|
||||
// NOTE: We cannot use a naive regex to capture the full class body because
|
||||
// decorators often contain object literals with braces (e.g. @ApiProperty({ ... })).
|
||||
// Instead, we locate the opening brace and then parse using a simple brace counter.
|
||||
const classRegex = /export\s+class\s+(\w+)\b/g;
|
||||
let classMatch: RegExpExecArray | null;
|
||||
|
||||
while ((classMatch = classRegex.exec(content)) !== null) {
|
||||
const className = classMatch[1];
|
||||
const classBody = classMatch[2];
|
||||
if (!className.endsWith('DTO') && !className.endsWith('Dto')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const declStartIndex = classMatch.index;
|
||||
const braceOpenIndex = content.indexOf('{', classRegex.lastIndex);
|
||||
if (braceOpenIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Walk forward to find the matching closing brace.
|
||||
// This is intentionally simple: it counts braces and does not attempt to fully
|
||||
// understand strings/comments. It's sufficient for our DTO files where braces
|
||||
// are primarily used for TS blocks and decorator object literals.
|
||||
let depth = 0;
|
||||
let i = braceOpenIndex;
|
||||
let braceCloseIndex = -1;
|
||||
|
||||
for (; i < content.length; i++) {
|
||||
const ch = content[i];
|
||||
if (ch === '{') depth++;
|
||||
if (ch === '}') depth--;
|
||||
|
||||
if (depth === 0) {
|
||||
braceCloseIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (braceCloseIndex === -1) {
|
||||
console.warn(` ⚠️ Could not find closing brace for ${className} in ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const classBody = content.slice(braceOpenIndex + 1, braceCloseIndex);
|
||||
|
||||
// Normalize class name to always use DTO suffix (not Dto)
|
||||
const normalizedName = className.endsWith('Dto') ?
|
||||
@@ -133,6 +171,11 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
|
||||
if (propertyMatch) {
|
||||
const [, propName, optional, propType] = propertyMatch;
|
||||
|
||||
// Strip any default initializer from the "type" capture.
|
||||
// Example: "sponsor: SponsorDTO = new SponsorDTO();" -> "SponsorDTO"
|
||||
// Without this, later mapping may fall back to "string" and corrupt the schema.
|
||||
const cleanedType = propType.split('=')[0].trim();
|
||||
|
||||
// Skip if propName is a TypeScript/decorator keyword
|
||||
if (['constructor', 'private', 'public', 'protected', 'static', 'readonly'].includes(propName)) {
|
||||
currentDecorators = [];
|
||||
@@ -150,7 +193,7 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
|
||||
}
|
||||
|
||||
// Extract type from @ApiProperty decorator if present
|
||||
let schema = extractTypeFromDecorators(currentDecorators, propType);
|
||||
let schema = extractTypeFromDecorators(currentDecorators, cleanedType);
|
||||
|
||||
properties[propName] = schema;
|
||||
currentDecorators = [];
|
||||
@@ -174,7 +217,8 @@ function extractTypeFromDecorators(decorators: string[], tsType: string): OpenAP
|
||||
const decoratorStr = decorators.join(' ');
|
||||
|
||||
// Check for @ApiProperty type specification
|
||||
const apiPropertyMatch = decoratorStr.match(/@ApiProperty\s*\(\s*\{([^}]*)\}\s*\)/s);
|
||||
// NOTE: Avoid the RegExp dotAll (/s) flag to keep compatibility with older TS targets.
|
||||
const apiPropertyMatch = decoratorStr.match(/@ApiProperty\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
||||
|
||||
if (apiPropertyMatch) {
|
||||
const apiPropertyContent = apiPropertyMatch[1];
|
||||
@@ -223,6 +267,28 @@ function mapTypeToSchema(type: string): OpenAPISchema {
|
||||
// Clean up the type
|
||||
type = type.replace(/[;!]/g, '').trim();
|
||||
|
||||
const normalizeDtoName = (name: string) => (name.endsWith('Dto') ? name.slice(0, -3) + 'DTO' : name);
|
||||
|
||||
// Handle Record<string, T>
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const recordMatch = type.match(/^Record<\s*string\s*,\s*(.+)\s*>$/);
|
||||
if (recordMatch) {
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: mapTypeToSchema(recordMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle index signature object types: { [key: string]: T }
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const indexSigMatch = type.match(/^\{\s*\[\s*\w+\s*:\s*(string|number)\s*\]\s*:\s*(.+)\s*\}$/);
|
||||
if (indexSigMatch) {
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: mapTypeToSchema(indexSigMatch[2]),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle union with null
|
||||
if (type.includes('| null') || type.includes('null |')) {
|
||||
const baseType = type.replace(/\|\s*null/g, '').replace(/null\s*\|/g, '').trim();
|
||||
@@ -274,11 +340,11 @@ function mapTypeToSchema(type: string): OpenAPISchema {
|
||||
|
||||
// Handle DTO references
|
||||
if (type.endsWith('DTO') || type.endsWith('Dto')) {
|
||||
return { $ref: `#/components/schemas/${type}` };
|
||||
return { $ref: `#/components/schemas/${normalizeDtoName(type)}` };
|
||||
}
|
||||
|
||||
// Default to string for unknown types
|
||||
return { type: 'string' };
|
||||
}
|
||||
|
||||
generateSpec().catch(console.error);
|
||||
generateSpec().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user