Compare commits
2
Commits
48e25ce30d
...
f014314413
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f014314413 | ||
|
|
5295945ab2 |
@@ -212,4 +212,24 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
ignores: [...tests, 'src/modules/investments/tbank-sync/**'],
|
||||
rules: {
|
||||
// Separate rule so it does not replace layer-specific no-restricted-imports configs.
|
||||
'import-x/no-restricted-paths': ['error', {
|
||||
zones: [{
|
||||
target: './src',
|
||||
from: './src/modules/investments/tbank-sync',
|
||||
except: [
|
||||
'./tbank-sync.api.ts',
|
||||
'./tbank-sync.dto.ts',
|
||||
'./tbank-sync.module.ts',
|
||||
'./tbank-sync-repository.module.ts',
|
||||
],
|
||||
message: 'Use tbank-sync.api for cross-module access.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
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 FIRST_PAGE_OFFSET = 0;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LRUCache } from 'lru-cache';
|
||||
|
||||
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 RATING_COLUMN = 2;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { investmentResourceMeta } from '../investment-resource.mapper';
|
||||
import { sumMoneyByCurrency } from '../portfolio/domain/portfolio-totals';
|
||||
import { PortfolioPositionDto, PortfolioService } from '../portfolio/portfolio.api';
|
||||
import { hourlyAttempt, TbankSyncRepository } from '../tbank-sync/tbank-sync.repository';
|
||||
import { hourlyAttempt, TbankSyncRepository } from '../tbank-sync/tbank-sync.api';
|
||||
import {
|
||||
BrokerageAccountDto,
|
||||
BrokerageAccountsResourceDto,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
utcDate,
|
||||
} from '../investment-calendar';
|
||||
|
||||
enum IncomeErrorCode { InvalidPeriod = 'INVALID_INCOME_PERIOD' }
|
||||
export enum IncomeErrorCode { InvalidPeriod = 'INVALID_INCOME_PERIOD' }
|
||||
|
||||
const MONDAY_WEEKDAY_INDEX = 1;
|
||||
const INCLUSIVE_DAY_OFFSET = 1;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import {
|
||||
hourlyAttempt,
|
||||
TbankSyncRepository,
|
||||
} from '../../tbank-sync/tbank-sync.repository';
|
||||
} from '../../tbank-sync/tbank-sync.api';
|
||||
import { OperationErrorCode } from '../domain/operation-error-code';
|
||||
import {
|
||||
confirmedOperationRanges,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { investmentResourceMeta } from '../../investment-resource.mapper';
|
||||
import {
|
||||
hourlyAttempt,
|
||||
TbankSyncRepository,
|
||||
} from '../../tbank-sync/tbank-sync.repository';
|
||||
} from '../../tbank-sync/tbank-sync.api';
|
||||
import { portfolioStructure } from '../domain/portfolio-structure';
|
||||
import { sumMoneyByCurrency } from '../domain/portfolio-totals';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { hourlyAttempt, TbankSyncRepository } from './tbank-sync.repository';
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
hourlyAttempt,
|
||||
TbankSyncRepository,
|
||||
} from '../tbank-sync/tbank-sync.repository';
|
||||
} from '../tbank-sync/tbank-sync.api';
|
||||
import { bondEvent, dividendEvent } from './upcoming-event.mapper';
|
||||
import {
|
||||
UpcomingEventDto,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
UpcomingEventsQueryDto,
|
||||
} from './upcoming-events.dto';
|
||||
|
||||
enum EventsErrorCode { InvalidPeriod = 'INVALID_EVENTS_PERIOD' }
|
||||
export enum EventsErrorCode { InvalidPeriod = 'INVALID_EVENTS_PERIOD' }
|
||||
|
||||
const FIRST_BATCH_OFFSET = 0;
|
||||
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
// 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'];
|
||||
|
||||
// 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) {
|
||||
const document = SwaggerModule.createDocument(
|
||||
app,
|
||||
@@ -18,5 +44,8 @@ export function createOpenApiDocument(app: INestApplication) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,55 @@ describe('OpenAPI contract (integration)', () => {
|
||||
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 () => {
|
||||
const schemas = createOpenApiDocument(app).components?.schemas;
|
||||
expect(schemas?.StockScreenerCriteriaDto).toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user