mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
188 changed files with 9586 additions and 47631 deletions
@ -1,44 +0,0 @@ |
|||
import { Logger } from '@nestjs/common'; |
|||
import { |
|||
Injectable, |
|||
NestInterceptor, |
|||
ExecutionContext, |
|||
CallHandler |
|||
} from '@nestjs/common'; |
|||
import { Observable } from 'rxjs'; |
|||
import { tap } from 'rxjs/operators'; |
|||
|
|||
@Injectable() |
|||
export class LoggingInterceptor implements NestInterceptor { |
|||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { |
|||
const methodName = |
|||
context.getClass().name + ':' + context.getHandler().name; |
|||
Logger.debug(`Before ${methodName}...`); |
|||
|
|||
const now = Date.now(); |
|||
return next |
|||
.handle() |
|||
.pipe( |
|||
tap(() => Logger.debug(`After ${methodName}... ${Date.now() - now}ms`)) |
|||
); |
|||
} |
|||
} |
|||
|
|||
export function LogPerformance( |
|||
target: any, |
|||
propertyKey: string, |
|||
descriptor: PropertyDescriptor |
|||
) { |
|||
const originalMethod = descriptor.value; |
|||
descriptor.value = function (...args: any[]) { |
|||
const time = Date.now(); |
|||
const result = originalMethod.apply(this, args); |
|||
const now = Date.now(); |
|||
if (now - time > 100) { |
|||
Logger.debug(`${propertyKey} returned within: ${now - time} ms`); |
|||
} |
|||
return result; |
|||
}; |
|||
|
|||
return descriptor; |
|||
} |
@ -0,0 +1,134 @@ |
|||
import { AccessService } from '@ghostfolio/api/app/access/access.service'; |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { UserService } from '@ghostfolio/api/app/user/user.service'; |
|||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; |
|||
import { getSum } from '@ghostfolio/common/helper'; |
|||
import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; |
|||
import type { RequestWithUser } from '@ghostfolio/common/types'; |
|||
|
|||
import { |
|||
Controller, |
|||
Get, |
|||
HttpException, |
|||
Inject, |
|||
Param, |
|||
UseInterceptors |
|||
} from '@nestjs/common'; |
|||
import { REQUEST } from '@nestjs/core'; |
|||
import { Big } from 'big.js'; |
|||
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|||
|
|||
@Controller('public') |
|||
export class PublicController { |
|||
public constructor( |
|||
private readonly accessService: AccessService, |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly exchangeRateDataService: ExchangeRateDataService, |
|||
private readonly portfolioService: PortfolioService, |
|||
@Inject(REQUEST) private readonly request: RequestWithUser, |
|||
private readonly userService: UserService |
|||
) {} |
|||
|
|||
@Get(':accessId/portfolio') |
|||
@UseInterceptors(TransformDataSourceInResponseInterceptor) |
|||
public async getPublicPortfolio( |
|||
@Param('accessId') accessId |
|||
): Promise<PublicPortfolioResponse> { |
|||
const access = await this.accessService.access({ id: accessId }); |
|||
|
|||
if (!access) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.NOT_FOUND), |
|||
StatusCodes.NOT_FOUND |
|||
); |
|||
} |
|||
|
|||
let hasDetails = true; |
|||
|
|||
const user = await this.userService.user({ |
|||
id: access.userId |
|||
}); |
|||
|
|||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { |
|||
hasDetails = user.subscription.type === 'Premium'; |
|||
} |
|||
|
|||
const [ |
|||
{ holdings }, |
|||
{ performance: performance1d }, |
|||
{ performance: performanceMax }, |
|||
{ performance: performanceYtd } |
|||
] = await Promise.all([ |
|||
this.portfolioService.getDetails({ |
|||
impersonationId: access.userId, |
|||
userId: user.id, |
|||
withMarkets: true |
|||
}), |
|||
...['1d', 'max', 'ytd'].map((dateRange) => { |
|||
return this.portfolioService.getPerformance({ |
|||
dateRange, |
|||
impersonationId: undefined, |
|||
userId: user.id |
|||
}); |
|||
}) |
|||
]); |
|||
|
|||
const publicPortfolioResponse: PublicPortfolioResponse = { |
|||
hasDetails, |
|||
alias: access.alias, |
|||
holdings: {}, |
|||
performance: { |
|||
'1d': { |
|||
relativeChange: |
|||
performance1d.netPerformancePercentageWithCurrencyEffect |
|||
}, |
|||
max: { |
|||
relativeChange: |
|||
performanceMax.netPerformancePercentageWithCurrencyEffect |
|||
}, |
|||
ytd: { |
|||
relativeChange: |
|||
performanceYtd.netPerformancePercentageWithCurrencyEffect |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const totalValue = getSum( |
|||
Object.values(holdings).map(({ currency, marketPrice, quantity }) => { |
|||
return new Big( |
|||
this.exchangeRateDataService.toCurrency( |
|||
quantity * marketPrice, |
|||
currency, |
|||
this.request.user?.Settings?.settings.baseCurrency ?? |
|||
DEFAULT_CURRENCY |
|||
) |
|||
); |
|||
}) |
|||
).toNumber(); |
|||
|
|||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { |
|||
publicPortfolioResponse.holdings[symbol] = { |
|||
allocationInPercentage: |
|||
portfolioPosition.valueInBaseCurrency / totalValue, |
|||
assetClass: hasDetails ? portfolioPosition.assetClass : undefined, |
|||
countries: hasDetails ? portfolioPosition.countries : [], |
|||
currency: hasDetails ? portfolioPosition.currency : undefined, |
|||
dataSource: portfolioPosition.dataSource, |
|||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, |
|||
markets: hasDetails ? portfolioPosition.markets : undefined, |
|||
name: portfolioPosition.name, |
|||
netPerformancePercentWithCurrencyEffect: |
|||
portfolioPosition.netPerformancePercentWithCurrencyEffect, |
|||
sectors: hasDetails ? portfolioPosition.sectors : [], |
|||
symbol: portfolioPosition.symbol, |
|||
url: portfolioPosition.url, |
|||
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue |
|||
}; |
|||
} |
|||
|
|||
return publicPortfolioResponse; |
|||
} |
|||
} |
@ -0,0 +1,49 @@ |
|||
import { AccessModule } from '@ghostfolio/api/app/access/access.module'; |
|||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; |
|||
import { AccountService } from '@ghostfolio/api/app/account/account.service'; |
|||
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; |
|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { UserModule } from '@ghostfolio/api/app/user/user.module'; |
|||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
|||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { PublicController } from './public.controller'; |
|||
|
|||
@Module({ |
|||
controllers: [PublicController], |
|||
imports: [ |
|||
AccessModule, |
|||
DataProviderModule, |
|||
ExchangeRateDataModule, |
|||
ImpersonationModule, |
|||
MarketDataModule, |
|||
OrderModule, |
|||
PortfolioSnapshotQueueModule, |
|||
PrismaModule, |
|||
RedisCacheModule, |
|||
SymbolProfileModule, |
|||
TransformDataSourceInRequestModule, |
|||
UserModule |
|||
], |
|||
providers: [ |
|||
AccountBalanceService, |
|||
AccountService, |
|||
CurrentRateService, |
|||
PortfolioCalculatorFactory, |
|||
PortfolioService, |
|||
RulesService |
|||
] |
|||
}) |
|||
export class PublicModule {} |
@ -0,0 +1,4 @@ |
|||
export interface PortfolioSnapshotValue { |
|||
expiration: number; |
|||
portfolioSnapshot: string; |
|||
} |
@ -1,7 +0,0 @@ |
|||
import { Cache } from 'cache-manager'; |
|||
|
|||
import type { RedisStore } from './redis-store.interface'; |
|||
|
|||
export interface RedisCache extends Cache { |
|||
store: RedisStore; |
|||
} |
@ -1,8 +0,0 @@ |
|||
import { Store } from 'cache-manager'; |
|||
import { createClient } from 'redis'; |
|||
|
|||
export interface RedisStore extends Store { |
|||
getClient: () => ReturnType<typeof createClient>; |
|||
isCacheableValue: (value: any) => boolean; |
|||
name: 'redis'; |
|||
} |
@ -1,13 +1,28 @@ |
|||
import { RedisCacheService } from './redis-cache.service'; |
|||
import { Filter } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Milliseconds } from 'cache-manager'; |
|||
|
|||
export const RedisCacheServiceMock = { |
|||
cache: new Map<string, string>(), |
|||
get: (key: string): Promise<string> => { |
|||
return Promise.resolve(null); |
|||
const value = RedisCacheServiceMock.cache.get(key) || null; |
|||
|
|||
return Promise.resolve(value); |
|||
}, |
|||
getPortfolioSnapshotKey: (userId: string): string => { |
|||
return `portfolio-snapshot-${userId}`; |
|||
getPortfolioSnapshotKey: ({ |
|||
filters, |
|||
userId |
|||
}: { |
|||
filters?: Filter[]; |
|||
userId: string; |
|||
}): string => { |
|||
const filtersHash = filters?.length; |
|||
|
|||
return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`; |
|||
}, |
|||
set: (key: string, value: string, ttlInSeconds?: number): Promise<string> => { |
|||
set: (key: string, value: string, ttl?: Milliseconds): Promise<string> => { |
|||
RedisCacheServiceMock.cache.set(key, value); |
|||
|
|||
return Promise.resolve(value); |
|||
} |
|||
}; |
|||
|
@ -0,0 +1,80 @@ |
|||
import { |
|||
Injectable, |
|||
NestInterceptor, |
|||
ExecutionContext, |
|||
CallHandler |
|||
} from '@nestjs/common'; |
|||
import { Observable } from 'rxjs'; |
|||
import { tap } from 'rxjs/operators'; |
|||
|
|||
import { PerformanceLoggingService } from './performance-logging.service'; |
|||
|
|||
@Injectable() |
|||
export class PerformanceLoggingInterceptor implements NestInterceptor { |
|||
public constructor( |
|||
private readonly performanceLoggingService: PerformanceLoggingService |
|||
) {} |
|||
|
|||
public intercept( |
|||
context: ExecutionContext, |
|||
next: CallHandler |
|||
): Observable<any> { |
|||
const startTime = performance.now(); |
|||
|
|||
const className = context.getClass().name; |
|||
const methodName = context.getHandler().name; |
|||
|
|||
return next.handle().pipe( |
|||
tap(() => { |
|||
return this.performanceLoggingService.logPerformance({ |
|||
className, |
|||
methodName, |
|||
startTime |
|||
}); |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
|
|||
export function LogPerformance( |
|||
target: any, |
|||
propertyKey: string, |
|||
descriptor: PropertyDescriptor |
|||
) { |
|||
const originalMethod = descriptor.value; |
|||
|
|||
descriptor.value = function (...args: any[]) { |
|||
const startTime = performance.now(); |
|||
const performanceLoggingService = new PerformanceLoggingService(); |
|||
|
|||
const result = originalMethod.apply(this, args); |
|||
|
|||
if (result instanceof Promise) { |
|||
// Handle async method
|
|||
return result |
|||
.then((res: any) => { |
|||
performanceLoggingService.logPerformance({ |
|||
startTime, |
|||
className: target.constructor.name, |
|||
methodName: propertyKey |
|||
}); |
|||
|
|||
return res; |
|||
}) |
|||
.catch((error: any) => { |
|||
throw error; |
|||
}); |
|||
} else { |
|||
// Handle sync method
|
|||
performanceLoggingService.logPerformance({ |
|||
startTime, |
|||
className: target.constructor.name, |
|||
methodName: propertyKey |
|||
}); |
|||
|
|||
return result; |
|||
} |
|||
}; |
|||
|
|||
return descriptor; |
|||
} |
@ -0,0 +1,10 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { PerformanceLoggingInterceptor } from './performance-logging.interceptor'; |
|||
import { PerformanceLoggingService } from './performance-logging.service'; |
|||
|
|||
@Module({ |
|||
exports: [PerformanceLoggingInterceptor, PerformanceLoggingService], |
|||
providers: [PerformanceLoggingInterceptor, PerformanceLoggingService] |
|||
}) |
|||
export class PerformanceLoggingModule {} |
@ -0,0 +1,21 @@ |
|||
import { Injectable, Logger } from '@nestjs/common'; |
|||
|
|||
@Injectable() |
|||
export class PerformanceLoggingService { |
|||
public logPerformance({ |
|||
className, |
|||
methodName, |
|||
startTime |
|||
}: { |
|||
className: string; |
|||
methodName: string; |
|||
startTime: number; |
|||
}) { |
|||
const endTime = performance.now(); |
|||
|
|||
Logger.debug( |
|||
`Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`, |
|||
className |
|||
); |
|||
} |
|||
} |
@ -1,11 +1,11 @@ |
|||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
|||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; |
|||
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|||
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config'; |
|||
|
@ -0,0 +1,7 @@ |
|||
import { Filter } from '@ghostfolio/common/interfaces'; |
|||
|
|||
export interface IPortfolioSnapshotQueueJob { |
|||
filters: Filter[]; |
|||
userCurrency: string; |
|||
userId: string; |
|||
} |
@ -0,0 +1,39 @@ |
|||
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module'; |
|||
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; |
|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
|||
import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; |
|||
|
|||
import { BullModule } from '@nestjs/bull'; |
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; |
|||
|
|||
@Module({ |
|||
exports: [BullModule, PortfolioSnapshotService], |
|||
imports: [ |
|||
AccountBalanceModule, |
|||
BullModule.registerQueue({ |
|||
name: PORTFOLIO_SNAPSHOT_QUEUE |
|||
}), |
|||
ConfigurationModule, |
|||
DataProviderModule, |
|||
ExchangeRateDataModule, |
|||
MarketDataModule, |
|||
OrderModule, |
|||
RedisCacheModule |
|||
], |
|||
providers: [ |
|||
CurrentRateService, |
|||
PortfolioCalculatorFactory, |
|||
PortfolioSnapshotProcessor, |
|||
PortfolioSnapshotService |
|||
] |
|||
}) |
|||
export class PortfolioSnapshotQueueModule {} |
@ -0,0 +1,114 @@ |
|||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; |
|||
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
|||
import { |
|||
PerformanceCalculationType, |
|||
PortfolioCalculatorFactory |
|||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; |
|||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { |
|||
CACHE_TTL_INFINITE, |
|||
DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, |
|||
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, |
|||
PORTFOLIO_SNAPSHOT_QUEUE |
|||
} from '@ghostfolio/common/config'; |
|||
|
|||
import { Process, Processor } from '@nestjs/bull'; |
|||
import { Injectable, Logger } from '@nestjs/common'; |
|||
import { Job } from 'bull'; |
|||
import { addMilliseconds } from 'date-fns'; |
|||
|
|||
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; |
|||
|
|||
@Injectable() |
|||
@Processor(PORTFOLIO_SNAPSHOT_QUEUE) |
|||
export class PortfolioSnapshotProcessor { |
|||
public constructor( |
|||
private readonly accountBalanceService: AccountBalanceService, |
|||
private readonly calculatorFactory: PortfolioCalculatorFactory, |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly orderService: OrderService, |
|||
private readonly redisCacheService: RedisCacheService |
|||
) {} |
|||
|
|||
@Process({ |
|||
concurrency: parseInt( |
|||
process.env.PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT ?? |
|||
DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT.toString(), |
|||
10 |
|||
), |
|||
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME |
|||
}) |
|||
public async calculatePortfolioSnapshot( |
|||
job: Job<IPortfolioSnapshotQueueJob> |
|||
) { |
|||
try { |
|||
const startTime = performance.now(); |
|||
|
|||
Logger.log( |
|||
`Portfolio snapshot calculation of user '${job.data.userId}' has been started`, |
|||
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})` |
|||
); |
|||
|
|||
const { activities } = |
|||
await this.orderService.getOrdersForPortfolioCalculator({ |
|||
filters: job.data.filters, |
|||
userCurrency: job.data.userCurrency, |
|||
userId: job.data.userId |
|||
}); |
|||
|
|||
const accountBalanceItems = |
|||
await this.accountBalanceService.getAccountBalanceItems({ |
|||
filters: job.data.filters, |
|||
userCurrency: job.data.userCurrency, |
|||
userId: job.data.userId |
|||
}); |
|||
|
|||
const portfolioCalculator = this.calculatorFactory.createCalculator({ |
|||
accountBalanceItems, |
|||
activities, |
|||
calculationType: PerformanceCalculationType.TWR, |
|||
currency: job.data.userCurrency, |
|||
filters: job.data.filters, |
|||
userId: job.data.userId |
|||
}); |
|||
|
|||
const snapshot = await portfolioCalculator.computeSnapshot(); |
|||
|
|||
Logger.log( |
|||
`Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${( |
|||
(performance.now() - startTime) / |
|||
1000 |
|||
).toFixed(3)} seconds`,
|
|||
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})` |
|||
); |
|||
|
|||
const expiration = addMilliseconds( |
|||
new Date(), |
|||
this.configurationService.get('CACHE_QUOTES_TTL') |
|||
); |
|||
|
|||
this.redisCacheService.set( |
|||
this.redisCacheService.getPortfolioSnapshotKey({ |
|||
filters: job.data.filters, |
|||
userId: job.data.userId |
|||
}), |
|||
JSON.stringify(<PortfolioSnapshotValue>(<unknown>{ |
|||
expiration: expiration.getTime(), |
|||
portfolioSnapshot: snapshot |
|||
})), |
|||
CACHE_TTL_INFINITE |
|||
); |
|||
|
|||
return snapshot; |
|||
} catch (error) { |
|||
Logger.error( |
|||
error, |
|||
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})` |
|||
); |
|||
|
|||
throw new Error(error); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
import { Job, JobOptions } from 'bull'; |
|||
import { setTimeout } from 'timers/promises'; |
|||
|
|||
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; |
|||
|
|||
export const PortfolioSnapshotServiceMock = { |
|||
addJobToQueue({ |
|||
data, |
|||
name, |
|||
opts |
|||
}: { |
|||
data: IPortfolioSnapshotQueueJob; |
|||
name: string; |
|||
opts?: JobOptions; |
|||
}): Promise<Job<any>> { |
|||
const mockJob: Partial<Job<any>> = { |
|||
finished: async () => { |
|||
await setTimeout(100); |
|||
|
|||
return Promise.resolve(); |
|||
} |
|||
}; |
|||
|
|||
this.jobsStore.set(opts?.jobId, mockJob); |
|||
|
|||
return Promise.resolve(mockJob as Job<any>); |
|||
}, |
|||
getJob(jobId: string): Promise<Job<any>> { |
|||
const job = this.jobsStore.get(jobId); |
|||
|
|||
return Promise.resolve(job as Job<any>); |
|||
}, |
|||
jobsStore: new Map<string, Partial<Job<any>>>() |
|||
}; |
@ -0,0 +1,31 @@ |
|||
import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; |
|||
|
|||
import { InjectQueue } from '@nestjs/bull'; |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { JobOptions, Queue } from 'bull'; |
|||
|
|||
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; |
|||
|
|||
@Injectable() |
|||
export class PortfolioSnapshotService { |
|||
public constructor( |
|||
@InjectQueue(PORTFOLIO_SNAPSHOT_QUEUE) |
|||
private readonly portfolioSnapshotQueue: Queue |
|||
) {} |
|||
|
|||
public async addJobToQueue({ |
|||
data, |
|||
name, |
|||
opts |
|||
}: { |
|||
data: IPortfolioSnapshotQueueJob; |
|||
name: string; |
|||
opts?: JobOptions; |
|||
}) { |
|||
return this.portfolioSnapshotQueue.add(name, data, opts); |
|||
} |
|||
|
|||
public async getJob(jobId: string) { |
|||
return this.portfolioSnapshotQueue.getJob(jobId); |
|||
} |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue