From dced06ebb54becf48c35815ba17e89ffd3deb830 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Feb 2022 18:41:12 +0100 Subject: [PATCH 01/34] Bugfix/improve allocations (#710) * Fix allocations by account for non-unique account names * Refactor calculations with big.js * Update changelog --- CHANGELOG.md | 4 ++ .../allocations/allocations-page.component.ts | 8 +++- .../allocations/allocations-page.html | 2 +- .../portfolio-proportion-chart.component.ts | 42 +++++++++++-------- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efe84cd0..8be92b28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the countries and sectors charts in the position detail dialog - Restructured the server modules +### Fixed + +- Fixed the allocations by account for non-unique account names + ## 1.116.0 - 16.02.2022 ### Added 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..5cf555049 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 @@ -14,7 +14,7 @@ import { } 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 +27,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 }; @@ -171,6 +174,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.portfolioDetails.accounts )) { this.accounts[id] = { + id, name, value: aPeriod === 'original' ? original : current }; 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..21b5618d5 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 @@ 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..75593a164 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 @@ -11,6 +11,7 @@ import { import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { getTextColor } from '@ghostfolio/common/helper'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import Big from 'big.js'; import { Tooltip } from 'chart.js'; import { LinearScale } from 'chart.js'; import { ArcElement } from 'chart.js'; @@ -78,16 +79,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 +98,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,7 +142,7 @@ 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(); @@ -152,7 +160,7 @@ export class PortfolioProportionChartComponent if (!unknownItem) { chartDataSorted.push([ UNKNOWN_KEY, - { name: UNKNOWN_KEY, subCategory: {}, value: 0 } + { name: UNKNOWN_KEY, subCategory: {}, value: new Big(0) } ]); unknownItem = chartDataSorted[chartDataSorted.length - 1]; } @@ -162,7 +170,7 @@ export class PortfolioProportionChartComponent unknownItem[1] = { name: UNKNOWN_KEY, subCategory: {}, - value: unknownItem[1].value + restItem[1].value + value: unknownItem[1].value.plus(restItem[1].value) }; } }); @@ -170,7 +178,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 +209,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 +223,7 @@ export class PortfolioProportionChartComponent }), borderWidth: 0, data: chartDataSorted.map(([, item]) => { - return item.value; + return item.value.toNumber(); }) } ]; From 4ec351369bbc19d118acf25eafc22be1bfadea43 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Feb 2022 18:51:16 +0100 Subject: [PATCH 02/34] Bugfix/add fallback to default account in import (#709) * Add fallback to default account if account id is invalid * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/account/account.module.ts | 1 + apps/api/src/app/import/import.module.ts | 2 ++ apps/api/src/app/import/import.service.ts | 10 +++++++++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be92b28f..8cf7b7b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 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/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..e1683c867 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1,3 +1,4 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; 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'; @@ -8,6 +9,7 @@ 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 @@ -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,7 +52,6 @@ export class ImportService { unitPrice } of orders) { await this.orderService.createOrder({ - accountId, currency, dataSource, fee, @@ -53,6 +60,7 @@ export class ImportService { type, unitPrice, userId, + accountId: accountIds.includes(accountId) ? accountId : undefined, date: parseISO((date)), SymbolProfile: { connectOrCreate: { From ca46a9827a586caf2827d8907d9da785ddbcc3e3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:29:49 +0100 Subject: [PATCH 03/34] Do not tweet on Sunday (#712) --- apps/api/src/services/twitter-bot/twitter-bot.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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..b18312279 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; } From 122107c8a1494bb7d058b64b69075320606122fc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:46:36 +0100 Subject: [PATCH 04/34] Feature/distinguish today s data point in admin panel (#711) * Distinguish today's data point * Update changelog --- CHANGELOG.md | 1 + .../admin-market-data-detail.component.html | 5 ++++- .../admin-market-data-detail.component.scss | 5 +++++ .../admin-market-data-detail.component.ts | 14 ++++++++++++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf7b7b8a..662336f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 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..00b42388b 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 @@ -19,7 +19,10 @@ marketDataByMonth[itemByMonth.key][ i + 1 < 10 ? '0' + (i + 1) : i + 1 ]?.day === - i + 1 + i + 1, + 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..210f48805 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,7 @@ 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 { format, isBefore, isSameDay, isValid, parse } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -82,6 +82,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 +94,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', From 06d5ec9182e0827ee51e74e9f4e430a7f38a5174 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:48:51 +0100 Subject: [PATCH 05/34] Release 1.117.0 (#713) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 662336f7e..c56f254d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.117.0 - 19.02.2022 ### Changed diff --git a/package.json b/package.json index 85fcb7747..64603018a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.116.0", + "version": "1.117.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 2a2a5f4da52d8d4c955b6774c981473465df9f1a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 20 Feb 2022 14:16:46 +0100 Subject: [PATCH 06/34] Feature/display features based on permissions (#714) * Display features based on permissions * Update changelog --- CHANGELOG.md | 6 ++++++ .../pages/features/features-page.component.ts | 16 ++++++++++++++-- .../src/app/pages/features/features-page.html | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56f254d9..f99a25f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Displayed features in features overview page based on permissions + ## 1.117.0 - 19.02.2022 ### Changed 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 From a5771f601d342a0a40d6f7815e1f09f775100a15 Mon Sep 17 00:00:00 2001 From: gizmodus Date: Sun, 20 Feb 2022 16:39:43 +0100 Subject: [PATCH 07/34] Improve calculation of overall performance percentage (#701) * Improve calculation of overall performance percentage Co-authored-by: Reto Kaul --- .../app/portfolio/portfolio-calculator-new.ts | 210 +++++++++++------- 1 file changed, 135 insertions(+), 75 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 83bbc663f..8df16f785 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -33,6 +33,8 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in import { TransactionPoint } from './interfaces/transaction-point.interface'; export class PortfolioCalculatorNew { + private static readonly ENABLE_LOGGING = false; + private currency: string; private currentRateService: CurrentRateService; private orders: PortfolioOrder[]; @@ -228,7 +230,7 @@ export class PortfolioCalculatorNew { const initialValues: { [symbol: string]: Big } = {}; const positions: TimelinePosition[] = []; - let hasErrorsInSymbolMetrics = false; + let hasAnySymbolMetricsErrors = false; for (const item of lastTransactionPoint.items) { const marketValue = marketSymbolMap[todayString]?.[item.symbol]; @@ -246,8 +248,7 @@ export class PortfolioCalculatorNew { symbol: item.symbol }); - hasErrorsInSymbolMetrics = hasErrorsInSymbolMetrics || hasErrors; - + hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; initialValues[item.symbol] = initialValue; positions.push({ @@ -272,12 +273,13 @@ export class PortfolioCalculatorNew { transactionCount: item.transactionCount }); } + const overall = this.calculateOverallPerformance(positions, initialValues); return { ...overall, positions, - hasErrors: hasErrorsInSymbolMetrics || overall.hasErrors + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; } @@ -395,16 +397,16 @@ export class PortfolioCalculatorNew { private calculateOverallPerformance( positions: TimelinePosition[], - initialValues: { [p: string]: Big } + initialValues: { [symbol: string]: Big } ) { - let hasErrors = false; let currentValue = new Big(0); - let totalInvestment = new Big(0); let grossPerformance = new Big(0); let grossPerformancePercentage = new Big(0); + let hasErrors = false; let netPerformance = new Big(0); let netPerformancePercentage = new Big(0); - let completeInitialValue = new Big(0); + let sumOfWeights = new Big(0); + let totalInvestment = new Big(0); for (const currentPosition of positions) { if (currentPosition.marketPrice) { @@ -414,27 +416,34 @@ export class PortfolioCalculatorNew { } else { hasErrors = true; } + totalInvestment = totalInvestment.plus(currentPosition.investment); + if (currentPosition.grossPerformance) { grossPerformance = grossPerformance.plus( currentPosition.grossPerformance ); + netPerformance = netPerformance.plus(currentPosition.netPerformance); } else if (!currentPosition.quantity.eq(0)) { hasErrors = true; } - if ( - currentPosition.grossPerformancePercentage && - initialValues[currentPosition.symbol] - ) { - const currentInitialValue = initialValues[currentPosition.symbol]; - completeInitialValue = completeInitialValue.plus(currentInitialValue); + if (currentPosition.grossPerformancePercentage) { + // Use the average from the initial value and the current investment as + // a weight + const weight = (initialValues[currentPosition.symbol] ?? new Big(0)) + .plus(currentPosition.investment) + .div(2); + + sumOfWeights = sumOfWeights.plus(weight); + grossPerformancePercentage = grossPerformancePercentage.plus( - currentPosition.grossPerformancePercentage.mul(currentInitialValue) + currentPosition.grossPerformancePercentage.mul(weight) ); + netPerformancePercentage = netPerformancePercentage.plus( - currentPosition.netPerformancePercentage.mul(currentInitialValue) + currentPosition.netPerformancePercentage.mul(weight) ); } else if (!currentPosition.quantity.eq(0)) { Logger.warn( @@ -444,11 +453,12 @@ export class PortfolioCalculatorNew { } } - if (!completeInitialValue.eq(0)) { - grossPerformancePercentage = - grossPerformancePercentage.div(completeInitialValue); - netPerformancePercentage = - netPerformancePercentage.div(completeInitialValue); + if (sumOfWeights.gt(0)) { + grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights); + netPerformancePercentage = netPerformancePercentage.div(sumOfWeights); + } else { + grossPerformancePercentage = new Big(0); + netPerformancePercentage = new Big(0); } return { @@ -657,6 +667,8 @@ export class PortfolioCalculatorNew { }; } + let averagePriceAtEndDate = new Big(0); + let averagePriceAtStartDate = new Big(0); let feesAtStartDate = new Big(0); let fees = new Big(0); let grossPerformance = new Big(0); @@ -666,17 +678,13 @@ export class PortfolioCalculatorNew { let lastAveragePrice = new Big(0); let lastTransactionInvestment = new Big(0); let lastValueOfInvestmentBeforeTransaction = new Big(0); + let maxTotalInvestment = new Big(0); let timeWeightedGrossPerformancePercentage = new Big(1); let timeWeightedNetPerformancePercentage = new Big(1); let totalInvestment = new Big(0); + let totalInvestmentWithGrossPerformanceFromSell = new Big(0); let totalUnits = new Big(0); - const holdingPeriodPerformances: { - grossReturn: Big; - netReturn: Big; - valueOfInvestment: Big; - }[] = []; - // Add a synthetic order at the start and the end date orders.push({ symbol, @@ -688,7 +696,7 @@ export class PortfolioCalculatorNew { name: '', quantity: new Big(0), type: TypeOfOrder.BUY, - unitPrice: unitPriceAtStartDate ?? new Big(0) + unitPrice: unitPriceAtStartDate }); orders.push({ @@ -701,7 +709,7 @@ export class PortfolioCalculatorNew { name: '', quantity: new Big(0), type: TypeOfOrder.BUY, - unitPrice: unitPriceAtEndDate ?? new Big(0) + unitPrice: unitPriceAtEndDate }); // Sort orders so that the start and end placeholder order are at the right @@ -724,9 +732,31 @@ export class PortfolioCalculatorNew { return order.itemType === 'start'; }); + const indexOfEndOrder = orders.findIndex((order) => { + return order.itemType === 'end'; + }); + for (let i = 0; i < orders.length; i += 1) { const order = orders[i]; + if (order.itemType === 'start') { + // Take the unit price of the order as the market price if there are no + // orders of this symbol before the start date + order.unitPrice = + indexOfStartOrder === 0 + ? orders[i + 1]?.unitPrice + : unitPriceAtStartDate; + } + + // Calculate the average start price as soon as any units are held + if ( + averagePriceAtStartDate.eq(0) && + i >= indexOfStartOrder && + totalUnits.gt(0) + ) { + averagePriceAtStartDate = totalInvestment.div(totalUnits); + } + const valueOfInvestmentBeforeTransaction = totalUnits.mul( order.unitPrice ); @@ -735,12 +765,25 @@ export class PortfolioCalculatorNew { .mul(order.unitPrice) .mul(this.getFactor(order.type)); - if ( - !initialValue && - order.itemType !== 'start' && - order.itemType !== 'end' - ) { - initialValue = transactionInvestment; + totalInvestment = totalInvestment.plus(transactionInvestment); + + if (totalInvestment.gt(maxTotalInvestment)) { + maxTotalInvestment = totalInvestment; + } + + if (i === indexOfEndOrder && totalUnits.gt(0)) { + averagePriceAtEndDate = totalInvestment.div(totalUnits); + } + + if (i >= indexOfStartOrder && !initialValue) { + if ( + i === indexOfStartOrder && + !valueOfInvestmentBeforeTransaction.eq(0) + ) { + initialValue = valueOfInvestmentBeforeTransaction; + } else if (transactionInvestment.gt(0)) { + initialValue = transactionInvestment; + } } fees = fees.plus(order.fee); @@ -760,16 +803,17 @@ export class PortfolioCalculatorNew { grossPerformanceFromSell ); - totalInvestment = totalInvestment - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); + totalInvestmentWithGrossPerformanceFromSell = + totalInvestmentWithGrossPerformanceFromSell + .plus(transactionInvestment) + .plus(grossPerformanceFromSell); lastAveragePrice = totalUnits.eq(0) ? new Big(0) - : totalInvestment.div(totalUnits); + : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); const newGrossPerformance = valueOfInvestment - .minus(totalInvestment) + .minus(totalInvestmentWithGrossPerformanceFromSell) .plus(grossPerformanceFromSells); if ( @@ -812,14 +856,6 @@ export class PortfolioCalculatorNew { timeWeightedNetPerformancePercentage.mul( new Big(1).plus(netHoldingPeriodReturn) ); - - holdingPeriodPerformances.push({ - grossReturn: grossHoldingPeriodReturn, - netReturn: netHoldingPeriodReturn, - valueOfInvestment: lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - }); } grossPerformance = newGrossPerformance; @@ -849,39 +885,63 @@ export class PortfolioCalculatorNew { .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); - let valueOfInvestmentSum = new Big(0); - - for (const holdingPeriodPerformance of holdingPeriodPerformances) { - valueOfInvestmentSum = valueOfInvestmentSum.plus( - holdingPeriodPerformance.valueOfInvestment - ); - } - - let totalWeightedGrossPerformance = new Big(0); - let totalWeightedNetPerformance = new Big(0); - - // Weight the holding period returns according to their value of investment - for (const holdingPeriodPerformance of holdingPeriodPerformances) { - totalWeightedGrossPerformance = totalWeightedGrossPerformance.plus( - holdingPeriodPerformance.grossReturn - .mul(holdingPeriodPerformance.valueOfInvestment) - .div(valueOfInvestmentSum) - ); - - totalWeightedNetPerformance = totalWeightedNetPerformance.plus( - holdingPeriodPerformance.netReturn - .mul(holdingPeriodPerformance.valueOfInvestment) - .div(valueOfInvestmentSum) + const grossPerformancePercentage = + averagePriceAtStartDate.eq(0) || + averagePriceAtEndDate.eq(0) || + orders[indexOfStartOrder].unitPrice.eq(0) + ? totalGrossPerformance.div(maxTotalInvestment) + : unitPriceAtEndDate + .div(averagePriceAtEndDate) + .div( + orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) + ) + .minus(1); + + const feesPerUnit = totalUnits.gt(0) + ? fees.minus(feesAtStartDate).div(totalUnits) + : new Big(0); + + const netPerformancePercentage = + averagePriceAtStartDate.eq(0) || + averagePriceAtEndDate.eq(0) || + orders[indexOfStartOrder].unitPrice.eq(0) + ? totalNetPerformance.div(maxTotalInvestment) + : unitPriceAtEndDate + .minus(feesPerUnit) + .div(averagePriceAtEndDate) + .div( + orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) + ) + .minus(1); + + if (PortfolioCalculatorNew.ENABLE_LOGGING) { + console.log( + ` + ${symbol} + Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( + 2 + )} -> ${unitPriceAtEndDate.toFixed(2)} + Average price: ${averagePriceAtStartDate.toFixed( + 2 + )} -> ${averagePriceAtEndDate.toFixed(2)} + Max. total investment: ${maxTotalInvestment.toFixed(2)} + Gross performance: ${totalGrossPerformance.toFixed( + 2 + )} / ${grossPerformancePercentage.mul(100).toFixed(2)}% + Fees per unit: ${feesPerUnit.toFixed(2)} + Net performance: ${totalNetPerformance.toFixed( + 2 + )} / ${netPerformancePercentage.mul(100).toFixed(2)}%` ); } return { initialValue, - hasErrors: !initialValue || !unitPriceAtEndDate, + grossPerformancePercentage, + netPerformancePercentage, + hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), netPerformance: totalNetPerformance, - netPerformancePercentage: totalWeightedNetPerformance, - grossPerformance: totalGrossPerformance, - grossPerformancePercentage: totalWeightedGrossPerformance + grossPerformance: totalGrossPerformance }; } From fed771525e8fed385b7ba99967582eac756988c6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 20 Feb 2022 16:57:46 +0100 Subject: [PATCH 08/34] Feature/extend market data in admin panel (#716) * Extend market data view * Update changelog --- CHANGELOG.md | 1 + .../yahoo-finance/yahoo-finance.service.ts | 6 ++- .../admin-market-data-detail.component.html | 3 +- .../admin-market-data-detail.component.ts | 43 ++++++++++++++++--- .../admin-market-data/admin-market-data.html | 1 + 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f99a25f35..db10283d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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 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..556cf71a1 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 @@ -25,7 +25,7 @@ import { @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 @@ -274,7 +274,9 @@ export class YahooFinanceService implements DataProviderInterface { name: value.name }); } - } catch {} + } catch (error) { + Logger.error(error); + } return { items }; } 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 00b42388b..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,7 @@ 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) ) 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 210f48805..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, isSameDay, 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 }; } } 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 @@ Date: Sun, 20 Feb 2022 17:01:06 +0100 Subject: [PATCH 09/34] Release 1.118.0 (#717) --- CHANGELOG.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db10283d7..5e2c44010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 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 diff --git a/package.json b/package.json index 64603018a..f98c3487b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.117.0", + "version": "1.118.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 02809a529ea976a3c14dd9513002746ba03425ba Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:06:39 +0100 Subject: [PATCH 10/34] Reorder (#718) --- docker/docker-compose.dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 68d07cc8d44e816127ec7198e2c489afa85d8341 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 22 Feb 2022 20:05:13 +0100 Subject: [PATCH 11/34] Release 1.119.0 (#719) --- CHANGELOG.md | 6 ++++++ .../pages/account/account-page.component.ts | 2 ++ .../src/app/pages/account/account-page.html | 18 +++++++++++++++--- package.json | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2c44010..7b496f727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.119.0 - 21.02.2022 + +### Added + +- Added a trial for the subscription + ## 1.118.0 - 20.02.2022 ### Changed diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index 7aed7479d..8352dc35e 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -55,6 +55,8 @@ export class AccountPageComponent implements OnDestroy, OnInit { public price: number; public priceId: string; public snackBarRef: MatSnackBarRef; + 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 Date: Tue, 22 Feb 2022 21:12:02 +0100 Subject: [PATCH 12/34] Feature/improve labels in portfolio proportion chart (#720) * Improve labels for OTHER and UNKNOWN * Update changelog --- CHANGELOG.md | 6 +++ .../portfolio-proportion-chart.component.ts | 38 ++++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b496f727..139caf897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Distinguished the labels _Other_ and _Unknown_ in the portfolio proportion chart component + ## 1.119.0 - 21.02.2022 ### Added 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 75593a164..1bcc0d373 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 @@ -47,9 +47,12 @@ export class PortfolioProportionChartComponent 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)` }; @@ -147,30 +150,24 @@ export class PortfolioProportionChartComponent .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: new Big(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.plus(restItem[1].value) + value: otherItem[1].value.plus(restItem[1].value) }; } }); @@ -287,8 +284,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; From 46b91d3c3b823e5634660f292778ba9bd9f0d0ad Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 23 Feb 2022 19:00:13 +0100 Subject: [PATCH 13/34] Feature/improve portfolio page (#721) * Improve page * Update changelog --- CHANGELOG.md | 1 + .../app/pages/portfolio/portfolio-page.html | 53 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 139caf897..206272366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Distinguished the labels _Other_ and _Unknown_ in the portfolio proportion chart component +- Improved the portfolio entry page ## 1.119.0 - 21.02.2022 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. -

-

+

+
From 745ba978a3c4db955557fa5f3ebc44f553512ffa Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 25 Feb 2022 08:04:42 +0100 Subject: [PATCH 14/34] Bugfix/fix zen mode (#723) * Fix Zen mode * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/portfolio/portfolio.controller.ts | 2 ++ .../home-holdings/home-holdings.component.ts | 4 +++- .../app/components/home-holdings/home-holdings.html | 2 +- .../home-overview/home-overview.component.ts | 10 +++++++++- .../app/components/home-overview/home-overview.html | 4 ++-- 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 206272366..5b4c46f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 38a083c75..5d15aa423 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -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'; @@ -213,6 +214,7 @@ export class PortfolioController { if ( impersonationId || + this.request.user.Settings.viewMode === ViewMode.ZEN || this.userService.isRestrictedView(this.request.user) ) { performanceInformation.performance = nullifyValuesInObject( 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..9e1fb9c9e 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 @@
-
+
(); @@ -79,7 +80,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(); } 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..82f45ed57 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -34,9 +34,9 @@ [isLoading]="isLoadingPerformance" [locale]="user?.settings?.locale" [performance]="performance" - [showDetails]="!hasImpersonationId && !user.settings.isRestrictedView" + [showDetails]="showDetails" > -
+
Date: Fri, 25 Feb 2022 08:06:01 +0100 Subject: [PATCH 15/34] Release 1.120.0 (#724) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b4c46f1c..0a0751ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.120.0 - 25.02.2022 ### Changed diff --git a/package.json b/package.json index 913ac835c..7f3a7f8a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.119.0", + "version": "1.120.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From c02bcd9bd8b23fcee3c8e026941a384d9fc38645 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:03:00 +0100 Subject: [PATCH 16/34] Feature/migrate to yahoo finance2 (#722) * Migrate to yahoo-finance2 * Add support for mutual funds * Add url to symbol profile * Clean up --- CHANGELOG.md | 15 + apps/api/src/app/import/import.service.ts | 8 +- .../src/app/portfolio/current-rate.service.ts | 2 +- .../app/portfolio/portfolio.service-new.ts | 7 +- .../src/app/portfolio/portfolio.service.ts | 12 +- apps/api/src/app/symbol/symbol.service.ts | 6 +- .../src/services/data-gathering.service.ts | 38 ++- .../alpha-vantage/alpha-vantage.service.ts | 34 ++- .../trackinsight/trackinsight.service.ts | 18 +- .../data-provider/data-provider.service.ts | 120 +++++--- .../ghostfolio-scraper-api.service.ts | 89 +++--- .../google-sheets/google-sheets.service.ts | 104 +++---- .../interfaces/data-enhancer.interface.ts | 6 +- .../interfaces/data-provider.interface.ts | 12 +- .../data-provider/manual/manual.service.ts | 20 +- .../rakuten-rapid-api.service.ts | 83 +++--- .../yahoo-finance/interfaces/interfaces.ts | 32 -- .../yahoo-finance/yahoo-finance.service.ts | 280 ++++++++++-------- .../services/exchange-rate-data.service.ts | 4 +- .../api/src/services/interfaces/interfaces.ts | 9 - .../position/position.component.html | 5 - package.json | 2 +- .../migration.sql | 2 + .../migration.sql | 2 + prisma/schema.prisma | 2 + yarn.lock | 112 ++----- 26 files changed, 526 insertions(+), 498 deletions(-) delete mode 100644 apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts create mode 100644 prisma/migrations/20220227092214_added_mutualfund_to_asset_sub_class/migration.sql create mode 100644 prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0751ba8..748de5d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### 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 diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index e1683c867..7d0f152b2 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -125,19 +125,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/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/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index adeea5c91..99e9496fc 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -327,7 +327,7 @@ export class PortfolioServiceNew { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -358,7 +358,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, @@ -578,7 +577,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 +678,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) ]); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0a164708c..ca0c25b03 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -315,7 +315,7 @@ export class PortfolioService { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -346,7 +346,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, @@ -552,9 +551,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 +563,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 +660,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) ]); diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 37b1c5864..8d73617c6 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[] = []; diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 81c9c884d..f1757e462 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -220,32 +220,41 @@ export class DataGatheringService { Logger.log('Profile data gathering has been started.'); 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 + ); } } @@ -256,8 +265,9 @@ export class DataGatheringService { currency, dataSource, name, - sectors - } = currentData[symbol]; + sectors, + url + } = assetProfiles[symbol]; try { await this.prismaService.symbolProfile.upsert({ @@ -269,7 +279,8 @@ export class DataGatheringService { dataSource, name, sectors, - symbol + symbol, + url }, update: { assetClass, @@ -277,7 +288,8 @@ export class DataGatheringService { countries, currency, name, - sectors + sectors, + url }, where: { dataSource_symbol: { @@ -300,6 +312,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,7 +363,7 @@ 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 )}.` 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..c1c0583d9 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,15 +1,15 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +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'; @@ -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: { @@ -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..71cc293d4 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', @@ -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..dbc7dc97c 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 @@ -14,7 +14,7 @@ import { } 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'; @@ -32,57 +32,25 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return isGhostfolioScraperApiSymbol(symbol); } - 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] ); @@ -115,6 +83,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); + } + + 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..3bc427fd3 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, @@ -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); + } + + 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..47f7eba40 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,19 +1,19 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +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() @@ -29,50 +29,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 +103,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); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } 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 556cf71a1..e7fb7e820 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,27 +1,27 @@ 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 { + 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 yahooFinance2 from 'yahoo-finance2'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; -import { - IYahooFinanceHistoricalResponse, - IYahooFinancePrice, - IYahooFinanceQuoteResponse -} from './interfaces/interfaces'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -73,145 +73,113 @@ 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 yahooFinance2.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 yahooFinance2.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}` + ); 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 yahooFinance2.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); + + 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,19 +276,24 @@ 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 (error) { @@ -281,7 +303,7 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private parseAssetClass(aPrice: IYahooFinancePrice): { + private parseAssetClass(aPrice: any): { assetClass: AssetClass; assetSubClass: AssetSubClass; } { @@ -301,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..0770cf0c4 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 }; }) 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/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 8d254bb98..d846ecd43 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -39,11 +39,6 @@
{{ position?.name }}
{{ position?.symbol | gfSymbol }} - ({{ position.exchange }})
= 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== @@ -13985,11 +13995,6 @@ nx@13.8.1: 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== - oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -15952,49 +15957,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 +16846,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 +16988,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 +17478,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 +17487,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 +18716,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.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.1.9.tgz#28b157e1cddc5b56e6b354f6b00b453a41bbe8a4" + integrity sha512-xLlDqcbK+4Y4oSV7Vq1KcvNcjMuODHQrk2uLyBR4SlXDNjRV7XFpTrwMrDnSLu4pErenj0gXG3ARiCWidFjqzg== 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" From e3f8b0cf5245fb7de87b0f903e8f1982e2a80b7f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:07:30 +0100 Subject: [PATCH 17/34] Release 1.121.0 (#726) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748de5d63..95b6bfa09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.121.0 - 27.02.2022 ### Added diff --git a/package.json b/package.json index 02669c832..a2f378791 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.120.0", + "version": "1.121.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 93d6746739c322fc351266c540089a3e5aad247f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 28 Feb 2022 20:42:14 +0100 Subject: [PATCH 18/34] Refactoring (#727) --- .../alpha-vantage/alpha-vantage.service.ts | 2 +- .../rakuten-rapid-api/rakuten-rapid-api.service.ts | 3 +-- .../yahoo-finance/yahoo-finance.service.ts | 11 +++++------ 3 files changed, 7 insertions(+), 9 deletions(-) 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 c1c0583d9..e85bc9ba8 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,5 +1,6 @@ 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 @@ -10,7 +11,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { isAfter, isBefore, parse } from 'date-fns'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; @Injectable() 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 47f7eba40..8d5a8d5e7 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,5 +1,6 @@ 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, @@ -14,8 +15,6 @@ import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import { format, subMonths, subWeeks, subYears } from 'date-fns'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; - @Injectable() export class RakutenRapidApiService implements DataProviderInterface { public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index'; 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 e7fb7e820..706119c29 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,5 +1,6 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, IDataProviderResponse, @@ -19,9 +20,7 @@ import * as bent from 'bent'; import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; -import yahooFinance2 from 'yahoo-finance2'; - -import { DataProviderInterface } from '../interfaces/data-provider.interface'; +import yahooFinance from 'yahoo-finance2'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -80,7 +79,7 @@ export class YahooFinanceService implements DataProviderInterface { try { const symbol = this.convertToYahooFinanceSymbol(aSymbol); - const assetProfile = await yahooFinance2.quoteSummary(symbol, { + const assetProfile = await yahooFinance.quoteSummary(symbol, { modules: ['price', 'summaryProfile'] }); @@ -143,7 +142,7 @@ export class YahooFinanceService implements DataProviderInterface { const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol); try { - const historicalResult = await yahooFinance2.historical( + const historicalResult = await yahooFinance.historical( yahooFinanceSymbol, { interval: '1d', @@ -202,7 +201,7 @@ export class YahooFinanceService implements DataProviderInterface { try { const response: { [symbol: string]: IDataProviderResponse } = {}; - const quotes = await yahooFinance2.quote(yahooFinanceSymbols); + const quotes = await yahooFinance.quote(yahooFinanceSymbols); for (const quote of quotes) { // Convert symbols back From b3e58d182ab6c0c2b181679a9e38c00df27fbd46 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 28 Feb 2022 21:35:52 +0100 Subject: [PATCH 19/34] Feature/add support for click in portfolio proportion chart (#729) * Add support for click * Update changelog --- CHANGELOG.md | 6 +++ apps/api/src/app/admin/admin.service.ts | 16 ++------ .../src/services/data-gathering.service.ts | 9 +--- apps/api/src/services/market-data.service.ts | 9 +--- .../admin-market-data.component.ts | 41 +++---------------- .../positions-table.component.ts | 12 ++---- .../allocations/allocations-page.component.ts | 17 +++++++- .../allocations/allocations-page.html | 2 + apps/client/src/app/services/admin.service.ts | 25 ++++------- .../lib/interfaces/admin-data.interface.ts | 2 - libs/common/src/lib/interfaces/index.ts | 2 + .../lib/interfaces/unique-asset.interface.ts | 6 +++ .../activities-table.component.ts | 9 +--- .../portfolio-proportion-chart.component.ts | 24 ++++++++++- 14 files changed, 81 insertions(+), 99 deletions(-) create mode 100644 libs/common/src/lib/interfaces/unique-asset.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b6bfa09..267584c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support for click in the portfolio proportion chart component + ## 1.121.0 - 27.02.2022 ### Added 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/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index f1757e462..9292709c3 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 { @@ -121,13 +122,7 @@ export class DataGatheringService { } } - 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 } }); 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/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/positions-table/positions-table.component.ts b/apps/client/src/app/components/positions-table/positions-table.component.ts index d338244da..9dadb27df 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.ts +++ b/apps/client/src/app/components/positions-table/positions-table.component.ts @@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client'; +import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; @Component({ @@ -75,13 +75,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { this.dataSource.filter = filterValue.trim().toLowerCase(); }*/ - 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/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 5cf555049..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,6 +10,7 @@ import { prettifySymbol } from '@ghostfolio/common/helper'; import { PortfolioDetails, PortfolioPosition, + UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -64,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; @@ -281,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 @@ -295,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 21b5618d5..276de32ce 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -89,12 +89,14 @@ 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/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/index.ts b/libs/common/src/lib/interfaces/index.ts index 5a0c71590..feeaaabd4 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -22,6 +22,7 @@ import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioSummary } from './portfolio-summary.interface'; import { Position } from './position.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'; @@ -49,6 +50,7 @@ export { PortfolioSummary, Position, TimelinePosition, + UniqueAsset, User, UserSettings, UserWithSettings 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/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 1bcc0d373..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,17 @@ 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'; @@ -30,6 +33,7 @@ export class PortfolioProportionChartComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() baseCurrency: string; + @Input() cursor: string; @Input() isInPercent = false; @Input() keys: string[] = []; @Input() locale = ''; @@ -37,11 +41,14 @@ 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; @@ -256,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) => { From 5bb20f6d5f3ccefd0a524be22cb495e2b1d5979b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Mar 2022 21:32:19 +0100 Subject: [PATCH 20/34] Bugfix/fix undefined currencies after creating an activity (#731) * Fix issue with undefined currencies after creating an activity * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/order/order.service.ts | 14 +++++++------- .../api/src/services/exchange-rate-data.service.ts | 6 +++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 267584c2b..8b9c2b0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 16971ee38..2613c2c13 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -93,6 +93,13 @@ export class OrderService { data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase(); } + await this.dataGatheringService.gatherProfileData([ + { + dataSource: data.dataSource, + symbol: data.SymbolProfile.connectOrCreate.create.symbol + } + ]); + const isDraft = isAfter(data.date as Date, endOfToday()); if (!isDraft) { @@ -106,13 +113,6 @@ export class OrderService { ]); } - this.dataGatheringService.gatherProfileData([ - { - dataSource: data.dataSource, - symbol: data.SymbolProfile.connectOrCreate.create.symbol - } - ]); - await this.cacheService.flush(); delete data.accountId; diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index 0770cf0c4..8e639e7c0 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -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); }); @@ -206,7 +210,7 @@ export class ExchangeRateDataService { currencies = currencies.concat(customCurrencies); } - return uniq(currencies).sort(); + return uniq(currencies).filter(Boolean).sort(); } private prepareCurrencyPairs(aCurrencies: string[]) { From 63ed227f3f01c5946536a337ef46b455ec17a8f4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Mar 2022 21:36:09 +0100 Subject: [PATCH 21/34] Release 1.122.0 (#732) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9c2b0c6..8e615161a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.122.0 - 01.03.2022 ### Added diff --git a/package.json b/package.json index a2f378791..dd5740030 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.121.0", + "version": "1.122.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 3de7d3f60e8dbf922afe5d03e78749c32b547390 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 4 Mar 2022 21:31:31 +0100 Subject: [PATCH 22/34] Bugfix/improve account calculations (#737) * Improve account calculations * Update changelog --- CHANGELOG.md | 6 ++ .../api/src/app/account/account.controller.ts | 8 ++- apps/api/src/app/account/account.service.ts | 22 ++++--- .../interfaces/cash-details.interface.ts | 2 +- .../app/portfolio/portfolio.service-new.ts | 61 +++++++++++-------- .../src/app/portfolio/portfolio.service.ts | 61 +++++++++++-------- .../accounts-table.component.html | 6 +- .../accounts-table.component.ts | 4 +- .../pages/accounts/accounts-page.component.ts | 33 ++++++---- .../src/app/pages/accounts/accounts-page.html | 4 +- .../src/lib/interfaces/accounts.interface.ts | 4 +- .../src/lib/types/account-with-value.type.ts | 3 +- 12 files changed, 129 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e615161a..84ce8700b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Improved the account calculations + ## 1.122.0 - 01.03.2022 ### Added 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.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/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 99e9496fc..d90182c39 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -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 { @@ -869,7 +885,7 @@ export class PortfolioServiceNew { const performanceInformation = await this.getPerformance(aImpersonationId); - const { balance } = await this.accountService.getCashDetails( + const { balanceInBaseCurrency } = await this.accountService.getCashDetails( userId, userCurrency ); @@ -887,7 +903,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(); @@ -917,7 +933,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'; @@ -1153,17 +1169,12 @@ 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) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index ca0c25b03..eb1a463ee 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -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 { @@ -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'; @@ -1113,17 +1129,12 @@ 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) { 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/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 8c3e145aa..60eb1463b 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -28,8 +28,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit { public hasPermissionToCreateAccount: boolean; public hasPermissionToDeleteAccount: boolean; public routeQueryParams: Subscription; - public totalBalance = 0; - public totalValue = 0; + public totalBalanceInBaseCurrency = 0; + public totalValueInBaseCurrency = 0; public transactionCount = 0; public user: User; @@ -106,18 +106,25 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.dataService .fetchAccounts() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => { - 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/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/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; }; From 86acbf06f47eafd37f3ee0822217c0a7b005c1a2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Mar 2022 11:00:02 +0100 Subject: [PATCH 23/34] Feature/add data provider errors to api response (#738) * Add data provider error details * Update changelog --- CHANGELOG.md | 4 ++++ .../interfaces/current-positions.interface.ts | 5 ++--- ...olio-calculator-new-baln-buy-and-sell.spec.ts | 1 + .../portfolio-calculator-new-baln-buy.spec.ts | 1 + .../app/portfolio/portfolio-calculator-new.ts | 13 ++++++++++++- .../src/app/portfolio/portfolio.controller.ts | 5 +++-- .../src/app/portfolio/portfolio.service-new.ts | 5 +++-- apps/api/src/app/portfolio/portfolio.service.ts | 4 ++-- ...nsform-data-source-in-response.interceptor.ts | 8 ++++++++ .../home-overview/home-overview.component.ts | 8 +++++++- .../components/home-overview/home-overview.html | 1 + .../portfolio-performance.component.html | 1 + .../portfolio-performance.component.ts | 14 +++++++++++++- apps/client/src/app/services/data.service.ts | 16 +++++++++------- libs/common/src/lib/interfaces/index.ts | 4 ++++ .../lib/interfaces/responses/errors.interface.ts | 6 ++++++ .../portfolio-performance-response.interface.ts | 6 ++++++ 17 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 libs/common/src/lib/interfaces/responses/errors.interface.ts create mode 100644 libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ce8700b..043680fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Included data provider errors in API response + ### Fixed - Improved the account calculations 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 8df16f785..972d4db3c 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'; @@ -232,6 +236,8 @@ 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]; @@ -272,12 +278,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 }; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 5d15aa423..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 @@ -204,10 +204,11 @@ 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); diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index d90182c39..e078c5410 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, @@ -730,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 } = @@ -776,6 +776,7 @@ export class PortfolioServiceNew { currentPositions.netPerformancePercentage.toNumber(); return { + errors: currentPositions.errors, hasErrors: currentPositions.hasErrors || hasErrors, performance: { currentGrossPerformance, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index eb1a463ee..108c55d27 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, @@ -712,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( 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..2aeb895fe 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 @@ -41,6 +41,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) { 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 d6ae7e2b0..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; @@ -126,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 82f45ed57..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,6 +28,7 @@ class="pb-4" [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" + [errors]="errors" [hasError]="hasError" [isAllTimeHigh]="isAllTimeHigh" [isAllTimeLow]="isAllTimeLow" diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index cd61f901e..5601e42cc 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -7,6 +7,7 @@ ? 'Sorry! Our data provider partner is experiencing the hiccups.' : '' " + (click)="errors?.length > 0 && onShowErrors()" > { + return `${error.symbol} (${error.dataSource})`; + }); + + alert(errorMessageParts.join('\n')); + } } 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/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index feeaaabd4..d2ad50742 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -21,6 +21,8 @@ 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'; @@ -43,12 +45,14 @@ export { PortfolioItem, PortfolioOverview, PortfolioPerformance, + PortfolioPerformanceResponse, PortfolioPosition, PortfolioPublicDetails, PortfolioReport, PortfolioReportRule, PortfolioSummary, Position, + ResponseError, TimelinePosition, UniqueAsset, User, 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; +} From c216ab1d7665fe9f59444387c5368b11c68712e1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Mar 2022 11:07:27 +0100 Subject: [PATCH 24/34] Eliminate data source from order model (#730) * Eliminate currency, data source and symbol from order model * Remove prefix for symbols with data source GHOSTFOLIO * Update changelog --- CHANGELOG.md | 9 +++ apps/api/src/app/export/export.service.ts | 5 +- apps/api/src/app/import/import-data.dto.ts | 3 +- apps/api/src/app/import/import.service.ts | 17 +++-- apps/api/src/app/order/order.controller.ts | 9 +++ apps/api/src/app/order/order.service.ts | 71 ++++++++++++------- .../app/portfolio/portfolio.service-new.ts | 29 ++++---- .../src/app/portfolio/portfolio.service.ts | 29 ++++---- ...orm-data-source-in-response.interceptor.ts | 8 --- .../src/services/data-gathering.service.ts | 27 ++++--- .../ghostfolio-scraper-api.service.ts | 8 +-- .../services/exchange-rate-data.service.ts | 7 +- ...-or-update-transaction-dialog.component.ts | 14 ++-- libs/common/src/lib/helper.ts | 4 -- .../activities-table.component.html | 4 +- .../migration.sql | 2 + .../migration.sql | 2 + .../migration.sql | 2 + .../migration.sql | 5 ++ prisma/schema.prisma | 5 +- prisma/seed.js | 40 +++-------- 21 files changed, 148 insertions(+), 152 deletions(-) create mode 100644 prisma/migrations/20220302184222_removed_data_source_from_order/migration.sql create mode 100644 prisma/migrations/20220302191841_removed_currency_from_order/migration.sql create mode 100644 prisma/migrations/20220302193633_removed_symbol_from_order/migration.sql create mode 100644 prisma/migrations/20220302200727_changed_currency_to_required_in_symbol_profile/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 043680fff..555060133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Included data provider errors in 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 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.service.ts b/apps/api/src/app/import/import.service.ts index 7d0f152b2..d68e60bac 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -3,8 +3,8 @@ 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'; +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; @Injectable() export class ImportService { @@ -19,7 +19,7 @@ export class ImportService { orders, userId }: { - orders: Partial[]; + orders: Partial[]; userId: string; }): Promise { for (const order of orders) { @@ -52,11 +52,8 @@ export class ImportService { unitPrice } of orders) { await this.orderService.createOrder({ - currency, - dataSource, fee, quantity, - symbol, type, unitPrice, userId, @@ -65,6 +62,7 @@ export class ImportService { SymbolProfile: { connectOrCreate: { create: { + currency, dataSource, symbol }, @@ -85,7 +83,7 @@ export class ImportService { orders, userId }: { - orders: Partial[]; + orders: Partial[]; userId: string; }) { if ( @@ -99,6 +97,7 @@ export class ImportService { } const existingOrders = await this.orderService.orders({ + include: { SymbolProfile: true }, orderBy: { date: 'desc' }, where: { userId } }); @@ -109,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 ); 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 2613c2c13..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; @@ -95,7 +99,7 @@ export class OrderService { await this.dataGatheringService.gatherProfileData([ { - dataSource: data.dataSource, + dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, symbol: data.SymbolProfile.connectOrCreate.create.symbol } ]); @@ -106,7 +110,7 @@ export class OrderService { // 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 } @@ -116,6 +120,9 @@ export class OrderService { 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/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index e078c5410..b34a9206f 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -450,7 +450,7 @@ export class PortfolioServiceNew { }; } - const positionCurrency = orders[0].currency; + const positionCurrency = orders[0].SymbolProfile.currency; const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ aSymbol ]); @@ -460,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) })); @@ -1023,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 ); }) @@ -1042,7 +1042,7 @@ export class PortfolioServiceNew { .map((order) => { return this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1064,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 ); }) @@ -1117,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 ) ) @@ -1180,7 +1180,8 @@ export class PortfolioServiceNew { 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') { @@ -1230,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 108c55d27..f3df18c30 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -438,7 +438,7 @@ export class PortfolioService { }; } - const positionCurrency = orders[0].currency; + const positionCurrency = orders[0].SymbolProfile.currency; const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ aSymbol ]); @@ -448,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) })); @@ -987,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 ); }) @@ -1006,7 +1006,7 @@ export class PortfolioService { .map((order) => { return this.exchangeRateDataService.toCurrency( order.fee, - order.currency, + order.SymbolProfile.currency, this.request.user.Settings.currency ); }) @@ -1028,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 ); }) @@ -1080,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 ) ) @@ -1139,7 +1139,8 @@ export class PortfolioService { 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') { @@ -1189,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/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts index 2aeb895fe..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; }); } @@ -66,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 9292709c3..ea70d7be8 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -549,19 +549,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/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts index dbc7dc97c..837dc2af3 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,11 +7,7 @@ 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, SymbolProfile } from '@prisma/client'; @@ -29,7 +25,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { ) {} public canHandle(symbol: string) { - return isGhostfolioScraperApiSymbol(symbol); + return true; } public async getAssetProfile( diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index 8e639e7c0..53a6f0f6e 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -191,12 +191,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); 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/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/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/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 bea0e3381..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? 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 From f46533107d3de2f7b36e76e51e85d846310568d4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Mar 2022 11:09:07 +0100 Subject: [PATCH 25/34] Release 1.123.0 (#739) --- CHANGELOG.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 555060133..a9594d8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.123.0 - 05.03.2022 ### Added -- Included data provider errors in API response +- Included data provider errors in the API response ### Changed diff --git a/package.json b/package.json index dd5740030..737c8e7c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.122.0", + "version": "1.123.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 50184284e1c8518d348aa73928cfcb8d5c78a503 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Mar 2022 10:47:30 +0100 Subject: [PATCH 26/34] Feature/upgrade ngx skeleton loader to 5.0.0 (#740) * Upgrade ngx-skeleton-loader to version 5.0.0 * Update changelog --- CHANGELOG.md | 6 ++++++ .../app/components/position/position.component.html | 1 + .../app/components/position/position.component.scss | 2 -- apps/client/src/styles.scss | 4 ++++ package.json | 2 +- yarn.lock | 10 +++++----- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9594d8f5..96211f034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Upgraded `ngx-skeleton-loader` from version `2.9.1` to `5.0.0` + ## 1.123.0 - 05.03.2022 ### Added diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index d846ecd43..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 @@
Date: Sun, 6 Mar 2022 12:04:38 +0100 Subject: [PATCH 27/34] Feature/upgrade yahoo finance2 to 2.2.0 (#741) * Upgrade yahoo-finance2 to version 2.2.0 * Update changelog --- CHANGELOG.md | 1 + package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96211f034..a8d43ac9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgraded `ngx-skeleton-loader` from version `2.9.1` to `5.0.0` +- Upgraded `yahoo-finance2` from version `2.1.9` to `2.2.0` ## 1.123.0 - 05.03.2022 diff --git a/package.json b/package.json index 8c0a9f983..de8a97f97 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", - "yahoo-finance2": "2.1.9", + "yahoo-finance2": "2.2.0", "zone.js": "0.11.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 9ef6f3df6..40fc9c795 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18716,10 +18716,10 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yahoo-finance2@2.1.9: - version "2.1.9" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.1.9.tgz#28b157e1cddc5b56e6b354f6b00b453a41bbe8a4" - integrity sha512-xLlDqcbK+4Y4oSV7Vq1KcvNcjMuODHQrk2uLyBR4SlXDNjRV7XFpTrwMrDnSLu4pErenj0gXG3ARiCWidFjqzg== +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: ajv "8.10.0" ajv-formats "2.1.1" From b602e7690b30fcda629bf373ede0bc17be2e1b25 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Mar 2022 12:06:07 +0100 Subject: [PATCH 28/34] Feature/upgrade prisma to version 3.10.0 (#742) * Upgrade prisma to version 3.10.0 * Update changelog --- CHANGELOG.md | 1 + package.json | 4 ++-- yarn.lock | 36 ++++++++++++++++++------------------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d43ac9e..440e41418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 diff --git a/package.json b/package.json index de8a97f97..d9bdfe470 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", "@nrwl/angular": "13.8.1", - "@prisma/client": "3.9.1", + "@prisma/client": "3.10.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -108,7 +108,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index 40fc9c795..7f317823a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3470,22 +3470,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" @@ -15214,12 +15214,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" From 99655604d9e53b8763d55b6d654db012ff2fee3a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Mar 2022 12:26:04 +0100 Subject: [PATCH 29/34] Feature/add support for coupon duration (#743) * Add support for coupon duration * Update changelog --- CHANGELOG.md | 4 +++ apps/api/src/app/import/import.service.ts | 2 +- .../subscription/subscription.controller.ts | 17 ++++++----- .../app/subscription/subscription.service.ts | 19 ++++++++---- .../admin-overview.component.ts | 11 ++++++- .../admin-overview/admin-overview.html | 30 +++++++++++++++---- .../admin-overview/admin-overview.module.ts | 7 ++++- .../admin-overview/admin-overview.scss | 6 ++++ .../src/lib/interfaces/coupon.interface.ts | 3 ++ package.json | 1 + yarn.lock | 5 ++++ 11 files changed, 83 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 440e41418..3275fb54b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### 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` diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index d68e60bac..3ddd29040 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1,10 +1,10 @@ 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 { isSameDay, parseISO } from 'date-fns'; -import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; @Injectable() export class ImportService { diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 1f68c8f72..e064b01a5 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,7 @@ 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}` ); return { diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 97e910e14..1111ac0e0 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 diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index dc15cca12..013633c00 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -20,6 +20,7 @@ import { parseISO } from 'date-fns'; import { uniq } from 'lodash'; +import { StringValue } from 'ms'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -29,6 +30,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-overview.html' }) export class AdminOverviewComponent implements OnDestroy, OnInit { + public couponDuration: StringValue = '30 days'; public coupons: Coupon[]; public customCurrencies: string[]; public dataGatheringInProgress: boolean; @@ -105,7 +107,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } public onAddCoupon() { - const coupons = [...this.coupons, { code: this.generateCouponCode(16) }]; + const coupons = [ + ...this.coupons, + { code: this.generateCouponCode(16), duration: this.couponDuration } + ]; this.putCoupons(coupons); } @@ -118,6 +123,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } } + public onChangeCouponDuration(aCouponDuration: StringValue) { + this.couponDuration = aCouponDuration; + } + public onDeleteCoupon(aCouponCode: string) { const confirmation = confirm('Do you really want to delete this coupon?'); diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index 04f8ba4ec..2457a12b6 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -156,11 +156,14 @@ >
-
+
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/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/package.json b/package.json index d9bdfe470..eed08aee0 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "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": "5.0.0", diff --git a/yarn.lock b/yarn.lock index 7f317823a..32db36af8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13556,6 +13556,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" From 718b0de0a7b9d0431cca566750378ea22f3196ac Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Mar 2022 13:48:17 +0100 Subject: [PATCH 30/34] Release 1.124.0 (#744) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3275fb54b..d2aea9101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.124.0 - 06.03.2022 ### Added diff --git a/package.json b/package.json index eed08aee0..3915091d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.123.0", + "version": "1.124.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From e4908b51aa80ac7c4060ce8c75aedfcc3a4d297b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Mar 2022 17:20:07 +0100 Subject: [PATCH 31/34] Feature/add context to logger (#745) * Add contexts * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/auth/google.strategy.ts | 2 +- apps/api/src/app/auth/web-auth.service.ts | 4 +- apps/api/src/app/import/import.controller.ts | 2 +- apps/api/src/app/info/info.service.ts | 4 +- .../app/portfolio/portfolio-calculator-new.ts | 6 +- .../src/app/portfolio/portfolio-calculator.ts | 14 ++-- .../subscription/subscription.controller.ts | 10 ++- .../app/subscription/subscription.service.ts | 2 +- apps/api/src/app/symbol/symbol.service.ts | 2 +- .../src/services/data-gathering.service.ts | 64 +++++++++++++------ .../alpha-vantage/alpha-vantage.service.ts | 2 +- .../data-provider/data-provider.service.ts | 2 +- .../ghostfolio-scraper-api.service.ts | 4 +- .../google-sheets/google-sheets.service.ts | 4 +- .../rakuten-rapid-api.service.ts | 4 +- .../yahoo-finance/yahoo-finance.service.ts | 7 +- .../services/exchange-rate-data.service.ts | 3 +- .../twitter-bot/twitter-bot.service.ts | 5 +- 19 files changed, 98 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2aea9101..d6391dab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added the contexts to the logger commands + ## 1.124.0 - 06.03.2022 ### Added 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/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/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/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 972d4db3c..d1ed8fa91 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -458,7 +458,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; } @@ -523,7 +524,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/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index e064b01a5..aabc46d24 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -72,7 +72,8 @@ export class SubscriptionController { }); Logger.log( - `Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}` + `Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`, + 'SubscriptionController' ); return { @@ -87,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`); } @@ -104,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 1111ac0e0..f7db04728 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -98,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 8d73617c6..c45f45cd1 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -95,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/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index ea70d7be8..c3a7f64c7 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -40,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({ @@ -64,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({ @@ -73,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'); } } @@ -84,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({ @@ -108,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({ @@ -117,7 +123,10 @@ 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'); } } @@ -128,7 +137,10 @@ export class DataGatheringService { }); 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({ @@ -159,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({ @@ -168,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'); } } @@ -205,14 +220,17 @@ 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?.filter( @@ -248,7 +266,8 @@ export class DataGatheringService { } catch (error) { Logger.error( `Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`, - error + error, + 'DataGatheringService' ); } } @@ -294,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'); } @@ -361,7 +387,8 @@ export class DataGatheringService { `Failed to gather data for symbol ${symbol} from ${dataSource} at ${format( currentDate, DATE_FORMAT - )}.` + )}.`, + 'DataGatheringService' ); } @@ -377,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' ); } @@ -474,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: { 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 e85bc9ba8..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 @@ -76,7 +76,7 @@ export class AlphaVantageService implements DataProviderInterface { return response; } catch (error) { - Logger.error(error, symbol); + Logger.error(error, 'AlphaVantageService'); return {}; } 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 71cc293d4..fd44f2426 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -82,7 +82,7 @@ export class DataProviderService { return r; }, {}); } catch (error) { - Logger.error(error); + Logger.error(error, 'DataProviderService'); } finally { return response; } 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 837dc2af3..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 @@ -69,7 +69,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { } }; } catch (error) { - Logger.error(error); + Logger.error(error, 'GhostfolioScraperApiService'); } return {}; @@ -110,7 +110,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { } }; } catch (error) { - Logger.error(error); + Logger.error(error, 'GhostfolioScraperApiService'); } return {}; 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 3bc427fd3..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 @@ -72,7 +72,7 @@ export class GoogleSheetsService implements DataProviderInterface { [symbol]: historicalData }; } catch (error) { - Logger.error(error); + Logger.error(error, 'GoogleSheetsService'); } return {}; @@ -121,7 +121,7 @@ export class GoogleSheetsService implements DataProviderInterface { return response; } catch (error) { - Logger.error(error); + Logger.error(error, 'GoogleSheetsService'); } return {}; 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 8d5a8d5e7..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 @@ -125,7 +125,7 @@ export class RakutenRapidApiService implements DataProviderInterface { }; } } catch (error) { - Logger.error(error); + Logger.error(error, 'RakutenRapidApiService'); } return {}; @@ -160,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/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 706119c29..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 @@ -177,7 +177,8 @@ export class YahooFinanceService implements DataProviderInterface { return response; } catch (error) { Logger.warn( - `Skipping yahooFinance2.getHistorical("${aSymbol}"): [${error.name}] ${error.message}` + `Skipping yahooFinance2.getHistorical("${aSymbol}"): [${error.name}] ${error.message}`, + 'YahooFinanceService' ); return {}; @@ -232,7 +233,7 @@ export class YahooFinanceService implements DataProviderInterface { return response; } catch (error) { - Logger.error(error); + Logger.error(error, 'YahooFinanceService'); return {}; } @@ -296,7 +297,7 @@ export class YahooFinanceService implements DataProviderInterface { }); } } catch (error) { - Logger.error(error); + Logger.error(error, 'YahooFinanceService'); } return { items }; diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index 53a6f0f6e..8092f1804 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -149,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; } 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 b18312279..58052872b 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -54,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'); } } } From 0d897bc4618bf6ac99e3d1dc5c9a33984f8702c8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 8 Mar 2022 20:15:59 +0100 Subject: [PATCH 32/34] Improve table header (#746) --- apps/client/src/app/components/admin-users/admin-users.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 9cdef6a7cb6ab37e0613a0d3fc22b92916c97ec1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 9 Mar 2022 18:19:48 +0100 Subject: [PATCH 33/34] Feature/upgrade nx to version 13.8.5 (#747) * Upgrade Nx to version 13.8.5 * Update changelog --- CHANGELOG.md | 4 + angular.json | 4 +- package.json | 22 +-- yarn.lock | 501 +++++++++++++++++++++++++++++++-------------------- 4 files changed, 325 insertions(+), 206 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6391dab6..88b9a9251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 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/package.json b/package.json index 3915091d6..f2d990a6e 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@nestjs/platform-express": "8.2.3", "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", - "@nrwl/angular": "13.8.1", + "@nrwl/angular": "13.8.5", "@prisma/client": "3.10.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", @@ -132,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", @@ -166,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/yarn.lock b/yarn.lock index 32db36af8..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" @@ -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" @@ -6773,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== @@ -8844,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" @@ -12143,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" @@ -13993,12 +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" + "@nrwl/cli" "13.8.5" oauth@0.9.x: version "0.9.15" From 07799573cbb2143b36a0ca272d776c5120a1ef2b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Mar 2022 20:11:32 +0100 Subject: [PATCH 34/34] Harmonize button style (#748) --- apps/client/src/app/components/home-holdings/home-holdings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e1fb9c9e..5fa1ec5c9 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -25,7 +25,7 @@
Manage Activities