diff --git a/src/modules/investments/instruments/stocks/presentation/stock.dto.ts b/src/modules/investments/instruments/stocks/presentation/stock.dto.ts index c4d47f4..14b0e60 100644 --- a/src/modules/investments/instruments/stocks/presentation/stock.dto.ts +++ b/src/modules/investments/instruments/stocks/presentation/stock.dto.ts @@ -93,7 +93,7 @@ export class StockScreenerItemDto { @ApiProperty({ nullable: true, type: String }) declare currency: null | string; @ApiProperty({ nullable: true, type: String }) declare realExchange: null | string; @ApiProperty({ nullable: true, type: String }) declare countryOfRisk: null | string; - @ApiProperty({ nullable: true, type: 'integer' }) declare lot: null | number; + @ApiProperty({ nullable: true, minimum: MIN_TICKER_LENGTH, type: 'integer' }) declare lot: null | number; } export class StockScreenerPageDto { diff --git a/src/openapi.ts b/src/openapi.ts index 46e13fb..3ea3fda 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -3,9 +3,20 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; export const API_PREFIX = 'api/v1'; +// Nest Swagger cannot express additionalProperties on a class, so closed request +// bodies are restored here after generation; the runtime allowlist stays in the controller. +const CLOSED_SCHEMAS = ['StockScreenerCriteriaDto']; + export function createOpenApiDocument(app: INestApplication) { - return SwaggerModule.createDocument( + const document = SwaggerModule.createDocument( app, new DocumentBuilder().setTitle('WealthLedger API').setVersion('v1').build(), ); + const schemas = document.components?.schemas ?? {}; + for (const [name, schema] of Object.entries(schemas)) { + if (CLOSED_SCHEMAS.includes(name) && 'properties' in schema) { + schema.additionalProperties = false; + } + } + return document; } diff --git a/test/openapi-contract.e2e-spec.ts b/test/openapi-contract.e2e-spec.ts index d95b945..f9eef12 100644 --- a/test/openapi-contract.e2e-spec.ts +++ b/test/openapi-contract.e2e-spec.ts @@ -122,6 +122,13 @@ describe('OpenAPI contract (integration)', () => { }); it('generates the runtime contract without server startup or deployment configuration', async () => { + const schemas = createOpenApiDocument(app).components?.schemas; + expect(schemas?.StockScreenerCriteriaDto).toMatchObject({ + additionalProperties: false, + }); + expect(schemas?.StockScreenerItemDto).toMatchObject({ + properties: { lot: { type: 'integer', minimum: 1, nullable: true } }, + }); expect( createOpenApiDocument(app).components?.schemas?.ProblemDetailsDto, ).toMatchObject({