Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
3c322cca0d | |||
e965d12e31 | |||
3daf55a0dd | |||
aafedd5f75 | |||
32956ae04c | |||
bfd0241b2d | |||
5eff8402db | |||
ffa020ee2a | |||
80a3668aa9 | |||
7378900050 | |||
9be457943c | |||
93454c6c15 | |||
fccbd76993 | |||
922876a893 | |||
654446f068 | |||
947460abdd | |||
c5635b0050 | |||
8a3a6308a3 | |||
290a07fe79 | |||
4c907d56f0 | |||
56b437ca74 | |||
e23ff33e6f | |||
f2d206262e | |||
1ed5690b33 | |||
4451514ec5 | |||
8f73f85276 | |||
9a5d7b664b | |||
7d2d1d971a | |||
d111493eed |
95
CHANGELOG.md
95
CHANGELOG.md
@ -5,7 +5,98 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.252.0 - 2023-04-10
|
||||
## 1.259.0 - 2023-04-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added a fallback to historical market data if a data provider does not provide live data
|
||||
- Added a general health check endpoint
|
||||
- Added health check endpoints for data providers
|
||||
|
||||
### Changed
|
||||
|
||||
- Persisted today's market data continuously
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the alignment of the performance column header in the holdings table
|
||||
- Removed the unnecessary sort header of the comment column in the activities table
|
||||
- Fixed the targets in `proxy.conf.json` from `http://localhost:3333` to `http://0.0.0.0:3333` for local development
|
||||
|
||||
## 1.258.0 - 2023-04-20
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a data source mapping
|
||||
|
||||
## 1.257.0 - 2023-04-18
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced the allocations by ETF provider chart on the allocations page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the global heat map component caused by manipulating an input property
|
||||
- Fixed an issue with the currency inconsistency in the _EOD Historical Data_ service (convert from `GBX` to `GBp`)
|
||||
|
||||
## 1.256.0 - 2023-04-17
|
||||
|
||||
### Added
|
||||
|
||||
- Added the _Yahoo Finance_ data enhancer for countries, sectors and urls
|
||||
|
||||
### Changed
|
||||
|
||||
- Enabled the configuration to immediately remove queue jobs on complete
|
||||
- Refactored the implementation of removing queue jobs
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the unique job ids of the gather asset profile process
|
||||
- Fixed the style of the button to fetch the current market price
|
||||
|
||||
## 1.255.0 - 2023-04-15
|
||||
|
||||
### Added
|
||||
|
||||
- Made the system message expandable
|
||||
|
||||
### Changed
|
||||
|
||||
- Skipped creating queue jobs for asset profiles with `MANUAL` data source not having a scraper configuration
|
||||
- Reduced the execution interval of the data gathering to every hour
|
||||
- Upgraded `prisma` from version `4.11.0` to `4.12.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the style of the system message
|
||||
|
||||
## 1.254.0 - 2023-04-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the queue jobs implementation by adding in bulk
|
||||
- Improved the queue jobs implementation by introducing unique job ids
|
||||
- Reverted the execution interval of the data gathering from every 12 hours to every 4 hours
|
||||
|
||||
## 1.253.0 - 2023-04-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Reduced the execution interval of the data gathering to every 12 hours
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the background color of dialogs in dark mode
|
||||
|
||||
## 1.252.2 - 2023-04-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Deprecated the `auth` endpoint of the login with _Security Token_ (`GET`)
|
||||
|
||||
## 1.252.1 - 2023-04-10
|
||||
|
||||
### Changed
|
||||
|
||||
@ -14,6 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Decreased the density of the theme
|
||||
- Migrated the style of various components to `@angular/material` `15` (mdc)
|
||||
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
|
||||
- Upgraded `bull` from version `4.10.2` to `4.10.4`
|
||||
|
||||
## 1.251.0 - 2023-04-07
|
||||
|
||||
@ -32,7 +124,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `BenchmarkComponent`
|
||||
- `HoldingsTableComponent`
|
||||
- Upgraded `angular` from version `15.1.5` to `15.2.5`
|
||||
- Upgraded `nestjs` from version `9.1.4` to `9.4.0`
|
||||
- Upgraded `Nx` from version `15.7.2` to `15.9.2`
|
||||
|
||||
## 1.250.0 - 2023-04-02
|
||||
|
@ -200,7 +200,9 @@ Set the header for each request as follows:
|
||||
"Authorization": "Bearer eyJh..."
|
||||
```
|
||||
|
||||
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||
You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: <INSERT_SECURITY_TOKEN_OF_ACCOUNT> }`)
|
||||
|
||||
Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||
|
||||
### Import Activities
|
||||
|
||||
|
@ -100,16 +100,21 @@ export class AdminController {
|
||||
|
||||
const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
|
||||
|
||||
for (const { dataSource, symbol } of uniqueAssets) {
|
||||
await this.dataGatheringService.addJobToQueue(
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
);
|
||||
}
|
||||
await this.dataGatheringService.addJobsToQueue(
|
||||
uniqueAssets.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
data: {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}`
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.dataGatheringService.gatherMax();
|
||||
}
|
||||
@ -131,16 +136,21 @@ export class AdminController {
|
||||
|
||||
const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
|
||||
|
||||
for (const { dataSource, symbol } of uniqueAssets) {
|
||||
await this.dataGatheringService.addJobToQueue(
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
);
|
||||
}
|
||||
await this.dataGatheringService.addJobsToQueue(
|
||||
uniqueAssets.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
data: {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}`
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Post('gather/profile-data/:dataSource/:symbol')
|
||||
@ -161,14 +171,17 @@ export class AdminController {
|
||||
);
|
||||
}
|
||||
|
||||
await this.dataGatheringService.addJobToQueue(
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
{
|
||||
await this.dataGatheringService.addJobToQueue({
|
||||
data: {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
);
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Post('gather/:dataSource/:symbol')
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from '@ghostfolio/common/config';
|
||||
import { AdminJobs } from '@ghostfolio/common/interfaces';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JobStatus, Queue } from 'bull';
|
||||
|
||||
@Injectable()
|
||||
@ -23,14 +23,11 @@ export class QueueService {
|
||||
}: {
|
||||
status?: JobStatus[];
|
||||
}) {
|
||||
const jobs = await this.dataGatheringQueue.getJobs(status);
|
||||
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
await job.remove();
|
||||
} catch (error) {
|
||||
Logger.warn(error, 'QueueService');
|
||||
}
|
||||
for (const statusItem of status) {
|
||||
await this.dataGatheringQueue.clean(
|
||||
300,
|
||||
statusItem === 'waiting' ? 'wait' : statusItem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,18 +41,23 @@ export class QueueService {
|
||||
const jobs = await this.dataGatheringQueue.getJobs(status);
|
||||
|
||||
const jobsWithState = await Promise.all(
|
||||
jobs.slice(0, limit).map(async (job) => {
|
||||
return {
|
||||
attemptsMade: job.attemptsMade + 1,
|
||||
data: job.data,
|
||||
finishedOn: job.finishedOn,
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
stacktrace: job.stacktrace,
|
||||
state: await job.getState(),
|
||||
timestamp: job.timestamp
|
||||
};
|
||||
})
|
||||
jobs
|
||||
.filter((job) => {
|
||||
return job;
|
||||
})
|
||||
.slice(0, limit)
|
||||
.map(async (job) => {
|
||||
return {
|
||||
attemptsMade: job.attemptsMade + 1,
|
||||
data: job.data,
|
||||
finishedOn: job.finishedOn,
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
stacktrace: job.stacktrace,
|
||||
state: await job.getState(),
|
||||
timestamp: job.timestamp
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -24,6 +24,7 @@ import { CacheModule } from './cache/cache.module';
|
||||
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
||||
import { ExportModule } from './export/export.module';
|
||||
import { FrontendMiddleware } from './frontend.middleware';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { ImportModule } from './import/import.module';
|
||||
import { InfoModule } from './info/info.module';
|
||||
import { LogoModule } from './logo/logo.module';
|
||||
@ -57,6 +58,7 @@ import { UserModule } from './user/user.module';
|
||||
ExchangeRateModule,
|
||||
ExchangeRateDataModule,
|
||||
ExportModule,
|
||||
HealthModule,
|
||||
ImportModule,
|
||||
InfoModule,
|
||||
LogoModule,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
@ -32,6 +33,26 @@ export class AuthController {
|
||||
private readonly webAuthService: WebAuthService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Get('anonymous/:accessToken')
|
||||
public async accessTokenLoginGet(
|
||||
@Param('accessToken') accessToken: string
|
||||
): Promise<OAuthResponse> {
|
||||
try {
|
||||
const authToken = await this.authService.validateAnonymousLogin(
|
||||
accessToken
|
||||
);
|
||||
return { authToken };
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('anonymous')
|
||||
public async accessTokenLogin(
|
||||
@Body() body: { accessToken: string }
|
||||
|
44
apps/api/src/app/health/health.controller.ts
Normal file
44
apps/api/src/app/health/health.controller.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { HealthService } from './health.service';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
public constructor(private readonly healthService: HealthService) {}
|
||||
|
||||
@Get()
|
||||
public async getHealth() {}
|
||||
|
||||
@Get('data-provider/:dataSource')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
public async getHealthOfDataProvider(
|
||||
@Param('dataSource') dataSource: DataSource
|
||||
) {
|
||||
if (!DataSource[dataSource]) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
const hasResponse = await this.healthService.hasResponseFromDataProvider(
|
||||
dataSource
|
||||
);
|
||||
|
||||
if (hasResponse !== true) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE),
|
||||
StatusCodes.SERVICE_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
13
apps/api/src/app/health/health.module.ts
Normal file
13
apps/api/src/app/health/health.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
imports: [ConfigurationModule, DataProviderModule],
|
||||
providers: [HealthService]
|
||||
})
|
||||
export class HealthModule {}
|
14
apps/api/src/app/health/health.service.ts
Normal file
14
apps/api/src/app/health/health.service.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService
|
||||
) {}
|
||||
|
||||
public async hasResponseFromDataProvider(aDataSource: DataSource) {
|
||||
return this.dataProviderService.checkQuote(aDataSource);
|
||||
}
|
||||
}
|
@ -112,14 +112,17 @@ export class OrderService {
|
||||
};
|
||||
}
|
||||
|
||||
await this.dataGatheringService.addJobToQueue(
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
{
|
||||
await this.dataGatheringService.addJobToQueue({
|
||||
data: {
|
||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
);
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}`
|
||||
}
|
||||
});
|
||||
|
||||
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns';
|
||||
|
||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||
import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||
import { GetValuesObject } from './interfaces/get-values-object.interface';
|
||||
|
||||
function mockGetValue(symbol: string, date: Date) {
|
||||
switch (symbol) {
|
||||
@ -49,11 +49,9 @@ export const CurrentRateServiceMock = {
|
||||
getValues: ({
|
||||
dataGatheringItems,
|
||||
dateQuery
|
||||
}: GetValuesParams): Promise<{
|
||||
dataProviderInfos: DataProviderInfo[];
|
||||
values: GetValueObject[];
|
||||
}> => {
|
||||
}: GetValuesParams): Promise<GetValuesObject> => {
|
||||
const values: GetValueObject[] = [];
|
||||
|
||||
if (dateQuery.lt) {
|
||||
for (
|
||||
let date = resetHours(dateQuery.gte);
|
||||
@ -85,6 +83,7 @@ export const CurrentRateServiceMock = {
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve({ values, dataProviderInfos: [] });
|
||||
|
||||
return Promise.resolve({ values, dataProviderInfos: [], errors: [] });
|
||||
}
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||
import { GetValuesObject } from './interfaces/get-values-object.interface';
|
||||
|
||||
jest.mock('@ghostfolio/api/services/market-data.service', () => {
|
||||
return {
|
||||
@ -67,14 +67,32 @@ jest.mock('@ghostfolio/api/services/exchange-rate-data.service', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@ghostfolio/api/services/property/property.service', () => {
|
||||
return {
|
||||
PropertyService: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getByKey: (key: string) => Promise.resolve({})
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('CurrentRateService', () => {
|
||||
let currentRateService: CurrentRateService;
|
||||
let dataProviderService: DataProviderService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let marketDataService: MarketDataService;
|
||||
let propertyService: PropertyService;
|
||||
|
||||
beforeAll(async () => {
|
||||
dataProviderService = new DataProviderService(null, [], null);
|
||||
propertyService = new PropertyService(null);
|
||||
|
||||
dataProviderService = new DataProviderService(
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
propertyService
|
||||
);
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
null,
|
||||
@ -104,21 +122,14 @@ describe('CurrentRateService', () => {
|
||||
},
|
||||
userCurrency: 'CHF'
|
||||
})
|
||||
).toMatchObject<{
|
||||
dataProviderInfos: DataProviderInfo[];
|
||||
values: GetValueObject[];
|
||||
}>({
|
||||
).toMatchObject<GetValuesObject>({
|
||||
dataProviderInfos: [],
|
||||
errors: [],
|
||||
values: [
|
||||
{
|
||||
date: undefined,
|
||||
marketPriceInBaseCurrency: 1841.823902,
|
||||
symbol: 'AMZN'
|
||||
},
|
||||
{
|
||||
date: undefined,
|
||||
marketPriceInBaseCurrency: 1847.839966,
|
||||
symbol: 'AMZN'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -2,13 +2,14 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isBefore, isToday } from 'date-fns';
|
||||
import { flatten } from 'lodash';
|
||||
import { flatten, isEmpty, uniqBy } from 'lodash';
|
||||
|
||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||
import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||
import { GetValuesObject } from './interfaces/get-values-object.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CurrentRateService {
|
||||
@ -23,10 +24,7 @@ export class CurrentRateService {
|
||||
dataGatheringItems,
|
||||
dateQuery,
|
||||
userCurrency
|
||||
}: GetValuesParams): Promise<{
|
||||
dataProviderInfos: DataProviderInfo[];
|
||||
values: GetValueObject[];
|
||||
}> {
|
||||
}: GetValuesParams): Promise<GetValuesObject> {
|
||||
const dataProviderInfos: DataProviderInfo[] = [];
|
||||
const includeToday =
|
||||
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
|
||||
@ -34,9 +32,10 @@ export class CurrentRateService {
|
||||
(!dateQuery.in || this.containsToday(dateQuery.in));
|
||||
|
||||
const promises: Promise<GetValueObject[]>[] = [];
|
||||
const quoteErrors: ResponseError['errors'] = [];
|
||||
const today = resetHours(new Date());
|
||||
|
||||
if (includeToday) {
|
||||
const today = resetHours(new Date());
|
||||
promises.push(
|
||||
this.dataProviderService
|
||||
.getQuotes(dataGatheringItems)
|
||||
@ -51,18 +50,26 @@ export class CurrentRateService {
|
||||
);
|
||||
}
|
||||
|
||||
result.push({
|
||||
date: today,
|
||||
marketPriceInBaseCurrency:
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
dataResultProvider?.[dataGatheringItem.symbol]
|
||||
?.marketPrice ?? 0,
|
||||
dataResultProvider?.[dataGatheringItem.symbol]?.currency,
|
||||
userCurrency
|
||||
),
|
||||
symbol: dataGatheringItem.symbol
|
||||
});
|
||||
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
|
||||
result.push({
|
||||
date: today,
|
||||
marketPriceInBaseCurrency:
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
dataResultProvider?.[dataGatheringItem.symbol]
|
||||
?.marketPrice,
|
||||
dataResultProvider?.[dataGatheringItem.symbol]?.currency,
|
||||
userCurrency
|
||||
),
|
||||
symbol: dataGatheringItem.symbol
|
||||
});
|
||||
} else {
|
||||
quoteErrors.push({
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
symbol: dataGatheringItem.symbol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
@ -94,10 +101,60 @@ export class CurrentRateService {
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
const values = flatten(await Promise.all(promises));
|
||||
|
||||
const response: GetValuesObject = {
|
||||
dataProviderInfos,
|
||||
values: flatten(await Promise.all(promises))
|
||||
errors: quoteErrors.map(({ dataSource, symbol }) => {
|
||||
return { dataSource, symbol };
|
||||
}),
|
||||
values: uniqBy(values, ({ date, symbol }) => `${date}-${symbol}`)
|
||||
};
|
||||
|
||||
if (!isEmpty(quoteErrors)) {
|
||||
for (const { symbol } of quoteErrors) {
|
||||
try {
|
||||
// If missing quote, fallback to the latest available historical market price
|
||||
let value: GetValueObject = response.values.find((currentValue) => {
|
||||
return currentValue.symbol === symbol && isToday(currentValue.date);
|
||||
});
|
||||
|
||||
if (!value) {
|
||||
value = {
|
||||
symbol,
|
||||
date: today,
|
||||
marketPriceInBaseCurrency: 0
|
||||
};
|
||||
|
||||
response.values.push(value);
|
||||
}
|
||||
|
||||
const [latestValue] = response.values
|
||||
.filter((currentValue) => {
|
||||
return (
|
||||
currentValue.symbol === symbol &&
|
||||
currentValue.marketPriceInBaseCurrency
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.date < b.date) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.date > b.date) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
value.marketPriceInBaseCurrency =
|
||||
latestValue.marketPriceInBaseCurrency;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private containsToday(dates: Date[]): boolean {
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { GetValueObject } from './get-value-object.interface';
|
||||
|
||||
export interface GetValuesObject {
|
||||
dataProviderInfos: DataProviderInfo[];
|
||||
errors: ResponseError['errors'];
|
||||
values: GetValueObject[];
|
||||
}
|
@ -24,9 +24,10 @@ import {
|
||||
isSameYear,
|
||||
max,
|
||||
min,
|
||||
set
|
||||
set,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { first, flatten, isNumber, last, sortBy } from 'lodash';
|
||||
import { first, flatten, isNumber, last, sortBy, uniq } from 'lodash';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
import { CurrentPositions } from './interfaces/current-positions.interface';
|
||||
@ -360,7 +361,7 @@ export class PortfolioCalculator {
|
||||
|
||||
let firstTransactionPoint: TransactionPoint = null;
|
||||
let firstIndex = transactionPointsBeforeEndDate.length;
|
||||
const dates = [];
|
||||
let dates = [];
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
|
||||
@ -389,15 +390,37 @@ export class PortfolioCalculator {
|
||||
|
||||
dates.push(resetHours(end));
|
||||
|
||||
const { dataProviderInfos, values: marketSymbols } =
|
||||
await this.currentRateService.getValues({
|
||||
currencies,
|
||||
dataGatheringItems,
|
||||
dateQuery: {
|
||||
in: dates
|
||||
},
|
||||
userCurrency: this.currency
|
||||
});
|
||||
// Add dates of last week for fallback
|
||||
dates.push(subDays(resetHours(new Date()), 7));
|
||||
dates.push(subDays(resetHours(new Date()), 6));
|
||||
dates.push(subDays(resetHours(new Date()), 5));
|
||||
dates.push(subDays(resetHours(new Date()), 4));
|
||||
dates.push(subDays(resetHours(new Date()), 3));
|
||||
dates.push(subDays(resetHours(new Date()), 2));
|
||||
dates.push(subDays(resetHours(new Date()), 1));
|
||||
dates.push(resetHours(new Date()));
|
||||
|
||||
dates = uniq(
|
||||
dates.map((date) => {
|
||||
return date.getTime();
|
||||
})
|
||||
).map((timestamp) => {
|
||||
return new Date(timestamp);
|
||||
});
|
||||
dates.sort((a, b) => a.getTime() - b.getTime());
|
||||
|
||||
const {
|
||||
dataProviderInfos,
|
||||
errors: currentRateErrors,
|
||||
values: marketSymbols
|
||||
} = await this.currentRateService.getValues({
|
||||
currencies,
|
||||
dataGatheringItems,
|
||||
dateQuery: {
|
||||
in: dates
|
||||
},
|
||||
userCurrency: this.currency
|
||||
});
|
||||
|
||||
this.dataProviderInfos = dataProviderInfos;
|
||||
|
||||
@ -472,7 +495,13 @@ export class PortfolioCalculator {
|
||||
transactionCount: item.transactionCount
|
||||
});
|
||||
|
||||
if (hasErrors && item.investment.gt(0)) {
|
||||
if (
|
||||
(hasErrors ||
|
||||
currentRateErrors.find(({ dataSource, symbol }) => {
|
||||
return dataSource === item.dataSource && symbol === item.symbol;
|
||||
})) &&
|
||||
item.investment.gt(0)
|
||||
) {
|
||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||
}
|
||||
}
|
||||
@ -722,7 +751,7 @@ export class PortfolioCalculator {
|
||||
);
|
||||
} else if (!currentPosition.quantity.eq(0)) {
|
||||
Logger.warn(
|
||||
`Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`,
|
||||
`Missing historical market data for symbol ${currentPosition.symbol}`,
|
||||
'PortfolioCalculator'
|
||||
);
|
||||
hasErrors = true;
|
||||
|
@ -19,8 +19,8 @@ export class CronService {
|
||||
private readonly twitterBotService: TwitterBotService
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_4_HOURS)
|
||||
public async runEveryFourHours() {
|
||||
@Cron(CronExpression.EVERY_HOUR)
|
||||
public async runEveryHour() {
|
||||
await this.dataGatheringService.gather7Days();
|
||||
}
|
||||
|
||||
@ -38,15 +38,20 @@ export class CronService {
|
||||
public async runEverySundayAtTwelvePm() {
|
||||
const uniqueAssets = await this.dataGatheringService.getUniqueAssets();
|
||||
|
||||
for (const { dataSource, symbol } of uniqueAssets) {
|
||||
await this.dataGatheringService.addJobToQueue(
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
);
|
||||
}
|
||||
await this.dataGatheringService.addJobsToQueue(
|
||||
uniqueAssets.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
data: {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}`
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
|
||||
import {
|
||||
DATA_GATHERING_QUEUE,
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||
QUEUE_JOB_STATUS_LIST
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
@ -12,6 +11,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { JobOptions, Queue } from 'bull';
|
||||
import { format, min, subDays, subYears } from 'date-fns';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { DataProviderService } from './data-provider/data-provider.service';
|
||||
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||
@ -34,17 +34,22 @@ export class DataGatheringService {
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public async addJobToQueue(name: string, data: any, options?: JobOptions) {
|
||||
const hasJob = await this.hasJob(name, data);
|
||||
public async addJobToQueue({
|
||||
data,
|
||||
name,
|
||||
opts
|
||||
}: {
|
||||
data: any;
|
||||
name: string;
|
||||
opts?: JobOptions;
|
||||
}) {
|
||||
return this.dataGatheringQueue.add(name, data, opts);
|
||||
}
|
||||
|
||||
if (hasJob) {
|
||||
Logger.log(
|
||||
`Job ${name} with data ${JSON.stringify(data)} already exists.`,
|
||||
'DataGatheringService'
|
||||
);
|
||||
} else {
|
||||
return this.dataGatheringQueue.add(name, data, options);
|
||||
}
|
||||
public async addJobsToQueue(
|
||||
jobs: { data: any; name: string; opts?: JobOptions }[]
|
||||
) {
|
||||
return this.dataGatheringQueue.addBulk(jobs);
|
||||
}
|
||||
|
||||
public async gather7Days() {
|
||||
@ -209,59 +214,22 @@ export class DataGatheringService {
|
||||
}
|
||||
|
||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
||||
await this.addJobToQueue(
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||
{
|
||||
dataSource,
|
||||
date,
|
||||
symbol
|
||||
},
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||
const startDate =
|
||||
(
|
||||
await this.prismaService.order.findFirst({
|
||||
orderBy: [{ date: 'asc' }]
|
||||
})
|
||||
)?.date ?? new Date();
|
||||
|
||||
const currencyPairsToGather = this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.map(({ dataSource, symbol }) => {
|
||||
await this.addJobsToQueue(
|
||||
aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
symbol,
|
||||
date: min([startDate, subYears(new Date(), 10)])
|
||||
};
|
||||
});
|
||||
|
||||
const symbolProfilesToGather = (
|
||||
await this.prismaService.symbolProfile.findMany({
|
||||
orderBy: [{ symbol: 'asc' }],
|
||||
select: {
|
||||
dataSource: true,
|
||||
Order: {
|
||||
orderBy: [{ date: 'asc' }],
|
||||
select: { date: true },
|
||||
take: 1
|
||||
data: {
|
||||
dataSource,
|
||||
date,
|
||||
symbol
|
||||
},
|
||||
scraperConfiguration: true,
|
||||
symbol: true
|
||||
}
|
||||
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||
opts: {
|
||||
...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}`
|
||||
}
|
||||
};
|
||||
})
|
||||
).map((symbolProfile) => {
|
||||
return {
|
||||
...symbolProfile,
|
||||
date: symbolProfile.Order?.[0]?.date ?? startDate
|
||||
};
|
||||
});
|
||||
|
||||
return [...currencyPairsToGather, ...symbolProfilesToGather];
|
||||
);
|
||||
}
|
||||
|
||||
public async getUniqueAssets(): Promise<UniqueAsset[]> {
|
||||
@ -298,7 +266,7 @@ export class DataGatheringService {
|
||||
|
||||
// Only consider symbols with incomplete market data for the last
|
||||
// 7 days
|
||||
const symbolsNotToGather = (
|
||||
const symbolsWithCompleteMarketData = (
|
||||
await this.prismaService.marketData.groupBy({
|
||||
_count: true,
|
||||
by: ['symbol'],
|
||||
@ -316,8 +284,14 @@ export class DataGatheringService {
|
||||
});
|
||||
|
||||
const symbolProfilesToGather = symbolProfiles
|
||||
.filter(({ symbol }) => {
|
||||
return !symbolsNotToGather.includes(symbol);
|
||||
.filter(({ dataSource, scraperConfiguration, symbol }) => {
|
||||
const manualDataSourceWithScraperConfiguration =
|
||||
dataSource === 'MANUAL' && !isEmpty(scraperConfiguration);
|
||||
|
||||
return (
|
||||
!symbolsWithCompleteMarketData.includes(symbol) &&
|
||||
(dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration)
|
||||
);
|
||||
})
|
||||
.map((symbolProfile) => {
|
||||
return {
|
||||
@ -329,7 +303,7 @@ export class DataGatheringService {
|
||||
const currencyPairsToGather = this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.filter(({ symbol }) => {
|
||||
return !symbolsNotToGather.includes(symbol);
|
||||
return !symbolsWithCompleteMarketData.includes(symbol);
|
||||
})
|
||||
.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
@ -342,17 +316,56 @@ export class DataGatheringService {
|
||||
return [...currencyPairsToGather, ...symbolProfilesToGather];
|
||||
}
|
||||
|
||||
private async hasJob(name: string, data: any) {
|
||||
const jobs = await this.dataGatheringQueue.getJobs(
|
||||
QUEUE_JOB_STATUS_LIST.filter((status) => {
|
||||
return status !== 'completed';
|
||||
})
|
||||
);
|
||||
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||
const startDate =
|
||||
(
|
||||
await this.prismaService.order.findFirst({
|
||||
orderBy: [{ date: 'asc' }]
|
||||
})
|
||||
)?.date ?? new Date();
|
||||
|
||||
return jobs.some((job) => {
|
||||
return (
|
||||
job.name === name && JSON.stringify(job.data) === JSON.stringify(data)
|
||||
);
|
||||
});
|
||||
const currencyPairsToGather = this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
symbol,
|
||||
date: min([startDate, subYears(new Date(), 10)])
|
||||
};
|
||||
});
|
||||
|
||||
const symbolProfilesToGather = (
|
||||
await this.prismaService.symbolProfile.findMany({
|
||||
orderBy: [{ symbol: 'asc' }],
|
||||
select: {
|
||||
dataSource: true,
|
||||
Order: {
|
||||
orderBy: [{ date: 'asc' }],
|
||||
select: { date: true },
|
||||
take: 1
|
||||
},
|
||||
scraperConfiguration: true,
|
||||
symbol: true
|
||||
}
|
||||
})
|
||||
)
|
||||
.filter((symbolProfile) => {
|
||||
const manualDataSourceWithScraperConfiguration =
|
||||
symbolProfile.dataSource === 'MANUAL' &&
|
||||
!isEmpty(symbolProfile.scraperConfiguration);
|
||||
|
||||
return (
|
||||
symbolProfile.dataSource !== 'MANUAL' ||
|
||||
manualDataSourceWithScraperConfiguration
|
||||
);
|
||||
})
|
||||
.map((symbolProfile) => {
|
||||
return {
|
||||
...symbolProfile,
|
||||
date: symbolProfile.Order?.[0]?.date ?? startDate
|
||||
};
|
||||
});
|
||||
|
||||
return [...currencyPairsToGather, ...symbolProfilesToGather];
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import { format, isAfter, isBefore, parse } from 'date-fns';
|
||||
|
||||
@ -33,7 +33,8 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
dataSource: this.getName()
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
@ -109,6 +110,10 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const result = await this.alphaVantage.data.search(aQuery);
|
||||
|
||||
|
@ -160,6 +160,10 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
return results;
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'bitcoin';
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
|
@ -1,15 +1,27 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
|
||||
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service';
|
||||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({
|
||||
exports: ['DataEnhancers', TrackinsightDataEnhancerService],
|
||||
exports: [
|
||||
'DataEnhancers',
|
||||
TrackinsightDataEnhancerService,
|
||||
YahooFinanceDataEnhancerService
|
||||
],
|
||||
imports: [ConfigurationModule, CryptocurrencyModule],
|
||||
providers: [
|
||||
TrackinsightDataEnhancerService,
|
||||
YahooFinanceDataEnhancerService,
|
||||
{
|
||||
inject: [TrackinsightDataEnhancerService],
|
||||
inject: [
|
||||
TrackinsightDataEnhancerService,
|
||||
YahooFinanceDataEnhancerService
|
||||
],
|
||||
provide: 'DataEnhancers',
|
||||
useFactory: (trackinsight) => [trackinsight]
|
||||
},
|
||||
TrackinsightDataEnhancerService
|
||||
useFactory: (trackinsight, yahooFinance) => [trackinsight, yahooFinance]
|
||||
}
|
||||
]
|
||||
})
|
||||
export class DataEnhancerModule {}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import bent from 'bent';
|
||||
|
||||
const getJSON = bent('json');
|
||||
|
||||
@Injectable()
|
||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
private static baseUrl = 'https://data.trackinsight.com';
|
||||
private static countries = require('countries-list/dist/countries.json');
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
|
||||
import { YahooFinanceService } from './yahoo-finance.service';
|
||||
import { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
|
||||
|
||||
jest.mock(
|
||||
'@ghostfolio/api/services/cryptocurrency/cryptocurrency.service',
|
||||
@ -25,16 +25,16 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
describe('YahooFinanceService', () => {
|
||||
describe('YahooFinanceDataEnhancerService', () => {
|
||||
let configurationService: ConfigurationService;
|
||||
let cryptocurrencyService: CryptocurrencyService;
|
||||
let yahooFinanceService: YahooFinanceService;
|
||||
let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
|
||||
|
||||
beforeAll(async () => {
|
||||
configurationService = new ConfigurationService();
|
||||
cryptocurrencyService = new CryptocurrencyService();
|
||||
|
||||
yahooFinanceService = new YahooFinanceService(
|
||||
yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
|
||||
configurationService,
|
||||
cryptocurrencyService
|
||||
);
|
||||
@ -42,25 +42,37 @@ describe('YahooFinanceService', () => {
|
||||
|
||||
it('convertFromYahooFinanceSymbol', async () => {
|
||||
expect(
|
||||
await yahooFinanceService.convertFromYahooFinanceSymbol('BRK-B')
|
||||
await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
|
||||
'BRK-B'
|
||||
)
|
||||
).toEqual('BRK-B');
|
||||
expect(
|
||||
await yahooFinanceService.convertFromYahooFinanceSymbol('BTC-USD')
|
||||
await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
|
||||
'BTC-USD'
|
||||
)
|
||||
).toEqual('BTCUSD');
|
||||
expect(
|
||||
await yahooFinanceService.convertFromYahooFinanceSymbol('EURUSD=X')
|
||||
await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
|
||||
'EURUSD=X'
|
||||
)
|
||||
).toEqual('EURUSD');
|
||||
});
|
||||
|
||||
it('convertToYahooFinanceSymbol', async () => {
|
||||
expect(
|
||||
await yahooFinanceService.convertToYahooFinanceSymbol('BTCUSD')
|
||||
await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
|
||||
'BTCUSD'
|
||||
)
|
||||
).toEqual('BTC-USD');
|
||||
expect(
|
||||
await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD')
|
||||
await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
|
||||
'DOGEUSD'
|
||||
)
|
||||
).toEqual('DOGE-USD');
|
||||
expect(
|
||||
await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF')
|
||||
await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
|
||||
'USDCHF'
|
||||
)
|
||||
).toEqual('USDCHF=X');
|
||||
});
|
||||
});
|
@ -0,0 +1,325 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { countries } from 'countries-list';
|
||||
import yahooFinance from 'yahoo-finance2';
|
||||
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a symbol to a Yahoo Finance symbol
|
||||
*
|
||||
* Currency: USDCHF -> USDCHF=X
|
||||
* Cryptocurrency: BTCUSD -> BTC-USD
|
||||
* DOGEUSD -> DOGE-USD
|
||||
*/
|
||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.includes(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
aSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Add a dash before the last three characters
|
||||
// BTCUSD -> BTC-USD
|
||||
// DOGEUSD -> DOGE-USD
|
||||
// SOL1USD -> SOL1-USD
|
||||
return aSymbol.replace(
|
||||
new RegExp(`-?${this.baseCurrency}$`),
|
||||
`-${this.baseCurrency}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return aSymbol;
|
||||
}
|
||||
|
||||
public async enhance({
|
||||
response,
|
||||
symbol
|
||||
}: {
|
||||
response: Partial<SymbolProfile>;
|
||||
symbol: string;
|
||||
}): Promise<Partial<SymbolProfile>> {
|
||||
if (response.dataSource !== 'YAHOO' && !response.isin) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
let yahooSymbol: string;
|
||||
|
||||
if (response.dataSource === 'YAHOO') {
|
||||
yahooSymbol = symbol;
|
||||
} else {
|
||||
const { quotes } = await yahooFinance.search(response.isin);
|
||||
yahooSymbol = quotes[0].symbol;
|
||||
}
|
||||
|
||||
const { countries, sectors, url } = await this.getAssetProfile(
|
||||
yahooSymbol
|
||||
);
|
||||
|
||||
if (countries) {
|
||||
response.countries = countries;
|
||||
}
|
||||
|
||||
if (sectors) {
|
||||
response.sectors = sectors;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
response.url = url;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'YahooFinanceDataEnhancerService');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public formatName({
|
||||
longName,
|
||||
quoteType,
|
||||
shortName,
|
||||
symbol
|
||||
}: {
|
||||
longName: Price['longName'];
|
||||
quoteType: Price['quoteType'];
|
||||
shortName: Price['shortName'];
|
||||
symbol: Price['symbol'];
|
||||
}) {
|
||||
let name = longName;
|
||||
|
||||
if (name) {
|
||||
name = name.replace('Amundi Index Solutions - ', '');
|
||||
name = name.replace('iShares ETF (CH) - ', '');
|
||||
name = name.replace('iShares III Public Limited Company - ', '');
|
||||
name = name.replace('iShares V PLC - ', '');
|
||||
name = name.replace('iShares VI Public Limited Company - ', '');
|
||||
name = name.replace('iShares VII PLC - ', '');
|
||||
name = name.replace('Multi Units Luxembourg - ', '');
|
||||
name = name.replace('VanEck ETFs N.V. - ', '');
|
||||
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
|
||||
name = name.replace('Vanguard Funds Public Limited Company - ', '');
|
||||
name = name.replace('Vanguard Index Funds - ', '');
|
||||
name = name.replace('Xtrackers (IE) Plc - ', '');
|
||||
}
|
||||
|
||||
if (quoteType === 'FUTURE') {
|
||||
// "Gold Jun 22" -> "Gold"
|
||||
name = shortName?.slice(0, -7);
|
||||
}
|
||||
|
||||
return name || shortName || symbol;
|
||||
}
|
||||
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
const response: Partial<SymbolProfile> = {};
|
||||
|
||||
try {
|
||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||
});
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
quoteType: assetProfile.price.quoteType,
|
||||
shortName: assetProfile.price.shortName
|
||||
});
|
||||
|
||||
response.assetClass = assetClass;
|
||||
response.assetSubClass = assetSubClass;
|
||||
response.currency = assetProfile.price.currency;
|
||||
response.dataSource = this.getName();
|
||||
response.name = this.formatName({
|
||||
longName: assetProfile.price.longName,
|
||||
quoteType: assetProfile.price.quoteType,
|
||||
shortName: assetProfile.price.shortName,
|
||||
symbol: assetProfile.price.symbol
|
||||
});
|
||||
response.symbol = aSymbol;
|
||||
|
||||
if (assetSubClass === AssetSubClass.MUTUALFUND) {
|
||||
response.sectors = [];
|
||||
|
||||
for (const sectorWeighting of assetProfile.topHoldings
|
||||
?.sectorWeightings ?? []) {
|
||||
for (const [sector, weight] of Object.entries(sectorWeighting)) {
|
||||
response.sectors.push({ weight, name: this.parseSector(sector) });
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
assetSubClass === AssetSubClass.STOCK &&
|
||||
assetProfile.summaryProfile?.country
|
||||
) {
|
||||
// Add country if asset is stock and country available
|
||||
|
||||
try {
|
||||
const [code] = Object.entries(countries).find(([, country]) => {
|
||||
return country.name === assetProfile.summaryProfile?.country;
|
||||
});
|
||||
|
||||
if (code) {
|
||||
response.countries = [{ code, weight: 1 }];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (assetProfile.summaryProfile?.sector) {
|
||||
response.sectors = [
|
||||
{ name: assetProfile.summaryProfile?.sector, weight: 1 }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const url = assetProfile.summaryProfile?.website;
|
||||
if (url) {
|
||||
response.url = url;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'YahooFinanceService');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return DataSource.YAHOO;
|
||||
}
|
||||
|
||||
public parseAssetClass({
|
||||
quoteType,
|
||||
shortName
|
||||
}: {
|
||||
quoteType: string;
|
||||
shortName: string;
|
||||
}): {
|
||||
assetClass: AssetClass;
|
||||
assetSubClass: AssetSubClass;
|
||||
} {
|
||||
let assetClass: AssetClass;
|
||||
let assetSubClass: AssetSubClass;
|
||||
|
||||
switch (quoteType?.toLowerCase()) {
|
||||
case 'cryptocurrency':
|
||||
assetClass = AssetClass.CASH;
|
||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||
break;
|
||||
case 'equity':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.STOCK;
|
||||
break;
|
||||
case 'etf':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.ETF;
|
||||
break;
|
||||
case 'future':
|
||||
assetClass = AssetClass.COMMODITY;
|
||||
assetSubClass = AssetSubClass.COMMODITY;
|
||||
|
||||
if (
|
||||
shortName?.toLowerCase()?.startsWith('gold') ||
|
||||
shortName?.toLowerCase()?.startsWith('palladium') ||
|
||||
shortName?.toLowerCase()?.startsWith('platinum') ||
|
||||
shortName?.toLowerCase()?.startsWith('silver')
|
||||
) {
|
||||
assetSubClass = AssetSubClass.PRECIOUS_METAL;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'mutualfund':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.MUTUALFUND;
|
||||
break;
|
||||
}
|
||||
|
||||
return { assetClass, assetSubClass };
|
||||
}
|
||||
|
||||
private parseSector(aString: string): string {
|
||||
let sector = UNKNOWN_KEY;
|
||||
|
||||
switch (aString) {
|
||||
case 'basic_materials':
|
||||
sector = 'Basic Materials';
|
||||
break;
|
||||
case 'communication_services':
|
||||
sector = 'Communication Services';
|
||||
break;
|
||||
case 'consumer_cyclical':
|
||||
sector = 'Consumer Cyclical';
|
||||
break;
|
||||
case 'consumer_defensive':
|
||||
sector = 'Consumer Staples';
|
||||
break;
|
||||
case 'energy':
|
||||
sector = 'Energy';
|
||||
break;
|
||||
case 'financial_services':
|
||||
sector = 'Financial Services';
|
||||
break;
|
||||
case 'healthcare':
|
||||
sector = 'Healthcare';
|
||||
break;
|
||||
case 'industrials':
|
||||
sector = 'Industrials';
|
||||
break;
|
||||
case 'realestate':
|
||||
sector = 'Real Estate';
|
||||
break;
|
||||
case 'technology':
|
||||
sector = 'Technology';
|
||||
break;
|
||||
case 'utilities':
|
||||
sector = 'Utilities';
|
||||
break;
|
||||
}
|
||||
|
||||
return sector;
|
||||
}
|
||||
}
|
@ -11,13 +11,18 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DataEnhancerModule } from './data-enhancer/data-enhancer.module';
|
||||
import { YahooFinanceDataEnhancerService } from './data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||
import { DataProviderService } from './data-provider.service';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
CryptocurrencyModule,
|
||||
DataEnhancerModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
providers: [
|
||||
@ -57,7 +62,8 @@ import { DataProviderService } from './data-provider.service';
|
||||
rapidApiService,
|
||||
yahooFinanceService
|
||||
]
|
||||
}
|
||||
},
|
||||
YahooFinanceDataEnhancerService
|
||||
],
|
||||
exports: [DataProviderService, YahooFinanceService]
|
||||
})
|
||||
|
@ -7,22 +7,54 @@ import {
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import { groupBy, isEmpty } from 'lodash';
|
||||
import { groupBy, isEmpty, isNumber } from 'lodash';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config';
|
||||
|
||||
@Injectable()
|
||||
export class DataProviderService {
|
||||
private dataProviderMapping: { [dataProviderName: string]: string };
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
@Inject('DataProviderInterfaces')
|
||||
private readonly dataProviderInterfaces: DataProviderInterface[],
|
||||
private readonly prismaService: PrismaService
|
||||
) {}
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService
|
||||
) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.dataProviderMapping =
|
||||
((await this.propertyService.getByKey(PROPERTY_DATA_SOURCE_MAPPING)) as {
|
||||
[dataProviderName: string]: string;
|
||||
}) ?? {};
|
||||
}
|
||||
|
||||
public async checkQuote(dataSource: DataSource) {
|
||||
const dataProvider = this.getDataProvider(dataSource);
|
||||
const symbol = dataProvider.getTestSymbol();
|
||||
|
||||
const quotes = await this.getQuotes([
|
||||
{
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
]);
|
||||
|
||||
if (quotes[symbol]?.marketPrice > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getDividends({
|
||||
dataSource,
|
||||
@ -227,7 +259,7 @@ export class DataProviderService {
|
||||
const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk));
|
||||
|
||||
promises.push(
|
||||
promise.then((result) => {
|
||||
promise.then(async (result) => {
|
||||
for (const [symbol, dataProviderResponse] of Object.entries(
|
||||
result
|
||||
)) {
|
||||
@ -242,6 +274,38 @@ export class DataProviderService {
|
||||
1000
|
||||
).toFixed(3)} seconds`
|
||||
);
|
||||
|
||||
try {
|
||||
const date = getStartOfUtcDate(new Date());
|
||||
|
||||
// Upsert quotes by imitating missing upsertMany functionality
|
||||
// with $transaction
|
||||
const upsertPromises = Object.keys(response)
|
||||
.filter((symbol) => {
|
||||
return (
|
||||
isNumber(response[symbol].marketPrice) &&
|
||||
response[symbol].marketPrice > 0
|
||||
);
|
||||
})
|
||||
.map((symbol) =>
|
||||
this.prismaService.marketData.upsert({
|
||||
create: {
|
||||
date,
|
||||
symbol,
|
||||
dataSource: response[symbol].dataSource,
|
||||
marketPrice: response[symbol].marketPrice
|
||||
},
|
||||
update: {
|
||||
marketPrice: response[symbol].marketPrice
|
||||
},
|
||||
where: {
|
||||
date_symbol: { date, symbol }
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await this.prismaService.$transaction(upsertPromises);
|
||||
} catch {}
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -314,6 +378,21 @@ export class DataProviderService {
|
||||
|
||||
private getDataProvider(providerName: DataSource) {
|
||||
for (const dataProviderInterface of this.dataProviderInterfaces) {
|
||||
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
|
||||
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
|
||||
(currentDataProviderInterface) => {
|
||||
return (
|
||||
currentDataProviderInterface.getName() ===
|
||||
this.dataProviderMapping[dataProviderInterface.getName()]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (mappedDataProviderInterface) {
|
||||
return mappedDataProviderInterface;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataProviderInterface.getName() === providerName) {
|
||||
return dataProviderInterface;
|
||||
}
|
||||
|
@ -40,10 +40,11 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return {
|
||||
assetClass: searchResult?.assetClass,
|
||||
assetSubClass: searchResult?.assetSubClass,
|
||||
currency: searchResult?.currency,
|
||||
currency: this.convertCurrency(searchResult?.currency),
|
||||
dataSource: this.getName(),
|
||||
isin: searchResult?.isin,
|
||||
name: searchResult?.name
|
||||
name: searchResult?.name,
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
@ -145,12 +146,20 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
result: { [symbol: string]: IDataProviderResponse },
|
||||
{ close, code, timestamp }
|
||||
) => {
|
||||
result[code] = {
|
||||
currency: searchResponse?.items[0]?.currency,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
};
|
||||
const currency = this.convertCurrency(
|
||||
searchResponse?.items[0]?.currency
|
||||
);
|
||||
|
||||
if (currency) {
|
||||
result[code] = {
|
||||
currency,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000))
|
||||
? 'open'
|
||||
: 'closed'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@ -163,6 +172,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'AAPL.US';
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const searchResult = await this.getSearchResult(aQuery);
|
||||
|
||||
@ -183,16 +196,26 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency,
|
||||
dataSource,
|
||||
name,
|
||||
symbol
|
||||
symbol,
|
||||
currency: this.convertCurrency(currency)
|
||||
};
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
private convertCurrency(aCurrency: string) {
|
||||
let currency = aCurrency;
|
||||
|
||||
if (currency === 'GBX') {
|
||||
currency = 'GBp';
|
||||
}
|
||||
|
||||
return currency;
|
||||
}
|
||||
|
||||
private async getSearchResult(aQuery: string): Promise<
|
||||
(LookupItem & {
|
||||
assetClass: AssetClass;
|
||||
@ -212,14 +235,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
const response = await get();
|
||||
|
||||
searchResult = response.map(
|
||||
({
|
||||
Code,
|
||||
Currency: currency,
|
||||
Exchange,
|
||||
ISIN: isin,
|
||||
Name: name,
|
||||
Type
|
||||
}) => {
|
||||
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
Exchange,
|
||||
Type
|
||||
@ -228,9 +244,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency,
|
||||
isin,
|
||||
name,
|
||||
currency: this.convertCurrency(Currency),
|
||||
dataSource: this.getName(),
|
||||
symbol: `${Code}.${Exchange}`
|
||||
};
|
||||
|
@ -30,7 +30,8 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
dataSource: this.getName()
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
@ -142,6 +143,10 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'INDEXSP:.INX';
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
|
@ -40,5 +40,7 @@ export interface DataProviderInterface {
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
||||
|
||||
getTestSymbol(): string;
|
||||
|
||||
search(aQuery: string): Promise<{ items: LookupItem[] }>;
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ export class ManualService implements DataProviderInterface {
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
dataSource: this.getName()
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
@ -162,6 +163,10 @@ export class ManualService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
let items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
|
@ -27,7 +27,8 @@ export class RapidApiService implements DataProviderInterface {
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
dataSource: this.getName()
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
@ -112,6 +113,10 @@ export class RapidApiService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
return { items: [] };
|
||||
}
|
||||
|
@ -1,26 +1,19 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { countries } from 'countries-list';
|
||||
import { addDays, format, isSameDay } from 'date-fns';
|
||||
import yahooFinance from 'yahoo-finance2';
|
||||
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceService implements DataProviderInterface {
|
||||
@ -28,7 +21,8 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService
|
||||
private readonly cryptocurrencyService: CryptocurrencyService,
|
||||
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
@ -37,128 +31,20 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a symbol to a Yahoo Finance symbol
|
||||
*
|
||||
* Currency: USDCHF -> USDCHF=X
|
||||
* Cryptocurrency: BTCUSD -> BTC-USD
|
||||
* DOGEUSD -> DOGE-USD
|
||||
*/
|
||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.includes(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
aSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Add a dash before the last three characters
|
||||
// BTCUSD -> BTC-USD
|
||||
// DOGEUSD -> DOGE-USD
|
||||
// SOL1USD -> SOL1-USD
|
||||
return aSymbol.replace(
|
||||
new RegExp(`-?${this.baseCurrency}$`),
|
||||
`-${this.baseCurrency}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return aSymbol;
|
||||
}
|
||||
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
const response: Partial<SymbolProfile> = {};
|
||||
const { assetClass, assetSubClass, currency, name } =
|
||||
await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol);
|
||||
|
||||
try {
|
||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||
});
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
quoteType: assetProfile.price.quoteType,
|
||||
shortName: assetProfile.price.shortName
|
||||
});
|
||||
|
||||
response.assetClass = assetClass;
|
||||
response.assetSubClass = assetSubClass;
|
||||
response.currency = assetProfile.price.currency;
|
||||
response.dataSource = this.getName();
|
||||
response.name = this.formatName({
|
||||
longName: assetProfile.price.longName,
|
||||
quoteType: assetProfile.price.quoteType,
|
||||
shortName: assetProfile.price.shortName,
|
||||
symbol: assetProfile.price.symbol
|
||||
});
|
||||
response.symbol = aSymbol;
|
||||
|
||||
if (assetSubClass === AssetSubClass.MUTUALFUND) {
|
||||
response.sectors = [];
|
||||
|
||||
for (const sectorWeighting of assetProfile.topHoldings
|
||||
?.sectorWeightings ?? []) {
|
||||
for (const [sector, weight] of Object.entries(sectorWeighting)) {
|
||||
response.sectors.push({ weight, name: this.parseSector(sector) });
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
assetSubClass === AssetSubClass.STOCK &&
|
||||
assetProfile.summaryProfile?.country
|
||||
) {
|
||||
// Add country if asset is stock and country available
|
||||
|
||||
try {
|
||||
const [code] = Object.entries(countries).find(([, country]) => {
|
||||
return country.name === assetProfile.summaryProfile?.country;
|
||||
});
|
||||
|
||||
if (code) {
|
||||
response.countries = [{ code, weight: 1 }];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (assetProfile.summaryProfile?.sector) {
|
||||
response.sectors = [
|
||||
{ name: assetProfile.summaryProfile?.sector, weight: 1 }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const url = assetProfile.summaryProfile?.website;
|
||||
if (url) {
|
||||
response.url = url;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'YahooFinanceService');
|
||||
}
|
||||
|
||||
return response;
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency,
|
||||
name,
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
public async getDividends({
|
||||
@ -178,7 +64,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
try {
|
||||
const historicalResult = await yahooFinance.historical(
|
||||
this.convertToYahooFinanceSymbol(symbol),
|
||||
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
|
||||
symbol
|
||||
),
|
||||
{
|
||||
events: 'dividends',
|
||||
interval: granularity === 'month' ? '1mo' : '1d',
|
||||
@ -228,7 +116,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
try {
|
||||
const historicalResult = await yahooFinance.historical(
|
||||
this.convertToYahooFinanceSymbol(aSymbol),
|
||||
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(
|
||||
aSymbol
|
||||
),
|
||||
{
|
||||
interval: '1d',
|
||||
period1: format(from, DATE_FORMAT),
|
||||
@ -277,8 +167,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const yahooFinanceSymbols = aSymbols.map((symbol) =>
|
||||
this.convertToYahooFinanceSymbol(symbol)
|
||||
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol)
|
||||
);
|
||||
|
||||
try {
|
||||
@ -288,7 +179,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
for (const quote of quotes) {
|
||||
// Convert symbols back
|
||||
const symbol = this.convertFromYahooFinanceSymbol(quote.symbol);
|
||||
const symbol =
|
||||
this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
|
||||
quote.symbol
|
||||
);
|
||||
|
||||
response[symbol] = {
|
||||
currency: quote.currency,
|
||||
@ -358,6 +252,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'AAPL';
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const items: LookupItem[] = [];
|
||||
|
||||
@ -405,14 +303,16 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return currentQuote.symbol === marketDataItem.symbol;
|
||||
});
|
||||
|
||||
const symbol = this.convertFromYahooFinanceSymbol(
|
||||
marketDataItem.symbol
|
||||
);
|
||||
const symbol =
|
||||
this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol(
|
||||
marketDataItem.symbol
|
||||
);
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
quoteType: quote.quoteType,
|
||||
shortName: quote.shortname
|
||||
});
|
||||
const { assetClass, assetSubClass } =
|
||||
this.yahooFinanceDataEnhancerService.parseAssetClass({
|
||||
quoteType: quote.quoteType,
|
||||
shortName: quote.shortname
|
||||
});
|
||||
|
||||
items.push({
|
||||
assetClass,
|
||||
@ -420,7 +320,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
symbol,
|
||||
currency: marketDataItem.currency,
|
||||
dataSource: this.getName(),
|
||||
name: this.formatName({
|
||||
name: this.yahooFinanceDataEnhancerService.formatName({
|
||||
longName: quote.longname,
|
||||
quoteType: quote.quoteType,
|
||||
shortName: quote.shortname,
|
||||
@ -435,42 +335,6 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return { items };
|
||||
}
|
||||
|
||||
private formatName({
|
||||
longName,
|
||||
quoteType,
|
||||
shortName,
|
||||
symbol
|
||||
}: {
|
||||
longName: Price['longName'];
|
||||
quoteType: Price['quoteType'];
|
||||
shortName: Price['shortName'];
|
||||
symbol: Price['symbol'];
|
||||
}) {
|
||||
let name = longName;
|
||||
|
||||
if (name) {
|
||||
name = name.replace('Amundi Index Solutions - ', '');
|
||||
name = name.replace('iShares ETF (CH) - ', '');
|
||||
name = name.replace('iShares III Public Limited Company - ', '');
|
||||
name = name.replace('iShares V PLC - ', '');
|
||||
name = name.replace('iShares VI Public Limited Company - ', '');
|
||||
name = name.replace('iShares VII PLC - ', '');
|
||||
name = name.replace('Multi Units Luxembourg - ', '');
|
||||
name = name.replace('VanEck ETFs N.V. - ', '');
|
||||
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
|
||||
name = name.replace('Vanguard Funds Public Limited Company - ', '');
|
||||
name = name.replace('Vanguard Index Funds - ', '');
|
||||
name = name.replace('Xtrackers (IE) Plc - ', '');
|
||||
}
|
||||
|
||||
if (quoteType === 'FUTURE') {
|
||||
// "Gold Jun 22" -> "Gold"
|
||||
name = shortName?.slice(0, -6);
|
||||
}
|
||||
|
||||
return name || shortName || symbol;
|
||||
}
|
||||
|
||||
private getConvertedValue({
|
||||
symbol,
|
||||
value
|
||||
@ -491,95 +355,4 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private parseAssetClass({
|
||||
quoteType,
|
||||
shortName
|
||||
}: {
|
||||
quoteType: string;
|
||||
shortName: string;
|
||||
}): {
|
||||
assetClass: AssetClass;
|
||||
assetSubClass: AssetSubClass;
|
||||
} {
|
||||
let assetClass: AssetClass;
|
||||
let assetSubClass: AssetSubClass;
|
||||
|
||||
switch (quoteType?.toLowerCase()) {
|
||||
case 'cryptocurrency':
|
||||
assetClass = AssetClass.CASH;
|
||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||
break;
|
||||
case 'equity':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.STOCK;
|
||||
break;
|
||||
case 'etf':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.ETF;
|
||||
break;
|
||||
case 'future':
|
||||
assetClass = AssetClass.COMMODITY;
|
||||
assetSubClass = AssetSubClass.COMMODITY;
|
||||
|
||||
if (
|
||||
shortName?.toLowerCase()?.startsWith('gold') ||
|
||||
shortName?.toLowerCase()?.startsWith('palladium') ||
|
||||
shortName?.toLowerCase()?.startsWith('platinum') ||
|
||||
shortName?.toLowerCase()?.startsWith('silver')
|
||||
) {
|
||||
assetSubClass = AssetSubClass.PRECIOUS_METAL;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'mutualfund':
|
||||
assetClass = AssetClass.EQUITY;
|
||||
assetSubClass = AssetSubClass.MUTUALFUND;
|
||||
break;
|
||||
}
|
||||
|
||||
return { assetClass, assetSubClass };
|
||||
}
|
||||
|
||||
private parseSector(aString: string): string {
|
||||
let sector = UNKNOWN_KEY;
|
||||
|
||||
switch (aString) {
|
||||
case 'basic_materials':
|
||||
sector = 'Basic Materials';
|
||||
break;
|
||||
case 'communication_services':
|
||||
sector = 'Communication Services';
|
||||
break;
|
||||
case 'consumer_cyclical':
|
||||
sector = 'Consumer Cyclical';
|
||||
break;
|
||||
case 'consumer_defensive':
|
||||
sector = 'Consumer Staples';
|
||||
break;
|
||||
case 'energy':
|
||||
sector = 'Energy';
|
||||
break;
|
||||
case 'financial_services':
|
||||
sector = 'Financial Services';
|
||||
break;
|
||||
case 'healthcare':
|
||||
sector = 'Healthcare';
|
||||
break;
|
||||
case 'industrials':
|
||||
sector = 'Industrials';
|
||||
break;
|
||||
case 'realestate':
|
||||
sector = 'Real Estate';
|
||||
break;
|
||||
case 'technology':
|
||||
sector = 'Technology';
|
||||
break;
|
||||
case 'utilities':
|
||||
sector = 'Utilities';
|
||||
break;
|
||||
}
|
||||
|
||||
return sector;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,8 @@ export class ExchangeRateDataService {
|
||||
getYesterday()
|
||||
);
|
||||
|
||||
if (Object.keys(result).length !== this.currencyPairs.length) {
|
||||
// TODO: add fallback
|
||||
/*if (Object.keys(result).length !== this.currencyPairs.length) {
|
||||
// Load currencies directly from data provider as a fallback
|
||||
// if historical data is not fully available
|
||||
const historicalData = await this.dataProviderService.getQuotes(
|
||||
@ -72,13 +73,15 @@ export class ExchangeRateDataService {
|
||||
);
|
||||
|
||||
Object.keys(historicalData).forEach((key) => {
|
||||
result[key] = {
|
||||
[format(getYesterday(), DATE_FORMAT)]: {
|
||||
marketPrice: historicalData[key].marketPrice
|
||||
}
|
||||
};
|
||||
if (isNumber(historicalData[key].marketPrice)) {
|
||||
result[key] = {
|
||||
[format(getYesterday(), DATE_FORMAT)]: {
|
||||
marketPrice: historicalData[key].marketPrice
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
const resultExtended = result;
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:3333",
|
||||
"target": "http://0.0.0.0:3333",
|
||||
"secure": false
|
||||
},
|
||||
"/assets": {
|
||||
"target": "http://localhost:3333",
|
||||
"target": "http://0.0.0.0:3333",
|
||||
"secure": false
|
||||
},
|
||||
"/ionicons": {
|
||||
"target": "http://localhost:3333",
|
||||
"target": "http://0.0.0.0:3333",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,8 @@
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||
class="d-inline-block info-message px-3 py-2"
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2 text-truncate"
|
||||
(click)="onShowSystemMessage()"
|
||||
>
|
||||
{{ info.systemMessage }}
|
||||
</div>
|
||||
|
@ -13,9 +13,10 @@
|
||||
margin-top: -0.5rem;
|
||||
|
||||
.info-message {
|
||||
background-color: rgba(0, 0, 0, $alpha-hover);
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 2rem;
|
||||
font-size: 80%;
|
||||
max-width: 100%;
|
||||
|
||||
.a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
@ -30,3 +31,13 @@
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
main {
|
||||
.info-message-container {
|
||||
.info-message {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.tokenStorageService.signOut();
|
||||
}
|
||||
|
||||
public onShowSystemMessage() {
|
||||
alert(this.info.systemMessage);
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
this.tokenStorageService.signOut();
|
||||
this.userService.remove();
|
||||
|
@ -33,7 +33,7 @@
|
||||
<span class="ml-2" matTextSuffix>{{ data.currency }}</span>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="apply-current-market-price ml-2 no-min-width"
|
||||
class="ml-2 mt-1 no-min-width"
|
||||
mat-button
|
||||
title="Fetch market price"
|
||||
(click)="onFetchSymbolForDate()"
|
||||
|
@ -3,11 +3,5 @@
|
||||
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
.mat-mdc-button {
|
||||
&.apply-current-market-price {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
// Create a copy before manipulating countries object
|
||||
this.countries = structuredClone(this.countries);
|
||||
|
||||
if (this.countries) {
|
||||
this.isLoading = true;
|
||||
|
||||
|
@ -153,7 +153,7 @@
|
||||
</mat-form-field>
|
||||
<button
|
||||
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
|
||||
class="apply-current-market-price ml-2 no-min-width"
|
||||
class="ml-2 mt-1 no-min-width"
|
||||
mat-button
|
||||
title="Apply current market price"
|
||||
type="button"
|
||||
|
@ -15,12 +15,6 @@
|
||||
color: var(--dark-primary-text);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-button {
|
||||
&.apply-current-market-price {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
| 'exchange'
|
||||
| 'name'
|
||||
| 'value'
|
||||
>;
|
||||
> & { etfProvider: string };
|
||||
};
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
@ -249,7 +249,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
public initializeAnalysisData() {
|
||||
this.initialize();
|
||||
|
||||
for (const [id, { current, name, original }] of Object.entries(
|
||||
for (const [id, { current, name }] of Object.entries(
|
||||
this.portfolioDetails.accounts
|
||||
)) {
|
||||
this.accounts[id] = {
|
||||
@ -275,6 +275,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
assetClass: position.assetClass,
|
||||
assetSubClass: position.assetSubClass,
|
||||
currency: position.currency,
|
||||
etfProvider: this.extractEtfProvider({
|
||||
assetSubClass: position.assetSubClass,
|
||||
name: position.name
|
||||
}),
|
||||
exchange: position.exchange,
|
||||
name: position.name
|
||||
};
|
||||
@ -452,4 +456,19 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private extractEtfProvider({
|
||||
assetSubClass,
|
||||
name
|
||||
}: {
|
||||
assetSubClass: PortfolioPosition['assetSubClass'];
|
||||
name: string;
|
||||
}) {
|
||||
if (assetSubClass === 'ETF') {
|
||||
const [firstWord] = name.split(' ');
|
||||
return firstWord;
|
||||
}
|
||||
|
||||
return UNKNOWN_KEY;
|
||||
}
|
||||
}
|
||||
|
@ -249,4 +249,29 @@
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header class="overflow-hidden w-100">
|
||||
<mat-card-title class="align-items-center d-flex text-truncate"
|
||||
><span i18n>By ETF Provider</span
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[keys]="['etfProvider']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -237,7 +237,7 @@ body {
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-container {
|
||||
background: var(--dark-background);
|
||||
--mdc-dialog-container-color: var(--dark-background);
|
||||
|
||||
.mdc-dialog__content {
|
||||
--mdc-dialog-supporting-text-color: rgba(var(--light-primary-text));
|
||||
@ -464,7 +464,7 @@ ngx-skeleton-loader {
|
||||
}
|
||||
|
||||
.with-info-message {
|
||||
height: calc(100vh - 5rem - 3.5rem) !important;
|
||||
height: calc(100vh - 5rem - 3.5rem + 0.5rem) !important;
|
||||
}
|
||||
|
||||
.with-placeholder-as-option {
|
||||
|
@ -49,9 +49,7 @@ export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
|
||||
type: 'exponential'
|
||||
},
|
||||
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||
removeOnComplete: {
|
||||
age: ms('2 weeks') / 1000
|
||||
}
|
||||
removeOnComplete: true
|
||||
};
|
||||
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS =
|
||||
'GATHER_HISTORICAL_MARKET_DATA';
|
||||
@ -62,9 +60,7 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
|
||||
type: 'exponential'
|
||||
},
|
||||
priority: DATA_GATHERING_QUEUE_PRIORITY_LOW,
|
||||
removeOnComplete: {
|
||||
age: ms('2 weeks') / 1000
|
||||
}
|
||||
removeOnComplete: true
|
||||
};
|
||||
|
||||
export const HEADER_KEY_IMPERSONATION = 'Impersonation-Id';
|
||||
@ -77,6 +73,7 @@ export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
|
||||
export const PROPERTY_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS';
|
||||
export const PROPERTY_COUPONS = 'COUPONS';
|
||||
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||
export const PROPERTY_DATA_SOURCE_MAPPING = 'DATA_SOURCE_MAPPING';
|
||||
export const PROPERTY_DEMO_USER_ID = 'DEMO_USER_ID';
|
||||
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
|
||||
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
|
||||
|
@ -152,6 +152,13 @@ export function getNumberFormatGroup(aLocale?: string) {
|
||||
}).value;
|
||||
}
|
||||
|
||||
export function getStartOfUtcDate(aDate: Date) {
|
||||
const date = new Date(aDate);
|
||||
date.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getSum(aArray: Big[]) {
|
||||
if (aArray?.length > 0) {
|
||||
return aArray.reduce((a, b) => a.plus(b), new Big(0));
|
||||
|
@ -391,7 +391,6 @@
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
></th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
|
@ -108,7 +108,7 @@
|
||||
<ng-container matColumnDef="performance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
class="d-none d-lg-table-cell px-1 justify-content-end"
|
||||
mat-header-cell
|
||||
mat-sort-header="netPerformancePercent"
|
||||
>
|
||||
|
22
package.json
22
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.252.0",
|
||||
"version": "1.259.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -71,16 +71,16 @@
|
||||
"@dfinity/principal": "0.15.1",
|
||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||
"@nestjs/bull": "0.6.3",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/config": "2.3.1",
|
||||
"@nestjs/core": "9.4.0",
|
||||
"@nestjs/jwt": "10.0.3",
|
||||
"@nestjs/passport": "9.0.3",
|
||||
"@nestjs/platform-express": "9.4.0",
|
||||
"@nestjs/schedule": "2.2.1",
|
||||
"@nestjs/serve-static": "3.0.1",
|
||||
"@nestjs/common": "9.1.4",
|
||||
"@nestjs/config": "2.2.0",
|
||||
"@nestjs/core": "9.1.4",
|
||||
"@nestjs/jwt": "9.0.0",
|
||||
"@nestjs/passport": "9.0.0",
|
||||
"@nestjs/platform-express": "9.1.4",
|
||||
"@nestjs/schedule": "2.1.0",
|
||||
"@nestjs/serve-static": "3.0.0",
|
||||
"@nrwl/angular": "15.9.2",
|
||||
"@prisma/client": "4.11.0",
|
||||
"@prisma/client": "4.12.0",
|
||||
"@simplewebauthn/browser": "5.2.1",
|
||||
"@simplewebauthn/server": "5.2.1",
|
||||
"@stripe/stripe-js": "1.47.0",
|
||||
@ -120,7 +120,7 @@
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "4.11.0",
|
||||
"prisma": "4.12.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "11.12.0",
|
||||
|
318
yarn.lock
318
yarn.lock
@ -3524,11 +3524,6 @@
|
||||
resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz"
|
||||
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
||||
|
||||
"@lukeed/csprng@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe"
|
||||
integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==
|
||||
|
||||
"@material/animation@15.0.0-canary.684e33d25.0":
|
||||
version "15.0.0-canary.684e33d25.0"
|
||||
resolved "https://registry.yarnpkg.com/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz#d42ecdd31da5635ff5b44a53c6fc8746de7f5a5a"
|
||||
@ -4318,69 +4313,70 @@
|
||||
"@nestjs/bull-shared" "^0.1.3"
|
||||
tslib "2.5.0"
|
||||
|
||||
"@nestjs/common@9.4.0":
|
||||
version "9.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.4.0.tgz#3597e4f3a1278486fc2e015c94e58bcbbb4f72ca"
|
||||
integrity sha512-RUcVAQsEF4WPrmzFXEOUfZnPwrLTe1UVlzXTlSyfqfqbdWDPKDGlIPVelBLfc5/+RRUQ0I5iE4+CQvpCmkqldw==
|
||||
"@nestjs/common@9.1.4":
|
||||
version "9.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.1.4.tgz#83ed4f5627db12c0e0aaf1438becba8927fb97e2"
|
||||
integrity sha512-hmGTZ8ShKFDqqlU02uU8e/8PNE4bnES4pcFa6s/T1pLDYWjyf/75Klunro1W4aQPHcxnnohBmB27WxMqFTPEfw==
|
||||
dependencies:
|
||||
uid "2.0.2"
|
||||
iterare "1.2.1"
|
||||
tslib "2.5.0"
|
||||
|
||||
"@nestjs/config@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.3.1.tgz#6ac151f818db4ccf987c7ff8ef5b2c1f4eeec913"
|
||||
integrity sha512-Ckzel0NZ9CWhNsLfE1hxfDuxJuEbhQvGxSlmZ1/X8awjRmAA/g3kT6M1+MO1SHj1wMtPyUfd9WpwkiqFbiwQgA==
|
||||
dependencies:
|
||||
dotenv "16.0.3"
|
||||
dotenv-expand "10.0.0"
|
||||
lodash "4.17.21"
|
||||
tslib "2.4.0"
|
||||
uuid "9.0.0"
|
||||
|
||||
"@nestjs/core@9.4.0":
|
||||
version "9.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.4.0.tgz#bca5128138fcf9b4668bc524b578f3805a325183"
|
||||
integrity sha512-yTLryCgFD0462wPe4HIzhyTcDgibt8Stfwb5YzcX7Ma0NM4m8uBIpcPG109KBubp8ZmV85e5mw4rl20qLQQVsQ==
|
||||
"@nestjs/config@2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.2.0.tgz#9f3da35f7c4a58724c0a0817d6f04b66e6703430"
|
||||
integrity sha512-78Eg6oMbCy3D/YvqeiGBTOWei1Jwi3f2pSIZcZ1QxY67kYsJzTRTkwRT8Iv30DbK0sGKc1mcloDLD5UXgZAZtg==
|
||||
dependencies:
|
||||
dotenv "16.0.1"
|
||||
dotenv-expand "8.0.3"
|
||||
lodash "4.17.21"
|
||||
uuid "8.3.2"
|
||||
|
||||
"@nestjs/core@9.1.4":
|
||||
version "9.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.1.4.tgz#7d1ed6df9c4de6d955cacf60c18882c1ded01a83"
|
||||
integrity sha512-S6KpGeKotPYh126hhRqYLhvg9lxSbAmGfEbK8m09crIK7CYP05t32KtT6n12xl5/iva1G4Ch87Z/3rYP76etUg==
|
||||
dependencies:
|
||||
uid "2.0.2"
|
||||
"@nuxtjs/opencollective" "0.3.2"
|
||||
fast-safe-stringify "2.1.1"
|
||||
iterare "1.2.1"
|
||||
object-hash "3.0.0"
|
||||
path-to-regexp "3.2.0"
|
||||
tslib "2.5.0"
|
||||
|
||||
"@nestjs/jwt@10.0.3":
|
||||
version "10.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.0.3.tgz#e74e992cde99df266616c8bedf2404898eec4819"
|
||||
integrity sha512-WO8MI3uEMOFKpbO+SAg6l4aRCr+9KvaL+raFMZaXuEUDphXek6pqdox+4tex9242pNSJUA0trfAMaiy/yVrXQg==
|
||||
dependencies:
|
||||
"@types/jsonwebtoken" "9.0.1"
|
||||
jsonwebtoken "9.0.0"
|
||||
|
||||
"@nestjs/passport@9.0.3":
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-9.0.3.tgz#4df0e6de3176e04a5770cb432e58f129c8e49f9e"
|
||||
integrity sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg==
|
||||
|
||||
"@nestjs/platform-express@9.4.0":
|
||||
version "9.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.4.0.tgz#e1f9e6c60cdd8d7889abbc6a04ab95279976175b"
|
||||
integrity sha512-PpnfghpNq7mwG43z3+pacHulsabUCBMla4nUikntXT525ORpZSDvh/nLi1HLfE4w5+FcINc8/RBOyYTeRVmiRQ==
|
||||
dependencies:
|
||||
body-parser "1.20.2"
|
||||
cors "2.8.5"
|
||||
express "4.18.2"
|
||||
multer "1.4.4-lts.1"
|
||||
tslib "2.5.0"
|
||||
|
||||
"@nestjs/schedule@2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-2.2.1.tgz#404e79133e50e4b70dec3b7c194d6b796485cb13"
|
||||
integrity sha512-7jev9Q3aFnRajKAi/At+9rzwflZNN10SA5PcdCvxc35GFfTdM2a6O5GA7tiIbLuOOzdjPYPbC3RxP4tpXOHVWw==
|
||||
dependencies:
|
||||
cron "2.3.0"
|
||||
tslib "2.4.0"
|
||||
uuid "9.0.0"
|
||||
|
||||
"@nestjs/jwt@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-9.0.0.tgz#73e01338d2853a55033528b540cfd92c7996bae9"
|
||||
integrity sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==
|
||||
dependencies:
|
||||
"@types/jsonwebtoken" "8.5.8"
|
||||
jsonwebtoken "8.5.1"
|
||||
|
||||
"@nestjs/passport@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-9.0.0.tgz#0571bb08f8043456bc6df44cd4f59ca5f10c9b9f"
|
||||
integrity sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==
|
||||
|
||||
"@nestjs/platform-express@9.1.4":
|
||||
version "9.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.1.4.tgz#4829d56c9ce16c5ec11dc107f7df98b20c77b92d"
|
||||
integrity sha512-SLJWDa6V54QrUvzKI4Eyt7gyrjV7F9FY1uHFihshjmQfpf0ebCGacR9jzNwf01aHl0BJX3DUn/KYteBjz6DJXw==
|
||||
dependencies:
|
||||
body-parser "1.20.0"
|
||||
cors "2.8.5"
|
||||
express "4.18.1"
|
||||
multer "1.4.4-lts.1"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@nestjs/schedule@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-2.1.0.tgz#a2e21b2078a0c3d56552d40cc97336aa5bbfecdf"
|
||||
integrity sha512-4Xaw56WiW3VsxEPPnj/iDtfjcO+sUZyYAeRxD0gnF5havncxjAnv52Iw7UH3DuzzUA784xPGgGje3Fq0Gu925g==
|
||||
dependencies:
|
||||
cron "2.0.0"
|
||||
uuid "8.3.2"
|
||||
|
||||
"@nestjs/schematics@9.1.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.1.0.tgz#8afc4b1e7c7988c18d3ab44cffe56773b7507272"
|
||||
@ -4402,10 +4398,10 @@
|
||||
jsonc-parser "3.0.0"
|
||||
pluralize "8.0.0"
|
||||
|
||||
"@nestjs/serve-static@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-3.0.1.tgz#d7d736b47171923d9e87262e19cc58ade8f4ba56"
|
||||
integrity sha512-i766UJPYOqvQ2BbRKh0/+Mmq5NkJnmKcShjWV1i5qpXyeM0KDZTn0n7g7ykWq/3LbQgjpMzrhYtGv35GX7GVQw==
|
||||
"@nestjs/serve-static@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-3.0.0.tgz#085e7ebbb3c6d6e35b227ea57c8e988c87847309"
|
||||
integrity sha512-TpXjgs4136dQqWUjEcONqppqXDsrJhRkmKWzuBMOUAnP4HjHpNmlycvkHnDnWSoG2YD4a7Enh4ViYGWqCfHStA==
|
||||
dependencies:
|
||||
path-to-regexp "0.2.5"
|
||||
|
||||
@ -4891,22 +4887,22 @@
|
||||
dependencies:
|
||||
esquery "^1.0.1"
|
||||
|
||||
"@prisma/client@4.11.0":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.11.0.tgz#41d5664dea4172c954190a432f70b86d3e2e629b"
|
||||
integrity sha512-0INHYkQIqgAjrt7NzhYpeDQi8x3Nvylc2uDngKyFDDj1tTRQ4uV1HnVmd1sQEraeVAN63SOK0dgCKQHlvjL0KA==
|
||||
"@prisma/client@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.12.0.tgz#119b692888b1fe0fd3305c7d0e0ac48520aa6839"
|
||||
integrity sha512-j9/ighfWwux97J2dS15nqhl60tYoH8V0IuSsgZDb6bCFcQD3fXbXmxjYC8GHhIgOk3lB7Pq+8CwElz2MiDpsSg==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "4.11.0-57.8fde8fef4033376662cad983758335009d522acb"
|
||||
"@prisma/engines-version" "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7"
|
||||
|
||||
"@prisma/engines-version@4.11.0-57.8fde8fef4033376662cad983758335009d522acb":
|
||||
version "4.11.0-57.8fde8fef4033376662cad983758335009d522acb"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.11.0-57.8fde8fef4033376662cad983758335009d522acb.tgz#74af5ff56170c78e93ce46c56510160f58cd3c01"
|
||||
integrity sha512-3Vd8Qq06d5xD8Ch5WauWcUUrsVPdMC6Ge8ILji8RFfyhUpqon6qSyGM0apvr1O8n8qH8cKkEFqRPsYjuz5r83g==
|
||||
"@prisma/engines-version@4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7":
|
||||
version "4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7.tgz#51a1cc5c886564b542acde64a873645d0dee2566"
|
||||
integrity sha512-JIHNj5jlXb9mcaJwakM0vpgRYJIAurxTUqM0iX0tfEQA5XLZ9ONkIckkhuAKdAzocZ+80GYg7QSsfpjg7OxbOA==
|
||||
|
||||
"@prisma/engines@4.11.0":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.11.0.tgz#c99749bfe20f58e8f4d2b5e04fee0785eba440e1"
|
||||
integrity sha512-0AEBi2HXGV02cf6ASsBPhfsVIbVSDC9nbQed4iiY5eHttW9ZtMxHThuKZE1pnESbr8HRdgmFSa/Kn4OSNYuibg==
|
||||
"@prisma/engines@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.12.0.tgz#68d99078b70b2d9c339d0e8cbf2e99f00b72aa8c"
|
||||
integrity sha512-0alKtnxhNB5hYU+ymESBlGI4b9XrGGSdv7Ud+8TE/fBNOEhIud0XQsAR+TrvUZgS4na5czubiMsODw0TUrgkIA==
|
||||
|
||||
"@samverschueren/stream-to-observable@^0.3.0":
|
||||
version "0.3.1"
|
||||
@ -6151,10 +6147,10 @@
|
||||
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/jsonwebtoken@9.0.1":
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4"
|
||||
integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==
|
||||
"@types/jsonwebtoken@8.5.8":
|
||||
version "8.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44"
|
||||
integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@ -8051,24 +8047,6 @@ body-parser@1.20.1:
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
bonjour-service@^1.0.11:
|
||||
version "1.0.13"
|
||||
resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.13.tgz"
|
||||
@ -9212,11 +9190,6 @@ content-type@~1.0.4:
|
||||
resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
|
||||
content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
convert-source-map@^1.4.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz"
|
||||
@ -9457,12 +9430,12 @@ cron-parser@^4.2.1:
|
||||
dependencies:
|
||||
luxon "^1.28.0"
|
||||
|
||||
cron@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-2.3.0.tgz#20df6da18d4f7d2f8937def2eb5fc0d1a320c526"
|
||||
integrity sha512-ZN5HP8zDY41sJolMsbc+GksRATcbvkPKF5wR/qc8FrV4NBVi9ORQa1HmYa5GydaysUB80X9XpRlRkooa5uEtTA==
|
||||
cron@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cron/-/cron-2.0.0.tgz#15c6bf37c1cebf6da1d7a688b9ba1c68338bfe6b"
|
||||
integrity sha512-RPeRunBCFr/WEo7WLp8Jnm45F/ziGJiHVvVQEBSDTSGu6uHW49b2FOP2O14DcXlGJRLhwE7TIoDzHHK4KmlL6g==
|
||||
dependencies:
|
||||
luxon "^3.2.1"
|
||||
luxon "^1.23.x"
|
||||
|
||||
cross-fetch@^3.0.5:
|
||||
version "3.1.5"
|
||||
@ -10737,20 +10710,20 @@ dot-case@^3.0.4:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
dotenv-expand@10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37"
|
||||
integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==
|
||||
dotenv-expand@8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
|
||||
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
|
||||
|
||||
dotenv-expand@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz"
|
||||
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
||||
|
||||
dotenv@16.0.3:
|
||||
version "16.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
|
||||
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
|
||||
dotenv@16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||
|
||||
dotenv@^8.0.0:
|
||||
version "8.6.0"
|
||||
@ -11692,46 +11665,9 @@ expect@^29.0.0, expect@^29.5.0:
|
||||
jest-message-util "^29.5.0"
|
||||
jest-util "^29.5.0"
|
||||
|
||||
express@4.18.2:
|
||||
version "4.18.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
|
||||
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.1"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.5.0"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "1.2.0"
|
||||
fresh "0.5.2"
|
||||
http-errors "2.0.0"
|
||||
merge-descriptors "1.0.1"
|
||||
methods "~1.1.2"
|
||||
on-finished "2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "6.11.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "0.18.0"
|
||||
serve-static "1.15.0"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "2.0.1"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
express@^4.17.1, express@^4.17.3:
|
||||
express@4.18.1, express@^4.17.1, express@^4.17.3:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.npmjs.org/express/-/express-4.18.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
||||
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
@ -15276,19 +15212,9 @@ jsonparse@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz"
|
||||
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
|
||||
|
||||
jsonwebtoken@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d"
|
||||
integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==
|
||||
dependencies:
|
||||
jws "^3.2.2"
|
||||
lodash "^4.17.21"
|
||||
ms "^2.1.1"
|
||||
semver "^7.3.8"
|
||||
|
||||
jsonwebtoken@^8.2.0:
|
||||
jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
||||
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
|
||||
dependencies:
|
||||
jws "^3.2.2"
|
||||
@ -15767,16 +15693,16 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1:
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz"
|
||||
integrity sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==
|
||||
|
||||
luxon@^1.23.x:
|
||||
version "1.28.1"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.1.tgz#528cdf3624a54506d710290a2341aa8e6e6c61b0"
|
||||
integrity sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==
|
||||
|
||||
luxon@^1.28.0:
|
||||
version "1.28.0"
|
||||
resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz"
|
||||
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
|
||||
|
||||
luxon@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
|
||||
integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
|
||||
|
||||
magic-string@0.26.1:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.26.1.tgz"
|
||||
@ -16930,6 +16856,11 @@ object-copy@^0.1.0:
|
||||
define-property "^0.2.5"
|
||||
kind-of "^3.0.3"
|
||||
|
||||
object-hash@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
|
||||
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
|
||||
|
||||
object-inspect@^1.12.0, object-inspect@^1.9.0:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
|
||||
@ -18152,12 +18083,12 @@ pretty-hrtime@^1.0.3:
|
||||
resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz"
|
||||
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
||||
|
||||
prisma@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.11.0.tgz#9695ba4129a43eab3e76b5f7a033c6c020377725"
|
||||
integrity sha512-4zZmBXssPUEiX+GeL0MUq/Yyie4ltiKmGu7jCJFnYMamNrrulTBc+D+QwAQSJ01tyzeGHlD13kOnqPwRipnlNw==
|
||||
prisma@4.12.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.12.0.tgz#1080eda951928cb3b0274ad29da9ae4f93143d68"
|
||||
integrity sha512-xqVper4mbwl32BWzLpdznHAYvYDWQQWK2tBfXjdUD397XaveRyAP7SkBZ6kFlIg8kKayF4hvuaVtYwXd9BodAg==
|
||||
dependencies:
|
||||
"@prisma/engines" "4.11.0"
|
||||
"@prisma/engines" "4.12.0"
|
||||
|
||||
prismjs@^1.28.0:
|
||||
version "1.28.0"
|
||||
@ -18425,16 +18356,6 @@ raw-body@2.5.1:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-loader@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz"
|
||||
@ -20720,6 +20641,11 @@ tslib@2.3.1:
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tslib@2.4.0, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||
@ -20730,11 +20656,6 @@ tslib@^1.10.0, tslib@^1.8.1:
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
@ -20870,13 +20791,6 @@ uid2@0.0.x:
|
||||
resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz"
|
||||
integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==
|
||||
|
||||
uid@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9"
|
||||
integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==
|
||||
dependencies:
|
||||
"@lukeed/csprng" "^1.0.0"
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
|
||||
@ -21188,6 +21102,11 @@ uuid-browser@^3.1.0:
|
||||
resolved "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz"
|
||||
integrity sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==
|
||||
|
||||
uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz"
|
||||
@ -21198,11 +21117,6 @@ uuid@^3.3.2:
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuid@^8.3.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz"
|
||||
|
Reference in New Issue
Block a user