diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efe84cd0..88b9a9251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the contexts to the logger commands + +### Changed + +- Upgraded `Nx` from version `13.8.1` to `13.8.5` + +## 1.124.0 - 06.03.2022 + +### Added + +- Added support for setting a duration in the coupon system + +### Changed + +- Upgraded `ngx-skeleton-loader` from version `2.9.1` to `5.0.0` +- Upgraded `prisma` from version `3.9.1` to `3.10.0` +- Upgraded `yahoo-finance2` from version `2.1.9` to `2.2.0` + +## 1.123.0 - 05.03.2022 + +### Added + +- Included data provider errors in the API response + +### Changed + +- Removed the redundant attributes (`currency`, `dataSource`, `symbol`) of the activity model +- Removed the prefix for symbols with the data source `GHOSTFOLIO` + +### Fixed + +- Improved the account calculations + +### Todo + +- Apply data migration (`yarn database:migrate`) + +## 1.122.0 - 01.03.2022 + +### Added + +- Added support for click in the portfolio proportion chart component + +### Fixed + +- Fixed an issue with undefined currencies after creating an activity + +## 1.121.0 - 27.02.2022 + +### Added + +- Added support for mutual funds +- Added the url to the symbol profile model + +### Changed + +- Migrated from `yahoo-finance` to `yahoo-finance2` + +### Todo + +- Apply data migration (`yarn database:migrate`) + +## 1.120.0 - 25.02.2022 + +### Changed + +- Distinguished the labels _Other_ and _Unknown_ in the portfolio proportion chart component +- Improved the portfolio entry page + +### Fixed + +- Fixed the _Zen Mode_ + +## 1.119.0 - 21.02.2022 + +### Added + +- Added a trial for the subscription + +## 1.118.0 - 20.02.2022 + +### Changed + +- Improved the calculation of the overall performance percentage in the new calculation engine +- Displayed features in features overview page based on permissions +- Extended the data points of historical data in the admin control panel + +## 1.117.0 - 19.02.2022 + ### Changed - Moved the countries and sectors charts in the position detail dialog +- Distinguished today's data point of historical data in the admin control panel - Restructured the server modules +### Fixed + +- Fixed the allocations by account for non-unique account names +- Added a fallback to the default account if the `accountId` is invalid in the import functionality for activities + ## 1.116.0 - 16.02.2022 ### Added diff --git a/angular.json b/angular.json index 29d3a50ed..52f3e9cfe 100644 --- a/angular.json +++ b/angular.json @@ -9,7 +9,7 @@ "schematics": {}, "architect": { "build": { - "builder": "@nrwl/node:build", + "builder": "@nrwl/node:webpack", "options": { "outputPath": "dist/apps/api", "main": "apps/api/src/main.ts", @@ -33,7 +33,7 @@ "outputs": ["{options.outputPath}"] }, "serve": { - "builder": "@nrwl/node:execute", + "builder": "@nrwl/node:node", "options": { "buildTarget": "api:build" } diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index b73977f75..64530c377 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -101,16 +101,18 @@ export class AccountController { ) { accountsWithAggregations = { ...nullifyValuesInObject(accountsWithAggregations, [ - 'totalBalance', - 'totalValue' + 'totalBalanceInBaseCurrency', + 'totalValueInBaseCurrency' ]), accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ 'balance', + 'balanceInBaseCurrency', 'convertedBalance', 'fee', 'quantity', 'unitPrice', - 'value' + 'value', + 'valueInBaseCurrency' ]) }; } diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 2c11de472..90bf909fc 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -13,6 +13,7 @@ import { AccountService } from './account.service'; @Module({ controllers: [AccountController], + exports: [AccountService], imports: [ ConfigurationModule, DataProviderModule, diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 9f5aab1b2..ec1678131 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { Injectable } from '@nestjs/common'; import { Account, Order, Platform, Prisma } from '@prisma/client'; +import Big from 'big.js'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -105,21 +106,26 @@ export class AccountService { aUserId: string, aCurrency: string ): Promise { - let totalCashBalance = 0; + let totalCashBalanceInBaseCurrency = new Big(0); const accounts = await this.accounts({ where: { userId: aUserId } }); - accounts.forEach((account) => { - totalCashBalance += this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - aCurrency + for (const account of accounts) { + totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus( + this.exchangeRateDataService.toCurrency( + account.balance, + account.currency, + aCurrency + ) ); - }); + } - return { accounts, balance: totalCashBalance }; + return { + accounts, + balanceInBaseCurrency: totalCashBalanceInBaseCurrency.toNumber() + }; } public async updateAccount( diff --git a/apps/api/src/app/account/interfaces/cash-details.interface.ts b/apps/api/src/app/account/interfaces/cash-details.interface.ts index 146ee6b29..715343766 100644 --- a/apps/api/src/app/account/interfaces/cash-details.interface.ts +++ b/apps/api/src/app/account/interfaces/cash-details.interface.ts @@ -2,5 +2,5 @@ import { Account } from '@prisma/client'; export interface CashDetails { accounts: Account[]; - balance: number; + balanceInBaseCurrency: number; } diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 9c1de05b0..c1be0bedb 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -11,7 +11,8 @@ import { AdminData, AdminMarketData, AdminMarketDataDetails, - AdminMarketDataItem + AdminMarketDataItem, + UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { DataSource, Property } from '@prisma/client'; @@ -30,13 +31,7 @@ export class AdminService { private readonly symbolProfileService: SymbolProfileService ) {} - public async deleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async deleteProfileData({ dataSource, symbol }: UniqueAsset) { await this.marketDataService.deleteMany({ dataSource, symbol }); await this.symbolProfileService.delete({ dataSource, symbol }); } @@ -137,10 +132,7 @@ export class AdminService { public async getMarketDataBySymbol({ dataSource, symbol - }: { - dataSource: DataSource; - symbol: string; - }): Promise { + }: UniqueAsset): Promise { return { marketData: await this.marketDataService.marketDataItems({ orderBy: { diff --git a/apps/api/src/app/auth/google.strategy.ts b/apps/api/src/app/auth/google.strategy.ts index 43def1baf..c8fb260b7 100644 --- a/apps/api/src/app/auth/google.strategy.ts +++ b/apps/api/src/app/auth/google.strategy.ts @@ -42,7 +42,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { done(null, user); } catch (error) { - Logger.error(error); + Logger.error(error, 'GoogleStrategy'); done(error, false); } } diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts index ba60b028b..9212a2e07 100644 --- a/apps/api/src/app/auth/web-auth.service.ts +++ b/apps/api/src/app/auth/web-auth.service.ts @@ -95,7 +95,7 @@ export class WebAuthService { }; verification = await verifyRegistrationResponse(opts); } catch (error) { - Logger.error(error); + Logger.error(error, 'WebAuthService'); throw new InternalServerErrorException(error.message); } @@ -193,7 +193,7 @@ export class WebAuthService { }; verification = verifyAuthenticationResponse(opts); } catch (error) { - Logger.error(error); + Logger.error(error, 'WebAuthService'); throw new InternalServerErrorException({ error: error.message }); } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index b540fe363..124fe6325 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -18,8 +18,6 @@ export class ExportService { orderBy: { date: 'desc' }, select: { accountId: true, - currency: true, - dataSource: true, date: true, fee: true, id: true, @@ -42,7 +40,6 @@ export class ExportService { orders: orders.map( ({ accountId, - currency, date, fee, quantity, @@ -52,12 +49,12 @@ export class ExportService { }) => { return { accountId, - currency, date, fee, quantity, type, unitPrice, + currency: SymbolProfile.currency, dataSource: SymbolProfile.dataSource, symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol }; diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts index fa1b3aa99..488ac786f 100644 --- a/apps/api/src/app/import/import-data.dto.ts +++ b/apps/api/src/app/import/import-data.dto.ts @@ -1,5 +1,4 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; -import { Order } from '@prisma/client'; import { Type } from 'class-transformer'; import { IsArray, ValidateNested } from 'class-validator'; @@ -7,5 +6,5 @@ export class ImportDataDto { @IsArray() @Type(() => CreateOrderDto) @ValidateNested({ each: true }) - orders: Order[]; + orders: CreateOrderDto[]; } diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index 9ae66247d..d14bd69af 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -40,7 +40,7 @@ export class ImportController { userId: this.request.user.id }); } catch (error) { - Logger.error(error); + Logger.error(error, ImportController); throw new HttpException( { diff --git a/apps/api/src/app/import/import.module.ts b/apps/api/src/app/import/import.module.ts index 03ff2d3f8..62d227bf5 100644 --- a/apps/api/src/app/import/import.module.ts +++ b/apps/api/src/app/import/import.module.ts @@ -1,3 +1,4 @@ +import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { CacheModule } from '@ghostfolio/api/app/cache/cache.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; @@ -13,6 +14,7 @@ import { ImportService } from './import.service'; @Module({ controllers: [ImportController], imports: [ + AccountModule, CacheModule, ConfigurationModule, DataGatheringModule, diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index c365c22f5..3ddd29040 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1,13 +1,15 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { Injectable } from '@nestjs/common'; -import { Order } from '@prisma/client'; import { isSameDay, parseISO } from 'date-fns'; @Injectable() export class ImportService { public constructor( + private readonly accountService: AccountService, private readonly configurationService: ConfigurationService, private readonly dataProviderService: DataProviderService, private readonly orderService: OrderService @@ -17,7 +19,7 @@ export class ImportService { orders, userId }: { - orders: Partial[]; + orders: Partial[]; userId: string; }): Promise { for (const order of orders) { @@ -32,6 +34,12 @@ export class ImportService { await this.validateOrders({ orders, userId }); + const accountIds = (await this.accountService.getAccounts(userId)).map( + (account) => { + return account.id; + } + ); + for (const { accountId, currency, @@ -44,19 +52,17 @@ export class ImportService { unitPrice } of orders) { await this.orderService.createOrder({ - accountId, - currency, - dataSource, fee, quantity, - symbol, type, unitPrice, userId, + accountId: accountIds.includes(accountId) ? accountId : undefined, date: parseISO((date)), SymbolProfile: { connectOrCreate: { create: { + currency, dataSource, symbol }, @@ -77,7 +83,7 @@ export class ImportService { orders, userId }: { - orders: Partial[]; + orders: Partial[]; userId: string; }) { if ( @@ -91,6 +97,7 @@ export class ImportService { } const existingOrders = await this.orderService.orders({ + include: { SymbolProfile: true }, orderBy: { date: 'desc' }, where: { userId } }); @@ -101,12 +108,12 @@ export class ImportService { ] of orders.entries()) { const duplicateOrder = existingOrders.find((order) => { return ( - order.currency === currency && - order.dataSource === dataSource && + order.SymbolProfile.currency === currency && + order.SymbolProfile.dataSource === dataSource && isSameDay(order.date, parseISO((date))) && order.fee === fee && order.quantity === quantity && - order.symbol === symbol && + order.SymbolProfile.symbol === symbol && order.type === type && order.unitPrice === unitPrice ); @@ -117,19 +124,19 @@ export class ImportService { } if (dataSource !== 'MANUAL') { - const result = await this.dataProviderService.get([ + const quotes = await this.dataProviderService.getQuotes([ { dataSource, symbol } ]); - if (result[symbol] === undefined) { + if (quotes[symbol] === undefined) { throw new Error( `orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } - if (result[symbol].currency !== currency) { + if (quotes[symbol].currency !== currency) { throw new Error( - `orders.${index}.currency ("${currency}") does not match with "${result[symbol].currency}"` + `orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"` ); } } diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index f13679efc..67bd62a62 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -144,7 +144,7 @@ export class InfoService { const contributors = await get(); return contributors?.length; } catch (error) { - Logger.error(error); + Logger.error(error, 'InfoService'); return undefined; } @@ -165,7 +165,7 @@ export class InfoService { const { stargazers_count } = await get(); return stargazers_count; } catch (error) { - Logger.error(error); + Logger.error(error, 'InfoService'); return undefined; } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 58a043b82..740676950 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -114,6 +114,7 @@ export class OrderController { SymbolProfile: { connectOrCreate: { create: { + currency: data.currency, dataSource: data.dataSource, symbol: data.symbol }, @@ -171,6 +172,14 @@ export class OrderController { id_userId: { id: accountId, userId: this.request.user.id } } }, + SymbolProfile: { + connect: { + dataSource_symbol: { + dataSource: data.dataSource, + symbol: data.symbol + } + } + }, User: { connect: { id: this.request.user.id } } }, where: { diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 16971ee38..ee01b3092 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -53,7 +53,13 @@ export class OrderService { } public async createOrder( - data: Prisma.OrderCreateInput & { accountId?: string; userId: string } + data: Prisma.OrderCreateInput & { + accountId?: string; + currency?: string; + dataSource?: DataSource; + symbol?: string; + userId: string; + } ): Promise { const defaultAccount = ( await this.accountService.getAccounts(data.userId) @@ -71,15 +77,13 @@ export class OrderService { }; if (data.type === 'ITEM') { - const currency = data.currency; + const currency = data.SymbolProfile.connectOrCreate.create.currency; const dataSource: DataSource = 'MANUAL'; const id = uuidv4(); const name = data.SymbolProfile.connectOrCreate.create.symbol; Account = undefined; - data.dataSource = dataSource; data.id = id; - data.symbol = null; data.SymbolProfile.connectOrCreate.create.currency = currency; data.SymbolProfile.connectOrCreate.create.dataSource = dataSource; data.SymbolProfile.connectOrCreate.create.name = name; @@ -93,29 +97,32 @@ export class OrderService { data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase(); } + await this.dataGatheringService.gatherProfileData([ + { + dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, + symbol: data.SymbolProfile.connectOrCreate.create.symbol + } + ]); + const isDraft = isAfter(data.date as Date, endOfToday()); if (!isDraft) { // Gather symbol data of order in the background, if not draft this.dataGatheringService.gatherSymbols([ { - dataSource: data.dataSource, + dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, date: data.date, symbol: data.SymbolProfile.connectOrCreate.create.symbol } ]); } - this.dataGatheringService.gatherProfileData([ - { - dataSource: data.dataSource, - symbol: data.SymbolProfile.connectOrCreate.create.symbol - } - ]); - await this.cacheService.flush(); delete data.accountId; + delete data.currency; + delete data.dataSource; + delete data.symbol; delete data.userId; const orderData: Prisma.OrderCreateInput = data; @@ -193,50 +200,60 @@ export class OrderService { value, feeInBaseCurrency: this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, userCurrency ), valueInBaseCurrency: this.exchangeRateDataService.toCurrency( value, - order.currency, + order.SymbolProfile.currency, userCurrency ) }; }); } - public async updateOrder(params: { + public async updateOrder({ + data, + where + }: { + data: Prisma.OrderUpdateInput & { + currency?: string; + dataSource?: DataSource; + symbol?: string; + }; where: Prisma.OrderWhereUniqueInput; - data: Prisma.OrderUpdateInput; }): Promise { - const { data, where } = params; - if (data.Account.connect.id_userId.id === null) { delete data.Account; } + let isDraft = false; + if (data.type === 'ITEM') { - const name = data.symbol; + const name = data.SymbolProfile.connect.dataSource_symbol.symbol; - data.symbol = null; data.SymbolProfile = { update: { name } }; - } - - const isDraft = isAfter(data.date as Date, endOfToday()); - - if (!isDraft) { - // Gather symbol data of order in the background, if not draft - this.dataGatheringService.gatherSymbols([ - { - dataSource: data.dataSource, - date: data.date, - symbol: data.symbol - } - ]); + } else { + isDraft = isAfter(data.date as Date, endOfToday()); + + if (!isDraft) { + // Gather symbol data of order in the background, if not draft + this.dataGatheringService.gatherSymbols([ + { + dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource, + date: data.date, + symbol: data.SymbolProfile.connect.dataSource_symbol.symbol + } + ]); + } } await this.cacheService.flush(); + delete data.currency; + delete data.dataSource; + delete data.symbol; + return this.prismaService.order.update({ data: { ...data, diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index fac041837..0549596ce 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -40,7 +40,7 @@ export class CurrentRateService { const today = resetHours(new Date()); promises.push( this.dataProviderService - .get(dataGatheringItems) + .getQuotes(dataGatheringItems) .then((dataResultProvider) => { const result = []; for (const dataGatheringItem of dataGatheringItems) { diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index 29550b43a..48e6038f3 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -1,8 +1,7 @@ -import { TimelinePosition } from '@ghostfolio/common/interfaces'; +import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import Big from 'big.js'; -export interface CurrentPositions { - hasErrors: boolean; +export interface CurrentPositions extends ResponseError { positions: TimelinePosition[]; grossPerformance: Big; grossPerformancePercentage: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts index 8906431fb..5dddc53fd 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts @@ -66,6 +66,7 @@ describe('PortfolioCalculatorNew', () => { expect(currentPositions).toEqual({ currentValue: new Big('0'), + errors: [], grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), hasErrors: false, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts index 230fb04ab..de0f1f0bf 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts @@ -55,6 +55,7 @@ describe('PortfolioCalculatorNew', () => { expect(currentPositions).toEqual({ currentValue: new Big('297.8'), + errors: [], grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), hasErrors: false, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index b067e3a6c..016f1bc0e 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -1,7 +1,11 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; -import { TimelinePosition } from '@ghostfolio/common/interfaces'; +import { + ResponseError, + TimelinePosition, + UniqueAsset +} from '@ghostfolio/common/interfaces'; import { Logger } from '@nestjs/common'; import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; @@ -36,7 +40,7 @@ export class PortfolioCalculatorNew { private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = true; - private static readonly ENABLE_LOGGING = true; + private static readonly ENABLE_LOGGING = false; private currency: string; private currentRateService: CurrentRateService; @@ -234,6 +238,7 @@ export class PortfolioCalculatorNew { const positions: TimelinePosition[] = []; let hasAnySymbolMetricsErrors = false; + const errors: ResponseError['errors'] = []; for (const item of lastTransactionPoint.items) { const marketValue = marketSymbolMap[todayString]?.[item.symbol]; @@ -275,12 +280,17 @@ export class PortfolioCalculatorNew { symbol: item.symbol, transactionCount: item.transactionCount }); + + if (hasErrors) { + errors.push({ dataSource: item.dataSource, symbol: item.symbol }); + } } const overall = this.calculateOverallPerformance(positions, initialValues); return { ...overall, + errors, positions, hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; @@ -450,7 +460,8 @@ export class PortfolioCalculatorNew { ); } else if (!currentPosition.quantity.eq(0)) { Logger.warn( - `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` + `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`, + 'PortfolioCalculatorNew' ); hasErrors = true; } @@ -515,7 +526,8 @@ export class PortfolioCalculatorNew { } catch (error) { Logger.error( `Failed to fetch info for date ${startDate} with exception`, - error + error, + 'PortfolioCalculatorNew' ); return null; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 1bdc03deb..2dd11e0eb 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -238,7 +238,10 @@ export class PortfolioCalculator { if (!marketSymbolMap[nextDate]?.[item.symbol]) { invalidSymbols.push(item.symbol); hasErrors = true; - Logger.warn(`Missing value for symbol ${item.symbol} at ${nextDate}`); + Logger.warn( + `Missing value for symbol ${item.symbol} at ${nextDate}`, + 'PortfolioCalculator' + ); continue; } let lastInvestment: Big = new Big(0); @@ -270,7 +273,8 @@ export class PortfolioCalculator { invalidSymbols.push(item.symbol); hasErrors = true; Logger.warn( - `Missing value for symbol ${item.symbol} at ${currentDate}` + `Missing value for symbol ${item.symbol} at ${currentDate}`, + 'PortfolioCalculator' ); continue; } @@ -514,7 +518,8 @@ export class PortfolioCalculator { ); } else if (!currentPosition.quantity.eq(0)) { Logger.warn( - `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` + `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`, + 'PortfolioCalculator' ); hasErrors = true; } @@ -581,7 +586,8 @@ export class PortfolioCalculator { } catch (error) { Logger.error( `Failed to fetch info for date ${startDate} with exception`, - error + error, + 'PortfolioCalculator' ); return null; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 38a083c75..fd11334d9 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -14,7 +14,7 @@ import { PortfolioChart, PortfolioDetails, PortfolioInvestments, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, PortfolioSummary @@ -33,6 +33,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { ViewMode } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; @@ -203,16 +204,18 @@ export class PortfolioController { @Get('performance') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPerformance( @Headers('impersonation-id') impersonationId: string, @Query('range') range - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const performanceInformation = await this.portfolioServiceStrategy .get() .getPerformance(impersonationId, range); if ( impersonationId || + this.request.user.Settings.viewMode === ViewMode.ZEN || this.userService.isRestrictedView(this.request.user) ) { performanceInformation.performance = nullifyValuesInObject( diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index adeea5c91..b34a9206f 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -24,7 +24,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, PortfolioDetails, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioReport, PortfolioSummary, Position, @@ -100,15 +100,22 @@ export class PortfolioServiceNew { } } + const value = details.accounts[account.id]?.current ?? 0; + const result = { ...account, transactionCount, - convertedBalance: this.exchangeRateDataService.toCurrency( + value, + balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( account.balance, account.currency, userCurrency ), - value: details.accounts[account.id]?.current ?? 0 + valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + value, + account.currency, + userCurrency + ) }; delete result.Order; @@ -119,17 +126,26 @@ export class PortfolioServiceNew { public async getAccountsWithAggregations(aUserId: string): Promise { const accounts = await this.getAccounts(aUserId); - let totalBalance = 0; - let totalValue = 0; + let totalBalanceInBaseCurrency = new Big(0); + let totalValueInBaseCurrency = new Big(0); let transactionCount = 0; for (const account of accounts) { - totalBalance += account.convertedBalance; - totalValue += account.value; + totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus( + account.balanceInBaseCurrency + ); + totalValueInBaseCurrency = totalValueInBaseCurrency.plus( + account.valueInBaseCurrency + ); transactionCount += account.transactionCount; } - return { accounts, totalBalance, totalValue, transactionCount }; + return { + accounts, + transactionCount, + totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), + totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber() + }; } public async getInvestments( @@ -293,13 +309,11 @@ export class PortfolioServiceNew { orders: portfolioOrders }); - if (transactionPoints?.length <= 0) { - return { accounts: {}, holdings: {}, hasErrors: false }; - } - portfolioCalculator.setTransactionPoints(transactionPoints); - const portfolioStart = parseDate(transactionPoints[0].date); + const portfolioStart = parseDate( + transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) + ); const startDate = this.getStartDate(aDateRange, portfolioStart); const currentPositions = await portfolioCalculator.getCurrentPositions( startDate @@ -312,9 +326,11 @@ export class PortfolioServiceNew { const holdings: PortfolioDetails['holdings'] = {}; const totalInvestment = currentPositions.totalInvestment.plus( - cashDetails.balance + cashDetails.balanceInBaseCurrency + ); + const totalValue = currentPositions.currentValue.plus( + cashDetails.balanceInBaseCurrency ); - const totalValue = currentPositions.currentValue.plus(cashDetails.balance); const dataGatheringItems = currentPositions.positions.map((position) => { return { @@ -327,7 +343,7 @@ export class PortfolioServiceNew { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -358,7 +374,6 @@ export class PortfolioServiceNew { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -435,7 +450,7 @@ export class PortfolioServiceNew { }; } - const positionCurrency = orders[0].currency; + const positionCurrency = orders[0].SymbolProfile.currency; const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ aSymbol ]); @@ -445,13 +460,13 @@ export class PortfolioServiceNew { return order.type === 'BUY' || order.type === 'SELL'; }) .map((order) => ({ - currency: order.currency, - dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big(order.fee), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), - symbol: order.symbol, + symbol: order.SymbolProfile.symbol, type: order.type, unitPrice: new Big(order.unitPrice) })); @@ -578,7 +593,7 @@ export class PortfolioServiceNew { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -679,7 +694,7 @@ export class PortfolioServiceNew { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -715,7 +730,7 @@ export class PortfolioServiceNew { public async getPerformance( aImpersonationId: string, aDateRange: DateRange = 'max' - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const { portfolioOrders, transactionPoints } = @@ -761,6 +776,7 @@ export class PortfolioServiceNew { currentPositions.netPerformancePercentage.toNumber(); return { + errors: currentPositions.errors, hasErrors: currentPositions.hasErrors || hasErrors, performance: { currentGrossPerformance, @@ -870,7 +886,7 @@ export class PortfolioServiceNew { const performanceInformation = await this.getPerformance(aImpersonationId); - const { balance } = await this.accountService.getCashDetails( + const { balanceInBaseCurrency } = await this.accountService.getCashDetails( userId, userCurrency ); @@ -888,7 +904,7 @@ export class PortfolioServiceNew { const committedFunds = new Big(totalBuy).minus(totalSell); - const netWorth = new Big(balance) + const netWorth = new Big(balanceInBaseCurrency) .plus(performanceInformation.performance.currentValue) .plus(items) .toNumber(); @@ -918,7 +934,7 @@ export class PortfolioServiceNew { netWorth, totalBuy, totalSell, - cash: balance, + cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; @@ -1007,7 +1023,7 @@ export class PortfolioServiceNew { .map((order) => { return this.exchangeRateDataService.toCurrency( new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1026,7 +1042,7 @@ export class PortfolioServiceNew { .map((order) => { return this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1048,7 +1064,7 @@ export class PortfolioServiceNew { .map((order) => { return this.exchangeRateDataService.toCurrency( new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1101,24 +1117,24 @@ export class PortfolioServiceNew { } const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ - currency: order.currency, - dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big( this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, userCurrency ) ), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), - symbol: order.symbol, + symbol: order.SymbolProfile.symbol, type: order.type, unitPrice: new Big( this.exchangeRateDataService.toCurrency( order.unitPrice, - order.currency, + order.SymbolProfile.currency, userCurrency ) ) @@ -1154,22 +1170,18 @@ export class PortfolioServiceNew { return accountId === account.id; }); - const convertedBalance = this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ); accounts[account.id] = { - balance: convertedBalance, + balance: account.balance, currency: account.currency, - current: convertedBalance, + current: account.balance, name: account.name, - original: convertedBalance + original: account.balance }; for (const order of ordersByAccount) { let currentValueOfSymbol = - order.quantity * portfolioItemsNow[order.symbol].marketPrice; + order.quantity * + portfolioItemsNow[order.SymbolProfile.symbol].marketPrice; let originalValueOfSymbol = order.quantity * order.unitPrice; if (order.type === 'SELL') { @@ -1219,7 +1231,7 @@ export class PortfolioServiceNew { .map((order) => { return this.exchangeRateDataService.toCurrency( order.quantity * order.unitPrice, - order.currency, + order.SymbolProfile.currency, currency ); }) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0a164708c..f3df18c30 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -25,7 +25,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, PortfolioDetails, - PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioReport, PortfolioSummary, Position, @@ -99,15 +99,22 @@ export class PortfolioService { } } + const value = details.accounts[account.id]?.current ?? 0; + const result = { ...account, transactionCount, - convertedBalance: this.exchangeRateDataService.toCurrency( + value, + balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( account.balance, account.currency, userCurrency ), - value: details.accounts[account.id]?.current ?? 0 + valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + value, + account.currency, + userCurrency + ) }; delete result.Order; @@ -118,17 +125,26 @@ export class PortfolioService { public async getAccountsWithAggregations(aUserId: string): Promise { const accounts = await this.getAccounts(aUserId); - let totalBalance = 0; - let totalValue = 0; + let totalBalanceInBaseCurrency = new Big(0); + let totalValueInBaseCurrency = new Big(0); let transactionCount = 0; for (const account of accounts) { - totalBalance += account.convertedBalance; - totalValue += account.value; + totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus( + account.balanceInBaseCurrency + ); + totalValueInBaseCurrency = totalValueInBaseCurrency.plus( + account.valueInBaseCurrency + ); transactionCount += account.transactionCount; } - return { accounts, totalBalance, totalValue, transactionCount }; + return { + accounts, + transactionCount, + totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), + totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber() + }; } public async getInvestments( @@ -281,13 +297,11 @@ export class PortfolioService { userId }); - if (transactionPoints?.length <= 0) { - return { accounts: {}, holdings: {}, hasErrors: false }; - } - portfolioCalculator.setTransactionPoints(transactionPoints); - const portfolioStart = parseDate(transactionPoints[0].date); + const portfolioStart = parseDate( + transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) + ); const startDate = this.getStartDate(aDateRange, portfolioStart); const currentPositions = await portfolioCalculator.getCurrentPositions( startDate @@ -300,9 +314,11 @@ export class PortfolioService { const holdings: PortfolioDetails['holdings'] = {}; const totalInvestment = currentPositions.totalInvestment.plus( - cashDetails.balance + cashDetails.balanceInBaseCurrency + ); + const totalValue = currentPositions.currentValue.plus( + cashDetails.balanceInBaseCurrency ); - const totalValue = currentPositions.currentValue.plus(cashDetails.balance); const dataGatheringItems = currentPositions.positions.map((position) => { return { @@ -315,7 +331,7 @@ export class PortfolioService { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -346,7 +362,6 @@ export class PortfolioService { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -423,7 +438,7 @@ export class PortfolioService { }; } - const positionCurrency = orders[0].currency; + const positionCurrency = orders[0].SymbolProfile.currency; const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ aSymbol ]); @@ -433,13 +448,13 @@ export class PortfolioService { return order.type === 'BUY' || order.type === 'SELL'; }) .map((order) => ({ - currency: order.currency, - dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big(order.fee), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), - symbol: order.symbol, + symbol: order.SymbolProfile.symbol, type: order.type, unitPrice: new Big(order.unitPrice) })); @@ -552,9 +567,10 @@ export class PortfolioService { SymbolProfile, transactionCount, averagePrice: averagePrice.toNumber(), - grossPerformancePercent: position.grossPerformancePercentage.toNumber(), + grossPerformancePercent: + position.grossPerformancePercentage?.toNumber(), historicalData: historicalDataArray, - netPerformancePercent: position.netPerformancePercentage.toNumber(), + netPerformancePercent: position.netPerformancePercentage?.toNumber(), quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice).toNumber(), @@ -563,7 +579,7 @@ export class PortfolioService { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -660,7 +676,7 @@ export class PortfolioService { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -696,7 +712,7 @@ export class PortfolioService { public async getPerformance( aImpersonationId: string, aDateRange: DateRange = 'max' - ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( @@ -848,7 +864,7 @@ export class PortfolioService { const performanceInformation = await this.getPerformance(aImpersonationId); - const { balance } = await this.accountService.getCashDetails( + const { balanceInBaseCurrency } = await this.accountService.getCashDetails( userId, userCurrency ); @@ -866,7 +882,7 @@ export class PortfolioService { const committedFunds = new Big(totalBuy).minus(totalSell); - const netWorth = new Big(balance) + const netWorth = new Big(balanceInBaseCurrency) .plus(performanceInformation.performance.currentValue) .plus(items) .toNumber(); @@ -882,7 +898,7 @@ export class PortfolioService { totalSell, annualizedPerformancePercent: performanceInformation.performance.annualizedPerformancePercent, - cash: balance, + cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; @@ -971,7 +987,7 @@ export class PortfolioService { .map((order) => { return this.exchangeRateDataService.toCurrency( new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -990,7 +1006,7 @@ export class PortfolioService { .map((order) => { return this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1012,7 +1028,7 @@ export class PortfolioService { .map((order) => { return this.exchangeRateDataService.toCurrency( new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1064,24 +1080,24 @@ export class PortfolioService { } const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ - currency: order.currency, - dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big( this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, userCurrency ) ), name: order.SymbolProfile?.name, quantity: new Big(order.quantity), - symbol: order.symbol, + symbol: order.SymbolProfile.symbol, type: order.type, unitPrice: new Big( this.exchangeRateDataService.toCurrency( order.unitPrice, - order.currency, + order.SymbolProfile.currency, userCurrency ) ) @@ -1113,22 +1129,18 @@ export class PortfolioService { return accountId === account.id; }); - const convertedBalance = this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ); accounts[account.id] = { - balance: convertedBalance, + balance: account.balance, currency: account.currency, - current: convertedBalance, + current: account.balance, name: account.name, - original: convertedBalance + original: account.balance }; for (const order of ordersByAccount) { let currentValueOfSymbol = - order.quantity * portfolioItemsNow[order.symbol].marketPrice; + order.quantity * + portfolioItemsNow[order.SymbolProfile.symbol].marketPrice; let originalValueOfSymbol = order.quantity * order.unitPrice; if (order.type === 'SELL') { @@ -1178,7 +1190,7 @@ export class PortfolioService { .map((order) => { return this.exchangeRateDataService.toCurrency( order.quantity * order.unitPrice, - order.currency, + order.SymbolProfile.currency, currency ); }) diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 1f68c8f72..aabc46d24 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -46,22 +46,25 @@ export class SubscriptionController { ((await this.propertyService.getByKey(PROPERTY_COUPONS)) as Coupon[]) ?? []; - const isValid = coupons.some((coupon) => { - return coupon.code === couponCode; + const coupon = coupons.find((currentCoupon) => { + return currentCoupon.code === couponCode; }); - if (!isValid) { + if (coupon === undefined) { throw new HttpException( getReasonPhrase(StatusCodes.BAD_REQUEST), StatusCodes.BAD_REQUEST ); } - await this.subscriptionService.createSubscription(this.request.user.id); + await this.subscriptionService.createSubscription({ + duration: coupon.duration, + userId: this.request.user.id + }); // Destroy coupon - coupons = coupons.filter((coupon) => { - return coupon.code !== couponCode; + coupons = coupons.filter((currentCoupon) => { + return currentCoupon.code !== couponCode; }); await this.propertyService.put({ key: PROPERTY_COUPONS, @@ -69,7 +72,8 @@ export class SubscriptionController { }); Logger.log( - `Subscription for user '${this.request.user.id}' has been created with coupon` + `Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`, + 'SubscriptionController' ); return { @@ -84,7 +88,10 @@ export class SubscriptionController { req.query.checkoutSessionId ); - Logger.log(`Subscription for user '${userId}' has been created via Stripe`); + Logger.log( + `Subscription for user '${userId}' has been created via Stripe`, + 'SubscriptionController' + ); res.redirect(`${this.configurationService.get('ROOT_URL')}/account`); } @@ -101,7 +108,7 @@ export class SubscriptionController { userId: this.request.user.id }); } catch (error) { - Logger.error(error); + Logger.error(error, 'SubscriptionController'); throw new HttpException( getReasonPhrase(StatusCodes.BAD_REQUEST), diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 97e910e14..f7db04728 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -2,8 +2,9 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable, Logger } from '@nestjs/common'; -import { Subscription, User } from '@prisma/client'; -import { addDays, isBefore } from 'date-fns'; +import { Subscription } from '@prisma/client'; +import { addMilliseconds, isBefore } from 'date-fns'; +import ms, { StringValue } from 'ms'; import Stripe from 'stripe'; @Injectable() @@ -64,13 +65,19 @@ export class SubscriptionService { }; } - public async createSubscription(aUserId: string) { + public async createSubscription({ + duration = '1 year', + userId + }: { + duration?: StringValue; + userId: string; + }) { await this.prismaService.subscription.create({ data: { - expiresAt: addDays(new Date(), 365), + expiresAt: addMilliseconds(new Date(), ms(duration)), User: { connect: { - id: aUserId + id: userId } } } @@ -83,7 +90,7 @@ export class SubscriptionService { aCheckoutSessionId ); - await this.createSubscription(session.client_reference_id); + await this.createSubscription({ userId: session.client_reference_id }); await this.stripe.customers.update(session.customer as string, { description: session.client_reference_id @@ -91,7 +98,7 @@ export class SubscriptionService { return session.client_reference_id; } catch (error) { - Logger.error(error); + Logger.error(error, 'SubscriptionService'); } } diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 37b1c5864..c45f45cd1 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -27,8 +27,10 @@ export class SymbolService { dataGatheringItem: IDataGatheringItem; includeHistoricalData?: number; }): Promise { - const response = await this.dataProviderService.get([dataGatheringItem]); - const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {}; + const quotes = await this.dataProviderService.getQuotes([ + dataGatheringItem + ]); + const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {}; if (dataGatheringItem.dataSource && marketPrice) { let historicalData: HistoricalDataItem[] = []; @@ -93,7 +95,7 @@ export class SymbolService { results.items = items; return results; } catch (error) { - Logger.error(error); + Logger.error(error, 'SymbolService'); throw error; } diff --git a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts index 720f02b67..6c96b3965 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -32,7 +32,6 @@ export class TransformDataSourceInResponseInterceptor activity.SymbolProfile.dataSource = encodeDataSource( activity.SymbolProfile.dataSource ); - activity.dataSource = encodeDataSource(activity.dataSource); return activity; }); } @@ -41,6 +40,14 @@ export class TransformDataSourceInResponseInterceptor data.dataSource = encodeDataSource(data.dataSource); } + if (data.errors) { + for (const error of data.errors) { + if (error.dataSource) { + error.dataSource = encodeDataSource(error.dataSource); + } + } + } + if (data.holdings) { for (const symbol of Object.keys(data.holdings)) { if (data.holdings[symbol].dataSource) { @@ -58,13 +65,6 @@ export class TransformDataSourceInResponseInterceptor }); } - if (data.orders) { - data.orders.map((order) => { - order.dataSource = encodeDataSource(order.dataSource); - return order; - }); - } - if (data.positions) { data.positions.map((position) => { position.dataSource = encodeDataSource(position.dataSource); diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 81c9c884d..c3a7f64c7 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -4,6 +4,7 @@ import { PROPERTY_LOCKED_DATA_GATHERING } from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { @@ -39,7 +40,7 @@ export class DataGatheringService { const isDataGatheringNeeded = await this.isDataGatheringNeeded(); if (isDataGatheringNeeded) { - Logger.log('7d data gathering has been started.'); + Logger.log('7d data gathering has been started.', 'DataGatheringService'); console.time('data-gathering-7d'); await this.prismaService.property.create({ @@ -63,7 +64,7 @@ export class DataGatheringService { where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { - Logger.error(error); + Logger.error(error, 'DataGatheringService'); } await this.prismaService.property.delete({ @@ -72,7 +73,10 @@ export class DataGatheringService { } }); - Logger.log('7d data gathering has been completed.'); + Logger.log( + '7d data gathering has been completed.', + 'DataGatheringService' + ); console.timeEnd('data-gathering-7d'); } } @@ -83,7 +87,10 @@ export class DataGatheringService { }); if (!isDataGatheringLocked) { - Logger.log('Max data gathering has been started.'); + Logger.log( + 'Max data gathering has been started.', + 'DataGatheringService' + ); console.time('data-gathering-max'); await this.prismaService.property.create({ @@ -107,7 +114,7 @@ export class DataGatheringService { where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { - Logger.error(error); + Logger.error(error, 'DataGatheringService'); } await this.prismaService.property.delete({ @@ -116,24 +123,24 @@ export class DataGatheringService { } }); - Logger.log('Max data gathering has been completed.'); + Logger.log( + 'Max data gathering has been completed.', + 'DataGatheringService' + ); console.timeEnd('data-gathering-max'); } } - public async gatherSymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async gatherSymbol({ dataSource, symbol }: UniqueAsset) { const isDataGatheringLocked = await this.prismaService.property.findUnique({ where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); if (!isDataGatheringLocked) { - Logger.log(`Symbol data gathering for ${symbol} has been started.`); + Logger.log( + `Symbol data gathering for ${symbol} has been started.`, + 'DataGatheringService' + ); console.time('data-gathering-symbol'); await this.prismaService.property.create({ @@ -164,7 +171,7 @@ export class DataGatheringService { where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { - Logger.error(error); + Logger.error(error, 'DataGatheringService'); } await this.prismaService.property.delete({ @@ -173,7 +180,10 @@ export class DataGatheringService { } }); - Logger.log(`Symbol data gathering for ${symbol} has been completed.`); + Logger.log( + `Symbol data gathering for ${symbol} has been completed.`, + 'DataGatheringService' + ); console.timeEnd('data-gathering-symbol'); } } @@ -210,42 +220,55 @@ export class DataGatheringService { }); } } catch (error) { - Logger.error(error); + Logger.error(error, 'DataGatheringService'); } finally { return undefined; } } public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) { - Logger.log('Profile data gathering has been started.'); + Logger.log( + 'Profile data gathering has been started.', + 'DataGatheringService' + ); console.time('data-gathering-profile'); - let dataGatheringItems = aDataGatheringItems; + let dataGatheringItems = aDataGatheringItems?.filter( + (dataGatheringItem) => { + return dataGatheringItem.dataSource !== 'MANUAL'; + } + ); if (!dataGatheringItems) { dataGatheringItems = await this.getSymbolsProfileData(); } - const currentData = await this.dataProviderService.get(dataGatheringItems); + const assetProfiles = await this.dataProviderService.getAssetProfiles( + dataGatheringItems + ); const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( dataGatheringItems.map(({ symbol }) => { return symbol; }) ); - for (const [symbol, response] of Object.entries(currentData)) { + for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { const symbolMapping = symbolProfiles.find((symbolProfile) => { return symbolProfile.symbol === symbol; })?.symbolMapping; for (const dataEnhancer of this.dataEnhancers) { try { - currentData[symbol] = await dataEnhancer.enhance({ - response, + assetProfiles[symbol] = await dataEnhancer.enhance({ + response: assetProfile, symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol }); } catch (error) { - Logger.error(`Failed to enhance data for symbol ${symbol}`, error); + Logger.error( + `Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`, + error, + 'DataGatheringService' + ); } } @@ -256,8 +279,9 @@ export class DataGatheringService { currency, dataSource, name, - sectors - } = currentData[symbol]; + sectors, + url + } = assetProfiles[symbol]; try { await this.prismaService.symbolProfile.upsert({ @@ -269,7 +293,8 @@ export class DataGatheringService { dataSource, name, sectors, - symbol + symbol, + url }, update: { assetClass, @@ -277,7 +302,8 @@ export class DataGatheringService { countries, currency, name, - sectors + sectors, + url }, where: { dataSource_symbol: { @@ -287,11 +313,18 @@ export class DataGatheringService { } }); } catch (error) { - Logger.error(`${symbol}: ${error?.meta?.cause}`); + Logger.error( + `${symbol}: ${error?.meta?.cause}`, + error, + 'DataGatheringService' + ); } } - Logger.log('Profile data gathering has been completed.'); + Logger.log( + 'Profile data gathering has been completed.', + 'DataGatheringService' + ); console.timeEnd('data-gathering-profile'); } @@ -300,6 +333,10 @@ export class DataGatheringService { let symbolCounter = 0; for (const { dataSource, date, symbol } of aSymbolsWithStartDate) { + if (dataSource === 'MANUAL') { + continue; + } + this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length; try { @@ -347,10 +384,11 @@ export class DataGatheringService { } catch {} } else { Logger.warn( - `Failed to gather data for symbol ${symbol} at ${format( + `Failed to gather data for symbol ${symbol} from ${dataSource} at ${format( currentDate, DATE_FORMAT - )}.` + )}.`, + 'DataGatheringService' ); } @@ -366,14 +404,15 @@ export class DataGatheringService { } } catch (error) { hasError = true; - Logger.error(error); + Logger.error(error, 'DataGatheringService'); } if (symbolCounter > 0 && symbolCounter % 100 === 0) { Logger.log( `Data gathering progress: ${( this.dataGatheringProgress * 100 - ).toFixed(2)}%` + ).toFixed(2)}%`, + 'DataGatheringService' ); } @@ -463,7 +502,7 @@ export class DataGatheringService { } public async reset() { - Logger.log('Data gathering has been reset.'); + Logger.log('Data gathering has been reset.', 'DataGatheringService'); await this.prismaService.property.deleteMany({ where: { @@ -538,19 +577,24 @@ export class DataGatheringService { } private async getSymbolsProfileData(): Promise { - const distinctOrders = await this.prismaService.order.findMany({ - distinct: ['symbol'], - orderBy: [{ symbol: 'asc' }], - select: { dataSource: true, symbol: true } + const symbolProfiles = await this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }] }); - return distinctOrders.filter((distinctOrder) => { - return ( - distinctOrder.dataSource !== DataSource.GHOSTFOLIO && - distinctOrder.dataSource !== DataSource.MANUAL && - distinctOrder.dataSource !== DataSource.RAKUTEN - ); - }); + return symbolProfiles + .filter((symbolProfile) => { + return ( + symbolProfile.dataSource !== DataSource.GHOSTFOLIO && + symbolProfile.dataSource !== DataSource.MANUAL && + symbolProfile.dataSource !== DataSource.RAKUTEN + ); + }) + .map((symbolProfile) => { + return { + dataSource: symbolProfile.dataSource, + symbol: symbolProfile.symbol + }; + }); } private async isDataGatheringNeeded() { diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 838f1ae6e..bff966fe3 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -1,16 +1,16 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} 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 { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { isAfter, isBefore, parse } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse -} from '../../interfaces/interfaces'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; @Injectable() @@ -29,25 +29,23 @@ export class AlphaVantageService implements DataProviderInterface { return !!this.configurationService.get('ALPHA_VANTAGE_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - - const symbol = aSymbols[0]; + const symbol = aSymbol; try { const historicalData: { @@ -78,7 +76,7 @@ export class AlphaVantageService implements DataProviderInterface { return response; } catch (error) { - Logger.error(error, symbol); + Logger.error(error, 'AlphaVantageService'); return {}; } @@ -88,6 +86,12 @@ export class AlphaVantageService implements DataProviderInterface { return DataSource.ALPHA_VANTAGE; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const result = await this.alphaVantage.data.search(aQuery); diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index a469e57a5..f61297368 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -1,5 +1,7 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; -import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { Country } from '@ghostfolio/common/interfaces/country.interface'; +import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; +import { SymbolProfile } from '@prisma/client'; import bent from 'bent'; const getJSON = bent('json'); @@ -21,9 +23,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise { + }): Promise> { if ( !(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF') ) { @@ -40,7 +42,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { ); }); - if (!response.countries || response.countries.length === 0) { + if ( + !response.countries || + (response.countries as unknown as Country[]).length === 0 + ) { response.countries = []; for (const [name, value] of Object.entries(holdings.countries)) { let countryCode: string; @@ -65,7 +70,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { } } - if (!response.sectors || response.sectors.length === 0) { + if ( + !response.sectors || + (response.sectors as unknown as Sector[]).length === 0 + ) { response.sectors = []; for (const [name, value] of Object.entries(holdings.sectors)) { response.sectors.push({ diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 6ffd5b2dd..fd44f2426 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -10,7 +10,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { format, isValid } from 'date-fns'; import { groupBy, isEmpty } from 'lodash'; @@ -23,42 +23,6 @@ export class DataProviderService { private readonly prismaService: PrismaService ) {} - public async get(items: IDataGatheringItem[]): Promise<{ - [symbol: string]: IDataProviderResponse; - }> { - const response: { - [symbol: string]: IDataProviderResponse; - } = {}; - - const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); - - const promises = []; - - for (const [dataSource, dataGatheringItems] of Object.entries( - itemsGroupedByDataSource - )) { - const symbols = dataGatheringItems.map((dataGatheringItem) => { - return dataGatheringItem.symbol; - }); - - const promise = Promise.resolve( - this.getDataProvider(DataSource[dataSource]).get(symbols) - ); - - promises.push( - promise.then((result) => { - for (const [symbol, dataProviderResponse] of Object.entries(result)) { - response[symbol] = dataProviderResponse; - } - }) - ); - } - - await Promise.all(promises); - - return response; - } - public async getHistorical( aItems: IDataGatheringItem[], aGranularity: Granularity = 'month', @@ -118,7 +82,7 @@ export class DataProviderService { return r; }, {}); } catch (error) { - Logger.error(error); + Logger.error(error, 'DataProviderService'); } finally { return response; } @@ -144,7 +108,7 @@ export class DataProviderService { if (dataProvider.canHandle(symbol)) { promises.push( dataProvider - .getHistorical([symbol], undefined, from, to) + .getHistorical(symbol, undefined, from, to) .then((data) => ({ data: data?.[symbol], symbol })) ); } @@ -158,6 +122,82 @@ export class DataProviderService { return result; } + public getPrimaryDataSource(): DataSource { + return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; + } + + public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: Partial; + }> { + const response: { + [symbol: string]: Partial; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + for (const symbol of symbols) { + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol) + ); + + promises.push( + promise.then((symbolProfile) => { + response[symbol] = symbolProfile; + }) + ); + } + } + + await Promise.all(promises); + + return response; + } + + public async getQuotes(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: IDataProviderResponse; + }> { + const response: { + [symbol: string]: IDataProviderResponse; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getQuotes(symbols) + ); + + promises.push( + promise.then((result) => { + for (const [symbol, dataProviderResponse] of Object.entries(result)) { + response[symbol] = dataProviderResponse; + } + }) + ); + } + + await Promise.all(promises); + + return response; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const promises: Promise<{ items: LookupItem[] }>[] = []; let lookupItems: LookupItem[] = []; @@ -184,10 +224,6 @@ export class DataProviderService { }; } - public getPrimaryDataSource(): DataSource { - return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; - } - private getDataProvider(providerName: DataSource) { for (const dataProviderInterface of this.dataProviderInterfaces) { if (dataProviderInterface.getName() === providerName) { diff --git a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts index a34f7cf92..35c53bc7a 100644 --- a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts @@ -7,14 +7,10 @@ import { } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { - DATE_FORMAT, - getYesterday, - isGhostfolioScraperApiSymbol -} from '@ghostfolio/common/helper'; +import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import * as cheerio from 'cheerio'; import { format } from 'date-fns'; @@ -29,60 +25,28 @@ export class GhostfolioScraperApiService implements DataProviderInterface { ) {} public canHandle(symbol: string) { - return isGhostfolioScraperApiSymbol(symbol); + return true; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const [symbol] = aSymbols; - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( - [symbol] - ); - - const { marketPrice } = await this.prismaService.marketData.findFirst({ - orderBy: { - date: 'desc' - }, - where: { - symbol - } - }); - - return { - [symbol]: { - marketPrice, - currency: symbolProfile?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - } - }; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const [symbol] = aSymbols; + const symbol = aSymbol; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [symbol] ); @@ -105,7 +69,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { } }; } catch (error) { - Logger.error(error); + Logger.error(error, 'GhostfolioScraperApiService'); } return {}; @@ -115,6 +79,43 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return DataSource.GHOSTFOLIO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const [symbol] = aSymbols; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [symbol] + ); + + const { marketPrice } = await this.prismaService.marketData.findFirst({ + orderBy: { + date: 'desc' + }, + where: { + symbol + } + }); + + return { + [symbol]: { + marketPrice, + currency: symbolProfile?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + } + }; + } catch (error) { + Logger.error(error, 'GhostfolioScraperApiService'); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index fff9db21e..16e18f529 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -11,7 +11,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { format } from 'date-fns'; import { GoogleSpreadsheet } from 'google-spreadsheet'; @@ -27,65 +27,24 @@ export class GoogleSheetsService implements DataProviderInterface { return true; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( - aSymbols - ); - - const sheet = await this.getSheet({ - sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), - symbol: 'Overview' - }); - - const rows = await sheet.getRows(); - - for (const row of rows) { - const marketPrice = parseFloat(row['marketPrice']); - const symbol = row['symbol']; - - if (aSymbols.includes(symbol)) { - response[symbol] = { - marketPrice, - currency: symbolProfiles.find((symbolProfile) => { - return symbolProfile.symbol === symbol; - })?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - }; - } - } - - return response; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const [symbol] = aSymbols; + const symbol = aSymbol; const sheet = await this.getSheet({ symbol, @@ -113,7 +72,7 @@ export class GoogleSheetsService implements DataProviderInterface { [symbol]: historicalData }; } catch (error) { - Logger.error(error); + Logger.error(error, 'GoogleSheetsService'); } return {}; @@ -123,6 +82,51 @@ export class GoogleSheetsService implements DataProviderInterface { return DataSource.GOOGLE_SHEETS; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( + aSymbols + ); + + const sheet = await this.getSheet({ + sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), + symbol: 'Overview' + }); + + const rows = await sheet.getRows(); + + for (const row of rows) { + const marketPrice = parseFloat(row['marketPrice']); + const symbol = row['symbol']; + + if (aSymbols.includes(symbol)) { + response[symbol] = { + marketPrice, + currency: symbolProfiles.find((symbolProfile) => { + return symbolProfile.symbol === symbol; + })?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + }; + } + } + + return response; + } catch (error) { + Logger.error(error, 'GoogleSheetsService'); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { diff --git a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts index 26585b320..4e5ce8cba 100644 --- a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts @@ -1,13 +1,13 @@ -import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { SymbolProfile } from '@prisma/client'; export interface DataEnhancerInterface { enhance({ response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise; + }): Promise>; getName(): string; } diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index c5cf4c330..16cf44603 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts @@ -4,23 +4,27 @@ import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; export interface DataProviderInterface { canHandle(symbol: string): boolean; - get(aSymbols: string[]): Promise<{ [symbol: string]: IDataProviderResponse }>; + getAssetProfile(aSymbol: string): Promise>; getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity, from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; - }>; + }>; // TODO: Return only one symbol getName(): DataSource; + getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }>; + search(aQuery: string): Promise<{ items: LookupItem[] }>; } diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index 3a486f897..edcdd2cde 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; @Injectable() export class ManualService implements DataProviderInterface { @@ -16,14 +16,16 @@ export class ManualService implements DataProviderInterface { return false; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date @@ -37,6 +39,12 @@ export class ManualService implements DataProviderInterface { return DataSource.MANUAL; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } diff --git a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts index bdfc147dd..a15636956 100644 --- a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts @@ -1,21 +1,20 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { DATE_FORMAT, getToday, getYesterday } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import { format, subMonths, subWeeks, subYears } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; - @Injectable() export class RakutenRapidApiService implements DataProviderInterface { public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index'; @@ -29,50 +28,24 @@ export class RakutenRapidApiService implements DataProviderInterface { return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const symbol = aSymbols[0]; - - if (symbol === ghostfolioFearAndGreedIndexSymbol) { - const fgi = await this.getFearAndGreedIndex(); - - return { - [ghostfolioFearAndGreedIndexSymbol]: { - currency: undefined, - dataSource: this.getName(), - marketPrice: fgi.now.value, - marketState: MarketState.open, - name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME - } - }; - } - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const symbol = aSymbols[0]; + const symbol = aSymbol; if (symbol === ghostfolioFearAndGreedIndexSymbol) { const fgi = await this.getFearAndGreedIndex(); @@ -129,6 +102,35 @@ export class RakutenRapidApiService implements DataProviderInterface { return DataSource.RAKUTEN; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const symbol = aSymbols[0]; + + if (symbol === ghostfolioFearAndGreedIndexSymbol) { + const fgi = await this.getFearAndGreedIndex(); + + return { + [ghostfolioFearAndGreedIndexSymbol]: { + currency: undefined, + dataSource: this.getName(), + marketPrice: fgi.now.value, + marketState: MarketState.open + } + }; + } + } catch (error) { + Logger.error(error, 'RakutenRapidApiService'); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } @@ -158,7 +160,7 @@ export class RakutenRapidApiService implements DataProviderInterface { const { fgi } = await get(); return fgi; } catch (error) { - Logger.error(error); + Logger.error(error, 'RakutenRapidApiService'); return undefined; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts b/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts deleted file mode 100644 index d41a43d39..000000000 --- a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface IYahooFinanceHistoricalResponse { - adjClose: number; - close: number; - date: Date; - high: number; - low: number; - open: number; - symbol: string; - volume: number; -} - -export interface IYahooFinanceQuoteResponse { - price: IYahooFinancePrice; - summaryProfile: IYahooFinanceSummaryProfile; -} - -export interface IYahooFinancePrice { - currency: string; - exchangeName: string; - longName: string; - marketState: string; - quoteType: string; - regularMarketPrice: number; - shortName: string; -} - -export interface IYahooFinanceSummaryProfile { - country?: string; - industry?: string; - sector?: string; - website?: string; -} diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 2b4fe8f92..95a14c1ab 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -1,31 +1,30 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; import * as bent from 'bent'; import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; -import * as yahooFinance from 'yahoo-finance'; - -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; -import { - IYahooFinanceHistoricalResponse, - IYahooFinancePrice, - IYahooFinanceQuoteResponse -} from './interfaces/interfaces'; +import yahooFinance from 'yahoo-finance2'; @Injectable() export class YahooFinanceService implements DataProviderInterface { - private yahooFinanceHostname = 'https://query1.finance.yahoo.com'; + private readonly yahooFinanceHostname = 'https://query1.finance.yahoo.com'; public constructor( private readonly cryptocurrencyService: CryptocurrencyService @@ -73,145 +72,114 @@ export class YahooFinanceService implements DataProviderInterface { return aSymbol; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - const yahooFinanceSymbols = aSymbols.map((symbol) => - this.convertToYahooFinanceSymbol(symbol) - ); + public async getAssetProfile( + aSymbol: string + ): Promise> { + const response: Partial = {}; try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const data: { - [symbol: string]: IYahooFinanceQuoteResponse; - } = await yahooFinance.quote({ - modules: ['price', 'summaryProfile'], - symbols: yahooFinanceSymbols + const symbol = this.convertToYahooFinanceSymbol(aSymbol); + const assetProfile = await yahooFinance.quoteSummary(symbol, { + modules: ['price', 'summaryProfile'] }); - for (const [yahooFinanceSymbol, value] of Object.entries(data)) { - // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - - const { assetClass, assetSubClass } = this.parseAssetClass(value.price); + const { assetClass, assetSubClass } = this.parseAssetClass( + assetProfile.price + ); - response[symbol] = { - assetClass, - assetSubClass, - currency: value.price?.currency, - dataSource: this.getName(), - exchange: this.parseExchange(value.price?.exchangeName), - marketState: - value.price?.marketState === 'REGULAR' || - this.cryptocurrencyService.isCryptocurrency(symbol) - ? MarketState.open - : MarketState.closed, - marketPrice: value.price?.regularMarketPrice || 0, - name: value.price?.longName || value.price?.shortName || symbol - }; + response.assetClass = assetClass; + response.assetSubClass = assetSubClass; + response.currency = assetProfile.price.currency; + response.dataSource = this.getName(); + response.name = + assetProfile.price.longName || assetProfile.price.shortName || symbol; + response.symbol = aSymbol; + + if ( + assetSubClass === AssetSubClass.STOCK && + assetProfile.summaryProfile?.country + ) { + // Add country if asset is stock and country available - if (value.price?.currency === 'GBp') { - // Convert GBp (pence) to GBP - response[symbol].currency = 'GBP'; - response[symbol].marketPrice = new Big( - value.price?.regularMarketPrice ?? 0 - ) - .div(100) - .toNumber(); - } + try { + const [code] = Object.entries(countries).find(([, country]) => { + return country.name === assetProfile.summaryProfile?.country; + }); - // Add country if stock and available - if ( - assetSubClass === AssetSubClass.STOCK && - value.summaryProfile?.country - ) { - try { - const [code] = Object.entries(countries).find(([, country]) => { - return country.name === value.summaryProfile?.country; - }); - - if (code) { - response[symbol].countries = [{ code, weight: 1 }]; - } - } catch {} - - if (value.summaryProfile?.sector) { - response[symbol].sectors = [ - { name: value.summaryProfile?.sector, weight: 1 } - ]; + if (code) { + response.countries = [{ code, weight: 1 }]; } - } + } catch {} - // Add url if available - const url = value.summaryProfile?.website; - if (url) { - response[symbol].url = url; + if (assetProfile.summaryProfile?.sector) { + response.sectors = [ + { name: assetProfile.summaryProfile?.sector, weight: 1 } + ]; } } - return response; - } catch (error) { - Logger.error(error); + const url = assetProfile.summaryProfile?.website; + if (url) { + response.url = url; + } + } catch {} - return {}; - } + return response; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - if (isSameDay(from, to)) { to = addDays(to, 1); } - const yahooFinanceSymbols = aSymbols.map((symbol) => { - return this.convertToYahooFinanceSymbol(symbol); - }); + const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol); try { - const historicalData: { - [symbol: string]: IYahooFinanceHistoricalResponse[]; - } = await yahooFinance.historical({ - symbols: yahooFinanceSymbols, - from: format(from, DATE_FORMAT), - to: format(to, DATE_FORMAT) - }); + const historicalResult = await yahooFinance.historical( + yahooFinanceSymbol, + { + interval: '1d', + period1: format(from, DATE_FORMAT), + period2: format(to, DATE_FORMAT) + } + ); const response: { [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; } = {}; - for (const [yahooFinanceSymbol, timeSeries] of Object.entries( - historicalData - )) { - // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - response[symbol] = {}; + // Convert symbol back + const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - timeSeries.forEach((timeSerie) => { - response[symbol][format(timeSerie.date, DATE_FORMAT)] = { - marketPrice: timeSerie.close, - performance: timeSerie.open - timeSerie.close - }; - }); + response[symbol] = {}; + + for (const historicalItem of historicalResult) { + let marketPrice = historicalItem.close; + + if (symbol === 'USDGBp') { + // Convert GPB to GBp (pence) + marketPrice = new Big(marketPrice).mul(100).toNumber(); + } + + response[symbol][format(historicalItem.date, DATE_FORMAT)] = { + marketPrice, + performance: historicalItem.open - historicalItem.close + }; } return response; } catch (error) { - Logger.error(error); + Logger.warn( + `Skipping yahooFinance2.getHistorical("${aSymbol}"): [${error.name}] ${error.message}`, + 'YahooFinanceService' + ); return {}; } @@ -221,6 +189,56 @@ export class YahooFinanceService implements DataProviderInterface { return DataSource.YAHOO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + const yahooFinanceSymbols = aSymbols.map((symbol) => + this.convertToYahooFinanceSymbol(symbol) + ); + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const quotes = await yahooFinance.quote(yahooFinanceSymbols); + + for (const quote of quotes) { + // Convert symbols back + const symbol = this.convertFromYahooFinanceSymbol(quote.symbol); + + response[symbol] = { + currency: quote.currency, + dataSource: this.getName(), + marketState: + quote.marketState === 'REGULAR' || + this.cryptocurrencyService.isCryptocurrency(symbol) + ? MarketState.open + : MarketState.closed, + marketPrice: quote.regularMarketPrice || 0 + }; + + if (symbol === 'USDGBP' && yahooFinanceSymbols.includes('USDGBp=X')) { + // Convert GPB to GBp (pence) + response['USDGBp'] = { + ...response[symbol], + currency: 'GBp', + marketPrice: new Big(response[symbol].marketPrice) + .mul(100) + .toNumber() + }; + } + } + + return response; + } catch (error) { + Logger.error(error, 'YahooFinanceService'); + + return {}; + } + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items: LookupItem[] = []; @@ -236,7 +254,7 @@ export class YahooFinanceService implements DataProviderInterface { const searchResult = await get(); - const symbols: string[] = searchResult.quotes + const quotes = searchResult.quotes .filter((quote) => { // filter out undefined symbols return quote.symbol; @@ -247,8 +265,7 @@ export class YahooFinanceService implements DataProviderInterface { this.cryptocurrencyService.isCryptocurrency( symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency) )) || - quoteType === 'EQUITY' || - quoteType === 'ETF' + ['EQUITY', 'ETF', 'MUTUALFUND'].includes(quoteType) ); }) .filter(({ quoteType, symbol }) => { @@ -259,27 +276,34 @@ export class YahooFinanceService implements DataProviderInterface { } return true; - }) - .map(({ symbol }) => { - return symbol; }); - const marketData = await this.get(symbols); + const marketData = await this.getQuotes( + quotes.map(({ symbol }) => { + return symbol; + }) + ); for (const [symbol, value] of Object.entries(marketData)) { + const quote = quotes.find((currentQuote: any) => { + return currentQuote.symbol === symbol; + }); + items.push({ symbol, currency: value.currency, dataSource: this.getName(), - name: value.name + name: quote?.longname || quote?.shortname || symbol }); } - } catch {} + } catch (error) { + Logger.error(error, 'YahooFinanceService'); + } return { items }; } - private parseAssetClass(aPrice: IYahooFinancePrice): { + private parseAssetClass(aPrice: any): { assetClass: AssetClass; assetSubClass: AssetSubClass; } { @@ -299,16 +323,12 @@ export class YahooFinanceService implements DataProviderInterface { assetClass = AssetClass.EQUITY; assetSubClass = AssetSubClass.ETF; break; + case 'mutualfund': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.MUTUALFUND; + break; } return { assetClass, assetSubClass }; } - - private parseExchange(aString: string): string { - if (aString?.toLowerCase() === 'ccc') { - return UNKNOWN_KEY; - } - - return aString; - } } diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index f77f7ef79..8092f1804 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -2,7 +2,7 @@ import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; import { format } from 'date-fns'; -import { isEmpty, isNumber, uniq } from 'lodash'; +import { isNumber, uniq } from 'lodash'; import { DataProviderService } from './data-provider/data-provider.service'; import { IDataGatheringItem } from './interfaces/interfaces'; @@ -61,7 +61,7 @@ export class ExchangeRateDataService { 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.get( + const historicalData = await this.dataProviderService.getQuotes( this.currencyPairs.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }) @@ -114,6 +114,10 @@ export class ExchangeRateDataService { aFromCurrency: string, aToCurrency: string ) { + if (aValue === 0) { + return 0; + } + const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => { return isNaN(exchangeRate); }); @@ -145,7 +149,8 @@ export class ExchangeRateDataService { // Fallback with error, if currencies are not available Logger.error( - `No exchange rate has been found for ${aFromCurrency}${aToCurrency}` + `No exchange rate has been found for ${aFromCurrency}${aToCurrency}`, + 'ExchangeRateDataService' ); return aValue; } @@ -187,12 +192,7 @@ export class ExchangeRateDataService { await this.prismaService.symbolProfile.findMany({ distinct: ['currency'], orderBy: [{ currency: 'asc' }], - select: { currency: true }, - where: { - currency: { - not: null - } - } + select: { currency: true } }) ).forEach((symbolProfile) => { currencies.push(symbolProfile.currency); @@ -206,7 +206,7 @@ export class ExchangeRateDataService { currencies = currencies.concat(customCurrencies); } - return uniq(currencies).sort(); + return uniq(currencies).filter(Boolean).sort(); } private prepareCurrencyPairs(aCurrencies: string[]) { diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index c7d3a08f7..50fd6009f 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -33,19 +33,10 @@ export interface IDataProviderHistoricalResponse { } export interface IDataProviderResponse { - assetClass?: AssetClass; - assetSubClass?: AssetSubClass; - countries?: { code: string; weight: number }[]; currency: string; dataSource: DataSource; - exchange?: string; - marketChange?: number; - marketChangePercent?: number; marketPrice: number; marketState: MarketState; - name?: string; - sectors?: { name: string; weight: number }[]; - url?: string; } export interface IDataGatheringItem { diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index 582ee2593..0afb5a811 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/apps/api/src/services/market-data.service.ts @@ -2,6 +2,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { resetHours } from '@ghostfolio/common/helper'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { DataSource, MarketData, Prisma } from '@prisma/client'; @@ -9,13 +10,7 @@ import { DataSource, MarketData, Prisma } from '@prisma/client'; export class MarketDataService { public constructor(private readonly prismaService: PrismaService) {} - public async deleteMany({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async deleteMany({ dataSource, symbol }: UniqueAsset) { return this.prismaService.marketData.deleteMany({ where: { dataSource, diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index 750e4fe30..58052872b 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -6,6 +6,7 @@ import { } from '@ghostfolio/common/config'; import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; +import { isSunday } from 'date-fns'; import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2'; @Injectable() @@ -27,7 +28,10 @@ export class TwitterBotService { } public async tweetFearAndGreedIndex() { - if (!this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + if ( + !this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX') || + isSunday(new Date()) + ) { return; } @@ -50,11 +54,12 @@ export class TwitterBotService { ); Logger.log( - `Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}` + `Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}`, + 'TwitterBotService' ); } } catch (error) { - Logger.error(error); + Logger.error(error, 'TwitterBotService'); } } } diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.html b/apps/client/src/app/components/accounts-table/accounts-table.component.html index f08ea8430..51ad3e58d 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.html +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.html @@ -86,7 +86,7 @@ class="d-inline-block justify-content-end" [isCurrency]="true" [locale]="locale" - [value]="element.convertedBalance" + [value]="element.balance" > @@ -94,7 +94,7 @@ class="d-inline-block justify-content-end" [isCurrency]="true" [locale]="locale" - [value]="totalBalance" + [value]="totalBalanceInBaseCurrency" > @@ -116,7 +116,7 @@ class="d-inline-block justify-content-end" [isCurrency]="true" [locale]="locale" - [value]="totalValue" + [value]="totalValueInBaseCurrency" > diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.ts b/apps/client/src/app/components/accounts-table/accounts-table.component.ts index 34ddaea6c..994d6ab64 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.ts +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.ts @@ -24,8 +24,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { @Input() deviceType: string; @Input() locale: string; @Input() showActions: boolean; - @Input() totalBalance: number; - @Input() totalValue: number; + @Input() totalBalanceInBaseCurrency: number; + @Input() totalValueInBaseCurrency: number; @Input() transactionCount: number; @Output() accountDeleted = new EventEmitter(); diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html index 23c20178f..7264be84d 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html @@ -18,8 +18,10 @@ available: marketDataByMonth[itemByMonth.key][ i + 1 < 10 ? '0' + (i + 1) : i + 1 - ]?.day === - i + 1 + ]?.marketPrice, + today: isToday( + itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) + ) }" [title]=" (itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss index 128c63cca..13db0835b 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss @@ -25,5 +25,10 @@ &.available { background-color: var(--success); } + + &.today { + background-color: rgba(var(--palette-accent-500), 1); + cursor: default; + } } } diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts index fa24f5941..9f935cb91 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts @@ -12,7 +12,15 @@ import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { DataSource, MarketData } from '@prisma/client'; -import { format, isBefore, isValid, parse } from 'date-fns'; +import { + addDays, + format, + isBefore, + isSameDay, + isValid, + parse, + parseISO +} from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -26,6 +34,7 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data- }) export class AdminMarketDataDetailComponent implements OnChanges, OnInit { @Input() dataSource: DataSource; + @Input() dateOfFirstActivity: string; @Input() marketData: MarketData[]; @Input() symbol: string; @@ -36,7 +45,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { public deviceType: string; public historicalDataItems: LineChartItem[]; public marketDataByMonth: { - [yearMonth: string]: { [day: string]: MarketData & { day: number } }; + [yearMonth: string]: { + [day: string]: Pick & { day: number }; + }; } = {}; private unsubscribeSubject = new Subject(); @@ -57,9 +68,30 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { value: marketDataItem.marketPrice }; }); + + let date = parseISO(this.dateOfFirstActivity); + + const missingMarketData: Partial[] = []; + + if (this.historicalDataItems?.[0]?.date) { + while ( + isBefore( + date, + parse(this.historicalDataItems[0].date, DATE_FORMAT, new Date()) + ) + ) { + missingMarketData.push({ + date, + marketPrice: undefined + }); + + date = addDays(date, 1); + } + } + this.marketDataByMonth = {}; - for (const marketDataItem of this.marketData) { + for (const marketDataItem of [...missingMarketData, ...this.marketData]) { const currentDay = parseInt(format(marketDataItem.date, 'd'), 10); const key = format(marketDataItem.date, 'yyyy-MM'); @@ -70,8 +102,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { this.marketDataByMonth[key][ currentDay < 10 ? `0${currentDay}` : currentDay ] = { - ...marketDataItem, - day: currentDay + date: marketDataItem.date, + day: currentDay, + marketPrice: marketDataItem.marketPrice }; } } @@ -82,6 +115,11 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { return isValid(date) && isBefore(date, new Date()); } + public isToday(aDateString: string) { + const date = parse(aDateString, DATE_FORMAT, new Date()); + return isValid(date) && isSameDay(date, new Date()); + } + public onOpenMarketDataDetail({ day, yearMonth @@ -89,13 +127,18 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { day: string; yearMonth: string; }) { + const date = new Date(`${yearMonth}-${day}`); const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; + if (isSameDay(date, new Date())) { + return; + } + const dialogRef = this.dialog.open(MarketDataDetailDialog, { data: { + date, marketPrice, dataSource: this.dataSource, - date: new Date(`${yearMonth}-${day}`), symbol: this.symbol }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 5dd3a3fd2..a2900ae6b 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -8,6 +8,7 @@ import { import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { DataSource, MarketData } from '@prisma/client'; import { Subject } from 'rxjs'; @@ -44,39 +45,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } - public onDeleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { this.adminService .deleteProfileData({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => {}); } - public onGatherProfileDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .gatherProfileDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => {}); } - public onGatherSymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onGatherSymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .gatherSymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) @@ -93,13 +76,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { } } - public setCurrentProfile({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public setCurrentProfile({ dataSource, symbol }: UniqueAsset) { this.marketDataDetails = []; if (this.currentSymbol === symbol) { @@ -129,13 +106,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { }); } - private fetchAdminMarketDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .fetchAdminMarketDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 3ff7435b4..7638d6110 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -64,6 +64,7 @@ -
+
Coupons
- {{ coupon.code }} + {{ coupon.code }} ({{ coupon.duration }})
- +
+ + + 30 Days + 1 Year + + + +
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.module.ts b/apps/client/src/app/components/admin-overview/admin-overview.module.ts index f75f312ce..4f9dd7a2a 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.module.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.module.ts @@ -1,7 +1,9 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -12,11 +14,14 @@ import { AdminOverviewComponent } from './admin-overview.component'; declarations: [AdminOverviewComponent], exports: [], imports: [ + FormsModule, CommonModule, GfValueModule, MatButtonModule, MatCardModule, - MatSlideToggleModule + MatSelectModule, + MatSlideToggleModule, + ReactiveFormsModule ], providers: [CacheService], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/apps/client/src/app/components/admin-overview/admin-overview.scss b/apps/client/src/app/components/admin-overview/admin-overview.scss index 46cadd6d7..f44df0eba 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.scss +++ b/apps/client/src/app/components/admin-overview/admin-overview.scss @@ -20,4 +20,10 @@ } } } + + .subscription { + .mat-form-field { + max-width: 100%; + } + } } diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index e6f7b48c2..9556f99cb 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -14,12 +14,12 @@ Accounts - Transactions + Activities Engagement per Day - Last Activitiy + Last Request diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 50c452ea8..7d40a27ef 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -93,7 +93,9 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { }); this.dateRange = - this.settingsStorageService.getSetting(RANGE) || 'max'; + this.user.settings.viewMode === 'ZEN' + ? 'max' + : this.settingsStorageService.getSetting(RANGE) ?? 'max'; this.update(); } diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index 9b69a1a8e..5fa1ec5c9 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -1,5 +1,5 @@
-
+
Manage Activities diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index a102b1095..f959fca77 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -7,7 +7,11 @@ import { } from '@ghostfolio/client/services/settings-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { defaultDateRangeOptions } from '@ghostfolio/common/config'; -import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces'; +import { + PortfolioPerformance, + UniqueAsset, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; @@ -24,6 +28,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { public dateRange: DateRange; public dateRangeOptions = defaultDateRangeOptions; public deviceType: string; + public errors: UniqueAsset[]; public hasError: boolean; public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; @@ -32,6 +37,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { public isAllTimeLow: boolean; public isLoadingPerformance = true; public performance: PortfolioPerformance; + public showDetails = false; public user: User; private unsubscribeSubject = new Subject(); @@ -79,7 +85,14 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { }); this.dateRange = - this.settingsStorageService.getSetting(RANGE) || 'max'; + this.user.settings.viewMode === 'ZEN' + ? 'max' + : this.settingsStorageService.getSetting(RANGE) ?? 'max'; + + this.showDetails = + !this.hasImpersonationId && + !this.user.settings.isRestrictedView && + this.user.settings.viewMode !== 'ZEN'; this.update(); } @@ -118,6 +131,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { .fetchPortfolioPerformance({ range: this.dateRange }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { + this.errors = response.errors; this.hasError = response.hasErrors; this.performance = response.performance; this.isLoadingPerformance = false; diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index a6e1c63f8..7f804d990 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -28,15 +28,16 @@ class="pb-4" [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" + [errors]="errors" [hasError]="hasError" [isAllTimeHigh]="isAllTimeHigh" [isAllTimeLow]="isAllTimeLow" [isLoading]="isLoadingPerformance" [locale]="user?.settings?.locale" [performance]="performance" - [showDetails]="!hasImpersonationId && !user.settings.isRestrictedView" + [showDetails]="showDetails" > -
+
{ + return `${error.symbol} (${error.dataSource})`; + }); + + alert(errorMessageParts.join('\n')); + } } diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 8d254bb98..cbd12e094 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -2,6 +2,7 @@
{{ position?.symbol | gfSymbol }} - ({{ position.exchange }})
; + public trySubscriptionMail = + 'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards'; public user: User; private unsubscribeSubject = new Subject(); diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 081784bfb..ff518f082 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -23,12 +23,12 @@ name="diamond-outline" >
-
+
Valid until {{ user?.subscription?.expiresAt | date: defaultDateFormat }}
Try Premium + Redeem Coupon { - this.accounts = accounts; - this.totalBalance = totalBalance; - this.totalValue = totalValue; - this.transactionCount = transactionCount; - - if (this.accounts?.length <= 0) { - this.router.navigate([], { queryParams: { createDialog: true } }); - } + .subscribe( + ({ + accounts, + totalBalanceInBaseCurrency, + totalValueInBaseCurrency, + transactionCount + }) => { + this.accounts = accounts; + this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; + this.totalValueInBaseCurrency = totalValueInBaseCurrency; + this.transactionCount = transactionCount; + + if (this.accounts?.length <= 0) { + this.router.navigate([], { queryParams: { createDialog: true } }); + } - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + } + ); } public onDeleteAccount(aId: string) { diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 117a1f5d5..228ccdd78 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -9,8 +9,8 @@ [deviceType]="deviceType" [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" - [totalBalance]="totalBalance" - [totalValue]="totalValue" + [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" + [totalValueInBaseCurrency]="totalValueInBaseCurrency" [transactionCount]="transactionCount" (accountDeleted)="onDeleteAccount($event)" (accountToUpdate)="onUpdateAccount($event)" diff --git a/apps/client/src/app/pages/features/features-page.component.ts b/apps/client/src/app/pages/features/features-page.component.ts index 3137443cc..ff71a5259 100644 --- a/apps/client/src/app/pages/features/features-page.component.ts +++ b/apps/client/src/app/pages/features/features-page.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject, takeUntil } from 'rxjs'; @Component({ @@ -10,6 +12,8 @@ import { Subject, takeUntil } from 'rxjs'; templateUrl: './features-page.html' }) export class FeaturesPageComponent implements OnDestroy { + public hasPermissionForSubscription: boolean; + public info: InfoItem; public user: User; private unsubscribeSubject = new Subject(); @@ -19,8 +23,11 @@ export class FeaturesPageComponent implements OnDestroy { */ public constructor( private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private userService: UserService - ) {} + ) { + this.info = this.dataService.fetchInfo(); + } /** * Initializes the controller @@ -35,6 +42,11 @@ export class FeaturesPageComponent implements OnDestroy { this.changeDetectorRef.markForCheck(); } }); + + this.hasPermissionForSubscription = hasPermission( + this.info?.globalPermissions, + permissions.enableSubscription + ); } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index baa2aa844..42c460542 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -89,6 +89,7 @@

Portfolio Calculations @@ -107,6 +108,7 @@

Portfolio Allocations @@ -139,7 +141,10 @@

-
+

@@ -163,6 +168,7 @@

Static Analysis diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index f170a541e..2ca5f6930 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -10,11 +10,12 @@ import { prettifySymbol } from '@ghostfolio/common/helper'; import { PortfolioDetails, PortfolioPosition, + UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ToggleOption } from '@ghostfolio/common/types'; -import { AssetClass, DataSource } from '@prisma/client'; +import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -27,7 +28,10 @@ import { takeUntil } from 'rxjs/operators'; }) export class AllocationsPageComponent implements OnDestroy, OnInit { public accounts: { - [symbol: string]: Pick & { value: number }; + [id: string]: Pick & { + id: string; + value: number; + }; }; public continents: { [code: string]: { name: string; value: number }; @@ -61,7 +65,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { [name: string]: { name: string; value: number }; }; public symbols: { - [name: string]: { name: string; symbol: string; value: number }; + [name: string]: { + dataSource?: DataSource; + name: string; + symbol: string; + value: number; + }; }; public user: User; @@ -171,6 +180,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.portfolioDetails.accounts )) { this.accounts[id] = { + id, name, value: aPeriod === 'original' ? original : current }; @@ -277,6 +287,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (position.assetClass === AssetClass.EQUITY) { this.symbols[prettifySymbol(symbol)] = { + dataSource: position.dataSource, name: position.name, symbol: prettifySymbol(symbol), value: aPeriod === 'original' ? position.investment : position.value @@ -291,6 +302,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.initializeAnalysisData(this.period); } + public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) { + if (dataSource && symbol) { + this.router.navigate([], { + queryParams: { dataSource, symbol, positionDetailDialog: true } + }); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index a210c4cdb..276de32ce 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -20,7 +20,7 @@ @@ -89,12 +89,14 @@ diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.html b/apps/client/src/app/pages/portfolio/portfolio-page.html index 6644e9340..7c221d9d5 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.html +++ b/apps/client/src/app/pages/portfolio/portfolio-page.html @@ -1,11 +1,14 @@

Portfolio

-
- +
+

Activities

-

Manage your activities.

-

+

+ Manage your activities: stocks, ETFs, cryptocurrencies, dividend, and + valuables. +
+
-
- +
+

Allocations

-

Check the allocations of your portfolio.

-

+

+ Check the allocations of your portfolio by account, asset class, + currency, sector and region. +
+
-
-
-
- +
+

Analysis

-

Ghostfolio Analysis visualizes your portfolio.

-

+

+ Ghostfolio Analysis visualizes your portfolio and shows your top and + bottom performers. +
+
-
- +
+

X-ray

-

+

Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio. -

-

+

+
diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts index 40c38bb95..134adcdc5 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts @@ -158,11 +158,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { this.activityForm.controls['type'].disable(); } - if (this.data.activity?.symbol) { + if (this.data.activity?.SymbolProfile?.symbol) { this.dataService .fetchSymbolItem({ - dataSource: this.data.activity?.dataSource, - symbol: this.data.activity?.symbol + dataSource: this.data.activity?.SymbolProfile?.dataSource, + symbol: this.data.activity?.SymbolProfile?.symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ marketPrice }) => { @@ -196,9 +196,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { } else { this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true }); - this.data.activity.currency = null; - this.data.activity.dataSource = null; - this.data.activity.symbol = null; + this.data.activity.SymbolProfile = null; } this.changeDetectorRef.markForCheck(); @@ -259,9 +257,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { }) .pipe( catchError(() => { - this.data.activity.currency = null; - this.data.activity.dataSource = null; - this.data.activity.unitPrice = null; + this.data.activity.SymbolProfile = null; this.isLoading = false; diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index ff6e624d0..cf9b36b1f 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -3,7 +3,10 @@ import { Injectable } from '@angular/core'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; -import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; +import { + AdminMarketDataDetails, + UniqueAsset +} from '@ghostfolio/common/interfaces'; import { DataSource, MarketData } from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { Observable, map } from 'rxjs'; @@ -14,13 +17,7 @@ import { Observable, map } from 'rxjs'; export class AdminService { public constructor(private http: HttpClient) {} - public deleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public deleteProfileData({ dataSource, symbol }: UniqueAsset) { return this.http.delete( `/api/admin/profile-data/${dataSource}/${symbol}` ); @@ -53,13 +50,7 @@ export class AdminService { return this.http.post(`/api/admin/gather/profile-data`, {}); } - public gatherProfileDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { return this.http.post( `/api/admin/gather/profile-data/${dataSource}/${symbol}`, {} @@ -70,10 +61,8 @@ export class AdminService { dataSource, date, symbol - }: { - dataSource: DataSource; + }: UniqueAsset & { date?: Date; - symbol: string; }) { let url = `/api/admin/gather/${dataSource}/${symbol}`; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index fac56a1f2..c61730d3c 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -24,9 +24,11 @@ import { PortfolioDetails, PortfolioInvestments, PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, PortfolioSummary, + UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -188,13 +190,13 @@ export class DataService { }); } - public fetchPortfolioPerformance(aParams: { [param: string]: any }) { - return this.http.get<{ - hasErrors: boolean; - performance: PortfolioPerformance; - }>('/api/portfolio/performance', { - params: aParams - }); + public fetchPortfolioPerformance(params: { [param: string]: any }) { + return this.http.get( + '/api/portfolio/performance', + { + params + } + ); } public fetchPortfolioPublic(aId: string) { diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 12f0f3a2b..ed670f1d4 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -134,6 +134,10 @@ ngx-skeleton-loader { } } +.cursor-default { + cursor: default; +} + .cursor-pointer { cursor: pointer; } diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 8e5a8ec07..911d0dca4 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -4,10 +4,10 @@ services: image: postgres:12 container_name: postgres restart: unless-stopped - ports: - - 5432:5432 env_file: - ../.env + ports: + - 5432:5432 volumes: - postgres:/var/lib/postgresql/data diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index dbfc787f3..e337493c7 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -102,10 +102,6 @@ export function isCurrency(aSymbol = '') { return currencies[aSymbol]; } -export function isGhostfolioScraperApiSymbol(aSymbol = '') { - return aSymbol.startsWith(ghostfolioScraperApiSymbolPrefix); -} - export function resetHours(aDate: Date) { const year = getYear(aDate); const month = getMonth(aDate); diff --git a/libs/common/src/lib/interfaces/accounts.interface.ts b/libs/common/src/lib/interfaces/accounts.interface.ts index 14732f410..7100a6848 100644 --- a/libs/common/src/lib/interfaces/accounts.interface.ts +++ b/libs/common/src/lib/interfaces/accounts.interface.ts @@ -2,7 +2,7 @@ import { AccountWithValue } from '@ghostfolio/common/types'; export interface Accounts { accounts: AccountWithValue[]; - totalBalance: number; - totalValue: number; + totalBalanceInBaseCurrency: number; + totalValueInBaseCurrency: number; transactionCount: number; } diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index a061269e7..ce90dccc5 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,5 +1,3 @@ -import { Property } from '@prisma/client'; - export interface AdminData { dataGatheringProgress?: number; exchangeRates: { label1: string; label2: string; value: number }[]; diff --git a/libs/common/src/lib/interfaces/coupon.interface.ts b/libs/common/src/lib/interfaces/coupon.interface.ts index 3caa218e6..cbf8525a2 100644 --- a/libs/common/src/lib/interfaces/coupon.interface.ts +++ b/libs/common/src/lib/interfaces/coupon.interface.ts @@ -1,3 +1,6 @@ +import { StringValue } from 'ms'; + export interface Coupon { code: string; + duration?: StringValue; } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 5a0c71590..d2ad50742 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -21,7 +21,10 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface'; import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioSummary } from './portfolio-summary.interface'; import { Position } from './position.interface'; +import { ResponseError } from './responses/errors.interface'; +import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import { TimelinePosition } from './timeline-position.interface'; +import { UniqueAsset } from './unique-asset.interface'; import { UserSettings } from './user-settings.interface'; import { UserWithSettings } from './user-with-settings'; import { User } from './user.interface'; @@ -42,13 +45,16 @@ export { PortfolioItem, PortfolioOverview, PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPosition, PortfolioPublicDetails, PortfolioReport, PortfolioReportRule, PortfolioSummary, Position, + ResponseError, TimelinePosition, + UniqueAsset, User, UserSettings, UserWithSettings diff --git a/libs/common/src/lib/interfaces/responses/errors.interface.ts b/libs/common/src/lib/interfaces/responses/errors.interface.ts new file mode 100644 index 000000000..0b43592be --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/errors.interface.ts @@ -0,0 +1,6 @@ +import { UniqueAsset } from '../unique-asset.interface'; + +export interface ResponseError { + errors?: UniqueAsset[]; + hasErrors: boolean; +} diff --git a/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts new file mode 100644 index 000000000..3db6d3af4 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts @@ -0,0 +1,6 @@ +import { PortfolioPerformance } from '../portfolio-performance.interface'; +import { ResponseError } from './errors.interface'; + +export interface PortfolioPerformanceResponse extends ResponseError { + performance: PortfolioPerformance; +} diff --git a/libs/common/src/lib/interfaces/unique-asset.interface.ts b/libs/common/src/lib/interfaces/unique-asset.interface.ts new file mode 100644 index 000000000..745a0d9a7 --- /dev/null +++ b/libs/common/src/lib/interfaces/unique-asset.interface.ts @@ -0,0 +1,6 @@ +import { DataSource } from '@prisma/client'; + +export interface UniqueAsset { + dataSource: DataSource; + symbol: string; +} diff --git a/libs/common/src/lib/types/account-with-value.type.ts b/libs/common/src/lib/types/account-with-value.type.ts index a3b81f15b..7c0cca747 100644 --- a/libs/common/src/lib/types/account-with-value.type.ts +++ b/libs/common/src/lib/types/account-with-value.type.ts @@ -1,7 +1,8 @@ import { Account as AccountModel } from '@prisma/client'; export type AccountWithValue = AccountModel & { - convertedBalance: number; + balanceInBaseCurrency: number; transactionCount: number; value: number; + valueInBaseCurrency: number; }; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 4cb943f55..3fecdd906 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -144,7 +144,7 @@ class="d-none d-lg-table-cell px-1" mat-cell > - {{ element.currency }} + {{ element.SymbolProfile.currency }} {{ baseCurrency }} @@ -362,7 +362,7 @@ !row.isDraft && row.type !== 'ITEM' && onOpenPositionDialog({ - dataSource: row.dataSource, + dataSource: row.SymbolProfile.dataSource, symbol: row.SymbolProfile.symbol }) " diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index 4c19c73ff..9fce3f703 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -21,6 +21,7 @@ import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; @@ -199,13 +200,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.import.emit(); } - public onOpenPositionDialog({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }): void { + public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void { this.router.navigate([], { queryParams: { dataSource, symbol, positionDetailDialog: true } }); diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index ca0475d66..4fe51497d 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3,14 +3,18 @@ import { ChangeDetectionStrategy, Component, ElementRef, + EventEmitter, Input, OnChanges, OnDestroy, + Output, ViewChild } from '@angular/core'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { getTextColor } from '@ghostfolio/common/helper'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { DataSource } from '@prisma/client'; +import Big from 'big.js'; import { Tooltip } from 'chart.js'; import { LinearScale } from 'chart.js'; import { ArcElement } from 'chart.js'; @@ -29,6 +33,7 @@ export class PortfolioProportionChartComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() baseCurrency: string; + @Input() cursor: string; @Input() isInPercent = false; @Input() keys: string[] = []; @Input() locale = ''; @@ -36,19 +41,25 @@ export class PortfolioProportionChartComponent @Input() showLabels = false; @Input() positions: { [symbol: string]: Pick & { + dataSource?: DataSource; name: string; value: number; }; } = {}; + @Output() proportionChartClicked = new EventEmitter(); + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart; public isLoading = true; + private readonly OTHER_KEY = 'OTHER'; + private colorMap: { [symbol: string]: string; } = { + [this.OTHER_KEY]: `rgba(${getTextColor()}, 0.24)`, [UNKNOWN_KEY]: `rgba(${getTextColor()}, 0.12)` }; @@ -78,16 +89,17 @@ export class PortfolioProportionChartComponent [symbol: string]: { color?: string; name: string; - subCategory: { [symbol: string]: { value: number } }; - value: number; + subCategory: { [symbol: string]: { value: Big } }; + value: Big; }; } = {}; Object.keys(this.positions).forEach((symbol) => { if (this.positions[symbol][this.keys[0]]) { if (chartData[this.positions[symbol][this.keys[0]]]) { - chartData[this.positions[symbol][this.keys[0]]].value += - this.positions[symbol].value; + chartData[this.positions[symbol][this.keys[0]]].value = chartData[ + this.positions[symbol][this.keys[0]] + ].value.plus(this.positions[symbol].value); if ( chartData[this.positions[symbol][this.keys[0]]].subCategory[ @@ -96,37 +108,43 @@ export class PortfolioProportionChartComponent ) { chartData[this.positions[symbol][this.keys[0]]].subCategory[ this.positions[symbol][this.keys[1]] - ].value += this.positions[symbol].value; + ].value = chartData[ + this.positions[symbol][this.keys[0]] + ].subCategory[this.positions[symbol][this.keys[1]]].value.plus( + this.positions[symbol].value + ); } else { chartData[this.positions[symbol][this.keys[0]]].subCategory[ this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY - ] = { value: this.positions[symbol].value }; + ] = { value: new Big(this.positions[symbol].value) }; } } else { chartData[this.positions[symbol][this.keys[0]]] = { name: this.positions[symbol].name, subCategory: {}, - value: this.positions[symbol].value + value: new Big(this.positions[symbol].value) }; if (this.positions[symbol][this.keys[1]]) { chartData[this.positions[symbol][this.keys[0]]].subCategory = { [this.positions[symbol][this.keys[1]]]: { - value: this.positions[symbol].value + value: new Big(this.positions[symbol].value) } }; } } } else { if (chartData[UNKNOWN_KEY]) { - chartData[UNKNOWN_KEY].value += this.positions[symbol].value; + chartData[UNKNOWN_KEY].value = chartData[UNKNOWN_KEY].value.plus( + this.positions[symbol].value + ); } else { chartData[UNKNOWN_KEY] = { name: this.positions[symbol].name, subCategory: this.keys[1] - ? { [this.keys[1]]: { value: 0 } } + ? { [this.keys[1]]: { value: new Big(0) } } : undefined, - value: this.positions[symbol].value + value: new Big(this.positions[symbol].value) }; } } @@ -134,35 +152,29 @@ export class PortfolioProportionChartComponent let chartDataSorted = Object.entries(chartData) .sort((a, b) => { - return a[1].value - b[1].value; + return a[1].value.minus(b[1].value).toNumber(); }) .reverse(); if (this.maxItems && chartDataSorted.length > this.maxItems) { - // Add surplus items to unknown group + // Add surplus items to OTHER group const rest = chartDataSorted.splice( this.maxItems, chartDataSorted.length - 1 ); - let unknownItem = chartDataSorted.find((charDataItem) => { - return charDataItem[0] === UNKNOWN_KEY; - }); - - if (!unknownItem) { - chartDataSorted.push([ - UNKNOWN_KEY, - { name: UNKNOWN_KEY, subCategory: {}, value: 0 } - ]); - unknownItem = chartDataSorted[chartDataSorted.length - 1]; - } + chartDataSorted.push([ + this.OTHER_KEY, + { name: this.OTHER_KEY, subCategory: {}, value: new Big(0) } + ]); + const otherItem = chartDataSorted[chartDataSorted.length - 1]; rest.forEach((restItem) => { - if (unknownItem?.[1]) { - unknownItem[1] = { - name: UNKNOWN_KEY, + if (otherItem?.[1]) { + otherItem[1] = { + name: this.OTHER_KEY, subCategory: {}, - value: unknownItem[1].value + restItem[1].value + value: otherItem[1].value.plus(restItem[1].value) }; } }); @@ -170,7 +182,7 @@ export class PortfolioProportionChartComponent // Sort data again chartDataSorted = chartDataSorted .sort((a, b) => { - return a[1].value - b[1].value; + return a[1].value.minus(b[1].value).toNumber(); }) .reverse(); } @@ -201,7 +213,7 @@ export class PortfolioProportionChartComponent backgroundColorSubCategory.push( Color(item.color).lighten(lightnessRatio).hex() ); - dataSubCategory.push(item.subCategory[subCategory].value); + dataSubCategory.push(item.subCategory[subCategory].value.toNumber()); labelSubCategory.push(subCategory); lightnessRatio += 0.1; @@ -215,7 +227,7 @@ export class PortfolioProportionChartComponent }), borderWidth: 0, data: chartDataSorted.map(([, item]) => { - return item.value; + return item.value.toNumber(); }) } ]; @@ -251,6 +263,21 @@ export class PortfolioProportionChartComponent layout: { padding: this.showLabels === true ? 100 : 0 }, + onClick: (event, activeElements) => { + const dataIndex = activeElements[0].index; + const symbol: string = event.chart.data.labels[dataIndex]; + + const dataSource = this.positions[symbol]?.dataSource; + + this.proportionChartClicked.emit({ dataSource, symbol }); + }, + onHover: (event, chartElement) => { + if (this.cursor) { + event.native.target.style.cursor = chartElement[0] + ? this.cursor + : 'default'; + } + }, plugins: { datalabels: { color: (context) => { @@ -279,8 +306,13 @@ export class PortfolioProportionChartComponent const labelIndex = (data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) + context.dataIndex; - const symbol = - context.chart.data.labels?.[labelIndex] ?? ''; + let symbol = context.chart.data.labels?.[labelIndex] ?? ''; + + if (symbol === this.OTHER_KEY) { + symbol = 'Other'; + } else if (symbol === UNKNOWN_KEY) { + symbol = 'Unknown'; + } const name = this.positions[symbol]?.name; diff --git a/package.json b/package.json index 85fcb7747..f2d990a6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.116.0", + "version": "1.124.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { @@ -70,8 +70,8 @@ "@nestjs/platform-express": "8.2.3", "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", - "@nrwl/angular": "13.8.1", - "@prisma/client": "3.9.1", + "@nrwl/angular": "13.8.5", + "@prisma/client": "3.10.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -100,15 +100,16 @@ "http-status-codes": "2.2.0", "ionicons": "5.5.1", "lodash": "4.17.21", + "ms": "3.0.0-canary.1", "ngx-device-detector": "3.0.0", "ngx-markdown": "13.0.0", - "ngx-skeleton-loader": "2.9.1", + "ngx-skeleton-loader": "5.0.0", "ngx-stripe": "13.0.0", "papaparse": "5.3.1", "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.9.1", + "prisma": "3.10.0", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", @@ -117,7 +118,7 @@ "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", - "yahoo-finance": "0.3.6", + "yahoo-finance2": "2.2.0", "zone.js": "0.11.4" }, "devDependencies": { @@ -131,15 +132,15 @@ "@angular/localize": "13.2.2", "@nestjs/schematics": "8.0.5", "@nestjs/testing": "8.2.3", - "@nrwl/cli": "13.8.1", - "@nrwl/cypress": "13.8.1", - "@nrwl/eslint-plugin-nx": "13.8.1", - "@nrwl/jest": "13.8.1", - "@nrwl/nest": "13.8.1", - "@nrwl/node": "13.8.1", - "@nrwl/storybook": "13.8.1", - "@nrwl/tao": "13.8.1", - "@nrwl/workspace": "13.8.1", + "@nrwl/cli": "13.8.5", + "@nrwl/cypress": "13.8.5", + "@nrwl/eslint-plugin-nx": "13.8.5", + "@nrwl/jest": "13.8.5", + "@nrwl/nest": "13.8.5", + "@nrwl/node": "13.8.5", + "@nrwl/storybook": "13.8.5", + "@nrwl/tao": "13.8.5", + "@nrwl/workspace": "13.8.5", "@storybook/addon-essentials": "6.4.18", "@storybook/angular": "6.4.18", "@storybook/builder-webpack5": "6.4.18", @@ -165,7 +166,7 @@ "import-sort-parser-typescript": "6.0.0", "import-sort-style-module": "6.0.0", "jest": "27.2.3", - "jest-preset-angular": "11.0.0", + "jest-preset-angular": "11.1.1", "prettier": "2.5.1", "replace-in-file": "6.2.0", "rimraf": "3.0.2", diff --git a/prisma/migrations/20220227092214_added_mutualfund_to_asset_sub_class/migration.sql b/prisma/migrations/20220227092214_added_mutualfund_to_asset_sub_class/migration.sql new file mode 100644 index 000000000..b03c52f96 --- /dev/null +++ b/prisma/migrations/20220227092214_added_mutualfund_to_asset_sub_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetSubClass" ADD VALUE 'MUTUALFUND'; diff --git a/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql b/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql new file mode 100644 index 000000000..61456216c --- /dev/null +++ b/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT; diff --git a/prisma/migrations/20220302184222_removed_data_source_from_order/migration.sql b/prisma/migrations/20220302184222_removed_data_source_from_order/migration.sql new file mode 100644 index 000000000..5c0f278c0 --- /dev/null +++ b/prisma/migrations/20220302184222_removed_data_source_from_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" DROP COLUMN "dataSource"; diff --git a/prisma/migrations/20220302191841_removed_currency_from_order/migration.sql b/prisma/migrations/20220302191841_removed_currency_from_order/migration.sql new file mode 100644 index 000000000..7ec887e9a --- /dev/null +++ b/prisma/migrations/20220302191841_removed_currency_from_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" DROP COLUMN "currency"; diff --git a/prisma/migrations/20220302193633_removed_symbol_from_order/migration.sql b/prisma/migrations/20220302193633_removed_symbol_from_order/migration.sql new file mode 100644 index 000000000..0a6026acb --- /dev/null +++ b/prisma/migrations/20220302193633_removed_symbol_from_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" DROP COLUMN "symbol"; diff --git a/prisma/migrations/20220302200727_changed_currency_to_required_in_symbol_profile/migration.sql b/prisma/migrations/20220302200727_changed_currency_to_required_in_symbol_profile/migration.sql new file mode 100644 index 000000000..158d9318a --- /dev/null +++ b/prisma/migrations/20220302200727_changed_currency_to_required_in_symbol_profile/migration.sql @@ -0,0 +1,5 @@ +-- Set default value +UPDATE "SymbolProfile" SET "currency" = 'USD' WHERE "currency" IS NULL; + +-- AlterTable +ALTER TABLE "SymbolProfile" ALTER COLUMN "currency" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f0a1a173b..738797ed6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,14 +74,11 @@ model Order { accountId String? accountUserId String? createdAt DateTime @default(now()) - currency String? - dataSource DataSource? date DateTime fee Float id String @default(uuid()) isDraft Boolean @default(false) quantity Float - symbol String? SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) symbolProfileId String type Type @@ -119,7 +116,7 @@ model SymbolProfile { assetSubClass AssetSubClass? countries Json? createdAt DateTime @default(now()) - currency String? + currency String dataSource DataSource id String @id @default(uuid()) name String? @@ -129,6 +126,7 @@ model SymbolProfile { sectors Json? symbol String symbolMapping Json? + url String? @@unique([dataSource, symbol]) } @@ -178,6 +176,7 @@ enum AssetClass { enum AssetSubClass { CRYPTOCURRENCY ETF + MUTUALFUND STOCK } diff --git a/prisma/seed.js b/prisma/seed.js index 3e4996a1e..d33d8b581 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -192,14 +192,11 @@ async function main() { { accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)), fee: 30, id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1', quantity: 50, - symbol: 'TSLA', - symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', + symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', // TSLA type: Type.BUY, unitPrice: 42.97, userId: userDemo.id @@ -207,14 +204,11 @@ async function main() { { accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)), fee: 29.9, id: 'a1c5d73a-8631-44e5-ac44-356827a5212c', quantity: 0.5614682, - symbol: 'BTCUSD', - symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', + symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', // BTCUSD type: Type.BUY, unitPrice: 3562.089535970158, userId: userDemo.id @@ -222,14 +216,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)), fee: 80.79, id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b', quantity: 5, - symbol: 'AMZN', - symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc', + symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc', // AMZN type: Type.BUY, unitPrice: 2021.99, userId: userDemo.id @@ -237,14 +228,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)), fee: 19.9, id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e', quantity: 10, - symbol: 'VTI', - symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI type: Type.BUY, unitPrice: 144.38, userId: userDemo.id @@ -252,14 +240,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)), fee: 19.9, id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e', quantity: 10, - symbol: 'VTI', - symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI type: Type.BUY, unitPrice: 147.99, userId: userDemo.id @@ -267,14 +252,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)), fee: 19.9, id: '347b0430-a84f-4031-a0f9-390399066ad6', quantity: 10, - symbol: 'VTI', - symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI type: Type.BUY, unitPrice: 151.41, userId: userDemo.id @@ -282,14 +264,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)), fee: 19.9, id: '67ec3f47-3189-4b63-ba05-60d3a06b302f', quantity: 10, - symbol: 'VTI', - symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI type: Type.BUY, unitPrice: 177.69, userId: userDemo.id @@ -297,14 +276,11 @@ async function main() { { accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', accountUserId: userDemo.id, - currency: 'USD', - dataSource: DataSource.YAHOO, date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)), fee: 19.9, id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2', quantity: 10, - symbol: 'VTI', - symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI type: Type.BUY, unitPrice: 203.15, userId: userDemo.id diff --git a/yarn.lock b/yarn.lock index 756eca7bd..ce1504790 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3196,17 +3196,17 @@ node-gyp "^8.2.0" read-package-json-fast "^2.0.1" -"@nrwl/angular@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-13.8.1.tgz#081fcb7b7a94f15c3e52cc999cc55794ecb6553a" - integrity sha512-irKPeIkBvK2HVivwyamqNC1dMnV/dI1hup6y6pFsYDCygSBX8PWjZSXTLXEik9uviGwn+qOgEl7YTcxIOfKoag== +"@nrwl/angular@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-13.8.5.tgz#c9f585a08be22d4b94e54f36bd85b34fae24c180" + integrity sha512-S+BjdVHW/VuTPVWkWztkefQjMzikF3hF5wiN59s7wPeSkE+FjXj7YEdpUuR58/0W23gR0ao8eVisYriZaPvq8Q== dependencies: "@angular-devkit/schematics" "~13.2.0" - "@nrwl/cypress" "13.8.1" - "@nrwl/devkit" "13.8.1" - "@nrwl/jest" "13.8.1" - "@nrwl/linter" "13.8.1" - "@nrwl/storybook" "13.8.1" + "@nrwl/cypress" "13.8.5" + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/storybook" "13.8.5" "@phenomnomnominal/tsquery" "4.1.1" "@schematics/angular" "~13.2.0" ignore "^5.0.4" @@ -3218,26 +3218,26 @@ tslib "^2.3.0" webpack-merge "5.7.3" -"@nrwl/cli@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-13.8.1.tgz#31af91b27f4c19e736dd9793b0f36f69ef482256" - integrity sha512-oQtu0rkpEm3QdzqB/BCDsOl0OJ5P2afSfzu3Lxcrz6fHjmUf9aby0sd1JCrRNRrZkxK8GAdxRKZdPHkdWvr23A== +"@nrwl/cli@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-13.8.5.tgz#df9ca6f8841965195296e1642126ebcd77e204af" + integrity sha512-vxDZUCl1u2ZGZATyxBCAzMlR1cLnNwZMzl8yAW2ghnzWun5QynYeOg6GfcoE232E2rIov9YDbEeh2ZusMJeYuw== dependencies: - "@nrwl/tao" "13.8.1" + "@nrwl/tao" "13.8.5" chalk "4.1.0" enquirer "~2.3.6" v8-compile-cache "2.3.0" yargs-parser "20.0.0" -"@nrwl/cypress@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-13.8.1.tgz#e46de921a4b97862ce5756f55deec72fa955ed58" - integrity sha512-i4JAEZPCG/jPrDUmiWA3nBVICcCa+ZN4T4WcRGJrOVxLfa4IPfEJbdAW73Dh/ddDHQ47mN1x6DSDdNbthdmaQQ== +"@nrwl/cypress@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-13.8.5.tgz#ced128ede06ce1496aef1b0a2fbcf795606e18fd" + integrity sha512-D57S5EeUzW6ZmW+LSaRj47+uyKOwC0PQAYL5CP1SXkUDgUu+jh1o3glASPXbtfqFMXjlWk1Mo9eDEPxw9p814g== dependencies: "@cypress/webpack-preprocessor" "^5.9.1" - "@nrwl/devkit" "13.8.1" - "@nrwl/linter" "13.8.1" - "@nrwl/workspace" "13.8.1" + "@nrwl/devkit" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/workspace" "13.8.5" chalk "4.1.0" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" @@ -3248,44 +3248,37 @@ tslib "^2.3.0" webpack-node-externals "^3.0.0" -"@nrwl/devkit@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-13.8.1.tgz#03184e057b04b2a451dd7856e0e8008b8def1685" - integrity sha512-zznDaYf6yTBbr8xOb8l4Dn7L0QhCS7BMUoCq/PMCBLwRnRBDpbd801tD06qIVvhh3XkwEJVS2v7EEF3TOypIyw== +"@nrwl/devkit@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-13.8.5.tgz#f5cc8de7a66778b1763412b07ca3cf6e4039de3a" + integrity sha512-WSxK3sSVCU4+BIgARfe5dJvNn1xkLyjuIPilpOz7TTQffF3GZ1okGIik+sVHuumgbYodK7gVWihCyt/7+t4xig== dependencies: - "@nrwl/tao" "13.8.1" + "@nrwl/tao" "13.8.5" ejs "^3.1.5" ignore "^5.0.4" rxjs "^6.5.4" semver "7.3.4" tslib "^2.3.0" -"@nrwl/eslint-plugin-nx@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-13.8.1.tgz#6a5c045d0b95f63a2adbd07cfbdaa62045e7c9bd" - integrity sha512-kFuimLFKJXhaJU447fn6UTldfdQy5trjkvxVqNx8lc8Ole25E+ERb+eU239HijTR3YakfzyHN9ffdDguyp1f7w== +"@nrwl/eslint-plugin-nx@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-13.8.5.tgz#a9eaaa7f3db49319e5ef6fb25b3c37f051a0b03d" + integrity sha512-M/UvJIxyGW/e6Yj3pKrjT6GSibJXasBMy9YbwuvlmWXMHUfm3wUULPeyglxELvMhwNmE8pJAhh8a8bedDQeTfQ== dependencies: - "@nrwl/devkit" "13.8.1" - "@nrwl/workspace" "13.8.1" - "@swc-node/register" "^1.4.2" + "@nrwl/devkit" "13.8.5" + "@nrwl/workspace" "13.8.5" "@typescript-eslint/experimental-utils" "~5.10.0" chalk "4.1.0" confusing-browser-globals "^1.0.9" - tsconfig-paths "^3.9.0" - optionalDependencies: - "@swc/core-linux-arm64-gnu" "^1.2.136" - "@swc/core-linux-arm64-musl" "^1.2.136" - "@swc/core-linux-x64-gnu" "^1.2.136" - "@swc/core-linux-x64-musl" "^1.2.136" -"@nrwl/jest@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-13.8.1.tgz#32f2c9c28ae03e0f4a5bdd8fc6688c4bbca8ab09" - integrity sha512-kY6/Fg3aFODVk250qWcJPJWO+pDUN6VFOAUEz03sxkmkfZEA8MRG0xgQrYl9dXcLDK1apoEGJ4sGZ2r8QpA7AA== +"@nrwl/jest@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-13.8.5.tgz#9d6645d6efc2c64fd67110fb7485d79cd043ec08" + integrity sha512-yb4tThYusdBByFlrXp9DAy/Z6f+V9OnEB0CIRK/j8hFipFqQyMPIDP2DeMQw/F17DKB1FdaEX3vMEA6xP+V2eg== dependencies: "@jest/reporters" "27.2.2" "@jest/test-result" "27.2.2" - "@nrwl/devkit" "13.8.1" + "@nrwl/devkit" "13.8.5" chalk "4.1.0" identity-obj-proxy "3.0.0" jest-config "27.2.2" @@ -3295,37 +3288,58 @@ rxjs "^6.5.4" tslib "^2.3.0" -"@nrwl/linter@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-13.8.1.tgz#ee5c513c9c584ce7861736c574f12dfc0b266bcf" - integrity sha512-WBSpWUccaq1skr82VauvdRfjfmrkAXjHFalg72JqeDv0Ou5AhUWHLhEC1lvXZXPFMeFJtUaAEFbkSkOb6U+K2g== +"@nrwl/js@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/js/-/js-13.8.5.tgz#9527668f267f29f7410fd326e7b77eaab5650ea4" + integrity sha512-qSHmB0pbTbmWwHJRVqr1kWm2nnPgFUCXsTyvkAQiRyUGCRo1jdUM2rRyhwPjgH6JMnhr1HM1L4balfr2hURn7g== + dependencies: + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/workspace" "13.8.5" + "@parcel/watcher" "2.0.4" + chalk "4.1.0" + fast-glob "^3.2.7" + fs-extra "^9.1.0" + ignore "^5.0.4" + js-tokens "^4.0.0" + minimatch "3.0.4" + source-map-support "0.5.19" + tree-kill "1.2.2" + +"@nrwl/linter@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-13.8.5.tgz#526539abfe3393c62f6c5f6103a4e6af74571bf7" + integrity sha512-9R5yG35liLk8Q8ZtFSF7MKV8cktcG1lAQ2T5JVn4WxELfkrdAHYl/QfQ+R3AYSsdMiGh580sJBZ8875qcOwrYw== dependencies: - "@nrwl/devkit" "13.8.1" - "@nrwl/jest" "13.8.1" + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" "@phenomnomnominal/tsquery" "4.1.1" tmp "~0.2.1" tslib "^2.3.0" -"@nrwl/nest@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-13.8.1.tgz#1e172452956da908d4f728e73fed58e97372d3d0" - integrity sha512-vlYQPyT7NpPJR6YSXm+RdVQu0dbCvbrsyTDNpPTRQiQuz3Q6pcn/fLTaDhfi6I06aGqTzj6bASUJ9oHFVj/5Ww== +"@nrwl/nest@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-13.8.5.tgz#8ba6e4929ab88192c3697a2849effac4960b5901" + integrity sha512-N3xUYxJRPHK/jJIusrh+ryqqqCqQI9xtEobqE838ztjyVGGoXOHBkIU6u4kBQFkVyg5efCLoL7nUBp1CrhkBnA== dependencies: "@nestjs/schematics" "^8.0.0" - "@nrwl/devkit" "13.8.1" - "@nrwl/jest" "13.8.1" - "@nrwl/linter" "13.8.1" - "@nrwl/node" "13.8.1" - -"@nrwl/node@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-13.8.1.tgz#78b99b6bafe72b63ad0cf308f2bf0ccd05e0a423" - integrity sha512-D1ZjBV1gAr+CIu4h9fWlazAqeFBg1iAtBsVgzszn6iizaw3y66wq7oknZUozP4uALvkFdK2q+qLEwAsGrZBCyg== - dependencies: - "@nrwl/devkit" "13.8.1" - "@nrwl/jest" "13.8.1" - "@nrwl/linter" "13.8.1" - "@nrwl/workspace" "13.8.1" + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" + "@nrwl/js" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/node" "13.8.5" + +"@nrwl/node@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-13.8.5.tgz#435a8d42de4eb2577ac48fa8299ac6aaffa7e02a" + integrity sha512-W+Sf+pbfSJzvlIs8xNZ5dRjnYBC9UGNEnDPTuLQi+LIVo40c+3pPD1zXWK6YCpMLqakzKlil0xNJqGbEVRlttA== + dependencies: + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" + "@nrwl/js" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/workspace" "13.8.5" chalk "4.1.0" copy-webpack-plugin "^9.0.1" enhanced-resolve "^5.8.3" @@ -3347,48 +3361,51 @@ webpack-merge "^5.8.0" webpack-node-externals "^3.0.0" -"@nrwl/storybook@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.8.1.tgz#038e98225b236099b7d8af698ada06e2e53c9642" - integrity sha512-eHnaziiq87Pl2jbSq/CbF2FNfW2WPMfD1A8nCtar/9A6ukpT5xYY027e96hu3a816+WdjAIznIK28klK1Tuwuw== +"@nrwl/storybook@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.8.5.tgz#81915a707619b9eab36d17fe29f922a209d25a74" + integrity sha512-XAiNSxaRo7ZDM6sZx5wD0eBxWD7oikMxGUqLTC6sEhTdYoWOouepRDbVgOf5qHHZD7TSV9rdIU0vYVIhEbW66g== dependencies: - "@nrwl/cypress" "13.8.1" - "@nrwl/devkit" "13.8.1" - "@nrwl/linter" "13.8.1" - "@nrwl/workspace" "13.8.1" + "@nrwl/cypress" "13.8.5" + "@nrwl/devkit" "13.8.5" + "@nrwl/linter" "13.8.5" + "@nrwl/workspace" "13.8.5" core-js "^3.6.5" semver "7.3.4" ts-loader "^9.2.6" tsconfig-paths-webpack-plugin "3.5.2" -"@nrwl/tao@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.8.1.tgz#6d8168d5cb81ffc1e3e74352db4f5eef7e5ba3f0" - integrity sha512-eY05o0napek5b99DH+dir32q2pCemWmwF4ooimU4BnuY90lXC6FUXuB4+w8/tTGTI5TqjfXOnBokTqr3DPDRpQ== +"@nrwl/tao@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.8.5.tgz#223e93dbfe11b47c4c13a66cc9086c2f2572b1ae" + integrity sha512-ENT6wpxjSWBYKeLT0YueVFehlN1K2lJzgVOJTk4cQ0LbTw0fJCwcTe4ludiW4hPPTF7P5zzi0PmB9a4ss46tQg== dependencies: + "@swc-node/register" "^1.4.2" + "@swc/core" "^1.2.146" chalk "4.1.0" enquirer "~2.3.6" fast-glob "3.2.7" fs-extra "^9.1.0" ignore "^5.0.4" jsonc-parser "3.0.0" - nx "13.8.1" + nx "13.8.5" rxjs "^6.5.4" rxjs-for-await "0.0.2" semver "7.3.4" tmp "~0.2.1" + tsconfig-paths "^3.9.0" tslib "^2.3.0" yargs-parser "20.0.0" -"@nrwl/workspace@13.8.1": - version "13.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-13.8.1.tgz#4b27bdd752fdbfd8ca7718a23e204b9129884ac5" - integrity sha512-veemewkJtK3UwOGJDcrVw5h+cpjFh3JnmwSnTFHqxKpsN/hkCQk3CgOmBJ4w50qI/gmyuEm+HeGC5/ZNq3kRDA== +"@nrwl/workspace@13.8.5": + version "13.8.5" + resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-13.8.5.tgz#424a4967ef84be908920a30b83ac5d3a49323347" + integrity sha512-uc2IICiSu5hTE1OkVPjBuBlwMl/6zzNL5HnrTCul7dDxRMn0wQsqifTed1QPdgp8Bct6d1uYCc/19fO+wCw1RA== dependencies: - "@nrwl/cli" "13.8.1" - "@nrwl/devkit" "13.8.1" - "@nrwl/jest" "13.8.1" - "@nrwl/linter" "13.8.1" + "@nrwl/cli" "13.8.5" + "@nrwl/devkit" "13.8.5" + "@nrwl/jest" "13.8.5" + "@nrwl/linter" "13.8.5" "@parcel/watcher" "2.0.4" chalk "4.1.0" chokidar "^3.5.1" @@ -3470,22 +3487,22 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@prisma/client@3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.9.1.tgz#565c8121f1220637bcab4a1d1f106b8c1334406c" - integrity sha512-aLwfXKLvL+loQ0IuPPCXkcq8cXBg1IeoHHa5lqQu3dJHdj45wnislA/Ny4UxRQjD5FXqrfAb8sWtF+jhdmjFTg== +"@prisma/client@3.10.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.10.0.tgz#4782fe6f1b0e43c2a11a75ad4bb1098599d1dfb1" + integrity sha512-6P4sV7WFuODSfSoSEzCH1qfmWMrCUBk1LIIuTbQf6m1LI/IOpLN4lnqGDmgiBGprEzuWobnGLfe9YsXLn0inrg== dependencies: - "@prisma/engines-version" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" + "@prisma/engines-version" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" -"@prisma/engines-version@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009": - version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#ea03ffa723382a526dc6625ce6eae9b6ad984400" - integrity sha512-5Dh+qTDhpPR66w6NNAnPs+/W/Qt4r1DSd+qhfPFcDThUK4uxoZKGlPb2IYQn5LL+18aIGnmteDf7BnVMmvBNSQ== +"@prisma/engines-version@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86": + version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#82750856fa637dd89b8f095d2dcc6ac0631231c6" + integrity sha512-cVYs5gyQH/qyut24hUvDznCfPrWiNMKNfPb9WmEoiU6ihlkscIbCfkmuKTtspVLWRdl0LqjYEC7vfnPv17HWhw== -"@prisma/engines@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009": - version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#e5c345cdedb7be83d11c1e0c5ab61d866b411256" - integrity sha512-qM+uJbkelB21bnK44gYE049YTHIjHysOuj0mj5U2gDGyNLfmiazlggzFPCgEjgme4U5YB2tYs6Z5Hq08Kl8pjA== +"@prisma/engines@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86": + version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#2964113729a78b8b21e186b5592affd1fde73c16" + integrity sha512-LjRssaWu9w2SrXitofnutRIyURI7l0veQYIALz7uY4shygM9nMcK3omXcObRm7TAcw3Z+9ytfK1B+ySOsOesxQ== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -4505,66 +4522,131 @@ resolved "https://registry.yarnpkg.com/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.138.tgz#4605fa4afc0bb515798a7b7ebd274eb06f67775b" integrity sha512-N79aTHj/jZNa8nXjOrfAaYYBkJxCQ9ZVFikQKSbBETU8usk7qAWDdCs94Y0q/Sow+9uiqguRVOrPFKSrN8LMTg== +"@swc/core-android-arm-eabi@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.151.tgz#e44fe75b2d8ba4685fbbf5727082b58b13bb2775" + integrity sha512-Suk3IcHdha33K4hq9tfBCwkXJsENh7kjXCseLqL8Yvy8QobqkXjf1fcoJxX9BdCmPwsKmIw0ZgCBYR+Hl83M2w== + "@swc/core-android-arm64@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.138.tgz#7bb94a78d7253ca8b6ec92be435c5a7686dbd68c" integrity sha512-ZNRqTjZpNrB39pCX5OmtnNTnzU3X1GjZX2xDouS1jknEE+TPz1ZJsM4zNlz6AObd7caJhU7qRyWNDM0nlcnJZQ== +"@swc/core-android-arm64@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.151.tgz#8b7d02c8aed574a1cd5c312780abae9e17db159e" + integrity sha512-HZVy69dVWT5RgrMJMRK5aiicPmhzkyCHAexApYAHYLgAIhsxL7uoAIPmuRKRkrKNJjrwsWL7H27bBH5bddRDvg== + "@swc/core-darwin-arm64@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.138.tgz#8a31dbdb90626f503a837ee71fa3bb7866ac3eb1" integrity sha512-DlT0s3Iw3bmOCk4jln0Q9AC1H7q75bZojyODcPXQ2T24s6LcBeD1lNAfyQ2RmaQJTlBM04LjNYqvjA2HAR4ckw== +"@swc/core-darwin-arm64@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.151.tgz#dc241a17bc920b7ece073579e3f9059ce0dc5ae5" + integrity sha512-Ql7rXMu+IC76TemRtkt+opl5iSpX2ApAXVSfvf6afNVTrfTKLpDwiR3ySRRlG0FnNIv6TfOCJpHf655xp01S/g== + "@swc/core-darwin-x64@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.138.tgz#cc389708336dabc411a6d4705c2be17f9407054b" integrity sha512-+8ahwSnUTPCmpB1VkMTJdfcFU+ZGQ5JnA1dpSvDhB/u8wV2Dpk0ozpX+3xjqYXoUdhZvdHW1FxKZrhMhscJriA== +"@swc/core-darwin-x64@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.151.tgz#083dbf276d07c4537257bc25ad376602a34584b6" + integrity sha512-N1OBIB7xatR5eybLo91ZhvMJMxT0zxRQURV/a9I8o5CyP4iLd1k8gmrYvBbtj08ohS8F9z7k/dFjxk/9ve5Drw== + "@swc/core-freebsd-x64@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.138.tgz#2f29b1e8f133825fefb558a071f3bdb67dcf3c32" integrity sha512-4icXrpDBN2r24PIRF2DBZ9IPgnXnEqO7/bySIUoL7ul8su2yoRP4Xp3Xi+XP+uBvtrVttwYtzGPNikVggVSK1Q== +"@swc/core-freebsd-x64@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.151.tgz#568a35267f1cccdef2fdc3e53c4f9a6095173706" + integrity sha512-WVIRiDzuz+/W7BMjVtg1Cmk1+zmDT18Qq+Ygr9J6aFQ1JQUkLEE1pvtkGD3JIEa6Jhz/VwM6AFHtY5o1CrZ21w== + "@swc/core-linux-arm-gnueabihf@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.138.tgz#255c2011d865ff8f8118753f8900b51545c30000" integrity sha512-YdEKUvT9GGBEsKSyXc/YJ0cWSetBV3JhxouYLCv4AoQsTrDU5vDQDFUWlT21pzlbwC66ffbpYxnugpsqBm5XKg== -"@swc/core-linux-arm64-gnu@1.2.138", "@swc/core-linux-arm64-gnu@^1.2.136": +"@swc/core-linux-arm-gnueabihf@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.151.tgz#24859f442a255220ca1caa7f8f5f087f2c22fd08" + integrity sha512-pfBrIUwu3cR/M7DzDCUJAw9jFKXvJ/Ge8auFk07lRb+JcDnPm0XxLyrLqGvNQWdcHgXeXfmnS4fMQxdb9GUN1w== + +"@swc/core-linux-arm64-gnu@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.138.tgz#89813e14240bde17aaa914a47e84626a10ae13ec" integrity sha512-cn/YrVvghCgSpagzHins1BQnJ07J53aCvlp57iXDA2xfH/HwXTijIy+UzqpQaLeKKQ8gMXmfzj/M7WklccN8jw== -"@swc/core-linux-arm64-musl@1.2.138", "@swc/core-linux-arm64-musl@^1.2.136": +"@swc/core-linux-arm64-gnu@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.151.tgz#a4ae2d8f7b0cfb0836466ca57b608be7505b13e7" + integrity sha512-M+BTkTdPY7gteM+0dYz9wrU/j9taL4ccqPEHkDEKP21lS24y99UtuKsvdBLzDm/6ShBVLFAkgIBPu5cEb7y6ig== + +"@swc/core-linux-arm64-musl@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.138.tgz#c33351846218a4bd471505c9215233608f648ab9" integrity sha512-aYoeZ46gaewTYYShHwlYhL8ARrLILiEnTWJFEWoUfAfbDwi4zaLyymRYmdpUyRHr+D9jloM5BKFNWnRPBTyCEg== -"@swc/core-linux-x64-gnu@1.2.138", "@swc/core-linux-x64-gnu@^1.2.136": +"@swc/core-linux-arm64-musl@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.151.tgz#16785fa421244d9df02123ce8b4bf8964b37412a" + integrity sha512-7A+yTtSvPJVwO8X1cxUbD/PVCx8G9MKn83G9pH/r+9sQMBXqxyw6/NR0DG6nMMiyOmJkmYWgh5mO47BN7WC4dQ== + +"@swc/core-linux-x64-gnu@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.138.tgz#0be2226c7c701d8f58051ca47e78f24d479a9faa" integrity sha512-gt9qP426kkIx4Yu2Dd9U2S44OE8ynRi47rt2HvdHaBlMsGfMH28EyMet3UT61ZVHMEoDxADQctz0JD1/29Ha1Q== -"@swc/core-linux-x64-musl@1.2.138", "@swc/core-linux-x64-musl@^1.2.136": +"@swc/core-linux-x64-gnu@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.151.tgz#b0717cb662becec95d306632fbd40f612d3db700" + integrity sha512-ORlbN3wf1w0IQGjGToYYC/hV/Vwfcs88Ohfxc4X+IQaw/VxKG6/XT65c0btK640F2TVhvhH1MbYFJJlsycsW7g== + +"@swc/core-linux-x64-musl@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.138.tgz#07feede753206a4858dd275a0a4f99501909010e" integrity sha512-lySbIVGApaDQVKPwH8D+9J5dkrawJTrBm86vY7F9sDPR5yCq5Buxx6Pn1X6VKE6e5vlEEb1zbVQmCrFgdUcgig== +"@swc/core-linux-x64-musl@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.151.tgz#7d703b3a96da37538bd69e4582c8ee70c9d36a37" + integrity sha512-r6odKE3+9+ReVdnNTZnICt5tscyFFtP4GFcmPQzBSlVoD9LZX6O4WeOlFXn77rVK/+205n2ag/KkKgZH+vdPuQ== + "@swc/core-win32-arm64-msvc@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.138.tgz#04e7dbfefb2e933433be32254c52c65add15c086" integrity sha512-UmDtaC9ds1SNNfhYrHW1JvBhy7wKb/Y9RcQOsfG3StxqqnYkOWDkQt9dY5O9lAG8Iw/TCxzjJhm6ul48eMv9OQ== +"@swc/core-win32-arm64-msvc@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.151.tgz#aa97ef1df5e740c0ae1b4b0586f6c544983f11a7" + integrity sha512-jnjJTNHpLhBaPwRgiKv1TdrMljL88ePqMCdVMantyd7yl4lP0D2e5/xR9ysR9S4EGcUnOyo9w8WUYhx/TioMZw== + "@swc/core-win32-ia32-msvc@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.138.tgz#7d897c97ac5338e8a947d6c0c032e8068b521a2e" integrity sha512-evapKq/jVKMI5KDXUvpu3rhYf/L0VIg92TTphpxJSNjo7k5w9n68RY3MXtm1BmtCR4ZWtx0OEXzr9ckUDcqZDA== +"@swc/core-win32-ia32-msvc@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.151.tgz#6ab6889078ef820a7c644d7df72403cbb534d4e2" + integrity sha512-hSCxAiyDDXKvdUExj4jSIhzWFePqoqak1qdNUjlhEhEinDG8T8PTRCLalyW6fqZDcLf6Tqde7H79AqbfhRlYGQ== + "@swc/core-win32-x64-msvc@1.2.138": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.138.tgz#6a54a72ed035d3b327f2576f4a586da093dc4898" integrity sha512-wYrARtnPg/svsQd0oovbth2JAhOugAgbnaOS0CMiWB4vaFBx+1GHJl5wzdhh9jt1kzsu4xZ4237tUeMH+s6d0A== +"@swc/core-win32-x64-msvc@1.2.151": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.151.tgz#525c9554da57c0d4b07956349680b1dc9c4dee4f" + integrity sha512-HOkqcJWCChps83Maj0M5kifPDuZ2sGPqpLM67poawspTFkBh0QJ9TMmxW1doQw+74cqsTpRi1ewr/KhsN18i5g== + "@swc/core@^1.2.119": version "1.2.138" resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.138.tgz#e54d8488094f7f90cb00455cb0380693c0935865" @@ -4584,6 +4666,25 @@ "@swc/core-win32-ia32-msvc" "1.2.138" "@swc/core-win32-x64-msvc" "1.2.138" +"@swc/core@^1.2.146": + version "1.2.151" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.151.tgz#8d4154a2e4ced74c5fd215c5905baa08775553d6" + integrity sha512-oHgqKwK/Djv765zUHPiGqfMCaKIxXTgQyyCUBKLBQfAJwe/7FVobQ2fghBp4FsZA/NE1LZBmMPpRZNQwlGjeHw== + optionalDependencies: + "@swc/core-android-arm-eabi" "1.2.151" + "@swc/core-android-arm64" "1.2.151" + "@swc/core-darwin-arm64" "1.2.151" + "@swc/core-darwin-x64" "1.2.151" + "@swc/core-freebsd-x64" "1.2.151" + "@swc/core-linux-arm-gnueabihf" "1.2.151" + "@swc/core-linux-arm64-gnu" "1.2.151" + "@swc/core-linux-arm64-musl" "1.2.151" + "@swc/core-linux-x64-gnu" "1.2.151" + "@swc/core-linux-x64-musl" "1.2.151" + "@swc/core-win32-arm64-msvc" "1.2.151" + "@swc/core-win32-ia32-msvc" "1.2.151" + "@swc/core-win32-x64-msvc" "1.2.151" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -5720,6 +5821,16 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" +ajv@8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" + integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ajv@8.6.3: version "8.6.3" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" @@ -6561,7 +6672,7 @@ blob-util@2.0.2: resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== -bluebird@^3.3.5, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: +bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6763,7 +6874,7 @@ browserslist@^4.19.1: node-releases "^2.0.1" picocolors "^1.0.0" -bs-logger@0.x: +bs-logger@0.x, bs-logger@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== @@ -8834,208 +8945,219 @@ es6-shim@^0.35.5: resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.6.tgz#d10578301a83af2de58b9eadb7c2c9945f7388a0" integrity sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA== -esbuild-android-arm64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz#da07b5fb2daf7d83dcd725f7cf58a6758e6e702a" - integrity sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ== +esbuild-android-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.11.tgz#b8b34e35a5b43880664ac7a3fbc70243d7ed894f" + integrity sha512-6iHjgvMnC/SzDH8TefL+/3lgCjYWwAd1LixYfmz/TBPbDQlxcuSkX0yiQgcJB9k+ibZ54yjVXziIwGdlc+6WNw== esbuild-android-arm64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.14.tgz#3705f32f209deeb11c275af47c298c8783dd5f0c" integrity sha512-be/Uw6DdpQiPfula1J4bdmA+wtZ6T3BRCZsDMFB5X+k0Gp8TIh9UvmAcqvKNnbRAafSaXG3jPCeXxDKqnc8hFQ== -esbuild-darwin-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz#e94e9fd3b4b5455a2e675cd084a19a71b6904bbf" - integrity sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA== +esbuild-darwin-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.11.tgz#ba805de98c0412e50fcd0636451797da157b0625" + integrity sha512-olq84ikh6TiBcrs3FnM4eR5VPPlcJcdW8BnUz/lNoEWYifYQ+Po5DuYV1oz1CTFMw4k6bQIZl8T3yxL+ZT2uvQ== esbuild-darwin-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.14.tgz#c07e4eae6d938300a2d330ea82494c55bcea84e5" integrity sha512-BEexYmjWafcISK8cT6O98E3TfcLuZL8DKuubry6G54n2+bD4GkoRD6HYUOnCkfl2p7jodA+s4369IjSFSWjtHg== -esbuild-darwin-arm64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz#8c320eafbb3ba2c70d8062128c5b71503e342471" - integrity sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g== +esbuild-darwin-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.11.tgz#4d3573e448af76ce33e16231f3d9f878542d6fe8" + integrity sha512-Jj0ieWLREPBYr/TZJrb2GFH8PVzDqiQWavo1pOFFShrcmHWDBDrlDxPzEZ67NF/Un3t6sNNmeI1TUS/fe1xARg== esbuild-darwin-arm64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.14.tgz#a8631e13a51a6f784fb0906e2a64c6ab53988755" integrity sha512-tnBKm41pDOB1GtZ8q/w26gZlLLRzVmP8fdsduYjvM+yFD7E2DLG4KbPAqFMWm4Md9B+DitBglP57FY7AznxbTg== -esbuild-freebsd-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz#ce0ca5b8c4c274cfebc9326f9b316834bd9dd151" - integrity sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A== +esbuild-freebsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.11.tgz#9294e6ab359ec93590ab097b0f2017de7c78ab4d" + integrity sha512-C5sT3/XIztxxz/zwDjPRHyzj/NJFOnakAanXuyfLDwhwupKPd76/PPHHyJx6Po6NI6PomgVp/zi6GRB8PfrOTA== esbuild-freebsd-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.14.tgz#c280c2b944746b27ee6c6487c2691865c90bed2e" integrity sha512-Q9Rx6sgArOHalQtNwAaIzJ6dnQ8A+I7f/RsQsdkS3JrdzmnlFo8JEVofTmwVQLoIop7OKUqIVOGP4PoQcwfVMA== -esbuild-freebsd-arm64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz#463da17562fdcfdf03b3b94b28497d8d8dcc8f62" - integrity sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw== +esbuild-freebsd-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.11.tgz#ae3e0b09173350b66cf8321583c9a1c1fcb8bb55" + integrity sha512-y3Llu4wbs0bk4cwjsdAtVOesXb6JkdfZDLKMt+v1U3tOEPBdSu6w8796VTksJgPfqvpX22JmPLClls0h5p+L9w== esbuild-freebsd-arm64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.14.tgz#aa4e21276efcf20e5ab2487e91ca1d789573189b" integrity sha512-TJvq0OpLM7BkTczlyPIphcvnwrQwQDG1HqxzoYePWn26SMUAlt6wrLnEvxdbXAvNvDLVzG83kA+JimjK7aRNBA== -esbuild-linux-32@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz#2035793160da2c4be48a929e5bafb14a31789acc" - integrity sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w== +esbuild-linux-32@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.11.tgz#ddadbc7038aa5a6b1675bb1503cf79a0cbf1229a" + integrity sha512-Cg3nVsxArjyLke9EuwictFF3Sva+UlDTwHIuIyx8qpxRYAOUTmxr2LzYrhHyTcGOleLGXUXYsnUVwKqnKAgkcg== esbuild-linux-32@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.14.tgz#3db4d929239203ce38a9060d5419ac6a6d28846c" integrity sha512-h/CrK9Baimt5VRbu8gqibWV7e1P9l+mkanQgyOgv0Ng3jHT1NVFC9e6rb1zbDdaJVmuhWX5xVliUA5bDDCcJeg== -esbuild-linux-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz#fbe4802a8168c6d339d0749f977b099449b56f22" - integrity sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw== +esbuild-linux-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.11.tgz#d698e3ce3a231ddfeec6b5df8c546ae8883fcd88" + integrity sha512-oeR6dIrrojr8DKVrxtH3xl4eencmjsgI6kPkDCRIIFwv4p+K7ySviM85K66BN01oLjzthpUMvBVfWSJkBLeRbg== esbuild-linux-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.14.tgz#f880026254c1f565a7a10fdebb7cff9b083a127d" integrity sha512-IC+wAiIg/egp5OhQp4W44D9PcBOH1b621iRn1OXmlLzij9a/6BGr9NMIL4CRwz4j2kp3WNZu5sT473tYdynOuQ== -esbuild-linux-arm64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz#f08d98df28d436ed4aad1529615822bb74d4d978" - integrity sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng== +esbuild-linux-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.11.tgz#85faea9fa99ad355b5e3b283197a4dfd0a110fe7" + integrity sha512-+e6ZCgTFQYZlmg2OqLkg1jHLYtkNDksxWDBWNtI4XG4WxuOCUErLqfEt9qWjvzK3XBcCzHImrajkUjO+rRkbMg== esbuild-linux-arm64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.14.tgz#a34bc3076e50b109c3b8c8bad9c146e35942322b" integrity sha512-6QVul3RI4M5/VxVIRF/I5F+7BaxzR3DfNGoqEVSCZqUbgzHExPn+LXr5ly1C7af2Kw4AHpo+wDqx8A4ziP9avw== -esbuild-linux-arm@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz#6f968c3a98b64e30c80b212384192d0cfcb32e7f" - integrity sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ== +esbuild-linux-arm@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.11.tgz#74cbcf0b8a22c8401bcbcd6ebd4cbf2baca8b7b4" + integrity sha512-vcwskfD9g0tojux/ZaTJptJQU3a7YgTYsptK1y6LQ/rJmw7U5QJvboNawqM98Ca3ToYEucfCRGbl66OTNtp6KQ== esbuild-linux-arm@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.14.tgz#231ffd12fef69ee06365d4c94b69850e4830e927" integrity sha512-gxpOaHOPwp7zSmcKYsHrtxabScMqaTzfSQioAMUaB047YiMuDBzqVcKBG8OuESrYkGrL9DDljXr/mQNg7pbdaQ== -esbuild-linux-mips64le@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz#690c78dc4725efe7d06a1431287966fbf7774c7f" - integrity sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w== +esbuild-linux-mips64le@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.11.tgz#490429211a3233f5cbbd8575b7758b897e42979a" + integrity sha512-Rrs99L+p54vepmXIb87xTG6ukrQv+CzrM8eoeR+r/OFL2Rg8RlyEtCeshXJ2+Q66MXZOgPJaokXJZb9snq28bw== esbuild-linux-mips64le@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.14.tgz#bd00570e3a30422224b732c7a5f262146c357403" integrity sha512-4Jl5/+xoINKbA4cesH3f4R+q0vltAztZ6Jm8YycS8lNhN1pgZJBDxWfI6HUMIAdkKlIpR1PIkA9aXQgZ8sxFAg== -esbuild-linux-ppc64le@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz#7ec9048502de46754567e734aae7aebd2df6df02" - integrity sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ== +esbuild-linux-ppc64le@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.11.tgz#fc79d60710213b5b98345f5b138d48245616827a" + integrity sha512-JyzziGAI0D30Vyzt0HDihp4s1IUtJ3ssV2zx9O/c+U/dhUHVP2TmlYjzCfCr2Q6mwXTeloDcLS4qkyvJtYptdQ== esbuild-linux-ppc64le@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.14.tgz#430609413fd9e04d9def4e3f06726b031b23d825" integrity sha512-BitW37GxeebKxqYNl4SVuSdnIJAzH830Lr6Mkq3pBHXtzQay0vK+IeOR/Ele1GtNVJ+/f8wYM53tcThkv5SC5w== +esbuild-linux-s390x@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.11.tgz#ca4b93556bbba6cc95b0644f2ee93c982165ba07" + integrity sha512-DoThrkzunZ1nfRGoDN6REwmo8ZZWHd2ztniPVIR5RMw/Il9wiWEYBahb8jnMzQaSOxBsGp0PbyJeVLTUatnlcw== + esbuild-linux-s390x@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.14.tgz#2f0d8cbfe53cf3cb97f6372549a41a8051dbd689" integrity sha512-vLj6p76HOZG3wfuTr5MyO3qW5iu8YdhUNxuY+tx846rPo7GcKtYSPMusQjeVEfZlJpSYoR+yrNBBxq+qVF9zrw== -esbuild-netbsd-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz#439bdaefffa03a8fa84324f5d83d636f548a2de3" - integrity sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ== +esbuild-netbsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.11.tgz#edb340bc6653c88804cac2253e21b74258fce165" + integrity sha512-12luoRQz+6eihKYh1zjrw0CBa2aw3twIiHV/FAfjh2NEBDgJQOY4WCEUEN+Rgon7xmLh4XUxCQjnwrvf8zhACw== esbuild-netbsd-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.14.tgz#3e44de35e1add7e9582f3c0d2558d86aafbc813b" integrity sha512-fn8looXPQhpVqUyCBWUuPjesH+yGIyfbIQrLKG05rr1Kgm3rZD/gaYrd3Wpmf5syVZx70pKZPvdHp8OTA+y7cQ== -esbuild-openbsd-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz#c9958e5291a00a3090c1ec482d6bcdf2d5b5d107" - integrity sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w== +esbuild-openbsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.11.tgz#caeff5f946f79a60ce7bcf88871ca4c71d3476e8" + integrity sha512-l18TZDjmvwW6cDeR4fmizNoxndyDHamGOOAenwI4SOJbzlJmwfr0jUgjbaXCUuYVOA964siw+Ix+A+bhALWg8Q== esbuild-openbsd-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.14.tgz#04710ef1d01cd9f15d54f50d20b5a3778f8306a2" integrity sha512-HdAnJ399pPff3SKbd8g+P4o5znseni5u5n5rJ6Z7ouqOdgbOwHe2ofZbMow17WMdNtz1IyOZk2Wo9Ve6/lZ4Rg== -esbuild-sunos-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz#ac9ead8287379cd2f6d00bd38c5997fda9c1179e" - integrity sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ== +esbuild-sunos-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.11.tgz#90ce7e1749c2958a53509b4bae7b8f7d98f276d6" + integrity sha512-bmYzDtwASBB8c+0/HVOAiE9diR7+8zLm/i3kEojUH2z0aIs6x/S4KiTuT5/0VKJ4zk69kXel1cNWlHBMkmavQg== esbuild-sunos-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.14.tgz#8e583dd92c5c7ac4303ddc37f588e44211e04e19" integrity sha512-bmDHa99ulsGnYlh/xjBEfxoGuC8CEG5OWvlgD+pF7bKKiVTbtxqVCvOGEZeoDXB+ja6AvHIbPxrEE32J+m5nqQ== +esbuild-wasm@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.14.11.tgz#bd09f4c42969cddcae39007d284f8ef747aae85d" + integrity sha512-9e1R6hv0hiU+BkJI2edqUuWfXUbOP2Mox+Ijl/uY1vLLlSsunkrcADqD/4Rz+VCEDzw6ecscJM+uJqR2fRmEUg== + esbuild-wasm@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.14.14.tgz#d4c8d5fc405939a2234a31abf00967dfd1da1caa" integrity sha512-qTjK4MWnYtQHCMGg2qDUqeFYXfVvYq5qJkQTIsOV4VZCknoYePVaDTG9ygEB9Ct0kc0DWs7IrS6Ja+GjY62Kzw== -esbuild-windows-32@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz#a3820fc86631ca594cb7b348514b5cc3f058cfd6" - integrity sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw== +esbuild-windows-32@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.11.tgz#d067f4ce15b29efba6336e6a23597120fafe49ec" + integrity sha512-J1Ys5hMid8QgdY00OBvIolXgCQn1ARhYtxPnG6ESWNTty3ashtc4+As5nTrsErnv8ZGUcWZe4WzTP/DmEVX1UQ== esbuild-windows-32@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.14.tgz#6d293ddfb71229f21cc13d85d5d2f43e8131693b" integrity sha512-6tVooQcxJCNenPp5GHZBs/RLu31q4B+BuF4MEoRxswT+Eq2JGF0ZWDRQwNKB8QVIo3t6Svc5wNGez+CwKNQjBg== -esbuild-windows-64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz#1da748441f228d75dff474ddb7d584b81887323c" - integrity sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA== +esbuild-windows-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.11.tgz#13e86dd37a6cd61a5276fa2d271342d0f74da864" + integrity sha512-h9FmMskMuGeN/9G9+LlHPAoiQk9jlKDUn9yA0MpiGzwLa82E7r1b1u+h2a+InprbSnSLxDq/7p5YGtYVO85Mlg== esbuild-windows-64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.14.tgz#08a36844b69542f8ec1cb33a5ddcea02b9d0b2e8" integrity sha512-kl3BdPXh0/RD/dad41dtzj2itMUR4C6nQbXQCyYHHo4zoUoeIXhpCrSl7BAW1nv5EFL8stT1V+TQVXGZca5A2A== -esbuild-windows-arm64@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz#06dfa52a6b178a5932a9a6e2fdb240c09e6da30c" - integrity sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A== +esbuild-windows-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.11.tgz#e8edfdf1d712085e6dc3fba18a0c225aaae32b75" + integrity sha512-dZp7Krv13KpwKklt9/1vBFBMqxEQIO6ri7Azf8C+ob4zOegpJmha2XY9VVWP/OyQ0OWk6cEeIzMJwInRZrzBUQ== esbuild-windows-arm64@0.14.14: version "0.14.14" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.14.tgz#ca747ce4066d5b8a79dbe48fe6ecd92d202e5366" integrity sha512-dCm1wTOm6HIisLanmybvRKvaXZZo4yEVrHh1dY0v582GThXJOzuXGja1HIQgV09RpSHYRL3m4KoUBL00l6SWEg== -esbuild@0.13.13: - version "0.13.13" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.13.tgz#0b5399c20f219f663c8c1048436fb0f59ab17a41" - integrity sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q== +esbuild@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.11.tgz#ac4acb78907874832afb704c3afe58ad37715c27" + integrity sha512-xZvPtVj6yecnDeFb3KjjCM6i7B5TCAQZT77kkW/CpXTMnd6VLnRPKrUB1XHI1pSq6a4Zcy3BGueQ8VljqjDGCg== optionalDependencies: - esbuild-android-arm64 "0.13.13" - esbuild-darwin-64 "0.13.13" - esbuild-darwin-arm64 "0.13.13" - esbuild-freebsd-64 "0.13.13" - esbuild-freebsd-arm64 "0.13.13" - esbuild-linux-32 "0.13.13" - esbuild-linux-64 "0.13.13" - esbuild-linux-arm "0.13.13" - esbuild-linux-arm64 "0.13.13" - esbuild-linux-mips64le "0.13.13" - esbuild-linux-ppc64le "0.13.13" - esbuild-netbsd-64 "0.13.13" - esbuild-openbsd-64 "0.13.13" - esbuild-sunos-64 "0.13.13" - esbuild-windows-32 "0.13.13" - esbuild-windows-64 "0.13.13" - esbuild-windows-arm64 "0.13.13" + esbuild-android-arm64 "0.14.11" + esbuild-darwin-64 "0.14.11" + esbuild-darwin-arm64 "0.14.11" + esbuild-freebsd-64 "0.14.11" + esbuild-freebsd-arm64 "0.14.11" + esbuild-linux-32 "0.14.11" + esbuild-linux-64 "0.14.11" + esbuild-linux-arm "0.14.11" + esbuild-linux-arm64 "0.14.11" + esbuild-linux-mips64le "0.14.11" + esbuild-linux-ppc64le "0.14.11" + esbuild-linux-s390x "0.14.11" + esbuild-netbsd-64 "0.14.11" + esbuild-openbsd-64 "0.14.11" + esbuild-sunos-64 "0.14.11" + esbuild-windows-32 "0.14.11" + esbuild-windows-64 "0.14.11" + esbuild-windows-arm64 "0.14.11" esbuild@0.14.14: version "0.14.14" @@ -12133,15 +12255,18 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-preset-angular@11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-11.0.0.tgz#4b1913e4baddf37a8b96d6215d9a647dcdd6f324" - integrity sha512-+Vt6O2q/cvhbbrE4xplZjn3TqLcQpOtkk+zqoCFLW/Lo0fALEJIXECt1Ia288iJtxJU4qm7tLsQy1KmAaN+CzA== +jest-preset-angular@11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/jest-preset-angular/-/jest-preset-angular-11.1.1.tgz#cc1c0a1395727af332c439174fb689d92e853f6a" + integrity sha512-ZlYiKJhAQSU9wIjncX59xutcj49R4MiDsTPSwZiwdTAHQvHm32MS6SGimQIVBqh1DukfwYX0NXKS0D/onLAsLQ== dependencies: - esbuild "0.13.13" + bs-logger "^0.2.6" + esbuild-wasm "0.14.11" jest-environment-jsdom "^27.0.0" pretty-format "^27.0.0" ts-jest "^27.0.0" + optionalDependencies: + esbuild "0.14.11" jest-regex-util@^26.0.0: version "26.0.0" @@ -12976,7 +13101,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -13507,14 +13632,14 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@^0.5.10, moment-timezone@^0.5.x: +moment-timezone@^0.5.x: version "0.5.33" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.17.1, moment@^2.27.0: +"moment@>= 2.9.0", moment@^2.27.0: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -13546,6 +13671,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@3.0.0-canary.1: + version "3.0.0-canary.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-3.0.0-canary.1.tgz#c7b34fbce381492fd0b345d1cf56e14d67b77b80" + integrity sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g== + ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -13678,13 +13808,13 @@ ngx-markdown@13.0.0: prismjs "^1.25.0" tslib "^2.3.0" -ngx-skeleton-loader@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/ngx-skeleton-loader/-/ngx-skeleton-loader-2.9.1.tgz#1e419ef66696a2017afc9c8cd0bc129d2c680ffb" - integrity sha512-knFL2Ua/p60XUPH9ZrfgydiBHvvylny7jsVOXBtmACrYD7HcnuUl1uAEk/LvcN15tSjC9VnVwTIOU4i8LCSAgw== +ngx-skeleton-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ngx-skeleton-loader/-/ngx-skeleton-loader-5.0.0.tgz#e0042de20b0159d3f97d03a696d68f39ceee383b" + integrity sha512-6cz8UAu4WcYnBp/LnU053LCIwjKNZWX8GX1v3bvqQVdDa1ubsEeJm+CZxk5B8W2jP9CcFhvWrBlmmVUyl1Yxug== dependencies: perf-marks "^1.13.4" - tslib "^1.10.0" + tslib "^2.0.0" ngx-stripe@13.0.0: version "13.0.0" @@ -13978,17 +14108,12 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -nx@13.8.1: - version "13.8.1" - resolved "https://registry.yarnpkg.com/nx/-/nx-13.8.1.tgz#10e17dace55eb38f762ed212ded02e24797c8ac7" - integrity sha512-8oHkh/Hli/OGGkumH8C9NArFjvFoNEgSfVkzCB7zoGddnIJlGAx+sSpHp0zJbXJV55Lk7iXbpKyeOJNnQDsEyQ== +nx@13.8.5: + version "13.8.5" + resolved "https://registry.yarnpkg.com/nx/-/nx-13.8.5.tgz#4553170a7fd1c587677a4ce76cfb1f2c7c363493" + integrity sha512-s8Cyk6IwptpchPJ1JWYWzy9098BuC+tf24a7O3P6idRjX/C2/GLr+5vifgySk7wji5wwK4LNUmr1SV5H+3bLNw== dependencies: - "@nrwl/cli" "13.8.1" - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + "@nrwl/cli" "13.8.5" oauth@0.9.x: version "0.9.15" @@ -15209,12 +15334,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prisma@3.9.1: - version "3.9.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.9.1.tgz#7510a8bf06018a5313b9427b1127ce4750b1ce5c" - integrity sha512-IGcJAu5LzlFv+i+NNhOEh1J1xVVttsVdRBxmrMN7eIH+7mRN6L89Hz1npUAiz4jOpNlHC7n9QwaOYZGxTqlwQw== +prisma@3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.10.0.tgz#872d87afbeb1cbcaa77c3d6a63c125e0d704b04d" + integrity sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA== dependencies: - "@prisma/engines" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" + "@prisma/engines" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1" @@ -15952,49 +16077,6 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise@^4.2.1: - version "4.2.6" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2" - integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ== - dependencies: - bluebird "^3.5.0" - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.79.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -16884,11 +16966,6 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - store2@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf" @@ -17031,11 +17108,6 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" -string@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/string/-/string-3.3.3.tgz#5ea211cd92d228e184294990a6cc97b366a77cb0" - integrity sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA= - string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -17526,14 +17598,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@^2.3.2, tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -17543,6 +17607,14 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -18764,20 +18836,14 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yahoo-finance@0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/yahoo-finance/-/yahoo-finance-0.3.6.tgz#c99fe8ff6c9a80babbb7e75881a244a862f6739f" - integrity sha512-SyXGhtvJvoU8E7XQJzviCBeuJNAMZoERJLfWwAERfDDgoPCu3/zBDDDt7l8hp3HmtIygLpqGuRJ7jzkip2AcZA== +yahoo-finance2@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.2.0.tgz#8694b04e69f4a79996812b6d082e5b738c51cee6" + integrity sha512-ZxLCcoh+J51F7Tol1jpVBmy50IBQSoxsECWYDToBxjZwPloFNHtEVOXNqJlyzTysnzVbPA5TeCNT6G0DoaJnNQ== dependencies: - bluebird "^3.4.6" - debug "^2.3.3" - lodash "^4.17.2" - moment "^2.17.1" - moment-timezone "^0.5.10" - request "^2.79.0" - request-promise "^4.2.1" - string "^3.3.3" - tough-cookie "^2.3.2" + ajv "8.10.0" + ajv-formats "2.1.1" + node-fetch "^2.6.1" yallist@^3.0.2: version "3.1.1"