chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 11:04:18 +03:00
parent ca5dbcb382
commit 9b3558f96c
179 changed files with 4485 additions and 2716 deletions
@@ -1,22 +1,23 @@
import { accountHistory } from '../../investment-sync-plan';
import { Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
import {
TbankAccessService,
TbankClient,
} from '../../../../integrations/tbank/tbank.api';
import { accountHistory } from '../../investment-sync-plan';
import {
type OperationInterval,
operationIntervals,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
import {
BrokerageOperationsRepository,
type OperationAttempt,
} from '../infrastructure/brokerage-operations.repository';
import {
operationIntervals,
planOperationRead,
type OperationInterval,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
const operationsPageSize = 1000;
const priorities = { screen: 1, history: 2 } as const;
@@ -29,6 +30,7 @@ export class BrokerageOperationsSyncService {
string,
{ attempt: OperationAttempt; priority: number }
>();
private draining = false;
// Only short DB reservations/publications are serialized, never RPCs.
private reservations: Promise<unknown> = Promise.resolve();
@@ -44,10 +46,10 @@ export class BrokerageOperationsSyncService {
userId: string,
intervals: OperationInterval[],
options: {
movingEnd?: boolean;
force?: boolean;
observe?: boolean;
history?: boolean;
movingEnd?: boolean;
observe?: boolean;
revision?: string;
} = {},
) {
@@ -55,8 +57,9 @@ export class BrokerageOperationsSyncService {
if (
!connection.membershipAt ||
(options.revision && options.revision !== connection.revision)
)
) {
return { plan: null, handle: null };
}
if (
intervals.some(
(interval) =>
@@ -64,8 +67,9 @@ export class BrokerageOperationsSyncService {
(account) => account.accountId === interval.accountId,
),
)
)
) {
throw new NotFoundException();
}
const coverage = await this.repository.coverage(
userId,
connection.revision,
@@ -76,24 +80,24 @@ export class BrokerageOperationsSyncService {
...intervals.map((interval) => interval.to.valueOf()),
);
const targets = intervals.map((interval) =>
options.movingEnd && interval.to.valueOf() === movingTo
? { ...interval, to: now }
: interval,
options.movingEnd && interval.to.valueOf() === movingTo ?
{ ...interval, to: now } :
interval,
);
const plan = planOperationRead(targets, coverage, now, options);
const complete = plan.intervals.every(
(interval) => interval.availability === 'complete',
);
const handle =
!options.observe && plan.load.length
? await this.request(
!options.observe && plan.load.length ?
await this.request(
userId,
plan.load,
complete || Boolean(options.force),
'screen',
connection.revision,
)
: null;
) :
null;
if (!options.observe && !options.force && options.history) {
const accountIds = new Set(
intervals.map((interval) => interval.accountId),
@@ -108,7 +112,7 @@ export class BrokerageOperationsSyncService {
movingEnd: true,
}).load;
const remaining = uncoveredOperationIntervals(missing, targets);
if (remaining.length)
if (remaining.length) {
await this.request(
userId,
remaining,
@@ -116,6 +120,7 @@ export class BrokerageOperationsSyncService {
'history',
connection.revision,
);
}
}
return { plan, handle };
}
@@ -126,7 +131,7 @@ export class BrokerageOperationsSyncService {
atomic: boolean,
priority: keyof typeof priorities = 'screen',
revision?: string,
): Promise<string | null> {
): Promise<null | string> {
const connection = await this.access.local(userId);
if (
connection.membershipAt &&
@@ -136,8 +141,9 @@ export class BrokerageOperationsSyncService {
(account) => account.accountId === interval.accountId,
),
)
)
) {
throw new NotFoundException();
}
if (revision && revision !== connection.revision) return null;
const cutoff = new Date();
const bounded = operationIntervals(intervals)
@@ -155,6 +161,7 @@ export class BrokerageOperationsSyncService {
this.processRunId,
atomic,
this.runningAttempts,
// eslint-disable-next-line security/detect-object-injection -- The priority belongs to the internal priority union.
priorities[priority],
);
if (!requested) return null;
@@ -258,17 +265,18 @@ export class BrokerageOperationsSyncService {
Math.ceil(interval.to.valueOf() / MILLISECONDS_PER_SECOND) + 1,
),
},
...(interval.instrumentUid
? { instrumentId: interval.instrumentUid }
: {}),
...(interval.instrumentUid ?
{ instrumentId: interval.instrumentUid } :
{}),
cursor,
limit: operationsPageSize,
state: 'OPERATION_STATE_EXECUTED',
});
if (
!(await this.repository.stagePage(attempt, interval, cursor, response))
)
) {
return false;
}
if (!plan.atomic && !response.hasNext) {
if (
!(await this.repository.publish(
@@ -277,8 +285,9 @@ export class BrokerageOperationsSyncService {
new Date(),
true,
))
)
) {
return false;
}
}
// One page per turn: newly requested screen work can overtake backfill.
// TbankClient alone owns the bounded RPC queue and retries.
@@ -1,41 +1,42 @@
import {
ledgerRange,
accountHistory,
planInvestmentScreen,
} from '../../investment-sync-plan';
import { ConfigService } from '@nestjs/config';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import { BrokerageOperationsRepository } from '../infrastructure/brokerage-operations.repository';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
import {
planOperationRead,
uncoveredOperationIntervals,
operationIntervalsOverlap,
} from '../domain/operations-sync-plan';
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
import {
InstrumentMetadataService,
metadataKey,
TbankAccessService,
TbankMembershipService,
tbankSourceTtlMs,
type TbankOperationItem,
tbankSourceTtlMs,
} from '../../../../integrations/tbank/tbank.api';
import { DateOnlyRange, dateOnlyBounds } from '../../investment-calendar';
import { dateOnlyBounds, DateOnlyRange } from '../../investment-calendar';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import {
DEFAULT_OPERATIONS_PAGE_SIZE,
BrokerageOperationsQueryDto,
} from '../presentation/brokerage-operation.dto';
accountHistory,
ledgerRange,
planInvestmentScreen,
} from '../../investment-sync-plan';
import {
operationIntervalsOverlap,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
import { mapBrokerageOperation } from '../infrastructure/brokerage-operation.mapper';
import { BrokerageOperationsRepository } from '../infrastructure/brokerage-operations.repository';
import {
BrokerageOperationsQueryDto,
DEFAULT_OPERATIONS_PAGE_SIZE,
} from '../presentation/brokerage-operation.dto';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
type LocalOperationsCursor = {
movingEnd?: boolean;
binding: string;
to: string;
at: string;
accountId: string;
at: string;
binding: string;
id: string;
movingEnd?: boolean;
to: string;
};
@Injectable()
@@ -70,18 +71,19 @@ export class BrokerageOperationsService {
const from = range.from;
const to = cursor ? new Date(cursor.to) : range.to;
const movingEnd = cursor?.movingEnd ?? range.to.valueOf() === now.valueOf();
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
const categories = [...new Set(query.categories ?? [])].sort();
const limit = query.limit ?? DEFAULT_OPERATIONS_PAGE_SIZE;
const filterKey = JSON.stringify([
accountId,
from.toISOString(),
query.to
? new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to)
? `${query.to}T23:59:59.999Z`
: query.to,
).toISOString()
: 'now',
query.to ?
new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to) ?
`${query.to}T23:59:59.999Z` :
query.to,
).toISOString() :
'now',
query.instrumentUid ?? null,
categories,
limit,
@@ -118,9 +120,9 @@ export class BrokerageOperationsService {
source.priority === 'screen' &&
source.accountId &&
source.from &&
source.to
? [{ accountId: source.accountId, from: source.from, to: source.to }]
: [],
source.to ?
[{ accountId: source.accountId, from: source.from, to: source.to }] :
[],
);
if (query.sync !== 'observe') {
const discovery = await this.membership.request(
@@ -148,9 +150,9 @@ export class BrokerageOperationsService {
accountId === 'all' || account.accountId === accountId,
)
.flatMap((account) =>
currentRange.from < currentRange.to
? [{ accountId: account.accountId, ...currentRange }]
: [],
currentRange.from < currentRange.to ?
[{ accountId: account.accountId, ...currentRange }] :
[],
);
await this.sync.readPlan(userId, targets, {
movingEnd,
@@ -170,16 +172,16 @@ export class BrokerageOperationsService {
const saved = await this.repository.readPage(readInput);
const accounts = saved?.connection.accounts ?? [];
const targets = accounts.flatMap((account) =>
from < to
? [
from < to ?
[
{
accountId: account.accountId,
from,
to,
instrumentUid: query.instrumentUid,
},
]
: [],
] :
[],
);
const plan = planOperationRead(targets, saved?.coverage ?? [], now, {
movingEnd,
@@ -191,8 +193,8 @@ export class BrokerageOperationsService {
row.accountId === target.accountId &&
(!row.instrumentUid ||
row.instrumentUid === target.instrumentUid) &&
row.from < target.to &&
target.from < row.to,
row.from < target.to &&
target.from < row.to,
)
.map((row) => ({
accountId: target.accountId,
@@ -230,13 +232,8 @@ export class BrokerageOperationsService {
sources
.map((row) => row.fetchedAt)
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
(complete ? saved?.connection.membershipAt : null) ??
null;
const serialize = (range: { accountId: string; from: Date; to: Date }) => ({
accountId: range.accountId,
from: range.from.toISOString(),
to: range.to.toISOString(),
});
(complete ? saved?.connection.membershipAt : null) ??
null;
const historyTargets = accountHistory(accounts, now);
const history = planOperationRead(
historyTargets,
@@ -273,9 +270,11 @@ export class BrokerageOperationsService {
] as const;
}),
);
for (const identity of identities.values())
if (!instruments.get(metadataKey(identity))?.fresh)
for (const identity of identities.values()) {
if (!instruments.get(metadataKey(identity))?.fresh) {
await this.metadata.request(userId, connection.revision, identity);
}
}
}
const membershipMeta = investmentResourceMeta({
resourceKey: 'accounts/membership',
@@ -288,17 +287,17 @@ export class BrokerageOperationsService {
const meta = investmentResourceMeta({
resourceKey: `operations/${createHash('sha256').update(filterKey).digest('base64url')}`,
dataRevision: saved?.binding ?? 'missing',
availability: !available
? 'missing'
: complete
? 'complete'
: confirmed.length
? 'partial'
: 'missing',
availability: !available ?
'missing' :
complete ?
'complete' :
confirmed.length ?
'partial' :
'missing',
fetchedAt: lastSuccessfulAt,
freshForMs: plan.intervals.some((interval) => interval.stale)
? 0
: Number.POSITIVE_INFINITY,
freshForMs: plan.intervals.some((interval) => interval.stale) ?
0 :
Number.POSITIVE_INFINITY,
work: work ?? saved?.membershipWork ?? null,
dependencies: [membershipMeta, ...metadataDependencies],
coverage: {
@@ -312,8 +311,8 @@ export class BrokerageOperationsService {
(interval) =>
interval.accountId === account.accountId && interval.asOf,
),
)
? new Date(
) ?
new Date(
Math.min(
...accounts.map((account) =>
Math.max(
@@ -326,42 +325,42 @@ export class BrokerageOperationsService {
),
),
),
).toISOString()
: null,
).toISOString() :
null,
historyBoundary: knownBoundary ? 'known' : 'unknown',
historyState: saved?.work.some(
(work) => work.state === 'running' && work.priority > 1,
)
? 'loading'
: saved?.work.some(
(work) => work.state === 'failed' && work.priority > 1,
)
? 'failed'
: knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
)
? 'complete'
: saved?.coverage.length
? 'partial'
: 'missing',
) ?
'loading' :
saved?.work.some(
(work) => work.state === 'failed' && work.priority > 1,
) ?
'failed' :
knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
) ?
'complete' :
saved?.coverage.length ?
'partial' :
'missing',
},
});
const last = rows.at(-1);
const hasNext = Boolean(saved && saved.rows.length > limit);
return {
data:
meta.availability === 'missing'
? null
: {
meta.availability === 'missing' ?
null :
{
accountId,
items: rows.map((row) => ({
...mapBrokerageOperation(
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all'
? {
...(accountId === 'all' ?
{
account: {
id: row.accountId,
name:
@@ -369,8 +368,8 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
}
: {}),
} :
{}),
logoName:
instruments
.get(metadataKey(row.payload as TbankOperationItem))
@@ -378,16 +377,16 @@ export class BrokerageOperationsService {
})),
hasNext,
nextCursor:
hasNext && last && saved
? this.encodeCursor({
hasNext && last && saved ?
this.encodeCursor({
binding: saved.binding,
movingEnd,
to: to.toISOString(),
at: last.occurredAt.toISOString(),
accountId: last.accountId,
id: last.brokerOperationId,
})
: null,
}) :
null,
observedAt: now.toISOString(),
},
meta,
@@ -413,23 +412,25 @@ export class BrokerageOperationsService {
extra ||
actual.length !== expected.length ||
!timingSafeEqual(actual, expected)
)
) {
throw new Error();
}
const cursor = JSON.parse(
Buffer.from(body, 'base64url').toString(),
) as Partial<LocalOperationsCursor>;
if (
(cursor.movingEnd !== undefined &&
typeof cursor.movingEnd !== 'boolean') ||
!cursor.binding ||
!cursor.accountId ||
!cursor.id ||
!cursor.at ||
!cursor.to ||
!Number.isFinite(Date.parse(cursor.at)) ||
!Number.isFinite(Date.parse(cursor.to))
)
!cursor.binding ||
!cursor.accountId ||
!cursor.id ||
!cursor.at ||
!cursor.to ||
!Number.isFinite(Date.parse(cursor.at)) ||
!Number.isFinite(Date.parse(cursor.to))
) {
throw new Error();
}
return cursor as LocalOperationsCursor;
} catch {
throw new BadRequestException({ code: 'INVALID_OPERATIONS_CURSOR' });
@@ -465,12 +466,13 @@ export class BrokerageOperationsService {
...period,
})),
);
if (targets.length)
if (targets.length) {
await this.sync.readPlan(userId, targets, {
movingEnd: snapshot.periods?.some((period) => period.movingEnd),
history: true,
revision: connection.revision,
});
}
};
if (!observe) {
const discovery = await this.membership.request(
@@ -530,17 +532,8 @@ export class BrokerageOperationsService {
sources
.map((row) => row.fetchedAt)
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
(complete ? saved?.connection.membershipAt : null) ??
null;
const serialize = (range: {
accountId: string;
from: Date;
to: Date;
}) => ({
accountId: range.accountId,
from: range.from.toISOString(),
to: range.to.toISOString(),
});
(complete ? saved?.connection.membershipAt : null) ??
null;
const confirmed = plan.intervals.flatMap((target) =>
sources
.filter(
@@ -555,15 +548,16 @@ export class BrokerageOperationsService {
})),
);
const meta = investmentResourceMeta({
// eslint-disable-next-line security/detect-object-injection -- The numeric index enumerates the three comparison periods.
resourceKey: `income/${accountId}/${['selected', 'previous', 'yearAgo'][index]}/${period.key}`,
dataRevision: saved?.binding ?? 'missing',
availability: !known
? 'missing'
: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
availability: !known ?
'missing' :
complete ?
'complete' :
sources.length ?
'partial' :
'missing',
fetchedAt,
freshForMs: plan.intervals.some((range) => range.stale) ? 0 : Infinity,
work,
@@ -576,8 +570,8 @@ export class BrokerageOperationsService {
confirmed,
).map(serialize),
asOf:
complete && targets.length
? new Date(
complete && targets.length ?
new Date(
Math.min(
...targets.map((target) =>
Math.max(
@@ -592,24 +586,24 @@ export class BrokerageOperationsService {
),
),
),
).toISOString()
: null,
).toISOString() :
null,
historyBoundary: (saved?.connection.accounts ?? []).every(
(account) => account.openedOn,
)
? 'known'
: 'unknown',
historyState: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
) ?
'known' :
'unknown',
historyState: complete ?
'complete' :
sources.length ?
'partial' :
'missing',
},
});
return {
meta,
items: complete
? (saved?.rows ?? [])
items: complete ?
(saved?.rows ?? [])
.filter((row) =>
plan.intervals.some(
(interval) =>
@@ -623,8 +617,8 @@ export class BrokerageOperationsService {
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all'
? {
...(accountId === 'all' ?
{
account: {
id: row.accountId,
name:
@@ -632,11 +626,15 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
}
: {}),
}))
: null,
} :
{}),
})) :
null,
};
});
}
}
function serialize(range: { accountId: string; from: Date; to: Date }) {
return { accountId: range.accountId, from: range.from.toISOString(), to: range.to.toISOString() };
}