Compare commits
1
Commits
main
...
fe185a1f4a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe185a1f4a |
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||||
import { constants as httpStatus } from 'node:http2';
|
import { constants as httpStatus } from 'node:http2';
|
||||||
|
|
||||||
enum MoexErrorCode { Unavailable = 'MOEX_UNAVAILABLE' }
|
export enum MoexErrorCode { Unavailable = 'MOEX_UNAVAILABLE' }
|
||||||
|
|
||||||
const MIN_PRICE_PERCENT_EXCLUSIVE = 0;
|
const MIN_PRICE_PERCENT_EXCLUSIVE = 0;
|
||||||
const FIRST_PAGE_OFFSET = 0;
|
const FIRST_PAGE_OFFSET = 0;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { LRUCache } from 'lru-cache';
|
|||||||
|
|
||||||
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../common/time';
|
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../common/time';
|
||||||
|
|
||||||
enum NcrErrorCode { Unavailable = 'NCR_UNAVAILABLE' }
|
export enum NcrErrorCode { Unavailable = 'NCR_UNAVAILABLE' }
|
||||||
|
|
||||||
const EXPECTED_REGISTRY_TABLES = 1;
|
const EXPECTED_REGISTRY_TABLES = 1;
|
||||||
const RATING_COLUMN = 2;
|
const RATING_COLUMN = 2;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
utcDate,
|
utcDate,
|
||||||
} from '../investment-calendar';
|
} from '../investment-calendar';
|
||||||
|
|
||||||
enum IncomeErrorCode { InvalidPeriod = 'INVALID_INCOME_PERIOD' }
|
export enum IncomeErrorCode { InvalidPeriod = 'INVALID_INCOME_PERIOD' }
|
||||||
|
|
||||||
const MONDAY_WEEKDAY_INDEX = 1;
|
const MONDAY_WEEKDAY_INDEX = 1;
|
||||||
const INCLUSIVE_DAY_OFFSET = 1;
|
const INCLUSIVE_DAY_OFFSET = 1;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
UpcomingEventsQueryDto,
|
UpcomingEventsQueryDto,
|
||||||
} from './upcoming-events.dto';
|
} from './upcoming-events.dto';
|
||||||
|
|
||||||
enum EventsErrorCode { InvalidPeriod = 'INVALID_EVENTS_PERIOD' }
|
export enum EventsErrorCode { InvalidPeriod = 'INVALID_EVENTS_PERIOD' }
|
||||||
|
|
||||||
const FIRST_BATCH_OFFSET = 0;
|
const FIRST_BATCH_OFFSET = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { HttpErrorCode } from './common/error-code';
|
||||||
|
import { MoexErrorCode } from './integrations/moex/moex.client';
|
||||||
|
import { NcrErrorCode } from './integrations/ncr/ncr-ratings.client';
|
||||||
|
import { TbankErrorCode } from './integrations/tbank/tbank-error-code';
|
||||||
|
import { AuthErrorCode } from './modules/auth/application/auth-error-code';
|
||||||
|
import { BankErrorCode } from './modules/banking/bank-cards/application/bank-card-input';
|
||||||
|
import { IncomeErrorCode } from './modules/investments/income/income-period';
|
||||||
|
import { InstrumentErrorCode } from './modules/investments/instruments/domain/instrument-error-code';
|
||||||
|
import { OperationErrorCode } from './modules/investments/operations/domain/operation-error-code';
|
||||||
|
import { EventsErrorCode } from './modules/investments/upcoming-events/upcoming-events.service';
|
||||||
|
|
||||||
export const API_PREFIX = 'api/v1';
|
export const API_PREFIX = 'api/v1';
|
||||||
|
|
||||||
// Nest Swagger cannot express additionalProperties on a class, so closed request
|
// Nest Swagger cannot express additionalProperties on a class, so closed request
|
||||||
// bodies are restored here after generation; the runtime allowlist stays in the controller.
|
// bodies are restored here after generation; the runtime allowlist stays in the controller.
|
||||||
const CLOSED_SCHEMAS = ['StockScreenerCriteriaDto'];
|
const CLOSED_SCHEMAS = ['StockScreenerCriteriaDto'];
|
||||||
|
|
||||||
|
// Every code HttpExceptionFilter can emit. Assembled at the composition root so
|
||||||
|
// common/ProblemDetailsDto does not depend on modules.
|
||||||
|
export const PROBLEM_CODES = [
|
||||||
|
...Object.values(HttpErrorCode),
|
||||||
|
...Object.values(AuthErrorCode),
|
||||||
|
...Object.values(OperationErrorCode),
|
||||||
|
...Object.values(InstrumentErrorCode),
|
||||||
|
...Object.values(TbankErrorCode),
|
||||||
|
...Object.values(BankErrorCode),
|
||||||
|
...Object.values(MoexErrorCode),
|
||||||
|
...Object.values(NcrErrorCode),
|
||||||
|
...Object.values(EventsErrorCode),
|
||||||
|
...Object.values(IncomeErrorCode),
|
||||||
|
];
|
||||||
|
|
||||||
export function createOpenApiDocument(app: INestApplication) {
|
export function createOpenApiDocument(app: INestApplication) {
|
||||||
const document = SwaggerModule.createDocument(
|
const document = SwaggerModule.createDocument(
|
||||||
app,
|
app,
|
||||||
@@ -18,5 +44,8 @@ export function createOpenApiDocument(app: INestApplication) {
|
|||||||
schema.additionalProperties = false;
|
schema.additionalProperties = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const problem = schemas.ProblemDetailsDto;
|
||||||
|
const problemCode = problem && 'properties' in problem ? problem.properties?.code : undefined;
|
||||||
|
if (problemCode && !('$ref' in problemCode)) problemCode.enum = PROBLEM_CODES;
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,55 @@ describe('OpenAPI contract (integration)', () => {
|
|||||||
expect(analytics.properties).not.toHaveProperty('yearComparison');
|
expect(analytics.properties).not.toHaveProperty('yearComparison');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('publishes every emitted problem detail code as a named enum', () => {
|
||||||
|
const schemas = createOpenApiDocument(app).components?.schemas;
|
||||||
|
const expectedCodes = [
|
||||||
|
'BAD_REQUEST',
|
||||||
|
'INTERNAL_SERVER_ERROR',
|
||||||
|
'NOT_FOUND',
|
||||||
|
'UNAUTHORIZED',
|
||||||
|
'UNSUPPORTED_MEDIA_TYPE',
|
||||||
|
'VALIDATION_ERROR',
|
||||||
|
'EMAIL_ALREADY_REGISTERED',
|
||||||
|
'INVALID_CREDENTIALS',
|
||||||
|
'INVALID_OPERATIONS_CURSOR',
|
||||||
|
'BOND_HISTORY_START_UNAVAILABLE',
|
||||||
|
'BOND_TICKER_AMBIGUOUS',
|
||||||
|
'STOCK_HISTORY_START_UNAVAILABLE',
|
||||||
|
'STOCK_TICKER_AMBIGUOUS',
|
||||||
|
'INVALID_TBANK_TOKEN',
|
||||||
|
'TBANK_NOT_CONNECTED',
|
||||||
|
'TBANK_UNAVAILABLE',
|
||||||
|
'BANK_CARD_ARCHIVED',
|
||||||
|
'BANK_DEPENDENT_REFUNDS',
|
||||||
|
'BANK_DUPLICATE_CATEGORY',
|
||||||
|
'BANK_IMMUTABLE_OPERATION',
|
||||||
|
'BANK_INTENT_CONFLICT',
|
||||||
|
'BANK_INVALID_DATE',
|
||||||
|
'BANK_INVALID_INPUT',
|
||||||
|
'BANK_INVALID_MONEY',
|
||||||
|
'BANK_INVALID_REFUND',
|
||||||
|
'BANK_NOT_FOUND',
|
||||||
|
'BANK_OPENING_DATE_CONFLICT',
|
||||||
|
'BANK_OPERATION_DELETED',
|
||||||
|
'BANK_REFUND_LIMIT',
|
||||||
|
'BANK_VERSION_CONFLICT',
|
||||||
|
'BANK_WRITE_CONFLICT',
|
||||||
|
'MOEX_UNAVAILABLE',
|
||||||
|
'NCR_UNAVAILABLE',
|
||||||
|
'INVALID_EVENTS_PERIOD',
|
||||||
|
'INVALID_INCOME_PERIOD',
|
||||||
|
];
|
||||||
|
|
||||||
|
const problem = schemas?.ProblemDetailsDto;
|
||||||
|
assert(problem && !('$ref' in problem));
|
||||||
|
const codeSchema = problem.properties?.code;
|
||||||
|
assert(codeSchema && !('$ref' in codeSchema));
|
||||||
|
expect(codeSchema.type).toBe('string');
|
||||||
|
expect(codeSchema.enum).toEqual(expect.arrayContaining(expectedCodes));
|
||||||
|
expect(codeSchema.enum).toHaveLength(expectedCodes.length);
|
||||||
|
});
|
||||||
|
|
||||||
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
||||||
const schemas = createOpenApiDocument(app).components?.schemas;
|
const schemas = createOpenApiDocument(app).components?.schemas;
|
||||||
expect(schemas?.StockScreenerCriteriaDto).toMatchObject({
|
expect(schemas?.StockScreenerCriteriaDto).toMatchObject({
|
||||||
|
|||||||
Reference in New Issue
Block a user