chore: strengthen backend eslint profile
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
|
||||
import type { Prisma } from '../../../../../generated/prisma/client';
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import type { TbankSyncAttempt } from '../../../../integrations/tbank/tbank.api';
|
||||
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import {
|
||||
confirmedCandleCoverage,
|
||||
type CandleRange,
|
||||
confirmedCandleCoverage,
|
||||
type SavedCandle,
|
||||
} from '../domain/candle-coverage';
|
||||
|
||||
const provider = 'tbank';
|
||||
const interval = 'day';
|
||||
type CandleIntent = {
|
||||
instrumentType?: 'share' | 'bond';
|
||||
uid: string;
|
||||
ranges: CandleRange[];
|
||||
period?: string;
|
||||
now?: string;
|
||||
force?: boolean;
|
||||
instrumentType?: 'bond' | 'share';
|
||||
now?: string;
|
||||
period?: string;
|
||||
ranges: CandleRange[];
|
||||
uid: string;
|
||||
};
|
||||
@Injectable()
|
||||
export class CandleRepository {
|
||||
@@ -57,7 +58,7 @@ export class CandleRepository {
|
||||
}
|
||||
|
||||
reserve(
|
||||
owner: { userId: string; revision: string },
|
||||
owner: { revision: string; userId: string },
|
||||
scopeKey: string,
|
||||
intent: CandleIntent,
|
||||
runId: string,
|
||||
@@ -88,29 +89,31 @@ export class CandleRepository {
|
||||
(!intent.force ||
|
||||
(previousIntent.force === true &&
|
||||
(!intent.now || intent.now <= previousNow)))
|
||||
)
|
||||
) {
|
||||
return { work: previous, launch: false };
|
||||
}
|
||||
const work = await tx.tbankSyncWork.update({
|
||||
where: { id: previous.id },
|
||||
data: {
|
||||
priority: nextPriority,
|
||||
intent: {
|
||||
...previousIntent,
|
||||
...(intent.force
|
||||
? {
|
||||
...(intent.force ?
|
||||
{
|
||||
force: true,
|
||||
...(intent.now && intent.now > previousNow
|
||||
? { now: intent.now }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(intent.now && intent.now > previousNow ?
|
||||
{ now: intent.now } :
|
||||
{}),
|
||||
} :
|
||||
{}),
|
||||
},
|
||||
},
|
||||
});
|
||||
return { work, launch: false };
|
||||
}
|
||||
if (previous)
|
||||
if (previous) {
|
||||
await tx.tbankSyncStage.deleteMany({ where: { workId: previous.id } });
|
||||
}
|
||||
const data = {
|
||||
attemptId: randomUUID(),
|
||||
processRunId: runId,
|
||||
@@ -142,12 +145,15 @@ export class CandleRepository {
|
||||
fetchedAt: Date,
|
||||
) {
|
||||
// Reject malformed financial input instead of publishing a false empty range.
|
||||
for (const candle of candles)
|
||||
for (const candle of candles) {
|
||||
if (
|
||||
!Number.isFinite(Date.parse(candle.time)) ||
|
||||
// eslint-disable-next-line security/detect-unsafe-regex -- Anchored decimal grammar: the literal decimal point separates disjoint digit runs; backtracking is linear.
|
||||
!/^-?\d+(?:\.\d+)?$/.test(candle.close)
|
||||
)
|
||||
) {
|
||||
throw new Error('Invalid candle');
|
||||
}
|
||||
}
|
||||
const key = `${range.from.toISOString()}/${range.to.toISOString()}`;
|
||||
const rows = [
|
||||
...new Map(
|
||||
@@ -155,7 +161,7 @@ export class CandleRepository {
|
||||
.filter(
|
||||
(candle) =>
|
||||
new Date(candle.time) >= range.from &&
|
||||
new Date(candle.time) < range.to,
|
||||
new Date(candle.time) < range.to,
|
||||
)
|
||||
.map((candle) => [candle.time, candle]),
|
||||
).values(),
|
||||
@@ -222,8 +228,9 @@ export class CandleRepository {
|
||||
`${range.from.toISOString()}/${range.to.toISOString()}`,
|
||||
),
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new Error('Incomplete candle reconciliation');
|
||||
}
|
||||
const identity = { provider, instrumentUid: intent.uid, interval };
|
||||
for (const range of intent.ranges) {
|
||||
const row = staged.find(
|
||||
@@ -254,21 +261,22 @@ export class CandleRepository {
|
||||
// Preserve coverage outside the requested reconciliation boundaries.
|
||||
for (const previous of old) {
|
||||
for (const part of [
|
||||
...(previous.from < range.from
|
||||
? [{ from: previous.from, to: range.from }]
|
||||
: []),
|
||||
...(previous.to > range.to
|
||||
? [{ from: range.to, to: previous.to }]
|
||||
: []),
|
||||
])
|
||||
...(previous.from < range.from ?
|
||||
[{ from: previous.from, to: range.from }] :
|
||||
[]),
|
||||
...(previous.to > range.to ?
|
||||
[{ from: range.to, to: previous.to }] :
|
||||
[]),
|
||||
]) {
|
||||
await tx.tbankCandleCoverage.create({
|
||||
data: { ...previous, ...part },
|
||||
});
|
||||
}
|
||||
}
|
||||
await tx.tbankCandle.deleteMany({
|
||||
where: { ...identity, time: { gte: range.from, lt: range.to } },
|
||||
});
|
||||
for (const candle of payload.candles)
|
||||
for (const candle of payload.candles) {
|
||||
await tx.tbankCandle.create({
|
||||
data: {
|
||||
...identity,
|
||||
@@ -279,14 +287,16 @@ export class CandleRepository {
|
||||
fetchedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const covered of confirmedCandleCoverage(
|
||||
range,
|
||||
payload.candles,
|
||||
fetchedAt,
|
||||
))
|
||||
)) {
|
||||
await tx.tbankCandleCoverage.create({
|
||||
data: { ...identity, ...covered },
|
||||
});
|
||||
}
|
||||
}
|
||||
await tx.tbankSyncStage.deleteMany({
|
||||
where: { workId: attempt.id, attemptId: attempt.attemptId },
|
||||
|
||||
+61
-56
@@ -1,40 +1,42 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import type { Prisma } from '../../../../../generated/prisma/client';
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import type {
|
||||
TbankBond,
|
||||
TbankShare,
|
||||
TbankSyncAttempt,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
|
||||
export type CatalogQuery = {
|
||||
instrumentType: 'share' | 'bond';
|
||||
query: string;
|
||||
exactTicker?: boolean;
|
||||
instrumentType: 'bond' | 'share';
|
||||
query: string;
|
||||
};
|
||||
export type CatalogSource = {
|
||||
instrumentType: 'share' | 'bond';
|
||||
uid: string;
|
||||
instrumentType: 'bond' | 'share';
|
||||
kind:
|
||||
| 'detail'
|
||||
| 'last-price'
|
||||
| 'bond-current'
|
||||
| 'description'
|
||||
| 'fundamentals'
|
||||
| 'trading-status'
|
||||
| 'comparison'
|
||||
| 'coupons'
|
||||
| 'yield'
|
||||
| 'rating'
|
||||
| 'dividends'
|
||||
| 'bond-events';
|
||||
| 'bond-current' |
|
||||
'bond-events' |
|
||||
'comparison' |
|
||||
'coupons' |
|
||||
'description' |
|
||||
'detail' |
|
||||
'dividends' |
|
||||
'fundamentals' |
|
||||
'last-price' |
|
||||
'rating' |
|
||||
'trading-status' |
|
||||
'yield';
|
||||
rangeKey?: string;
|
||||
ticker?: string;
|
||||
uid: string;
|
||||
};
|
||||
export type CatalogWork = CatalogQuery | CatalogSource;
|
||||
export type CatalogInstrument = (TbankShare | TbankBond) & { uid: string };
|
||||
export type CatalogInstrument = (TbankBond | TbankShare) & { uid: string };
|
||||
const provider = 'tbank';
|
||||
export function catalogQuery(input: CatalogQuery): CatalogQuery {
|
||||
return {
|
||||
@@ -44,13 +46,14 @@ export function catalogQuery(input: CatalogQuery): CatalogQuery {
|
||||
};
|
||||
}
|
||||
export function catalogScope(input: CatalogWork) {
|
||||
if ('uid' in input)
|
||||
if ('uid' in input) {
|
||||
return `catalog-source:${JSON.stringify([
|
||||
input.kind,
|
||||
input.kind === 'detail' ? input.instrumentType : null,
|
||||
input.rangeKey ?? '',
|
||||
input.uid,
|
||||
])}`;
|
||||
}
|
||||
return `${input.exactTicker ? 'ticker' : 'catalog'}:${JSON.stringify([input.instrumentType, input.query])}`;
|
||||
}
|
||||
|
||||
@@ -60,8 +63,8 @@ export class CatalogDiscoveryRepository {
|
||||
|
||||
read(input: CatalogQuery) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const alias = input.exactTicker
|
||||
? await tx.tbankInstrumentLookup.findUnique({
|
||||
const alias = input.exactTicker ?
|
||||
await tx.tbankInstrumentLookup.findUnique({
|
||||
where: {
|
||||
provider_identifierType_identifier: {
|
||||
provider,
|
||||
@@ -69,17 +72,17 @@ export class CatalogDiscoveryRepository {
|
||||
identifier: input.query,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const discovery = input.exactTicker
|
||||
? alias
|
||||
? {
|
||||
}) :
|
||||
null;
|
||||
const discovery = input.exactTicker ?
|
||||
alias ?
|
||||
{
|
||||
instrumentUids: alias.matchedUids,
|
||||
fetchedAt: alias.fetchedAt,
|
||||
dataRevision: alias.dataRevision,
|
||||
}
|
||||
: null
|
||||
: await tx.tbankCatalogDiscovery.findUnique({
|
||||
} :
|
||||
null :
|
||||
await tx.tbankCatalogDiscovery.findUnique({
|
||||
where: {
|
||||
provider_instrumentType_query: {
|
||||
provider,
|
||||
@@ -101,12 +104,13 @@ export class CatalogDiscoveryRepository {
|
||||
});
|
||||
const byUid = new Map(rows.map((row) => [row.instrumentUid, row]));
|
||||
// A damaged/missing dependency is unknown, never a confirmed empty search.
|
||||
if (uids.some((uid) => !byUid.has(uid)))
|
||||
if (uids.some((uid) => !byUid.has(uid))) {
|
||||
return {
|
||||
data: null,
|
||||
fetchedAt: null,
|
||||
dataRevision: String(discovery.dataRevision),
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: uids.map((uid) => byUid.get(uid)?.payload as CatalogInstrument),
|
||||
fetchedAt: discovery.fetchedAt,
|
||||
@@ -127,9 +131,9 @@ export class CatalogDiscoveryRepository {
|
||||
kind: input.kind,
|
||||
source: sourceProvider(input),
|
||||
rangeKey:
|
||||
input.kind === 'detail'
|
||||
? input.instrumentType
|
||||
: (input.rangeKey ?? ''),
|
||||
input.kind === 'detail' ?
|
||||
input.instrumentType :
|
||||
(input.rangeKey ?? ''),
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -176,7 +180,7 @@ export class CatalogDiscoveryRepository {
|
||||
},
|
||||
});
|
||||
const instrument = (
|
||||
value as { instrument?: TbankShare | TbankBond | null }
|
||||
value as { instrument?: null | TbankBond | TbankShare }
|
||||
).instrument;
|
||||
if (
|
||||
(input.kind === 'detail' || input.kind === 'bond-current') &&
|
||||
@@ -225,9 +229,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previousAlias?.matchedUids,
|
||||
matchedUids,
|
||||
)
|
||||
? 0
|
||||
: 1,
|
||||
) ?
|
||||
0 :
|
||||
1,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -239,9 +243,9 @@ export class CatalogDiscoveryRepository {
|
||||
source: sourceProvider(input),
|
||||
kind: input.kind,
|
||||
rangeKey:
|
||||
input.kind === 'detail'
|
||||
? input.instrumentType
|
||||
: (input.rangeKey ?? ''),
|
||||
input.kind === 'detail' ?
|
||||
input.instrumentType :
|
||||
(input.rangeKey ?? ''),
|
||||
};
|
||||
const previous = await tx.tbankInstrumentSource.findUnique({
|
||||
where: { provider_instrumentUid_source_kind_rangeKey: key },
|
||||
@@ -262,7 +266,7 @@ export class CatalogDiscoveryRepository {
|
||||
}
|
||||
|
||||
reserve(
|
||||
owner: { userId: string; revision: string },
|
||||
owner: { revision: string; userId: string },
|
||||
input: CatalogWork,
|
||||
runId: string,
|
||||
active: ReadonlySet<string>,
|
||||
@@ -281,8 +285,9 @@ export class CatalogDiscoveryRepository {
|
||||
previous?.state === 'running' &&
|
||||
previous.processRunId === runId &&
|
||||
active.has(previous.attemptId)
|
||||
)
|
||||
) {
|
||||
return { work: previous, launch: false };
|
||||
}
|
||||
const data = {
|
||||
attemptId: randomUUID(),
|
||||
processRunId: runId,
|
||||
@@ -351,9 +356,9 @@ export class CatalogDiscoveryRepository {
|
||||
instrumentKind: instrument.instrumentKind,
|
||||
apiTradeAvailableFlag: instrument.apiTradeAvailableFlag,
|
||||
currency: instrument.currency,
|
||||
brand: instrument.brand
|
||||
? { logoName: instrument.brand.logoName }
|
||||
: undefined,
|
||||
brand: instrument.brand ?
|
||||
{ logoName: instrument.brand.logoName } :
|
||||
undefined,
|
||||
}),
|
||||
) as Prisma.InputJsonObject;
|
||||
await tx.tbankInstrument.upsert({
|
||||
@@ -410,9 +415,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previous?.matchedUids,
|
||||
instrumentUids,
|
||||
)
|
||||
? 0
|
||||
: 1,
|
||||
) ?
|
||||
0 :
|
||||
1,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -436,9 +441,9 @@ export class CatalogDiscoveryRepository {
|
||||
increment: isDeepStrictEqual(
|
||||
previous?.instrumentUids,
|
||||
instrumentUids,
|
||||
)
|
||||
? 0
|
||||
: 1,
|
||||
) ?
|
||||
0 :
|
||||
1,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -472,9 +477,9 @@ export class CatalogDiscoveryRepository {
|
||||
}
|
||||
|
||||
function sourceProvider(input: CatalogSource) {
|
||||
return input.kind === 'yield' || input.kind === 'bond-events'
|
||||
? 'moex'
|
||||
: input.kind === 'rating'
|
||||
? 'ncr'
|
||||
: 'tbank';
|
||||
return input.kind === 'yield' || input.kind === 'bond-events' ?
|
||||
'moex' :
|
||||
input.kind === 'rating' ?
|
||||
'ncr' :
|
||||
'tbank';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user