From 4b68e20f6e2df87ac996af37435f269aa416d4f4 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 14 Oct 2026 23:18:34 +0300 Subject: [PATCH] fix: restore screener OpenAPI constraints (WealthLedger/backend#70) --- .../instruments/stocks/presentation/stock.dto.ts | 2 +- src/openapi.ts | 13 ++++++++++++- test/openapi-contract.e2e-spec.ts | 7 +++++++ 3 files changed, 20 insertions(+), 2 deletions(-) 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({