chore: strengthen backend eslint profile
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { CatalogEnrichmentService } from './catalog-enrichment.service';
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
|
||||
import {
|
||||
TbankAccessService,
|
||||
@@ -17,9 +17,10 @@ import { investmentResourceMeta } from '../../investment-resource.mapper';
|
||||
import {
|
||||
CatalogDiscoveryRepository,
|
||||
catalogQuery,
|
||||
catalogScope,
|
||||
type CatalogQuery,
|
||||
catalogScope,
|
||||
} from '../infrastructure/catalog-discovery.repository';
|
||||
import { CatalogEnrichmentService } from './catalog-enrichment.service';
|
||||
import {
|
||||
catalogSearchLimit,
|
||||
selectCatalogInstruments,
|
||||
@@ -28,10 +29,10 @@ import {
|
||||
const tickerAmbiguityLimit = 2;
|
||||
|
||||
type Producer = {
|
||||
input: CatalogQuery;
|
||||
attempt: TbankSyncAttempt;
|
||||
observers: TbankSyncAttempt[];
|
||||
force: boolean;
|
||||
input: CatalogQuery;
|
||||
observers: TbankSyncAttempt[];
|
||||
};
|
||||
@Injectable()
|
||||
export class CatalogDiscoveryService {
|
||||
@@ -43,8 +44,9 @@ export class CatalogDiscoveryService {
|
||||
private running = false;
|
||||
private readonly completions = new Map<
|
||||
string,
|
||||
{ workId: string; done: Promise<void>; resolve: () => void }
|
||||
{ done: Promise<void>; resolve: () => void; workId: string }
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly repository: CatalogDiscoveryRepository,
|
||||
private readonly access: TbankAccessService,
|
||||
@@ -73,14 +75,14 @@ export class CatalogDiscoveryService {
|
||||
|
||||
async search(userId: string, input: CatalogQuery, observe = false) {
|
||||
const discovery = await this.read(userId, input, observe);
|
||||
const sources = discovery.data
|
||||
? await this.enrichment.readMany(
|
||||
const sources = discovery.data ?
|
||||
await this.enrichment.readMany(
|
||||
userId,
|
||||
input.instrumentType,
|
||||
discovery.data.map((item) => item.uid),
|
||||
observe,
|
||||
)
|
||||
: [];
|
||||
) :
|
||||
[];
|
||||
const dependencies = [
|
||||
discovery.meta,
|
||||
...sources.map((source) => source.meta),
|
||||
@@ -108,22 +110,22 @@ export class CatalogDiscoveryService {
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability:
|
||||
discovery.data === null
|
||||
? 'missing'
|
||||
: sources.every((source) => source.meta.availability === 'complete')
|
||||
? 'complete'
|
||||
: 'partial',
|
||||
fetchedAt: discovery.meta.lastSuccessfulAt
|
||||
? new Date(discovery.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
discovery.data === null ?
|
||||
'missing' :
|
||||
sources.every((source) => source.meta.availability === 'complete') ?
|
||||
'complete' :
|
||||
'partial',
|
||||
fetchedAt: discovery.meta.lastSuccessfulAt ?
|
||||
new Date(discovery.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
freshForMs: tbankSourceTtlMs.catalog,
|
||||
work: discovery.meta.sync.id
|
||||
? {
|
||||
work: discovery.meta.sync.id ?
|
||||
{
|
||||
id: discovery.meta.sync.id,
|
||||
state: discovery.meta.sync.state,
|
||||
errorCode: discovery.meta.sync.errorCode,
|
||||
}
|
||||
: null,
|
||||
} :
|
||||
null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -131,7 +133,7 @@ export class CatalogDiscoveryService {
|
||||
|
||||
async detail(
|
||||
userId: string,
|
||||
instrumentType: 'share' | 'bond',
|
||||
instrumentType: 'bond' | 'share',
|
||||
uid: string,
|
||||
observe = false,
|
||||
) {
|
||||
@@ -143,22 +145,24 @@ export class CatalogDiscoveryService {
|
||||
['detail'],
|
||||
);
|
||||
const source = sources[0];
|
||||
if (source.meta.availability === 'complete' && !source.instrument)
|
||||
if (source.meta.availability === 'complete' && !source.instrument) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const instrument = source.instrument;
|
||||
let slug = uid;
|
||||
const dependencies = [source.meta];
|
||||
if (
|
||||
instrument?.ticker &&
|
||||
/^[A-Za-z0-9][A-Za-z0-9._@+-]{0,79}$/.test(instrument.ticker)
|
||||
/^[A-Z0-9][\w.@+-]{0,79}$/i.test(instrument.ticker)
|
||||
) {
|
||||
const alias = await this.read(
|
||||
userId,
|
||||
{ instrumentType, query: instrument.ticker, exactTicker: true },
|
||||
observe,
|
||||
);
|
||||
if (alias.data?.length === 1 && alias.data[0].uid === uid)
|
||||
if (alias.data?.length === 1 && alias.data[0].uid === uid) {
|
||||
slug = instrument.ticker.toUpperCase();
|
||||
}
|
||||
dependencies.push(alias.meta);
|
||||
}
|
||||
return {
|
||||
@@ -169,17 +173,17 @@ export class CatalogDiscoveryService {
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability: source.meta.availability,
|
||||
fetchedAt: source.meta.lastSuccessfulAt
|
||||
? new Date(source.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
fetchedAt: source.meta.lastSuccessfulAt ?
|
||||
new Date(source.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
freshForMs: tbankSourceTtlMs.metadata,
|
||||
work: source.meta.sync.id
|
||||
? {
|
||||
work: source.meta.sync.id ?
|
||||
{
|
||||
id: source.meta.sync.id,
|
||||
state: source.meta.sync.state,
|
||||
errorCode: source.meta.sync.errorCode,
|
||||
}
|
||||
: null,
|
||||
} :
|
||||
null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -187,7 +191,7 @@ export class CatalogDiscoveryService {
|
||||
|
||||
async byTicker(
|
||||
userId: string,
|
||||
instrumentType: 'share' | 'bond',
|
||||
instrumentType: 'bond' | 'share',
|
||||
ticker: string,
|
||||
observe = false,
|
||||
) {
|
||||
@@ -197,38 +201,39 @@ export class CatalogDiscoveryService {
|
||||
observe,
|
||||
);
|
||||
if (lookup.data?.length === 0) throw new NotFoundException();
|
||||
if (lookup.data && lookup.data.length > 1)
|
||||
if (lookup.data && lookup.data.length > 1) {
|
||||
throw new ConflictException({
|
||||
code:
|
||||
instrumentType === 'share'
|
||||
? 'STOCK_TICKER_AMBIGUOUS'
|
||||
: 'BOND_TICKER_AMBIGUOUS',
|
||||
instrumentType === 'share' ?
|
||||
'STOCK_TICKER_AMBIGUOUS' :
|
||||
'BOND_TICKER_AMBIGUOUS',
|
||||
});
|
||||
const result = lookup.data
|
||||
? await this.detail(userId, instrumentType, lookup.data[0].uid, observe)
|
||||
: null;
|
||||
}
|
||||
const result = lookup.data ?
|
||||
await this.detail(userId, instrumentType, lookup.data[0].uid, observe) :
|
||||
null;
|
||||
const dependencies = [lookup.meta, ...(result ? [result.meta] : [])];
|
||||
return {
|
||||
data: result?.data
|
||||
? { ...result.data, slug: ticker.trim().toUpperCase() }
|
||||
: null,
|
||||
data: result?.data ?
|
||||
{ ...result.data, slug: ticker.trim().toUpperCase() } :
|
||||
null,
|
||||
meta: investmentResourceMeta({
|
||||
resourceKey: `${instrumentType}/ticker/${ticker.trim().toUpperCase()}`,
|
||||
dataRevision: dependencies
|
||||
.map((dependency) => dependency.dataRevision)
|
||||
.join('/'),
|
||||
availability: result?.meta.availability ?? 'missing',
|
||||
fetchedAt: lookup.meta.lastSuccessfulAt
|
||||
? new Date(lookup.meta.lastSuccessfulAt)
|
||||
: null,
|
||||
fetchedAt: lookup.meta.lastSuccessfulAt ?
|
||||
new Date(lookup.meta.lastSuccessfulAt) :
|
||||
null,
|
||||
freshForMs: tbankSourceTtlMs.metadata,
|
||||
work: lookup.meta.sync.id
|
||||
? {
|
||||
work: lookup.meta.sync.id ?
|
||||
{
|
||||
id: lookup.meta.sync.id,
|
||||
state: lookup.meta.sync.state,
|
||||
errorCode: lookup.meta.sync.errorCode,
|
||||
}
|
||||
: null,
|
||||
} :
|
||||
null,
|
||||
dependencies,
|
||||
}),
|
||||
};
|
||||
@@ -245,8 +250,9 @@ export class CatalogDiscoveryService {
|
||||
const saved = await this.repository.read(input);
|
||||
if (
|
||||
!tbankSourceNeedsRefresh('catalog', saved.fetchedAt, new Date(), force)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const reservation = await this.repository.reserve(
|
||||
{ userId, revision },
|
||||
input,
|
||||
@@ -291,12 +297,13 @@ export class CatalogDiscoveryService {
|
||||
|
||||
async refresh(userId: string, revision: string, query: CatalogQuery) {
|
||||
const id = await this.request(userId, revision, query, true);
|
||||
if (id)
|
||||
if (id) {
|
||||
await Promise.all(
|
||||
[...this.completions.values()]
|
||||
.filter((entry) => entry.workId === id)
|
||||
.map((entry) => entry.done),
|
||||
);
|
||||
}
|
||||
const saved = await this.repository.read(catalogQuery(query));
|
||||
const sources = await this.enrichment.readMany(
|
||||
userId,
|
||||
@@ -323,19 +330,19 @@ export class CatalogDiscoveryService {
|
||||
const token = await this.access.tokenForAttempt(attempt);
|
||||
if (token) {
|
||||
const found =
|
||||
input.instrumentType === 'share'
|
||||
? await this.tbank.findShares(token, input.query)
|
||||
: await this.tbank.findBonds(token, input.query);
|
||||
input.instrumentType === 'share' ?
|
||||
await this.tbank.findShares(token, input.query) :
|
||||
await this.tbank.findBonds(token, input.query);
|
||||
success = await this.repository.publish(
|
||||
attempt,
|
||||
input,
|
||||
selectCatalogInstruments(
|
||||
input.exactTicker
|
||||
? found.filter(
|
||||
input.exactTicker ?
|
||||
found.filter(
|
||||
(item) =>
|
||||
item.ticker?.trim().toUpperCase() === input.query,
|
||||
)
|
||||
: found,
|
||||
) :
|
||||
found,
|
||||
input.query,
|
||||
input.instrumentType,
|
||||
).slice(
|
||||
@@ -346,17 +353,18 @@ export class CatalogDiscoveryService {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'INVALID_TBANK_TOKEN')
|
||||
if (error instanceof Error && error.message === 'INVALID_TBANK_TOKEN') {
|
||||
await this.access.rejectMembershipToken(attempt);
|
||||
else
|
||||
} else {
|
||||
this.logger.event('warn', 'catalog-discovery.failed', {
|
||||
code: 'TBANK_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
const saved = await this.repository.read(input);
|
||||
for (const observer of [attempt, ...producer.observers])
|
||||
if (!input.exactTicker || saved.data?.length === 1)
|
||||
for (const observer of [attempt, ...producer.observers]) {
|
||||
if (!input.exactTicker || saved.data?.length === 1) {
|
||||
await this.enrichment.requestMany(
|
||||
observer.userId,
|
||||
observer.revision,
|
||||
@@ -365,6 +373,8 @@ export class CatalogDiscoveryService {
|
||||
producer.force,
|
||||
input.exactTicker ? ['detail'] : ['last-price', 'detail'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.producers.delete(catalogScope(input));
|
||||
for (const observer of [attempt, ...producer.observers]) {
|
||||
|
||||
Reference in New Issue
Block a user