-
+
- + +
+ {{ info.systemMessage }} +
diff --git a/apps/client/src/app/app.component.scss b/apps/client/src/app/app.component.scss index bcefe5d71..9afa6eaab 100644 --- a/apps/client/src/app/app.component.scss +++ b/apps/client/src/app/app.component.scss @@ -8,14 +8,13 @@ min-height: 100vh; padding-top: 5rem; - .create-account-container { + .info-message-container { height: 3.5rem; margin-top: -0.5rem; - .create-account-box { + .info-message { background-color: rgba(0, 0, 0, $alpha-hover); border-radius: 2rem; - cursor: pointer; font-size: 80%; a { 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 ce4a9cd53..76c78378a 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 @@ -5,9 +5,11 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT, - PROPERTY_CURRENCIES + PROPERTY_CURRENCIES, + PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; -import { User } from '@ghostfolio/common/interfaces'; +import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { differenceInSeconds, formatDistanceToNowStrict, @@ -29,6 +31,8 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { public dataGatheringProgress: number; public defaultDateFormat = DEFAULT_DATE_FORMAT; public exchangeRates: { label1: string; label2: string; value: number }[]; + public hasPermissionForSystemMessage: boolean; + public info: InfoItem; public lastDataGathering: string; public transactionCount: number; public userCount: number; @@ -45,7 +49,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private userService: UserService - ) {} + ) { + this.info = this.dataService.fetchInfo(); + + this.hasPermissionForSystemMessage = hasPermission( + this.info.globalPermissions, + permissions.enableSystemMessage + ); + } /** * Initializes the controller @@ -62,6 +73,21 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { }); } + public formatDistanceToNow(aDateString: string) { + if (aDateString) { + const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { + addSuffix: true + }); + + return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < + 60 + ? 'just now' + : distanceString; + } + + return ''; + } + public onAddCurrency() { const currency = prompt('Please add a currency:'); @@ -82,6 +108,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } } + public onDeleteSystemMessage() { + this.putSystemMessage(''); + } + public onFlushCache() { this.cacheService .flush() @@ -117,19 +147,12 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { .subscribe(() => {}); } - public formatDistanceToNow(aDateString: string) { - if (aDateString) { - const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { - addSuffix: true - }); + public onSetSystemMessage() { + const systemMessage = prompt('Please set your system message:'); - return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < - 60 - ? 'just now' - : distanceString; + if (systemMessage) { + this.putSystemMessage(systemMessage); } - - return ''; } public ngOnDestroy() { @@ -187,4 +210,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { }, 300); }); } + + private putSystemMessage(aSystemMessage: string) { + this.dataService + .putAdminSetting(PROPERTY_SYSTEM_MESSAGE, { + value: aSystemMessage + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } } 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 3d702ac90..ca81b5143 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -118,6 +118,34 @@
+
+
System Message
+
+
+ {{ info.systemMessage }} + +
+ +
+
diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index 9865e23c3..8d1e10797 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,4 +1,5 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { Subject } from 'rxjs'; @Component({ @@ -7,12 +8,22 @@ import { Subject } from 'rxjs'; templateUrl: './admin-page.html' }) export class AdminPageComponent implements OnDestroy, OnInit { + @HostBinding('class.with-info-message') get getHasMessage() { + return this.hasMessage; + } + + public hasMessage: boolean; + private unsubscribeSubject = new Subject(); /** * @constructor */ - public constructor() {} + public constructor(private dataService: DataService) { + const { systemMessage } = this.dataService.fetchInfo(); + + this.hasMessage = !!systemMessage; + } /** * Initializes the controller diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 06e533236..213c3854a 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -5,11 +5,10 @@ import { OnDestroy, OnInit } from '@angular/core'; -import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -19,11 +18,11 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './home-page.html' }) export class HomePageComponent implements OnDestroy, OnInit { - @HostBinding('class.with-create-account-container') get isDemo() { - return this.canCreateAccount; + @HostBinding('class.with-info-message') get getHasMessage() { + return this.hasMessage; } - public canCreateAccount: boolean; + public hasMessage: boolean; public hasPermissionToAccessFearAndGreedIndex: boolean; public tabs: { iconName: string; path: string }[] = []; public user: User; @@ -35,10 +34,11 @@ export class HomePageComponent implements OnDestroy, OnInit { */ public constructor( private changeDetectorRef: ChangeDetectorRef, - private deviceService: DeviceDetectorService, - private impersonationStorageService: ImpersonationStorageService, + private dataService: DataService, private userService: UserService ) { + const { systemMessage } = this.dataService.fetchInfo(); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -50,10 +50,11 @@ export class HomePageComponent implements OnDestroy, OnInit { ]; this.user = state.user; - this.canCreateAccount = hasPermission( - this.user?.permissions, - permissions.createUserAccount - ); + this.hasMessage = + hasPermission( + this.user?.permissions, + permissions.createUserAccount + ) || !!systemMessage; this.hasPermissionToAccessFearAndGreedIndex = hasPermission( this.user.permissions, diff --git a/apps/client/src/app/pages/home/home-page.scss b/apps/client/src/app/pages/home/home-page.scss index fb2adc79c..344e79d3c 100644 --- a/apps/client/src/app/pages/home/home-page.scss +++ b/apps/client/src/app/pages/home/home-page.scss @@ -10,10 +10,6 @@ padding-bottom: env(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom); - &.with-create-account-container { - height: calc(100vh - 5rem - 3.5rem); - } - ::ng-deep { gf-home-holdings, gf-home-market, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6a21db0da..b15dd1723 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -42,8 +42,6 @@ import { map } from 'rxjs/operators'; providedIn: 'root' }) export class DataService { - private info: InfoItem; - public constructor(private http: HttpClient) {} public createCheckoutSession({ @@ -241,7 +239,6 @@ export class DataService { } public putAdminSetting(key: string, aData: PropertyDto) { - console.log(key, aData); return this.http.put(`/api/admin/settings/${key}`, aData); } diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 6093c451f..5cc4d43c2 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -175,3 +175,7 @@ ngx-skeleton-loader { .text-decoration-underline { text-decoration: underline !important; } + +.with-info-message { + height: calc(100vh - 5rem - 3.5rem) !important; +} diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 5781fc65c..6697c92f9 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -34,5 +34,6 @@ export const PROPERTY_CURRENCIES = 'CURRENCIES'; export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING'; export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; +export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE'; export const UNKNOWN_KEY = 'UNKNOWN'; diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 630b37403..08b511cf3 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -8,10 +8,7 @@ export interface InfoItem { demoAuthToken: string; globalPermissions: string[]; lastDataGathering?: Date; - message?: { - text: string; - type: string; - }; + systemMessage?: string; platforms: { id: string; name: string }[]; primaryDataSource: DataSource; statistics: Statistics; diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 0dba2e81f..8feab7466 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -17,6 +17,7 @@ export const permissions = { enableSocialLogin: 'enableSocialLogin', enableStatistics: 'enableStatistics', enableSubscription: 'enableSubscription', + enableSystemMessage: 'enableSystemMessage', updateAccount: 'updateAccount', updateAuthDevice: 'updateAuthDevice', updateOrder: 'updateOrder', From 069660afe4abca540221ff067f9b0fad3d74ba49 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:10:40 +0100 Subject: [PATCH 002/337] Feature/increase fear and greed index to 10 days (#525) * Increase to 10 days * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/symbol/symbol.service.ts | 2 +- apps/client/src/app/components/home-market/home-market.html | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3503770e1..a41523ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 10 days - Upgraded `prisma` from version `2.30.2` to `3.6.0` ## 1.86.0 - 04.12.2021 diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 3f377c551..cdd13da8d 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -32,7 +32,7 @@ export class SymbolService { let historicalData: HistoricalDataItem[]; if (includeHistoricalData) { - const days = 7; + const days = 10; const marketData = await this.marketDataService.getRange({ dateQuery: { gte: subDays(new Date(), days) }, diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index 2705530bc..c721d4c78 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -12,7 +12,7 @@
- Last 7 Days + Last 10 Days
Date: Tue, 7 Dec 2021 20:24:15 +0100 Subject: [PATCH 003/337] Feature/read only mode (#520) * Setup read only mode and update permissions dynamically * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/access/access.controller.ts | 16 ++---- .../api/src/app/account/account.controller.ts | 21 ++------ apps/api/src/app/admin/admin.controller.ts | 21 +++----- .../app/auth-device/auth-device.controller.ts | 8 +-- apps/api/src/app/auth/auth.module.ts | 6 +-- apps/api/src/app/info/info.service.ts | 9 ++++ apps/api/src/app/order/order.controller.ts | 21 ++------ apps/api/src/app/user/user.controller.ts | 36 +++++++++----- apps/api/src/app/user/user.module.ts | 2 + apps/api/src/app/user/user.service.ts | 38 ++++++++++++-- .../api/src/services/configuration.service.ts | 1 + .../interfaces/environment.interface.ts | 1 + .../src/services/property/property.service.ts | 2 +- .../admin-overview.component.ts | 49 ++++++++++++++----- .../admin-overview/admin-overview.html | 10 ++++ .../admin-overview/admin-overview.module.ts | 9 +++- .../components/header/header.component.html | 2 +- .../transactions-table.component.html | 7 ++- .../transactions-table.component.ts | 1 + .../src/app/core/http-response.interceptor.ts | 28 ++++++++--- .../pages/accounts/accounts-page.component.ts | 2 +- .../transactions-page.component.ts | 4 +- .../transactions/transactions-page.html | 1 + .../pages/register/register-page.component.ts | 4 ++ .../src/app/pages/register/register-page.html | 2 +- libs/common/src/lib/config.ts | 1 + .../lib/interfaces/admin-data.interface.ts | 2 +- .../src/lib/interfaces/info-item.interface.ts | 3 +- libs/common/src/lib/permissions.ts | 21 +++++--- 30 files changed, 208 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a41523ec9..6dddbbfa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Supported the management of additional currencies in the admin control panel - Introduced the system message +- Introduced the read only mode ### Changed diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index bbf5423cc..405c53e68 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -1,9 +1,5 @@ import { Access } from '@ghostfolio/common/interfaces'; -import { - getPermissions, - hasPermission, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -66,10 +62,7 @@ export class AccessController { @Body() data: CreateAccessDto ): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.createAccess - ) + !hasPermission(this.request.user.permissions, permissions.createAccess) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -86,10 +79,7 @@ export class AccessController { @UseGuards(AuthGuard('jwt')) public async deleteAccess(@Param('id') id: string): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.deleteAccess - ) + !hasPermission(this.request.user.permissions, permissions.deleteAccess) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index a832bfca9..54fd912fd 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -6,11 +6,7 @@ import { } from '@ghostfolio/api/helper/object.helper'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { Accounts } from '@ghostfolio/common/interfaces'; -import { - getPermissions, - hasPermission, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -48,10 +44,7 @@ export class AccountController { @UseGuards(AuthGuard('jwt')) public async deleteAccount(@Param('id') id: string): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.deleteAccount - ) + !hasPermission(this.request.user.permissions, permissions.deleteAccount) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -143,10 +136,7 @@ export class AccountController { @Body() data: CreateAccountDto ): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.createAccount - ) + !hasPermission(this.request.user.permissions, permissions.createAccount) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -183,10 +173,7 @@ export class AccountController { @UseGuards(AuthGuard('jwt')) public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.updateAccount - ) + !hasPermission(this.request.user.permissions, permissions.updateAccount) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 054371458..ef6753894 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,17 +1,12 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; -import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; -import { - getPermissions, - hasPermission, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -45,7 +40,7 @@ export class AdminController { public async getAdminData(): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -63,7 +58,7 @@ export class AdminController { public async gatherMax(): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -87,7 +82,7 @@ export class AdminController { ): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -107,7 +102,7 @@ export class AdminController { public async gatherProfileData(): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -127,7 +122,7 @@ export class AdminController { public async getMarketData(): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -147,7 +142,7 @@ export class AdminController { ): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { @@ -168,7 +163,7 @@ export class AdminController { ) { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.accessAdminControl ) ) { diff --git a/apps/api/src/app/auth-device/auth-device.controller.ts b/apps/api/src/app/auth-device/auth-device.controller.ts index 89a44dd2f..33eae0cc0 100644 --- a/apps/api/src/app/auth-device/auth-device.controller.ts +++ b/apps/api/src/app/auth-device/auth-device.controller.ts @@ -1,9 +1,5 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; -import { - getPermissions, - hasPermission, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, @@ -29,7 +25,7 @@ export class AuthDeviceController { public async deleteAuthDevice(@Param('id') id: string): Promise { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.deleteAuthDevice ) ) { diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index d573f91fe..8a59ff82a 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -1,7 +1,7 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; -import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { Module } from '@nestjs/common'; @@ -19,7 +19,8 @@ import { JwtStrategy } from './jwt.strategy'; secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '180 days' } }), - SubscriptionModule + SubscriptionModule, + UserModule ], providers: [ AuthDeviceService, @@ -28,7 +29,6 @@ import { JwtStrategy } from './jwt.strategy'; GoogleStrategy, JwtStrategy, PrismaService, - UserService, WebAuthService ] }) diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 074313d5a..073a3dbd9 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { + PROPERTY_IS_READ_ONLY_MODE, PROPERTY_STRIPE_CONFIG, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; @@ -36,6 +37,7 @@ export class InfoService { public async get(): Promise { const info: Partial = {}; + let isReadOnlyMode: boolean; const platforms = await this.prismaService.platform.findMany({ orderBy: { name: 'asc' }, select: { id: true, name: true } @@ -52,6 +54,12 @@ export class InfoService { globalPermissions.push(permissions.enableImport); } + if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { + isReadOnlyMode = (await this.propertyService.getByKey( + PROPERTY_IS_READ_ONLY_MODE + )) as boolean; + } + if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { globalPermissions.push(permissions.enableSocialLogin); } @@ -77,6 +85,7 @@ export class InfoService { return { ...info, globalPermissions, + isReadOnlyMode, platforms, systemMessage, currencies: this.exchangeRateDataService.getCurrencies(), diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index d2c5d02e6..5281529b0 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,11 +1,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; -import { - getPermissions, - hasPermission, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -43,10 +39,7 @@ export class OrderController { @UseGuards(AuthGuard('jwt')) public async deleteOrder(@Param('id') id: string): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.deleteOrder - ) + !hasPermission(this.request.user.permissions, permissions.deleteOrder) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -115,10 +108,7 @@ export class OrderController { @UseGuards(AuthGuard('jwt')) public async createOrder(@Body() data: CreateOrderDto): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.createOrder - ) + !hasPermission(this.request.user.permissions, permissions.createOrder) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -161,10 +151,7 @@ export class OrderController { @UseGuards(AuthGuard('jwt')) public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.updateOrder - ) + !hasPermission(this.request.user.permissions, permissions.updateOrder) ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index f62599dfd..0a7ed21cf 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,7 +1,10 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { - getPermissions, hasPermission, + hasRole, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -20,7 +23,7 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; -import { Provider } from '@prisma/client'; +import { Provider, Role } from '@prisma/client'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -34,7 +37,9 @@ import { UserService } from './user.service'; @Controller('user') export class UserController { public constructor( + private readonly configurationService: ConfigurationService, private jwtService: JwtService, + private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @@ -43,10 +48,7 @@ export class UserController { @UseGuards(AuthGuard('jwt')) public async deleteUser(@Param('id') id: string): Promise { if ( - !hasPermission( - getPermissions(this.request.user.role), - permissions.deleteUser - ) || + !hasPermission(this.request.user.permissions, permissions.deleteUser) || id === this.request.user.id ) { throw new HttpException( @@ -68,6 +70,19 @@ export class UserController { @Post() public async signupUser(): Promise { + if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { + const isReadOnlyMode = (await this.propertyService.getByKey( + PROPERTY_IS_READ_ONLY_MODE + )) as boolean; + + if (isReadOnlyMode) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + } + const { accessToken, id } = await this.userService.createUser({ provider: Provider.ANONYMOUS }); @@ -85,7 +100,7 @@ export class UserController { public async updateUserSetting(@Body() data: UpdateUserSettingDto) { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.updateUserSettings ) ) { @@ -111,7 +126,7 @@ export class UserController { public async updateUserSettings(@Body() data: UpdateUserSettingsDto) { if ( !hasPermission( - getPermissions(this.request.user.role), + this.request.user.permissions, permissions.updateUserSettings ) ) { @@ -127,10 +142,7 @@ export class UserController { }; if ( - hasPermission( - getPermissions(this.request.user.role), - permissions.updateViewMode - ) + hasPermission(this.request.user.permissions, permissions.updateViewMode) ) { userSettings.viewMode = data.viewMode; } diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index 7d2fc3d8e..ffbdc80db 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -1,6 +1,7 @@ import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -13,6 +14,7 @@ import { UserService } from './user.service'; secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } }), + PropertyModule, SubscriptionModule ], controllers: [UserController], diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index a513cbba6..13a5b7d1d 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -1,12 +1,21 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; -import { baseCurrency, locale } from '@ghostfolio/common/config'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { + PROPERTY_IS_READ_ONLY_MODE, + baseCurrency, + locale +} from '@ghostfolio/common/config'; import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces'; -import { getPermissions, permissions } from '@ghostfolio/common/permissions'; +import { + getPermissions, + hasRole, + permissions +} from '@ghostfolio/common/permissions'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable } from '@nestjs/common'; -import { Prisma, Provider, User, ViewMode } from '@prisma/client'; +import { Prisma, Provider, Role, User, ViewMode } from '@prisma/client'; import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettings } from './interfaces/user-settings.interface'; @@ -20,6 +29,7 @@ export class UserService { public constructor( private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService, + private readonly propertyService: PropertyService, private readonly subscriptionService: SubscriptionService ) {} @@ -74,12 +84,32 @@ export class UserService { const user: UserWithSettings = userFromDatabase; - const currentPermissions = getPermissions(userFromDatabase.role); + let currentPermissions = getPermissions(userFromDatabase.role); if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { currentPermissions.push(permissions.accessFearAndGreedIndex); } + if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { + if (hasRole(user, Role.ADMIN)) { + currentPermissions.push(permissions.toggleReadOnlyMode); + } + + const isReadOnlyMode = (await this.propertyService.getByKey( + PROPERTY_IS_READ_ONLY_MODE + )) as boolean; + + if (isReadOnlyMode) { + currentPermissions = currentPermissions.filter((permission) => { + return !( + permission.startsWith('create') || + permission.startsWith('delete') || + permission.startsWith('update') + ); + }); + } + } + user.permissions = currentPermissions; if (userFromDatabase?.Settings) { diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 4a44e87db..1956a6b62 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -18,6 +18,7 @@ export class ConfigurationService { ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_IMPORT: bool({ default: true }), + ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), ENABLE_FEATURE_STATISTICS: bool({ default: false }), ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 6902a27ec..e475d32ec 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -9,6 +9,7 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_IMPORT: boolean; + ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_SOCIAL_LOGIN: boolean; ENABLE_FEATURE_STATISTICS: boolean; ENABLE_FEATURE_SUBSCRIPTION: boolean; diff --git a/apps/api/src/services/property/property.service.ts b/apps/api/src/services/property/property.service.ts index 5b576f9bc..4760c3a94 100644 --- a/apps/api/src/services/property/property.service.ts +++ b/apps/api/src/services/property/property.service.ts @@ -14,7 +14,7 @@ export class PropertyService { public async get() { const response: { - [key: string]: object | string | string[]; + [key: string]: boolean | object | string | string[]; } = { [PROPERTY_CURRENCIES]: [] }; 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 76c78378a..51105632b 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 @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -6,6 +7,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT, PROPERTY_CURRENCIES, + PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; import { InfoItem, User } from '@ghostfolio/common/interfaces'; @@ -32,6 +34,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { public defaultDateFormat = DEFAULT_DATE_FORMAT; public exchangeRates: { label1: string; label2: string; value: number }[]; public hasPermissionForSystemMessage: boolean; + public hasPermissionToToggleReadOnlyMode: boolean; public info: InfoItem; public lastDataGathering: string; public transactionCount: number; @@ -52,27 +55,32 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { ) { this.info = this.dataService.fetchInfo(); - this.hasPermissionForSystemMessage = hasPermission( - this.info.globalPermissions, - permissions.enableSystemMessage - ); - } - - /** - * Initializes the controller - */ - public ngOnInit() { - this.fetchAdminData(); - this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { this.user = state.user; + + this.hasPermissionForSystemMessage = hasPermission( + this.info.globalPermissions, + permissions.enableSystemMessage + ); + + this.hasPermissionToToggleReadOnlyMode = hasPermission( + this.user.permissions, + permissions.toggleReadOnlyMode + ); } }); } + /** + * Initializes the controller + */ + public ngOnInit() { + this.fetchAdminData(); + } + public formatDistanceToNow(aDateString: string) { if (aDateString) { const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { @@ -147,6 +155,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { .subscribe(() => {}); } + public onReadOnlyModeChange(aEvent: MatSlideToggleChange) { + this.setReadOnlyMode(aEvent.checked); + } + public onSetSystemMessage() { const systemMessage = prompt('Please set your system message:'); @@ -223,4 +235,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { }, 300); }); } + + private setReadOnlyMode(aValue: boolean) { + this.dataService + .putAdminSetting(PROPERTY_IS_READ_ONLY_MODE, { + value: aValue ? 'true' : '' + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } } 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 ca81b5143..80bbf1ab6 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -146,6 +146,16 @@
+
+
Read-only Mode
+
+ +
+
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 d87e0c5ad..f75f312ce 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 @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -10,7 +11,13 @@ import { AdminOverviewComponent } from './admin-overview.component'; @NgModule({ declarations: [AdminOverviewComponent], exports: [], - imports: [CommonModule, GfValueModule, MatButtonModule, MatCardModule], + imports: [ + CommonModule, + GfValueModule, + MatButtonModule, + MatCardModule, + MatSlideToggleModule + ], providers: [CacheService], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index 593ac23f3..4aeab5574 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -270,7 +270,7 @@ Sign In -
+
diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.ts b/apps/client/src/app/components/transactions-table/transactions-table.component.ts index e9a657406..ab2f18e10 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.ts +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.ts @@ -43,6 +43,7 @@ export class TransactionsTableComponent { @Input() baseCurrency: string; @Input() deviceType: string; + @Input() hasPermissionToCreateOrder: boolean; @Input() hasPermissionToImportOrders: boolean; @Input() locale: string; @Input() showActions: boolean; diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index afed17f4e..ea0cf8d2e 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -15,22 +15,28 @@ import { } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; +import { InfoItem } from '@ghostfolio/common/interfaces'; import { StatusCodes } from 'http-status-codes'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { TokenStorageService } from '../services/token-storage.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; @Injectable() export class HttpResponseInterceptor implements HttpInterceptor { + public info: InfoItem; public snackBarRef: MatSnackBarRef; public constructor( + private dataService: DataService, private router: Router, private tokenStorageService: TokenStorageService, private snackBar: MatSnackBar, private webAuthnService: WebAuthnService - ) {} + ) { + this.info = this.dataService.fetchInfo(); + } public intercept( request: HttpRequest, @@ -63,11 +69,19 @@ export class HttpResponseInterceptor implements HttpInterceptor { catchError((error: HttpErrorResponse) => { if (error.status === StatusCodes.FORBIDDEN) { if (!this.snackBarRef) { - this.snackBarRef = this.snackBar.open( - 'This feature requires a subscription.', - 'Upgrade Plan', - { duration: 6000 } - ); + if (this.info.isReadOnlyMode) { + this.snackBarRef = this.snackBar.open( + 'This feature is currently unavailable. Please try again later.', + undefined, + { duration: 6000 } + ); + } else { + this.snackBarRef = this.snackBar.open( + 'This feature requires a subscription.', + 'Upgrade Plan', + { duration: 6000 } + ); + } this.snackBarRef.afterDismissed().subscribe(() => { this.snackBarRef = undefined; 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 81ede2dc1..53191835c 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -51,7 +51,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['createDialog']) { + if (params['createDialog'] && this.hasPermissionToCreateAccount) { this.openCreateAccountDialog(); } else if (params['editDialog']) { if (this.accounts) { diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 0b79782a3..1827e156e 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -61,7 +61,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['createDialog']) { + if (params['createDialog'] && this.hasPermissionToCreateOrder) { this.openCreateTransactionDialog(); } else if (params['editDialog']) { if (this.transactions) { @@ -130,7 +130,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .subscribe((response) => { this.transactions = response; - if (this.transactions?.length <= 0) { + if (this.hasPermissionToCreateOrder && this.transactions?.length <= 0) { this.router.navigate([], { queryParams: { createDialog: true } }); } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index 5288d7313..f97727e38 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -5,6 +5,7 @@ (); @@ -37,6 +39,8 @@ export class RegisterPageComponent implements OnDestroy, OnInit { private router: Router, private tokenStorageService: TokenStorageService ) { + this.info = this.dataService.fetchInfo(); + this.tokenStorageService.signOut(); } diff --git a/apps/client/src/app/pages/register/register-page.html b/apps/client/src/app/pages/register/register-page.html index 5806e8f4c..96d49251b 100644 --- a/apps/client/src/app/pages/register/register-page.html +++ b/apps/client/src/app/pages/register/register-page.html @@ -22,7 +22,7 @@ color="primary" i18n mat-flat-button - [disabled]="!demoAuthToken" + [disabled]="!demoAuthToken || info?.isReadOnlyMode" (click)="createAccount()" > Create Account diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 6697c92f9..31076cccd 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -31,6 +31,7 @@ export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; +export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING'; export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index ce4ea8c0b..a061269e7 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -4,7 +4,7 @@ export interface AdminData { dataGatheringProgress?: number; exchangeRates: { label1: string; label2: string; value: number }[]; lastDataGathering?: Date | 'IN_PROGRESS'; - settings: { [key: string]: object | string | string[] }; + settings: { [key: string]: boolean | object | string | string[] }; transactionCount: number; userCount: number; users: { diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 08b511cf3..7ac5f3e9e 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -7,11 +7,12 @@ export interface InfoItem { currencies: string[]; demoAuthToken: string; globalPermissions: string[]; + isReadOnlyMode?: boolean; lastDataGathering?: Date; - systemMessage?: string; platforms: { id: string; name: string }[]; primaryDataSource: DataSource; statistics: Statistics; stripePublicKey?: string; subscriptions: Subscription[]; + systemMessage?: string; } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 8feab7466..dc574f7ef 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -1,5 +1,7 @@ import { Role } from '@prisma/client'; +import { UserWithSettings } from './interfaces'; + export const permissions = { accessAdminControl: 'accessAdminControl', accessFearAndGreedIndex: 'accessFearAndGreedIndex', @@ -18,6 +20,7 @@ export const permissions = { enableStatistics: 'enableStatistics', enableSubscription: 'enableSubscription', enableSystemMessage: 'enableSystemMessage', + toggleReadOnlyMode: 'toggleReadOnlyMode', updateAccount: 'updateAccount', updateAuthDevice: 'updateAuthDevice', updateOrder: 'updateOrder', @@ -25,13 +28,6 @@ export const permissions = { updateViewMode: 'updateViewMode' }; -export function hasPermission( - aPermissions: string[] = [], - aPermission: string -) { - return aPermissions.includes(aPermission); -} - export function getPermissions(aRole: Role): string[] { switch (aRole) { case 'ADMIN': @@ -75,3 +71,14 @@ export function getPermissions(aRole: Role): string[] { return []; } } + +export function hasPermission( + aPermissions: string[] = [], + aPermission: string +) { + return aPermissions.includes(aPermission); +} + +export function hasRole(aUser: UserWithSettings, aRole: Role): boolean { + return aUser?.role === aRole; +} From 606350b2ffe380a2d84cace336470df7b23e28ec Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:34:52 +0100 Subject: [PATCH 004/337] Release 1.87.0 (#526) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dddbbfa0..25f551751 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.87.0 - 07.12.2021 ### Added diff --git a/package.json b/package.json index 08c4c9e2b..b24789407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.86.0", + "version": "1.87.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 78e0fdb0ca7881d4248b641e014ed12687078914 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 9 Dec 2021 21:14:44 +0100 Subject: [PATCH 005/337] Add coupon system (#529) * Add coupon system * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/admin/admin.controller.ts | 1 - .../src/app/portfolio/portfolio.service.ts | 4 +- .../subscription/subscription.controller.ts | 56 ++++++++++++++++++- .../app/subscription/subscription.module.ts | 3 +- .../app/subscription/subscription.service.ts | 30 ++++++---- .../admin-overview.component.ts | 53 +++++++++++++++++- .../admin-overview/admin-overview.html | 23 +++++++- .../src/app/core/http-response.interceptor.ts | 5 +- .../pages/account/account-page.component.ts | 50 +++++++++++++++++ .../src/app/pages/account/account-page.html | 7 +++ .../app/pages/account/account-page.module.ts | 4 +- .../src/app/pages/account/account-page.scss | 9 +++ apps/client/src/app/services/data.service.ts | 6 ++ libs/common/src/lib/config.ts | 1 + .../src/lib/interfaces/coupon.interface.ts | 3 + libs/common/src/lib/interfaces/index.ts | 2 + package.json | 2 +- 18 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 libs/common/src/lib/interfaces/coupon.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f551751..92bcd8123 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.88.0 - 09.12.2021 + +### Added + +- Added a coupon system + ## 1.87.0 - 07.12.2021 ### Added diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index ef6753894..1de70e588 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -31,7 +31,6 @@ export class AdminController { public constructor( private readonly adminService: AdminService, private readonly dataGatheringService: DataGatheringService, - private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 887b1cbe5..0887175f1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -729,8 +729,8 @@ export class PortfolioService { currentNetPerformance, currentNetPerformancePercent, currentValue, - isAllTimeHigh: true, // TODO - isAllTimeLow: false // TODO + isAllTimeHigh: true, + isAllTimeLow: false } }; } diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 0eb345f63..9ffb86f2d 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -1,4 +1,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { PROPERTY_COUPONS } from '@ghostfolio/common/config'; +import { Coupon } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -14,6 +17,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { SubscriptionService } from './subscription.service'; @@ -22,13 +26,63 @@ import { SubscriptionService } from './subscription.service'; export class SubscriptionController { public constructor( private readonly configurationService: ConfigurationService, + private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly subscriptionService: SubscriptionService ) {} + @Post('redeem-coupon') + @UseGuards(AuthGuard('jwt')) + public async redeemCoupon( + @Body() { couponCode }: { couponCode: string }, + @Res() res: Response + ) { + if (!this.request.user) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + let coupons = + ((await this.propertyService.getByKey(PROPERTY_COUPONS)) as Coupon[]) ?? + []; + + const isValid = coupons.some((coupon) => { + return coupon.code === couponCode; + }); + + if (!isValid) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + + await this.subscriptionService.createSubscription(this.request.user.id); + + // Destroy coupon + coupons = coupons.filter((coupon) => { + return coupon.code !== couponCode; + }); + await this.propertyService.put({ + key: PROPERTY_COUPONS, + value: JSON.stringify(coupons) + }); + + Logger.log(`Coupon with code '${couponCode}' has been redeemed`); + + res.status(StatusCodes.OK); + + return res.json({ + message: getReasonPhrase(StatusCodes.OK), + statusCode: StatusCodes.OK + }); + } + @Get('stripe/callback') public async stripeCallback(@Req() req, @Res() res) { - await this.subscriptionService.createSubscription( + await this.subscriptionService.createSubscriptionViaStripe( req.query.checkoutSessionId ); diff --git a/apps/api/src/app/subscription/subscription.module.ts b/apps/api/src/app/subscription/subscription.module.ts index 48671550c..95d16fb4d 100644 --- a/apps/api/src/app/subscription/subscription.module.ts +++ b/apps/api/src/app/subscription/subscription.module.ts @@ -1,12 +1,13 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { Module } from '@nestjs/common'; import { SubscriptionController } from './subscription.controller'; import { SubscriptionService } from './subscription.service'; @Module({ - imports: [], + imports: [PropertyModule], controllers: [SubscriptionController], providers: [ConfigurationService, PrismaService, SubscriptionService], exports: [SubscriptionService] diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 2d40cbcc2..2c4a81b95 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -2,7 +2,7 @@ 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 } from '@prisma/client'; +import { Subscription, User } from '@prisma/client'; import { addDays, isBefore } from 'date-fns'; import Stripe from 'stripe'; @@ -64,22 +64,28 @@ export class SubscriptionService { }; } - public async createSubscription(aCheckoutSessionId: string) { + public async createSubscription(aUserId: string) { + await this.prismaService.subscription.create({ + data: { + expiresAt: addDays(new Date(), 365), + User: { + connect: { + id: aUserId + } + } + } + }); + + Logger.log(`Subscription for user '${aUserId}' has been created`); + } + + public async createSubscriptionViaStripe(aCheckoutSessionId: string) { try { const session = await this.stripe.checkout.sessions.retrieve( aCheckoutSessionId ); - await this.prismaService.subscription.create({ - data: { - expiresAt: addDays(new Date(), 365), - User: { - connect: { - id: session.client_reference_id - } - } - } - }); + await this.createSubscription(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 51105632b..dc15cca12 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 @@ -6,11 +6,12 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT, + PROPERTY_COUPONS, PROPERTY_CURRENCIES, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; -import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { Coupon, InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { differenceInSeconds, @@ -28,11 +29,13 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-overview.html' }) export class AdminOverviewComponent implements OnDestroy, OnInit { + public coupons: Coupon[]; public customCurrencies: string[]; public dataGatheringInProgress: boolean; public dataGatheringProgress: number; public defaultDateFormat = DEFAULT_DATE_FORMAT; public exchangeRates: { label1: string; label2: string; value: number }[]; + public hasPermissionForSubscription: boolean; public hasPermissionForSystemMessage: boolean; public hasPermissionToToggleReadOnlyMode: boolean; public info: InfoItem; @@ -61,6 +64,11 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.hasPermissionForSubscription = hasPermission( + this.info.globalPermissions, + permissions.enableSubscription + ); + this.hasPermissionForSystemMessage = hasPermission( this.info.globalPermissions, permissions.enableSystemMessage @@ -96,6 +104,11 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { return ''; } + public onAddCoupon() { + const coupons = [...this.coupons, { code: this.generateCouponCode(16) }]; + this.putCoupons(coupons); + } + public onAddCurrency() { const currency = prompt('Please add a currency:'); @@ -105,6 +118,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } } + public onDeleteCoupon(aCouponCode: string) { + const confirmation = confirm('Do you really want to delete this coupon?'); + + if (confirmation) { + const coupons = this.coupons.filter((coupon) => { + return coupon.code !== aCouponCode; + }); + this.putCoupons(coupons); + } + } + public onDeleteCurrency(aCurrency: string) { const confirmation = confirm('Do you really want to delete this currency?'); @@ -185,6 +209,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { transactionCount, userCount }) => { + this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; this.dataGatheringProgress = dataGatheringProgress; this.exchangeRates = exchangeRates; @@ -210,6 +235,32 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { ); } + private generateCouponCode(aLength: number) { + const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789'; + let couponCode = ''; + + for (let i = 0; i < aLength; i++) { + couponCode += characters.charAt( + Math.floor(Math.random() * characters.length) + ); + } + + return couponCode; + } + + private putCoupons(aCoupons: Coupon[]) { + this.dataService + .putAdminSetting(PROPERTY_COUPONS, { + value: JSON.stringify(aCoupons) + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } + private putCurrencies(aCurrencies: string[]) { this.dataService .putAdminSetting(PROPERTY_CURRENCIES, { 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 80bbf1ab6..04f8ba4ec 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -142,7 +142,7 @@ class="mr-1" name="information-circle-outline" > - Set System Message + Set Message
@@ -156,6 +156,27 @@ > +
+
Coupons
+
+
+ {{ coupon.code }} + +
+
+ +
+
+
diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index ea0cf8d2e..1df29e7ec 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -14,15 +14,14 @@ import { TextOnlySnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { InfoItem } from '@ghostfolio/common/interfaces'; import { StatusCodes } from 'http-status-codes'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { DataService } from '@ghostfolio/client/services/data.service'; -import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; - @Injectable() export class HttpResponseInterceptor implements HttpInterceptor { public info: InfoItem; 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 244c57cce..5d1d0d5e3 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -10,6 +10,11 @@ import { MatSlideToggle, MatSlideToggleChange } from '@angular/material/slide-toggle'; +import { + MatSnackBar, + MatSnackBarRef, + TextOnlySnackBar +} from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -49,6 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { public hasPermissionToUpdateUserSettings: boolean; public price: number; public priceId: string; + public snackBarRef: MatSnackBarRef; public user: User; private unsubscribeSubject = new Subject(); @@ -61,6 +67,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private snackBar: MatSnackBar, private route: ActivatedRoute, private router: Router, private stripeService: StripeService, @@ -185,6 +192,49 @@ export class AccountPageComponent implements OnDestroy, OnInit { }); } + public onRedeemCoupon() { + let couponCode = prompt('Please enter your coupon code:'); + couponCode = couponCode?.trim(); + + if (couponCode) { + this.dataService + .redeemCoupon(couponCode) + .pipe( + takeUntil(this.unsubscribeSubject), + catchError(() => { + this.snackBar.open('😞 Could not redeem coupon code', undefined, { + duration: 3000 + }); + + return EMPTY; + }) + ) + .subscribe(() => { + this.snackBarRef = this.snackBar.open( + '✅ Coupon code has been redeemed', + 'Reload', + { + duration: 3000 + } + ); + + this.snackBarRef + .afterDismissed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + window.location.reload(); + }); + + this.snackBarRef + .onAction() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + window.location.reload(); + }); + }); + } + } + public onRestrictedViewChange(aEvent: MatSlideToggleChange) { this.dataService .putUserSetting({ isRestrictedView: aEvent.checked }) diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 9ffd2159d..1155741f9 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -47,6 +47,13 @@ {{ price }} per year +
Redeem Coupon diff --git a/apps/client/src/app/pages/account/account-page.module.ts b/apps/client/src/app/pages/account/account-page.module.ts index 5b3d8ce37..cf0f52a03 100644 --- a/apps/client/src/app/pages/account/account-page.module.ts +++ b/apps/client/src/app/pages/account/account-page.module.ts @@ -8,6 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { RouterModule } from '@angular/router'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { AccountPageRoutingModule } from './account-page-routing.module'; @@ -30,7 +31,8 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di MatInputModule, MatSelectModule, MatSlideToggleModule, - ReactiveFormsModule + ReactiveFormsModule, + RouterModule ], providers: [] }) diff --git a/apps/client/src/app/pages/account/account-page.scss b/apps/client/src/app/pages/account/account-page.scss index 4a798522a..bafd8d6e4 100644 --- a/apps/client/src/app/pages/account/account-page.scss +++ b/apps/client/src/app/pages/account/account-page.scss @@ -2,6 +2,15 @@ color: rgb(var(--dark-primary-text)); display: block; + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; + + &:hover { + color: rgba(var(--palette-primary-300), 1); + } + } + gf-access-table { overflow-x: auto; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index b15dd1723..9ba5ca669 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -253,4 +253,10 @@ export class DataService { public putUserSettings(aData: UpdateUserSettingsDto) { return this.http.put(`/api/user/settings`, aData); } + + public redeemCoupon(couponCode: string) { + return this.http.post('/api/subscription/redeem-coupon', { + couponCode + }); + } } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 31076cccd..d9acd3e0b 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -30,6 +30,7 @@ export const warnColorRgb = { export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; +export const PROPERTY_COUPONS = 'COUPONS'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING'; diff --git a/libs/common/src/lib/interfaces/coupon.interface.ts b/libs/common/src/lib/interfaces/coupon.interface.ts new file mode 100644 index 000000000..3caa218e6 --- /dev/null +++ b/libs/common/src/lib/interfaces/coupon.interface.ts @@ -0,0 +1,3 @@ +export interface Coupon { + code: string; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 3192ece9f..d9bcc3a8b 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -3,6 +3,7 @@ import { Accounts } from './accounts.interface'; import { AdminData } from './admin-data.interface'; import { AdminMarketDataDetails } from './admin-market-data-details.interface'; import { AdminMarketData } from './admin-market-data.interface'; +import { Coupon } from './coupon.interface'; import { Export } from './export.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; @@ -27,6 +28,7 @@ export { AdminData, AdminMarketData, AdminMarketDataDetails, + Coupon, Export, InfoItem, PortfolioChart, diff --git a/package.json b/package.json index b24789407..cb98574e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.87.0", + "version": "1.88.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 39e6abfc8c82ca2a65fc81f4cf98a20681f765be Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 10 Dec 2021 19:43:53 +0100 Subject: [PATCH 006/337] Clean up preview features (#530) --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 58c2f0dc9..bfa575084 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,7 +9,7 @@ datasource db { generator client { provider = "prisma-client-js" binaryTargets = ["debian-openssl-1.1.x", "native"] - previewFeatures = ["orderByRelation", "selectRelationCount"] + previewFeatures = [] } model Access { From 7d3f1832b4c91226765b8d8e1555680397bd441b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 10 Dec 2021 19:50:46 +0100 Subject: [PATCH 007/337] Feature/upgrade nx and storybook dependencies (#531) * Upgrade dependencies * nx * storybook * Update changelog --- CHANGELOG.md | 7 + .../lib/line-chart/line-chart.component.ts | 7 +- .../no-transactions-info.component.stories.ts | 2 +- .../no-transactions-info.module.ts | 2 +- .../portfolio-proportion-chart.component.ts | 5 +- .../trend-indicator.component.ts | 5 +- libs/ui/src/lib/value/value.component.ts | 3 +- package.json | 28 +- yarn.lock | 1326 +++++++++-------- 9 files changed, 718 insertions(+), 667 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92bcd8123..3261c74e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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 `Nx` from version `13.2.2` to `13.3.0` +- Upgraded `storybook` from version `6.4.0-rc.3` to `6.4.9` + ## 1.88.0 - 09.12.2021 ### Added diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 601990715..d2c185b15 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -10,6 +10,8 @@ import { OnDestroy, ViewChild } from '@angular/core'; +import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; +import { getBackgroundColor } from '@ghostfolio/common/helper'; import { Chart, Filler, @@ -20,11 +22,6 @@ import { TimeScale } from 'chart.js'; -import { - primaryColorRgb, - secondaryColorRgb -} from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config -import { getBackgroundColor } from '../../../../common/src/lib/helper'; // TODO: @ghostfolio/common/helper import { LineChartItem } from './interfaces/line-chart.interface'; @Component({ diff --git a/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.stories.ts b/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.stories.ts index 86ee2c96e..e76e670ea 100644 --- a/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.stories.ts +++ b/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.stories.ts @@ -1,6 +1,6 @@ +import { GfLogoModule } from '@ghostfolio/ui/logo'; import { Meta, Story, moduleMetadata } from '@storybook/angular'; -import { GfLogoModule } from '../../../../ui/src/lib/logo'; // TODO: @ghostfolio/ui/logo import { NoTransactionsInfoComponent } from './no-transactions-info.component'; export default { diff --git a/libs/ui/src/lib/no-transactions-info/no-transactions-info.module.ts b/libs/ui/src/lib/no-transactions-info/no-transactions-info.module.ts index bad3aafec..78cda4ff9 100644 --- a/libs/ui/src/lib/no-transactions-info/no-transactions-info.module.ts +++ b/libs/ui/src/lib/no-transactions-info/no-transactions-info.module.ts @@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; +import { GfLogoModule } from '@ghostfolio/ui/logo'; -import { GfLogoModule } from '../../../../ui/src/lib/logo'; // TODO: @ghostfolio/ui/logo import { NoTransactionsInfoComponent } from './no-transactions-info.component'; @NgModule({ 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 7ec180b05..470d135c2 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 @@ -8,6 +8,8 @@ import { OnDestroy, ViewChild } from '@angular/core'; +import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { getTextColor } from '@ghostfolio/common/helper'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Tooltip } from 'chart.js'; import { LinearScale } from 'chart.js'; @@ -17,9 +19,6 @@ import { Chart } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import * as Color from 'color'; -import { UNKNOWN_KEY } from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config -import { getTextColor } from '../../../../common/src/lib/helper'; // TODO: @ghostfolio/common/helper - @Component({ selector: 'gf-portfolio-proportion-chart', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts index 43b3991ea..708b27dc9 100644 --- a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts +++ b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; - -import { MarketState } from '../../../../../apps/api/src/services/interfaces/interfaces'; // TODO: @ghostfolio/api/services/interfaces/interfaces -import { DateRange } from '../../../../common/src/lib/types'; // TODO: @ghostfolio/common/types +import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; +import { DateRange } from '@ghostfolio/common/types'; @Component({ selector: 'gf-trend-indicator', diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index e203d5615..9c805e859 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -4,11 +4,10 @@ import { Input, OnChanges } from '@angular/core'; +import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { format, isDate } from 'date-fns'; import { isNumber } from 'lodash'; -import { DEFAULT_DATE_FORMAT } from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config - @Component({ selector: 'gf-value', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/package.json b/package.json index cb98574e7..8a0576665 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@nestjs/platform-express": "8.2.3", "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", - "@nrwl/angular": "13.2.2", + "@nrwl/angular": "13.3.0", "@prisma/client": "3.6.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", @@ -126,19 +126,19 @@ "@angular/localize": "13.0.2", "@nestjs/schematics": "8.0.5", "@nestjs/testing": "8.2.3", - "@nrwl/cli": "13.2.2", - "@nrwl/cypress": "13.2.2", - "@nrwl/eslint-plugin-nx": "13.2.2", - "@nrwl/jest": "13.2.2", - "@nrwl/nest": "13.2.2", - "@nrwl/node": "13.2.2", - "@nrwl/storybook": "13.2.2", - "@nrwl/tao": "13.2.2", - "@nrwl/workspace": "13.2.2", - "@storybook/addon-essentials": "6.4.0-rc.3", - "@storybook/angular": "6.4.0-rc.3", - "@storybook/builder-webpack5": "6.4.0-rc.3", - "@storybook/manager-webpack5": "6.4.0-rc.3", + "@nrwl/cli": "13.3.0", + "@nrwl/cypress": "13.3.0", + "@nrwl/eslint-plugin-nx": "13.3.0", + "@nrwl/jest": "13.3.0", + "@nrwl/nest": "13.3.0", + "@nrwl/node": "13.3.0", + "@nrwl/storybook": "13.3.0", + "@nrwl/tao": "13.3.0", + "@nrwl/workspace": "13.3.0", + "@storybook/addon-essentials": "6.4.9", + "@storybook/angular": "6.4.9", + "@storybook/builder-webpack5": "6.4.9", + "@storybook/manager-webpack5": "6.4.9", "@types/big.js": "6.1.2", "@types/cache-manager": "3.4.2", "@types/color": "3.0.2", diff --git a/yarn.lock b/yarn.lock index a5f1c3de0..0d5194c38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -333,13 +333,6 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" @@ -2021,21 +2014,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== - dependencies: - ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - "@eslint/eslintrc@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31" @@ -2056,15 +2034,6 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" - "@humanwhocodes/config-array@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a" @@ -2610,58 +2579,49 @@ node-gyp "^8.2.0" read-package-json-fast "^2.0.1" -"@nrwl/angular@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-13.2.2.tgz#c2be1e8e8eb62a4fda1cdd711e613d36ffaaf534" - integrity sha512-KTtDVmhu793X8rKLiH+DKZDA++1YrDV9vkGHTwN/EVEKYnq1w0m5/TaHyhtfAXUBLl5itPrPBRs10Uviar8Cxw== +"@nrwl/angular@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-13.3.0.tgz#9362a4dcab80d01688cf31e828bd0cbeb088fdc3" + integrity sha512-3cJUKwd21iYHWPpdwsKLvKf3bGwrgG2JO4GxNmscOEyfR4hI9juiaB/fYgftMcWVSC/vXIcjFivPDZzdOZTq8Q== dependencies: "@angular-devkit/schematics" "~13.0.0" - "@nrwl/cypress" "13.2.2" - "@nrwl/devkit" "13.2.2" - "@nrwl/jest" "13.2.2" - "@nrwl/linter" "13.2.2" - "@nrwl/storybook" "13.2.2" + "@nrwl/cypress" "13.3.0" + "@nrwl/devkit" "13.3.0" + "@nrwl/jest" "13.3.0" + "@nrwl/linter" "13.3.0" + "@nrwl/storybook" "13.3.0" "@phenomnomnominal/tsquery" "4.1.1" "@schematics/angular" "~13.0.0" find-parent-dir "^0.3.1" ignore "^5.0.4" jasmine-marbles "~0.8.4" rxjs-for-await "0.0.2" + ts-node "~9.1.1" + tsconfig-paths "^3.9.0" tslib "^2.3.0" webpack-merge "5.7.3" -"@nrwl/cli@*": - version "12.9.0" - resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-12.9.0.tgz#f5fccd973006ad2802d7c823b28820ac1fa6c27c" - integrity sha512-YKTZ3G07f6Y4MedOOkBmCi1Y72gu3ssCk2J50wL76SaiSjJTUSAz1NkKLsPwO6S8/QloMSR71tI42HJG2bbpwQ== +"@nrwl/cli@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-13.3.0.tgz#dd26c0cedbe2fc652879b3cd06e6ba07069239f1" + integrity sha512-PiK4w1CCLQQ+KIP7jWZxhcUIZqLK5qajsyg6TjZvnsIWePm50pVJxQQZgbrK6M2ZxwmifqIGX1kcDPfd+55V4g== dependencies: - "@nrwl/tao" "12.9.0" - chalk "4.1.0" - v8-compile-cache "2.3.0" - yargs "15.4.1" - yargs-parser "20.0.0" - -"@nrwl/cli@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-13.2.2.tgz#cd6237ac5254bf6a686f6a5205c2d6e949a13083" - integrity sha512-iqWwULCgYV3r+sKA/jrh4Di5buesPDcmdG0vi4kFnMgNF3Pg+8Coa7qKXJXdTr1gugBfGPFFmHORmH1pE3CMYA== - dependencies: - "@nrwl/tao" "13.2.2" + "@nrwl/tao" "13.3.0" chalk "4.1.0" enquirer "~2.3.6" v8-compile-cache "2.3.0" yargs "15.4.1" yargs-parser "20.0.0" -"@nrwl/cypress@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-13.2.2.tgz#1fd761e280c15018e7f51407dffe12c797873734" - integrity sha512-duiUeE15i8DezrQ88s8GLoaT6xZdDAZ21iu/KjMwTm48za/pa2pcCUf8TtbpL8TWzVoFo/7ks61fycmUY+jJow== +"@nrwl/cypress@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-13.3.0.tgz#7eda115e5a8487f3886eb9763a56112af7560f61" + integrity sha512-dVeAqwfIQ4GhS2CPaelG9FrYOmxjFzhJIOvHM0Wc3jYHB7N1Vbt2P81riafvNlTUr3AxX6flZ0HcTvXIDPrRtw== dependencies: "@cypress/webpack-preprocessor" "^5.9.1" - "@nrwl/devkit" "13.2.2" - "@nrwl/linter" "13.2.2" - "@nrwl/workspace" "13.2.2" + "@nrwl/devkit" "13.3.0" + "@nrwl/linter" "13.3.0" + "@nrwl/workspace" "13.3.0" chalk "4.1.0" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" @@ -2673,38 +2633,38 @@ webpack-node-externals "^3.0.0" yargs-parser "20.0.0" -"@nrwl/devkit@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-13.2.2.tgz#f0cee712359ae7b82d222380c2cd5695ba38d6e8" - integrity sha512-2uMJF6j7dpR9DYkFWn9WeMeycrEAhSTzDUTdapWp9hPsqJIMV0rk2WD4PPseflAewHnrdDtm9b9Rqvo/mXz4wQ== +"@nrwl/devkit@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-13.3.0.tgz#d3805b7695527181969c9d51a0e6bd38f0958ed0" + integrity sha512-qGnBGB+P8CbzyuZYTwYbNZ1iHHYeSPTxvFlKAhctn285EMoSSIU32k9yKf8DksVOwx75nofwN7u75ORB/XM/7Q== dependencies: - "@nrwl/tao" "13.2.2" + "@nrwl/tao" "13.3.0" 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.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-13.2.2.tgz#999b4329bda8479c0f16341c0a656786163fb2f9" - integrity sha512-HAR7p2QnScXk0LwHvvUSnMZB0oTnYHhFiLOyTYCUQFdp6Mjx7scJdVsm2/NfQh7VCqzDe5qMvKqb5k47TRX2WQ== +"@nrwl/eslint-plugin-nx@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-13.3.0.tgz#66b215131e3365b08d66d0f7db18d009859e794f" + integrity sha512-gCAbktVGUvxHCvzIT//8awYHNyHtlPkmMlajAZRvvJJIkDV7AoKsE5bZTIGVv5X5Z+opjfx4htES65GIlrXD+g== dependencies: - "@nrwl/devkit" "13.2.2" - "@nrwl/workspace" "13.2.2" - "@typescript-eslint/experimental-utils" "~4.33.0" + "@nrwl/devkit" "13.3.0" + "@nrwl/workspace" "13.3.0" + "@typescript-eslint/experimental-utils" "~5.3.0" confusing-browser-globals "^1.0.9" ts-node "^9.1.1" tsconfig-paths "^3.9.0" -"@nrwl/jest@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-13.2.2.tgz#e9ffc1af695af9110581848fd47e83cd81f0af25" - integrity sha512-2fPwx0pSm557TrOPdaT4jcJH6JZor7+gCx3mW6BAVmAr5IZmKEbnLI9M3eWV0KhcIOt4teZXqJQ4YrrQ8Eb/Tw== +"@nrwl/jest@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-13.3.0.tgz#0c1eaa96eca76c00ef4adac545b6010b21195863" + integrity sha512-U1onvyI7kk8tC5GvpXT4aDUucXrw7uvKKima88uRJG2JBpkwOOYDD+n8NWEIMKUSQd8KMI1BBCfh+MP+rEgJ4A== dependencies: "@jest/reporters" "27.2.2" "@jest/test-result" "27.2.2" - "@nrwl/devkit" "13.2.2" + "@nrwl/devkit" "13.3.0" chalk "4.1.0" identity-obj-proxy "3.0.0" jest-config "27.2.2" @@ -2714,39 +2674,40 @@ rxjs "^6.5.4" tslib "^2.3.0" -"@nrwl/linter@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-13.2.2.tgz#0d9ec6dda8b0aaf80c91d583ccfc03bbd8a3d80a" - integrity sha512-WLEvlEDUZIVZuGfjgNahIBRO9P5rWoj6qcDk6mutpz3Fx/EYFSdH8thsJz6K+MMWk1gBZprwbXt8PiBdDrwIdQ== +"@nrwl/linter@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-13.3.0.tgz#c0f16030f3edf5f88030c9932daf59facf21542b" + integrity sha512-ELdCpKbrjD8Kh4fD95ROGUVXpBs8hsUMqHcyVCPxahBzwb6JYZVqxAKB1tp5PjRwaYaoJCgxlQ3FQpT9IQ96Ng== dependencies: - "@nrwl/devkit" "13.2.2" - "@nrwl/jest" "13.2.2" - eslint "7.32.0" + "@nrwl/devkit" "13.3.0" + "@nrwl/jest" "13.3.0" + "@phenomnomnominal/tsquery" "4.1.1" + eslint "8.2.0" glob "7.1.4" minimatch "3.0.4" tmp "~0.2.1" tslib "^2.3.0" -"@nrwl/nest@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-13.2.2.tgz#17b8b5e3cfeab3e13f849f397aca225b9ca5f0e2" - integrity sha512-rztsnez4vP6+BGFWfWxmTOLuPuRhL/aJ5rvVmlNJRCbpg0IUKs2ZCQWlM3RaSk6Q9hZ3QvfSYcy3/sF/mkF95w== +"@nrwl/nest@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-13.3.0.tgz#efe7f0fa7377914fc622eee6664294a6c6b28c42" + integrity sha512-rAih9cOURAvXtokXfki/h7nyS1VSl5eOKKF6HJnKQ3V6hcDmkKfhC3dK83wiBiJJM9ERfdfMz9m2TRILEmyRwQ== dependencies: "@nestjs/schematics" "^8.0.0" - "@nrwl/devkit" "13.2.2" - "@nrwl/jest" "13.2.2" - "@nrwl/linter" "13.2.2" - "@nrwl/node" "13.2.2" - -"@nrwl/node@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-13.2.2.tgz#67976a734dcfbed5bc4c4f804da48f083094b236" - integrity sha512-F1ibkoofGdzfjn3zVSs7lRETRDQ85txZXx2h2U4LcTcgv6s1hkXDwAGu/saK18PFYPbrrq7wXtNAqXlqUkapIA== - dependencies: - "@nrwl/devkit" "13.2.2" - "@nrwl/jest" "13.2.2" - "@nrwl/linter" "13.2.2" - "@nrwl/workspace" "13.2.2" + "@nrwl/devkit" "13.3.0" + "@nrwl/jest" "13.3.0" + "@nrwl/linter" "13.3.0" + "@nrwl/node" "13.3.0" + +"@nrwl/node@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-13.3.0.tgz#8782dc635da4c9654eb600c79907e7cd96fa64be" + integrity sha512-9wz1MsemmrZqCYemzMxK4KBvCLBOs6U45Wl+A165FXzvs2pG+l9B2IWWxUzBApfqUlS3QIB3oLAtdayQgu1qfQ== + dependencies: + "@nrwl/devkit" "13.3.0" + "@nrwl/jest" "13.3.0" + "@nrwl/linter" "13.3.0" + "@nrwl/workspace" "13.3.0" chalk "4.1.0" copy-webpack-plugin "^9.0.1" enhanced-resolve "^5.8.3" @@ -2766,47 +2727,32 @@ webpack-merge "^5.8.0" webpack-node-externals "^3.0.0" -"@nrwl/storybook@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.2.2.tgz#57a17218d5704a71eb5c97b28f92dc75c8f7bb5c" - integrity sha512-jYKUObbKOGne/8veF1czzKox/+6OE8glyIOjpBvFsk6z3PKM5m8motIZbQ9skcb0zOQhNLdVVFphHnUyA63Ljw== +"@nrwl/storybook@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.3.0.tgz#3d51d9ef5c613aa9b9c1e82fcd8b0cd4a7b33c21" + integrity sha512-JNCHCB8fLAvKZz0IHZROTy2PmOovdCDcXgrDIihEpmY0JshQ/o0DnFd7D4tDxIGnHG1NciYxIjg+EIg2h2/flQ== dependencies: - "@nrwl/cypress" "13.2.2" - "@nrwl/devkit" "13.2.2" - "@nrwl/linter" "13.2.2" - "@nrwl/workspace" "13.2.2" + "@nrwl/cypress" "13.3.0" + "@nrwl/devkit" "13.3.0" + "@nrwl/linter" "13.3.0" + "@nrwl/workspace" "13.3.0" core-js "^3.6.5" semver "7.3.4" ts-loader "^9.2.6" tsconfig-paths-webpack-plugin "3.4.1" -"@nrwl/tao@12.9.0": - version "12.9.0" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-12.9.0.tgz#f8470ac0dccd5df414afc244fbbf37c23eb75764" - integrity sha512-a97JYoLohhBRthnWAGMh3++8Ri/yvCQUG/INBAYxW6sWAk2owJ6DIEIERP4yhIW29HPdqZ/fA2k9iqU6EgIAew== - dependencies: - chalk "4.1.0" - enquirer "~2.3.6" - fs-extra "^9.1.0" - jsonc-parser "3.0.0" - nx "12.9.0" - rxjs "^6.5.4" - rxjs-for-await "0.0.2" - semver "7.3.4" - tmp "~0.2.1" - tslib "^2.0.0" - yargs-parser "20.0.0" - -"@nrwl/tao@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.2.2.tgz#704940de14c62a14d54ec69fd898e1e3d7066b5c" - integrity sha512-IQtiTuouagSYpxztJvxD1XYha1eLagFoJV/2Y7bu6jUx5KTT5chLzXbz18WUeKhkuZAHhhb2UeOmL9ggQpWnZQ== +"@nrwl/tao@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.3.0.tgz#5cc3753af9f3ff12b40896939e79ab9d73e11a15" + integrity sha512-BxTOOiFRb75YxpZpkJsG3dILBteJ/54etFdWgbbH4S14RxHd7Qa87o+vaJVCuJ1+MB2ygVpNmiZwW+itQYx1ew== dependencies: 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.2.2" + nx "13.3.0" rxjs "^6.5.4" rxjs-for-await "0.0.2" semver "7.3.4" @@ -2814,29 +2760,33 @@ tslib "^2.3.0" yargs-parser "20.0.0" -"@nrwl/workspace@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-13.2.2.tgz#aca7388413e1c853f1250059c807bc4e25907e29" - integrity sha512-DP+dTUee/hJ8mbF8x9gd5hL0dUmnT6j1c2zujneYMhvrFGdC8M5wbyK5V7tuW9tSUzjVnQeATGrCvJuFz6tOxQ== +"@nrwl/workspace@13.3.0": + version "13.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-13.3.0.tgz#c288cba8f7eb70af3b0245384bd8cf58472b86a8" + integrity sha512-0PkYejHwtA8RLScn0A3jVsfDsqob5AOYhBqTkQdgFxYb63UUKV6P3B9xbP3nWE+uqwi/DmoVBQ+Ozu+zgxzu7w== dependencies: - "@nrwl/cli" "13.2.2" - "@nrwl/devkit" "13.2.2" - "@nrwl/jest" "13.2.2" - "@nrwl/linter" "13.2.2" - "@parcel/watcher" "2.0.0-alpha.11" + "@nrwl/cli" "13.3.0" + "@nrwl/devkit" "13.3.0" + "@nrwl/jest" "13.3.0" + "@nrwl/linter" "13.3.0" + "@parcel/watcher" "2.0.4" chalk "4.1.0" chokidar "^3.5.1" cosmiconfig "^4.0.0" dotenv "~10.0.0" enquirer "~2.3.6" + figures "3.2.0" flat "^5.0.2" fs-extra "^9.1.0" glob "7.1.4" ignore "^5.0.4" + ink "^3.2.0" + ink-spinner "^4.0.3" minimatch "3.0.4" npm-run-all "^4.1.5" npm-run-path "^4.0.1" open "^7.4.2" + react "17.0.2" rxjs "^6.5.4" semver "7.3.4" strip-ansi "6.0.0" @@ -2854,13 +2804,13 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@parcel/watcher@2.0.0-alpha.11": - version "2.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.0-alpha.11.tgz#8d6233d4416880810438cd2628e6a35273241ab3" - integrity sha512-zMIAsFLcnB82kkk0kSOZ/zgyihb8sty0zVrsz+3ruoYXkchymWsCDsxiX4v+X2s8Jppk3JE8vlnD4DKs3QTOEQ== +"@parcel/watcher@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" + integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== dependencies: - node-addon-api "^3.0.2" - node-gyp-build "^4.2.3" + node-addon-api "^3.2.1" + node-gyp-build "^4.3.0" "@peculiar/asn1-android@^2.0.38": version "2.0.38" @@ -2990,21 +2940,21 @@ resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.8.0.tgz#87d950fbbf050dce1566f32ca48c98007239472b" integrity sha512-WazFGUMnbumg8ePNvej8cIOEcxvuZ0ugKQkkE1xFbDYcl7DgJd62MiG+bIqCcQlIdLEfhjAdoixxlFdJgrgjyA== -"@storybook/addon-actions@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.0-rc.3.tgz#66086cd7bfd84a79d3a7dba374a684466fe88b81" - integrity sha512-9OEMnXEEMOYqKUci5DLwsvQje7GFKvEwK6pYpXTtzkXlqnon634tKJslijqN8GrrGlqaPUMXMkrPfHZeJim9XA== +"@storybook/addon-actions@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.9.tgz#1d4e8c00ad304efe6722043dac759f4716b515ee" + integrity sha512-L1N66p/vr+wPUBfrH3qffjNAcWSS/wvuL370T7cWxALA9LLA8yY9U2EpITc5btuCC5QOxApCeyHkFnrBhNa94g== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/theming" "6.4.9" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" polished "^4.0.5" prop-types "^15.7.2" react-inspector "^5.1.0" @@ -3014,18 +2964,18 @@ util-deprecate "^1.0.2" uuid-browser "^3.1.0" -"@storybook/addon-backgrounds@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.0-rc.3.tgz#568d33d7d30b49cea3996a2a52eed228805920e6" - integrity sha512-jWT9FfRMGxiX0VArWGzukxudi5WuM7YuiRpO7BbsAcu3gdifjAPq8nu/cz3JF0yt5GgDegxnWl8K3UfM3XHXeg== +"@storybook/addon-backgrounds@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.9.tgz#89033aed6f01d6a2dc134cbdb1ce0c46afd130ec" + integrity sha512-/jqUZvk+x8TpDedyFnJamSYC91w/e8prj42xtgLG4+yBlb0UmewX7BAq9i/lhowhUjuLKaOX9E8E0AHftg8L6A== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/theming" "6.4.9" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" @@ -3033,28 +2983,28 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-controls@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.0-rc.3.tgz#3481ca9cdb05203193b5088444b58d587db415ba" - integrity sha512-vd5eqi04F+RzNKLx92TdczLvA+DGuvCt2prY7CN5zoKEi4aBrYgjiA/uTTuVfhbaV3HAkt4F/l9d9oCSZPZxJg== +"@storybook/addon-controls@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.9.tgz#286a184336a80981fdd805f44a68f60fb6e38e73" + integrity sha512-2eqtiYugCAOw8MCv0HOfjaZRQ4lHydMYoKIFy/QOv6/mjcJeG9dF01dA30n3miErQ18BaVyAB5+7rrmuqMwXVA== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-common" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/store" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/node-logger" "6.4.9" + "@storybook/store" "6.4.9" + "@storybook/theming" "6.4.9" core-js "^3.8.2" - lodash "^4.17.20" + lodash "^4.17.21" ts-dedent "^2.0.0" -"@storybook/addon-docs@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.0-rc.3.tgz#f067d0dcbd87e422c7249450a8b4ac74c223461b" - integrity sha512-IrDPSY0F08xkRTz1j30xlzuyvp7gNHR1i8/xK+RcdXDcyh34LI++htds0x9/E3I8//4hCozE9LUbi+YHj6fS4g== +"@storybook/addon-docs@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.9.tgz#dc34c6152085043a771623b2de344bc9d91f0563" + integrity sha512-sJvnbp6Z+e7B1+vDE8gZVhCg1eNotIa7bx9LYd1Y2QwJ4PEv9hE2YxnzmWt3NZJGtrn4gdGaMCk7pmksugHi7g== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -3065,21 +3015,21 @@ "@mdx-js/loader" "^1.6.22" "@mdx-js/mdx" "^1.6.22" "@mdx-js/react" "^1.6.22" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/builder-webpack4" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/builder-webpack4" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/postinstall" "6.4.0-rc.3" - "@storybook/preview-web" "6.4.0-rc.3" - "@storybook/source-loader" "6.4.0-rc.3" - "@storybook/store" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/csf-tools" "6.4.9" + "@storybook/node-logger" "6.4.9" + "@storybook/postinstall" "6.4.9" + "@storybook/preview-web" "6.4.9" + "@storybook/source-loader" "6.4.9" + "@storybook/store" "6.4.9" + "@storybook/theming" "6.4.9" acorn "^7.4.1" acorn-jsx "^5.3.1" acorn-walk "^7.2.0" @@ -3091,7 +3041,7 @@ html-tags "^3.1.0" js-string-escape "^1.0.1" loader-utils "^2.0.0" - lodash "^4.17.20" + lodash "^4.17.21" nanoid "^3.1.23" p-limit "^3.1.0" prettier "^2.2.1" @@ -3103,116 +3053,116 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-essentials@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.0-rc.3.tgz#b4f4dc3013fea0335d6e110eaa7f0d34bb89d7e0" - integrity sha512-pSCKGYal85M3dIAHfeiHHqzv/9j3d0aqWLdrOmhHWU57EznSRKX72XpG9+ryeySj+l3icLe/yybpaod3BGIYVg== - dependencies: - "@storybook/addon-actions" "6.4.0-rc.3" - "@storybook/addon-backgrounds" "6.4.0-rc.3" - "@storybook/addon-controls" "6.4.0-rc.3" - "@storybook/addon-docs" "6.4.0-rc.3" - "@storybook/addon-measure" "6.4.0-rc.3" - "@storybook/addon-outline" "6.4.0-rc.3" - "@storybook/addon-toolbars" "6.4.0-rc.3" - "@storybook/addon-viewport" "6.4.0-rc.3" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" +"@storybook/addon-essentials@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.9.tgz#e761a61a9ac9809b8a5d8b6f7c5a1b50f0e2cd91" + integrity sha512-3YOtGJsmS7A4aIaclnEqTgO+fUEX63pHq2CvqIKPGLVPgLmn6MnEhkkV2j30MfAkoe3oynLqFBvkCdYwzwJxNQ== + dependencies: + "@storybook/addon-actions" "6.4.9" + "@storybook/addon-backgrounds" "6.4.9" + "@storybook/addon-controls" "6.4.9" + "@storybook/addon-docs" "6.4.9" + "@storybook/addon-measure" "6.4.9" + "@storybook/addon-outline" "6.4.9" + "@storybook/addon-toolbars" "6.4.9" + "@storybook/addon-viewport" "6.4.9" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/node-logger" "6.4.9" core-js "^3.8.2" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-measure@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.0-rc.3.tgz#322575bc71132b45eeb312e7a4285ceb339cb616" - integrity sha512-z2llb2U/rE7n1eGsgiXjPlOyi3MBii/LTEZDLSXlSSXH7yPXwc+LN76vO2OvF7G/WeVXEpo/6hqpzqmCQX/+0Q== +"@storybook/addon-measure@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.9.tgz#d4446e0b0686f4f25bbd7eee8c4cf296d8bea216" + integrity sha512-c7r98kZM0i7ZrNf0BZe/12BwTYGDLUnmyNcLhugquvezkm32R1SaqXF8K1bGkWkSuzBvt49lAXXPPGUh+ByWEQ== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" -"@storybook/addon-outline@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.0-rc.3.tgz#738f0afdee3457a92be33aa5a65c1651ace19836" - integrity sha512-jWc1B6zkCRkgcOH5ATn6hvox35j0pXOtUTgbZ4kcWM2AwU1jwE04AHhSSBwjr5XzU1OricKJTvNQERoZWdLXqg== +"@storybook/addon-outline@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.9.tgz#0f6b20eb41580686cca4b9f12937932dd5f51c64" + integrity sha512-pXXfqisYfdoxyJuSogNBOUiqIugv0sZGYDJXuwEgEDZ27bZD6fCQmsK3mqSmRzAfXwDqTKvWuu2SRbEk/cRRGA== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-toolbars@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.0-rc.3.tgz#c71cd133d4880ea3d7c07a0b56faa9e2ce1b4264" - integrity sha512-w88guC25B9aH0lgBVppGKuEy3okHRpvJI3OcqSm/DTPMPoTeyTneqkAgv2O1bWPS67JLIg9C03a3Qu4V8ilwKA== +"@storybook/addon-toolbars@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.9.tgz#147534d0b185a1782f3381a47c627b4a4193297d" + integrity sha512-fep1lLDcyaQJdR8rC/lJTamiiJ8Ilio580d9aXDM651b7uHqhxM0dJvM9hObBU8dOj/R3hIAszgTvdTzYlL2cQ== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/theming" "6.4.9" core-js "^3.8.2" regenerator-runtime "^0.13.7" -"@storybook/addon-viewport@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.0-rc.3.tgz#103a060442319f1a624f6b89b70fc5e8a2ac1f11" - integrity sha512-9ceDt3bO9hLcXhFHWDTVDBxPscxpzqLCJkxyiedEHykpscsYHmll9WUx3swFcqm7n1GS7Z2p6QushSiWCgSsUA== - dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" +"@storybook/addon-viewport@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.9.tgz#73753ff62043d3d6e6d845590ed70caf775af960" + integrity sha512-iqDcfbOG3TClybDEIi+hOKq8PDKNldyAiqBeak4AfGp+lIZ4NvhHgS5RCNylMVKpOUMbGIeWiSFxQ/oglEN1zA== + dependencies: + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/theming" "6.4.9" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.0-rc.3.tgz#7459f5d508dc9f6ace40dc5e258211a293d02d1a" - integrity sha512-Rak+MzRsT9boCK5U1VUBoQnaKStyN/EAec+Sq6Hw1zRvo9hwHPyIh0oTnnNVraDfAknd+rKxfjjcyu5VfFZPPA== +"@storybook/addons@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.9.tgz#43b5dabf6781d863fcec0a0b293c236b4d5d4433" + integrity sha512-y+oiN2zd+pbRWwkf6aQj4tPDFn+rQkrv7fiVoMxsYub+kKyZ3CNOuTSJH+A1A+eBL6DmzocChUyO6jvZFuh6Dg== dependencies: - "@storybook/api" "6.4.0-rc.3" - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/api" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/router" "6.4.9" + "@storybook/theming" "6.4.9" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/angular@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.0-rc.3.tgz#9a7069ee0167d11ab62156ab34fceaa346e74c09" - integrity sha512-oLS/CQHgRmO3qjj+DPBJx9LLgJNzrtlr1styUt9ajevrNeKttszL0YtmNrlvPZO/E7QqWkRmDZ3dfJvH3+fNwg== +"@storybook/angular@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.9.tgz#966089d911630166838008e6fcae5f4a7fc5855e" + integrity sha512-lwHPHizr5m2wCqSUi8qbz79cOjudGqNWHFzjWIbhucAS28XBDSj8A2yV7b88JUuX2KH6KJP/TpTd9avTgPHeKw== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/core" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/core" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.0-rc.3" + "@storybook/node-logger" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.0-rc.3" + "@storybook/store" "6.4.9" "@types/webpack-env" "^1.16.0" autoprefixer "^9.8.6" core-js "^3.8.2" @@ -3235,22 +3185,22 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/api@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.0-rc.3.tgz#9221a8afa7cf85862437abc78653c7a687cf284e" - integrity sha512-fMmXRkQIWMQys396CkE1CJ9f4znE3gvMLuPk4iehGUzNUV9z1I38ouu/yFbKL6oxwIHIqCe60YlgR6Fz39nsfw== +"@storybook/api@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.9.tgz#6187d08658629580f0a583f2069d55b34964b34a" + integrity sha512-U+YKcDQg8xal9sE5eSMXB9vcqk8fD1pSyewyAjjbsW5hV0B3L3i4u7z/EAD9Ujbnor+Cvxq+XGvp+Qnc5Gd40A== dependencies: - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.0-rc.3" + "@storybook/router" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/theming" "6.4.9" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" memoizerific "^1.11.3" regenerator-runtime "^0.13.7" store2 "^2.12.0" @@ -3258,10 +3208,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.0-rc.3.tgz#1cc293d83bf11914f7109d57a0950365f93527dc" - integrity sha512-Hgp8EA+5oiCIIGnMuBUg8K1V7xOwQXOSNUNzKLUW3fJUTxGTze56zf6vGd3MrC1jRm4QoZD7hXzv/3daJmSn+Q== +"@storybook/builder-webpack4@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.9.tgz#86cd691c856eeb7a6a7bcafa57e9a66c1e0b9906" + integrity sha512-nDbXDd3A8dvalCiuBZuUT6/GQP14+fuxTj5g+AppCgV1gLO45lXWtX75Hc0IbZrIQte6tDg5xeFQamZSLPMcGg== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3284,22 +3234,22 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/channel-postmessage" "6.4.0-rc.3" - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/preview-web" "6.4.0-rc.3" - "@storybook/router" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/channel-postmessage" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/node-logger" "6.4.9" + "@storybook/preview-web" "6.4.9" + "@storybook/router" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" - "@storybook/ui" "6.4.0-rc.3" + "@storybook/store" "6.4.9" + "@storybook/theming" "6.4.9" + "@storybook/ui" "6.4.9" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -3334,10 +3284,10 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.2.2" -"@storybook/builder-webpack5@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.0-rc.3.tgz#dac1719d712852110a065282888f16f08cea0e07" - integrity sha512-6FaQC2VZTYiXY+p+hXe57k1LdFh67tT8MvN7vQRQ6PR9GNPjH8kV8HF37ADF4xhaLBNHaMG4j9xk55bLwDNg9A== +"@storybook/builder-webpack5@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.9.tgz#86dca9679b611d1e513bf3122e4eb05c785dfd4d" + integrity sha512-OB/PJHgsHwvX03smEsIM45oyYhLD8RK2wBIRS0PuGj1XdisOsiMal2a9hUHcqhZIiFstOEXUltMwBzJnixzprg== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3359,21 +3309,21 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/channel-postmessage" "6.4.0-rc.3" - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/preview-web" "6.4.0-rc.3" - "@storybook/router" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/channel-postmessage" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/node-logger" "6.4.9" + "@storybook/preview-web" "6.4.9" + "@storybook/router" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/store" "6.4.9" + "@storybook/theming" "6.4.9" "@types/node" "^14.0.10" babel-loader "^8.0.0" babel-plugin-macros "^3.0.1" @@ -3385,6 +3335,7 @@ glob "^7.1.6" glob-promise "^3.4.0" html-webpack-plugin "^5.0.0" + path-browserify "^1.0.1" react-dev-utils "^11.0.4" stable "^0.1.8" style-loader "^2.0.0" @@ -3396,57 +3347,57 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.4.1" -"@storybook/channel-postmessage@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.0-rc.3.tgz#f2ab1dd8a22da689c834a5f2719f55fef98196fd" - integrity sha512-LVrMuQekoGdJ0R2IX1jLI0NrDq2ibM8PyC7ohYHy2KCC6n+x/3qlFY2fyc9HkKEl7cCb8R9mT95AMNLwi86COA== +"@storybook/channel-postmessage@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.9.tgz#b20b7d66f0f2a8ba39fe9002f3a3dc16d9e1f681" + integrity sha512-0Oif4e6/oORv4oc2tHhIRts9faE/ID9BETn4uqIUWSl2CX1wYpKYDm04rEg3M6WvSzsi+6fzoSFvkr9xC5Ns2w== dependencies: - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-websocket@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.0-rc.3.tgz#d8f7bf03020bebdd9be97660ff4952ed28352dbc" - integrity sha512-etvd/+Tbv8HB/QwyXGOIEOpBLAZohQpYtMreydUXu5kBzgZg+W4czOfj7qMQzecYcNVsaiFxpBI0Qm0AbHdrrA== +"@storybook/channel-websocket@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.9.tgz#f012840894f73bac289ddcdc57efb385c4a0b7ef" + integrity sha512-R1O5yrNtN+dIAghqMXUqoaH7XWBcrKi5miVRn7QelKG3qZwPL8HQa7gIPc/b6S2D6hD3kQdSuv/zTIjjMg7wyw== dependencies: - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" core-js "^3.8.2" global "^4.4.0" telejson "^5.3.2" -"@storybook/channels@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.0-rc.3.tgz#539c7a75747d6e5cabb8351c30d5a228e7b61109" - integrity sha512-m43REhm7OC9XpFH4uQHBJLC1fAzeLrNq8guGEoYw6ARoh5NHNFQlRzs3squxJImj4LXZITAL2m9a2MNQ/o86Uw== +"@storybook/channels@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.9.tgz#132c574d3fb2e6aaa9c52312c592794699b9d8ec" + integrity sha512-DNW1qDg+1WFS2aMdGh658WJXh8xBXliO5KAn0786DKcWCsKjfsPPQg/QCHczHK0+s5SZyzQT5aOBb4kTRHELQA== dependencies: core-js "^3.8.2" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.0-rc.3.tgz#d991f078d803d1de4ba63d0490216c2b9c7fc4a4" - integrity sha512-VuJRalFr8EQzvsEl5U1bdiamgTI62NR0ixph8vvH+FpmuKfgNH5EyotQs+Ilw5BrjWSKhQTn3qe1XDSynxNw7Q== +"@storybook/client-api@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.9.tgz#e3d90c66356d6f53f8ceb4f31753f670f704fde0" + integrity sha512-1IljlTr+ea2pIr6oiPhygORtccOdEb7SqaVzWDfLCHOhUnJ2Ka5UY9ADqDa35jvSSdRdynfk9Yl5/msY0yY1yg== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/channel-postmessage" "6.4.0-rc.3" - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/channel-postmessage" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.0-rc.3" + "@storybook/store" "6.4.9" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" memoizerific "^1.11.3" qs "^6.10.0" regenerator-runtime "^0.13.7" @@ -3455,23 +3406,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.0-rc.3.tgz#5d306920abf80b75f36f5c7ff480ee66379069e9" - integrity sha512-KNhmMmLCHt2STdtWobrF+kDfxczOGKt5m66Ceijs99nzhvrip89GN+lyQASkFCe6efOSP1ZWoV9MlQBgFJw5TQ== +"@storybook/client-logger@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.9.tgz#ef6af30fac861fea69c8917120ed06b4c2f0b54e" + integrity sha512-BVagmmHcuKDZ/XROADfN3tiolaDW2qG0iLmDhyV1gONnbGE6X5Qm19Jt2VYu3LvjKF1zMPSWm4mz7HtgdwKbuQ== dependencies: core-js "^3.8.2" global "^4.4.0" -"@storybook/components@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.0-rc.3.tgz#266264a459666c8a12059edacdd99fd275304d77" - integrity sha512-vgh8Iviyyp2gQ2IIMAxs+p1ZSNj3e6eofXGi/VCb64MmLtcvoGP8+BurI5LKjZ76fIWSQTBFwW9H6FHt/CzcDA== +"@storybook/components@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.9.tgz#caed59eb3f09d1646da748186f718a0e54fb8fd7" + integrity sha512-uOUR97S6kjptkMCh15pYNM1vAqFXtpyneuonmBco5vADJb3ds0n2a8NeVd+myIbhIXn55x0OHKiSwBH/u7swCQ== dependencies: "@popperjs/core" "^2.6.0" - "@storybook/client-logger" "6.4.0-rc.3" + "@storybook/client-logger" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/theming" "6.4.9" "@types/color-convert" "^2.0.0" "@types/overlayscrollbars" "^1.12.0" "@types/react-syntax-highlighter" "11.0.5" @@ -3479,7 +3430,7 @@ core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" markdown-to-jsx "^7.1.3" memoizerific "^1.11.3" overlayscrollbars "^1.13.1" @@ -3493,36 +3444,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.0-rc.3.tgz#3677b63a7e9b07d612e238978be7d3bc3783c42a" - integrity sha512-tj9oI8SndMQu99f2axsLFfoKH26tn6JYe5WO/LNwfRqthG9gTWwq0Uv9/SIFWi6BgXCanGIYq3U1NMu4uZcwxA== - dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/channel-postmessage" "6.4.0-rc.3" - "@storybook/channel-websocket" "6.4.0-rc.3" - "@storybook/client-api" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" +"@storybook/core-client@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.9.tgz#324119a67609f758e244a7d58bac00e62020a21f" + integrity sha512-LZSpTtvBlpcn+Ifh0jQXlm/8wva2zZ2v13yxYIxX6tAwQvmB54U0N4VdGVmtkiszEp7TQUAzA8Pcyp4GWE+UMA== + dependencies: + "@storybook/addons" "6.4.9" + "@storybook/channel-postmessage" "6.4.9" + "@storybook/channel-websocket" "6.4.9" + "@storybook/client-api" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/preview-web" "6.4.0-rc.3" - "@storybook/store" "6.4.0-rc.3" - "@storybook/ui" "6.4.0-rc.3" + "@storybook/preview-web" "6.4.9" + "@storybook/store" "6.4.9" + "@storybook/ui" "6.4.9" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" qs "^6.10.0" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.0-rc.3.tgz#fd0ec8d5c3a6da3668a6bf768dd4fe1c2456e4bd" - integrity sha512-LuhNErorr1jXxW0HObh2u4nISYMxxqAeB/aPRFIJ2oIEUZ2wfaMUSEy3KpN4iDuiFLg8vX9FUHsMYqYj7rIyPA== +"@storybook/core-common@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.9.tgz#1a892903061f927b8f7b9fa8d25273a2f5c9e227" + integrity sha512-wVHRfUGnj/Tv8nGjv128NDQ5Zp6c63rSXd1lYLzfZPTJmGOz4rpPPez2IZSnnDwbAWeqUSMekFVZPj4v6yuujQ== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3545,7 +3496,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.4.0-rc.3" + "@storybook/node-logger" "6.4.9" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/pretty-hrtime" "^1.0.0" @@ -3574,29 +3525,29 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core-events@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.0-rc.3.tgz#5003f5f78e01078c25d59ae6ab398a037d6113db" - integrity sha512-uPG2onvnLANAj8kEOVEgnn/1mBl63jZ6YeUFqhmtFpFfjVUUtUvAHlkbyOeXPNu6cpuxhS848sivj+nR1PD01A== +"@storybook/core-events@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.9.tgz#7febedb8d263fbd6e4a69badbfcdce0101e6f782" + integrity sha512-YhU2zJr6wzvh5naYYuy/0UKNJ/SaXu73sIr0Tx60ur3bL08XkRg7eZ9vBhNBTlAa35oZqI0iiGCh0ljiX7yEVQ== dependencies: core-js "^3.8.2" -"@storybook/core-server@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.0-rc.3.tgz#5baf879eadf040180680a7fdb519efcd774ffc34" - integrity sha512-X3sJSm2ONn2bqAJC3jnRTfp4Z0rpbt9wK71G7WFAy/FCtSjgnsQZZYhGPQf4PelXEqx/jIYah/XouWV2AJItnA== +"@storybook/core-server@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.9.tgz#593fd4cc21a05c908e0eed20767eb6c9cddad428" + integrity sha512-Ht/e17/SNW9BgdvBsnKmqNrlZ6CpHeVsClEUnauMov8I5rxjvKBVmI/UsbJJIy6H6VLiL/RwrA3RvLoAoZE8dA== dependencies: "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-webpack4" "6.4.0-rc.3" - "@storybook/core-client" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/builder-webpack4" "6.4.9" + "@storybook/core-client" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.0-rc.3" - "@storybook/manager-webpack4" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" + "@storybook/csf-tools" "6.4.9" + "@storybook/manager-webpack4" "6.4.9" + "@storybook/node-logger" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.0-rc.3" + "@storybook/store" "6.4.9" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" "@types/pretty-hrtime" "^1.0.0" @@ -3615,7 +3566,7 @@ fs-extra "^9.0.1" globby "^11.0.2" ip "^1.1.5" - lodash "^4.17.20" + lodash "^4.17.21" node-fetch "^2.6.1" pretty-hrtime "^1.0.3" prompts "^2.4.0" @@ -3629,18 +3580,18 @@ webpack "4" ws "^8.2.3" -"@storybook/core@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.0-rc.3.tgz#5511de959c66c0c0a2e39f90efedda1954fc26e5" - integrity sha512-+rRdv4/C3ODPZ4sC7NBRs7/dbu6rwWAj2vc7MH7P537Z6zNqZEE1d1WjfD7hjhI30z6yTXV9rp/LVc1LU9KSqQ== +"@storybook/core@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.9.tgz#4bf910d7322b524f8166c97c28875e1e3775f391" + integrity sha512-Mzhiy13loMSd3PCygK3t7HIT3X3L35iZmbe6+2xVbVmc/3ypCmq4PQALCUoDOGk37Ifrhop6bo6sl4s9YQ6UFw== dependencies: - "@storybook/core-client" "6.4.0-rc.3" - "@storybook/core-server" "6.4.0-rc.3" + "@storybook/core-client" "6.4.9" + "@storybook/core-server" "6.4.9" -"@storybook/csf-tools@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.0-rc.3.tgz#ca3a32d1833c2983c1a0050c05a343b62733c14f" - integrity sha512-sugQXX7NFA9ChfUYoswRpyriUR9+rUJgkhXmNBobZYZ1uzgfBXa3sBub7Ti4xE5ssbaSOqerWRuOe0g+Xz9/aQ== +"@storybook/csf-tools@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.9.tgz#7cccb905875ba5962dda83825f763a111932464b" + integrity sha512-zbgsx9vY5XOA9bBmyw+KyuRspFXAjEJ6I3d/6Z3G1kNBeOEj9i3DT7O99Rd/THfL/3mWl8DJ/7CJVPk1ZYxunA== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -3655,7 +3606,7 @@ fs-extra "^9.0.1" global "^4.4.0" js-string-escape "^1.0.1" - lodash "^4.17.20" + lodash "^4.17.21" prettier "^2.2.1" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" @@ -3667,20 +3618,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.0-rc.3.tgz#b32e7c5aba06d3d1859c43595b1fca7919a878fe" - integrity sha512-g3ANZcodr3Tf4P3YvSo2PdmTtYLBGhzYxTqAhfyZSIyaoG/hbokTyMSJ3IKE/bmQmEBZiBPdTPaWbETLw0XD2Q== +"@storybook/manager-webpack4@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.9.tgz#76edd6f2c627dc64d3362a265c2fe6ae7ee22507" + integrity sha512-828x3rqMuzBNSb13MSDo2nchY7fuywh+8+Vk+fn00MvBYJjogd5RQFx5ocwhHzmwXbnESIerlGwe81AzMck8ng== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/core-client" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" - "@storybook/ui" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/core-client" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/node-logger" "6.4.9" + "@storybook/theming" "6.4.9" + "@storybook/ui" "6.4.9" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.0.0" @@ -3709,20 +3660,20 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/manager-webpack5@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.0-rc.3.tgz#e60a80f4cb8a3339c7d846c833a5845c0d486098" - integrity sha512-3KuyRUfP2ZH24UVcVJ1n5FQXbHlzKlSdPZZA46W6nTLTJTEjkqELX8ir+XLnRk1A0K2PhBim2rLAB2vJW6sUYQ== +"@storybook/manager-webpack5@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.9.tgz#dc553eeeade3de5da8832fed0da94ff2803dc089" + integrity sha512-WJfHs9nPAWx6NONzwoo4s72fqWW/HIBnw+StpRVMNJfi9YojTTFNGMHU0waSd22qT0zOV8XXrXi7XDuHissIwg== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/core-client" "6.4.0-rc.3" - "@storybook/core-common" "6.4.0-rc.3" - "@storybook/node-logger" "6.4.0-rc.3" - "@storybook/theming" "6.4.0-rc.3" - "@storybook/ui" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/core-client" "6.4.9" + "@storybook/core-common" "6.4.9" + "@storybook/node-logger" "6.4.9" + "@storybook/theming" "6.4.9" + "@storybook/ui" "6.4.9" "@types/node" "^14.0.10" babel-loader "^8.0.0" case-sensitive-paths-webpack-plugin "^2.3.0" @@ -3747,10 +3698,10 @@ webpack-dev-middleware "^4.1.0" webpack-virtual-modules "^0.4.1" -"@storybook/node-logger@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.0-rc.3.tgz#f1e46f878fa9bf6499920db153b5ace2212a7b9f" - integrity sha512-IfEFDM5gnJcUSyZPsTZ58Da//4GhwiAessxpP6sGAkUtDYC1uAYuxitLya2YPTOgcob5Fi3CVe2pDbx0o7c28A== +"@storybook/node-logger@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.9.tgz#7c28f16f5c61feda8f45fa2c06000ebb87b57df7" + integrity sha512-giil8dA85poH+nslKHIS9tSxp4MP4ensOec7el6GwKiqzAQXITrm3b7gw61ETj39jAQeLIcQYGHLq1oqQo4/YQ== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.1.0" @@ -3758,28 +3709,28 @@ npmlog "^5.0.1" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.0-rc.3.tgz#d8288d4242517bfe21c46e336ed74d6f79d5995e" - integrity sha512-f0cHglN8ZCipHUWbKrzbO/iZK1qvPdLBIu4Ds52fFKW8jmUZBgvVBUFdOmDZkBkRxgxILJlzERBYtO8RvpaxFQ== +"@storybook/postinstall@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.9.tgz#7b011a2e188bcc54180b16d06f21c9d52a5324ac" + integrity sha512-LNI5ku+Q4DI7DD3Y8liYVgGPasp8r/5gzNLSJZ1ad03OW/mASjhSsOKp2eD8Jxud2T5JDe3/yKH9u/LP6SepBQ== dependencies: core-js "^3.8.2" -"@storybook/preview-web@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.0-rc.3.tgz#f9e29b1a5ff8efe89753b766368c87ef2177db7b" - integrity sha512-lGVx9tWGCK/NlhvpLVNkTJiDMLGzUWPA8NbZR2kk+6+IwUo+r6YC7tCL4ThoP0vPsOe0EGLwFtjFwsEbfz/oww== +"@storybook/preview-web@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.9.tgz#21f7d251af0de64ae796834ead08ae4ed67e6456" + integrity sha512-fMB/akK14oc+4FBkeVJBtZQdxikOraXQSVn6zoVR93WVDR7JVeV+oz8rxjuK3n6ZEWN87iKH946k4jLoZ95tdw== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/channel-postmessage" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/channel-postmessage" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.0-rc.3" + "@storybook/store" "6.4.9" ansi-to-html "^0.6.11" core-js "^3.8.2" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" qs "^6.10.0" regenerator-runtime "^0.13.7" synchronous-promise "^2.0.15" @@ -3787,21 +3738,21 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/router@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.0-rc.3.tgz#5f89c8027e4d31b868220f20d33478695608545d" - integrity sha512-IqRCKWu2iOh3RCk5M2V6IcyOyOicRXDlYbaVi1wJLtDyB/ev2SeqUXeLP60nYxlLk7jhFTQec4XfhJ74FGB/Bg== +"@storybook/router@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.9.tgz#7cc3f85494f4e14d38925e2802145df69a071201" + integrity sha512-GT2KtVHo/mBjxDBFB5ZtVJVf8vC+3p5kRlQC4jao68caVp7H24ikPOkcY54VnQwwe4A1aXpGbJXUyTisEPFlhQ== dependencies: - "@storybook/client-logger" "6.4.0-rc.3" + "@storybook/client-logger" "6.4.9" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" history "5.0.0" - lodash "^4.17.20" + lodash "^4.17.21" memoizerific "^1.11.3" qs "^6.10.0" - react-router "^6.0.0-beta.8" - react-router-dom "^6.0.0-beta.8" + react-router "^6.0.0" + react-router-dom "^6.0.0" ts-dedent "^2.0.0" "@storybook/semver@^7.3.2": @@ -3812,35 +3763,35 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.0-rc.3.tgz#7cb6bbb77e6c0414677feaa5c26261db1c7e39bc" - integrity sha512-4yUC8uzdZHcaia/hxdGJCS8iyP3S0ymaxz/1Xkh8j324Jo2NL9H9ispiqIGRPlAEcPI2E1IyzxSTvOQWeGU1vA== +"@storybook/source-loader@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.9.tgz#918fe93e4bd52622a664398db79d5f71b384ce0b" + integrity sha512-J/Jpcc15hnWa2DB/EZ4gVJvdsY3b3CDIGW/NahuNXk36neS+g4lF3qqVNAEqQ1pPZ0O8gMgazyZPGm0MHwUWlw== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/client-logger" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" estraverse "^5.2.0" global "^4.4.0" loader-utils "^2.0.0" - lodash "^4.17.20" + lodash "^4.17.21" prettier "^2.2.1" regenerator-runtime "^0.13.7" -"@storybook/store@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.0-rc.3.tgz#5d902274756958bbc44bf04394994c2aa3d060a5" - integrity sha512-RZv+bhxMy1LDEwionHlu3lGqb7NlSw3mtD4uW1EWxwWC/+s7skF6n13K4niImTqVRHf2JWdFPF9NNvUx6z1RqA== +"@storybook/store@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.9.tgz#613c6f13271276837c6a603a16199d2abf90153e" + integrity sha512-H30KfiM2XyGMJcLaOepCEUsU7S3C/f7t46s6Nhw0lc5w/6HTQc2jGV3GgG3lUAUAzEQoxmmu61w3N2a6eyRzmg== dependencies: - "@storybook/addons" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/core-events" "6.4.9" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" memoizerific "^1.11.3" regenerator-runtime "^0.13.7" slash "^3.0.0" @@ -3849,15 +3800,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/theming@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.0-rc.3.tgz#45849a855167a85993e0000ef438ec4476e3d02b" - integrity sha512-2r1AFEzD1ml9pXJqZ+Z8I4u9Uc3OpO1M9JWkahAP+CHb2ujHz9WWXvgNs7xtjmU/GVcykGWRuHf4lS1O7JQKgQ== +"@storybook/theming@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.9.tgz#8ece44007500b9a592e71eca693fbeac90803b0d" + integrity sha512-Do6GH6nKjxfnBg6djcIYAjss5FW9SRKASKxLYxX2RyWJBpz0m/8GfcGcRyORy0yFTk6jByA3Hs+WFH3GnEbWkw== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.27" - "@storybook/client-logger" "6.4.0-rc.3" + "@storybook/client-logger" "6.4.9" core-js "^3.8.2" deep-object-diff "^1.1.0" emotion-theming "^10.0.27" @@ -3867,21 +3818,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.4.0-rc.3": - version "6.4.0-rc.3" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.0-rc.3.tgz#5c6972211dbd0ab64453907823e30f1b6a8ec004" - integrity sha512-lM8OaLTesqcYQp/vnokOfcnBoAlvSN1DlrbwQZ4gn/GzNdx8y1NtT0CZXAJrEbwAP0u6/JHZUumGhuh/ss+ORg== +"@storybook/ui@6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.9.tgz#c01413ca919ede20f84d19e556bf93dd2e7c5110" + integrity sha512-lJbsaMTH4SyhqUTmt+msSYI6fKSSfOnrzZVu6bQ73+MfRyGKh1ki2Nyhh+w8BiGEIOz02WlEpZC0y11FfgEgXw== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.4.0-rc.3" - "@storybook/api" "6.4.0-rc.3" - "@storybook/channels" "6.4.0-rc.3" - "@storybook/client-logger" "6.4.0-rc.3" - "@storybook/components" "6.4.0-rc.3" - "@storybook/core-events" "6.4.0-rc.3" - "@storybook/router" "6.4.0-rc.3" + "@storybook/addons" "6.4.9" + "@storybook/api" "6.4.9" + "@storybook/channels" "6.4.9" + "@storybook/client-logger" "6.4.9" + "@storybook/components" "6.4.9" + "@storybook/core-events" "6.4.9" + "@storybook/router" "6.4.9" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.0-rc.3" + "@storybook/theming" "6.4.9" copy-to-clipboard "^3.3.1" core-js "^3.8.2" core-js-pure "^3.8.2" @@ -3889,7 +3840,7 @@ emotion-theming "^10.0.27" fuse.js "^3.6.1" global "^4.4.0" - lodash "^4.17.20" + lodash "^4.17.21" markdown-to-jsx "^7.1.3" memoizerific "^1.11.3" polished "^4.0.5" @@ -4099,7 +4050,7 @@ jest-diff "^27.0.0" pretty-format "^27.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -4385,6 +4336,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yoga-layout@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a" + integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== + "@typescript-eslint/eslint-plugin@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.4.0.tgz#05e711a2e7b68342661fde61bccbd1531c19521a" @@ -4423,15 +4379,15 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/experimental-utils@~4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" - integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== +"@typescript-eslint/experimental-utils@~5.3.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.1.tgz#bbd8f9b67b4d5fdcb9d2f90297d8fcda22561e05" + integrity sha512-RgFn5asjZ5daUhbK5Sp0peq0SSMytqcrkNfU4pnDma2D8P3ElZ6JbYjY8IMSFfZAJ0f3x3tnO3vXHweYg0g59w== dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.3.1" + "@typescript-eslint/types" "5.3.1" + "@typescript-eslint/typescript-estree" "5.3.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -4445,14 +4401,6 @@ "@typescript-eslint/typescript-estree" "5.4.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" - integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== - dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - "@typescript-eslint/scope-manager@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.0.tgz#97d0ccc7c9158e89e202d5e24ce6ba49052d432e" @@ -4461,6 +4409,14 @@ "@typescript-eslint/types" "5.3.0" "@typescript-eslint/visitor-keys" "5.3.0" +"@typescript-eslint/scope-manager@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.1.tgz#3cfbfbcf5488fb2a9a6fbbe97963ee1e8d419269" + integrity sha512-XksFVBgAq0Y9H40BDbuPOTUIp7dn4u8oOuhcgGq7EoDP50eqcafkMVGrypyVGvDYHzjhdUCUwuwVUK4JhkMAMg== + dependencies: + "@typescript-eslint/types" "5.3.1" + "@typescript-eslint/visitor-keys" "5.3.1" + "@typescript-eslint/scope-manager@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz#aaab08415f4a9cf32b870c7750ae8ba4607126a1" @@ -4469,34 +4425,21 @@ "@typescript-eslint/types" "5.4.0" "@typescript-eslint/visitor-keys" "5.4.0" -"@typescript-eslint/types@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" - integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== - "@typescript-eslint/types@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416" integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg== +"@typescript-eslint/types@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.1.tgz#afaa715b69ebfcfde3af8b0403bf27527912f9b7" + integrity sha512-bG7HeBLolxKHtdHG54Uac750eXuQQPpdJfCYuw4ZI3bZ7+GgKClMWM8jExBtp7NSP4m8PmLRM8+lhzkYnSmSxQ== + "@typescript-eslint/types@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.4.0.tgz#b1c130f4b381b77bec19696c6e3366f9781ce8f2" integrity sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA== -"@typescript-eslint/typescript-estree@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" - integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== - dependencies: - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/visitor-keys" "4.33.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf" @@ -4510,6 +4453,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.1.tgz#50cc4bfb93dc31bc75e08ae52e29fcb786d606ec" + integrity sha512-PwFbh/PKDVo/Wct6N3w+E4rLZxUDgsoII/GrWM2A62ETOzJd4M6s0Mu7w4CWsZraTbaC5UQI+dLeyOIFF1PquQ== + dependencies: + "@typescript-eslint/types" "5.3.1" + "@typescript-eslint/visitor-keys" "5.3.1" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/typescript-estree@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz#fe524fb308973c68ebeb7428f3b64499a6ba5fc0" @@ -4523,14 +4479,6 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" - integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== - dependencies: - "@typescript-eslint/types" "4.33.0" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz#a6258790f3b7b2547f70ed8d4a1e0c3499994523" @@ -4539,6 +4487,14 @@ "@typescript-eslint/types" "5.3.0" eslint-visitor-keys "^3.0.0" +"@typescript-eslint/visitor-keys@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.1.tgz#c2860ff22939352db4f3806f34b21d8ad00588ba" + integrity sha512-3cHUzUuVTuNHx0Gjjt5pEHa87+lzyqOiHXy/Gz+SJOCW1mpw9xQHIIEwnKn+Thph1mgWyZ90nboOcSuZr/jTTQ== + dependencies: + "@typescript-eslint/types" "5.3.1" + eslint-visitor-keys "^3.0.0" + "@typescript-eslint/visitor-keys@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz#09bc28efd3621f292fe88c86eef3bf4893364c8c" @@ -4874,7 +4830,7 @@ acorn@^6.0.7, acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.1, acorn@^7.4.0, acorn@^7.4.1: +acorn@^7.1.1, acorn@^7.4.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -4993,7 +4949,7 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1: +ajv@^8.0.0: version "8.6.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== @@ -5427,6 +5383,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +auto-bind@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" + integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== + autoprefixer@^9.6.1, autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" @@ -6467,7 +6428,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-boxes@^2.2.1: +cli-boxes@^2.2.0, cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== @@ -6493,6 +6454,11 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-spinners@^2.3.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" @@ -6516,6 +6482,14 @@ cli-truncate@^0.2.1: slice-ansi "0.0.4" string-width "^1.0.1" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" @@ -6577,6 +6551,13 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-excerpt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-3.0.0.tgz#fcfb6748c03dba8431c19f5474747fad3f250f10" + integrity sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw== + dependencies: + convert-to-spaces "^1.0.1" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -6816,6 +6797,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, dependencies: safe-buffer "~5.1.1" +convert-to-spaces@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715" + integrity sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU= + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -8338,6 +8324,14 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978" + integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-scope@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" @@ -8353,13 +8347,6 @@ eslint-utils@^1.3.1: dependencies: eslint-visitor-keys "^1.1.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - eslint-utils@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" @@ -8367,7 +8354,7 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== @@ -8382,37 +8369,36 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== -eslint@7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +eslint@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815" + integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" + "@eslint/eslintrc" "^1.0.4" + "@humanwhocodes/config-array" "^0.6.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + eslint-scope "^6.0.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.0.0" + espree "^9.0.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" + glob-parent "^6.0.1" globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" @@ -8420,11 +8406,10 @@ eslint@7.32.0: natural-compare "^1.4.0" optionator "^0.9.1" progress "^2.0.0" - regexpp "^3.1.0" + regexpp "^3.2.0" semver "^7.2.1" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -8523,15 +8508,6 @@ espree@^5.0.1: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - espree@^9.0.0, espree@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/espree/-/espree-9.1.0.tgz#ba9d3c9b34eeae205724124e31de4543d59fbf74" @@ -8802,6 +8778,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@3.2.7, fast-glob@^3.1.1, fast-glob@^3.2.5, fast-glob@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -8814,17 +8801,6 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.1.1, fast-glob@^3.2.5, fast-glob@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -8885,6 +8861,13 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== +figures@3.2.0, figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -8900,13 +8883,6 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" @@ -10327,6 +10303,42 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ink-spinner@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/ink-spinner/-/ink-spinner-4.0.3.tgz#0d0f4a787ae1a4270928e063d9c52527cb264feb" + integrity sha512-uJ4nbH00MM9fjTJ5xdw0zzvtXMkeGb0WV6dzSWvFv2/+ks6FIhpkt+Ge/eLdh0Ah6Vjw5pLMyNfoHQpRDRVFbQ== + dependencies: + cli-spinners "^2.3.0" + +ink@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ink/-/ink-3.2.0.tgz#434793630dc57d611c8fe8fffa1db6b56f1a16bb" + integrity sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg== + dependencies: + ansi-escapes "^4.2.1" + auto-bind "4.0.0" + chalk "^4.1.0" + cli-boxes "^2.2.0" + cli-cursor "^3.1.0" + cli-truncate "^2.1.0" + code-excerpt "^3.0.0" + indent-string "^4.0.0" + is-ci "^2.0.0" + lodash "^4.17.20" + patch-console "^1.0.0" + react-devtools-core "^4.19.1" + react-reconciler "^0.26.2" + scheduler "^0.20.2" + signal-exit "^3.0.2" + slice-ansi "^3.0.0" + stack-utils "^2.0.2" + string-width "^4.2.2" + type-fest "^0.12.0" + widest-line "^3.1.0" + wrap-ansi "^6.2.0" + ws "^7.5.5" + yoga-layout-prebuilt "^1.9.6" + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -12107,11 +12119,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -12172,11 +12179,6 @@ lodash.set@4.3.2: resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - lodash.uniq@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -12874,7 +12876,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-addon-api@^3.0.0, node-addon-api@^3.0.2: +node-addon-api@^3.0.0, node-addon-api@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== @@ -12894,7 +12896,7 @@ node-gyp-build@^4.2.2: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== -node-gyp-build@^4.2.3: +node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== @@ -13141,19 +13143,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@12.9.0: - version "12.9.0" - resolved "https://registry.yarnpkg.com/nx/-/nx-12.9.0.tgz#8553ce806fd19181ad8f81395f7812ab9fe5d4e2" - integrity sha512-AOyMJPpioeMtY1UJ2Zgxyjfsc6rg31uztqiCZIQEOLwXoYIYiPuz54IhTngW7c1MjtxDl8B62G8xCjlRv2zjhw== - dependencies: - "@nrwl/cli" "*" - -nx@13.2.2: - version "13.2.2" - resolved "https://registry.yarnpkg.com/nx/-/nx-13.2.2.tgz#ffeeaeeb255e061cd0c462541c446a462fa2419b" - integrity sha512-d13Xb3pswItWS9sP4+ed4JarkTzo6+gCBNWSgxuO6MMkmKZPXH3yM3an9KlA5jCyFygcsny7CIgwwxJhz1SJCg== +nx@13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/nx/-/nx-13.3.0.tgz#c153ca5c1f3669e809ab8179d03fc9a127c90595" + integrity sha512-S5nmkbMsV1fBhwpSoYDWZ0XLzt7BAj03bdcpfYMFBjXCNIpHd9gcmA9wrqJfVQrP7GsNBBc6nvuQHllxqDEgIQ== dependencies: - "@nrwl/cli" "*" + "@nrwl/cli" "13.3.0" oauth-sign@~0.9.0: version "0.9.0" @@ -13728,11 +13723,21 @@ passport@0.4.1: passport-strategy "1.x.x" pause "0.0.1" +patch-console@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-1.0.0.tgz#19b9f028713feb8a3c023702a8cc8cb9f7466f9d" + integrity sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA== + path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -14726,6 +14731,14 @@ react-dev-utils@^11.0.4: strip-ansi "6.0.0" text-table "0.2.0" +react-devtools-core@^4.19.1: + version "4.21.0" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.21.0.tgz#a54c9a0fd7261491e616d6c87d1869e011d8521d" + integrity sha512-clGWwJHV5MHwTwYyKc+7FZHwzdbzrD2/AoZSkicUcr6YLc3Za9a9FaLhccWDHfjQ+ron9yzNhDT6Tv+FiPkD3g== + dependencies: + shell-quote "^1.6.1" + ws "^7" + react-dom@16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" @@ -14810,18 +14823,27 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-router-dom@^6.0.0-beta.8: - version "6.0.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.0.2.tgz#860cefa697b9d4965eced3f91e82cdbc5995f3ad" - integrity sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA== +react-reconciler@^0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" + integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-router-dom@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.1.0.tgz#94e650ee43b0cdeb80049dea72223402a27c7f61" + integrity sha512-ONVhgCiEHTPZvsz05AUiqKj9mrlkDyxt96FB51JrZH7Jlau2B8GJ+NfWw7rbu+oj9gfpZOsK14xJppLAWg2ifQ== dependencies: history "^5.1.0" - react-router "6.0.2" + react-router "6.1.0" -react-router@6.0.2, react-router@^6.0.0-beta.8: - version "6.0.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.0.2.tgz#bd2b0fa84fd1d152671e9f654d9c0b1f5a7c86da" - integrity sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q== +react-router@6.1.0, react-router@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.1.0.tgz#a34a2dbba7f6167d64a4c209009f28b635ea8477" + integrity sha512-96x4YGONBBz41ikne/I9Zw4jZm+KeFyUZraiNkSA384g/B8ps8nRNAfN8339EpZRt+t16RM+TA9z+NqlYATGzw== dependencies: history "^5.1.0" @@ -14864,6 +14886,14 @@ react@16.14.0: object-assign "^4.1.1" prop-types "^15.6.2" +react@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -15053,7 +15083,7 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpp@^3.1.0, regexpp@^3.2.0: +regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -15532,6 +15562,14 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -15829,10 +15867,10 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== dependencies: ansi-styles "^4.0.0" astral-regex "^2.0.0" @@ -16091,6 +16129,13 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-utils@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + stack-utils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" @@ -16506,18 +16551,6 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" -table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== - dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" - tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -16845,7 +16878,7 @@ ts-loader@^9.2.6: micromatch "^4.0.0" semver "^7.3.4" -ts-node@9.1.1, ts-node@^9.1.1: +ts-node@9.1.1, ts-node@^9.1.1, ts-node@~9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== @@ -16963,6 +16996,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" + integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -17919,6 +17957,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@^7, ws@^7.5.5: + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + ws@^7.4.6: version "7.5.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.4.tgz#56bfa20b167427e138a7795de68d134fe92e21f9" @@ -18089,6 +18132,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoga-layout-prebuilt@^1.9.6: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz#2936fbaf4b3628ee0b3e3b1df44936d6c146faa6" + integrity sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g== + dependencies: + "@types/yoga-layout" "1.9.2" + zone.js@0.11.4: version "0.11.4" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.4.tgz#0f70dcf6aba80f698af5735cbb257969396e8025" From ebee851b2349f6796748f0949afd4f9140ff4512 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Dec 2021 11:45:13 +0100 Subject: [PATCH 008/337] Feature/support data gathering by symbol and date (#532) * Support data gathering by symbol and date * Update changelog --- CHANGELOG.md | 4 ++ apps/api/src/app/admin/admin.controller.ts | 46 +++++++++++++++++-- .../src/services/data-gathering.service.ts | 40 +++++++++++++++- .../yahoo-finance/yahoo-finance.service.ts | 6 ++- .../admin-market-data-detail.component.html | 23 ++++++---- .../admin-market-data-detail.component.ts | 23 ++++++++-- .../interfaces/interfaces.ts | 5 +- .../market-data-detail-dialog.component.ts | 22 ++++++++- .../market-data-detail-dialog.html | 27 +++++++++-- .../market-data-detail-dialog.module.ts | 2 + .../market-data-detail-dialog.scss | 16 +++++++ .../admin-market-data/admin-market-data.html | 2 + apps/client/src/app/services/admin.service.ts | 17 +++++-- 13 files changed, 200 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3261c74e8..e3c984066 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 + +- Extended the data gathering by symbol endpoint with an optional date + ### Changed - Upgraded `Nx` from version `13.2.2` to `13.3.0` diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 1de70e588..2c2130da3 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -21,7 +21,8 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { DataSource } from '@prisma/client'; +import { DataSource, MarketData } from '@prisma/client'; +import { isDate, isValid } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AdminService } from './admin.service'; @@ -73,6 +74,26 @@ export class AdminController { return; } + @Post('gather/profile-data') + @UseGuards(AuthGuard('jwt')) + public async gatherProfileData(): Promise { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + this.dataGatheringService.gatherProfileData(); + + return; + } + @Post('gather/:dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async gatherSymbol( @@ -96,9 +117,13 @@ export class AdminController { return; } - @Post('gather/profile-data') + @Post('gather/:dataSource/:symbol/:dateString') @UseGuards(AuthGuard('jwt')) - public async gatherProfileData(): Promise { + public async gatherSymbolForDate( + @Param('dataSource') dataSource: DataSource, + @Param('dateString') dateString: string, + @Param('symbol') symbol: string + ): Promise { if ( !hasPermission( this.request.user.permissions, @@ -111,9 +136,20 @@ export class AdminController { ); } - this.dataGatheringService.gatherProfileData(); + const date = new Date(dateString); - return; + if (!isDate(date)) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + + return this.dataGatheringService.gatherSymbolForDate({ + dataSource, + date, + symbol + }); } @Get('market-data') diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 48ce87e02..203a496d6 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, MarketData } from '@prisma/client'; import { differenceInHours, format, @@ -181,6 +181,44 @@ export class DataGatheringService { } } + public async gatherSymbolForDate({ + dataSource, + date, + symbol + }: { + dataSource: DataSource; + date: Date; + symbol: string; + }) { + try { + const historicalData = await this.dataProviderService.getHistoricalRaw( + [{ dataSource, symbol }], + date, + date + ); + + const marketPrice = + historicalData[symbol][format(date, DATE_FORMAT)].marketPrice; + + if (marketPrice) { + return await this.prismaService.marketData.upsert({ + create: { + dataSource, + date, + marketPrice, + symbol + }, + update: { marketPrice }, + where: { date_symbol: { date, symbol } } + }); + } + } catch (error) { + Logger.error(error); + } finally { + return undefined; + } + } + public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) { Logger.log('Profile data gathering has been started.'); console.time('data-gathering-profile'); 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 4079b437b..ec4640d77 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 @@ -8,7 +8,7 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import * as bent from 'bent'; import Big from 'big.js'; import { countries } from 'countries-list'; -import { format } from 'date-fns'; +import { addDays, format, isSameDay } from 'date-fns'; import * as yahooFinance from 'yahoo-finance'; import { @@ -135,6 +135,10 @@ export class YahooFinanceService implements DataProviderInterface { return {}; } + if (isSameDay(from, to)) { + to = addDays(to, 1); + } + const yahooFinanceSymbols = aSymbols.map((symbol) => { return this.convertToYahooFinanceSymbol(symbol); }); 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 2c67da932..6aab2161b 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 @@ -5,20 +5,25 @@
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 83f4d65a3..3c6db767a 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 @@ -8,7 +8,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; -import { MarketData } from '@prisma/client'; +import { DataSource, MarketData } from '@prisma/client'; import { format, isBefore, isValid, parse } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -22,7 +22,9 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data- templateUrl: './admin-market-data-detail.component.html' }) export class AdminMarketDataDetailComponent implements OnChanges, OnInit { + @Input() dataSource: DataSource; @Input() marketData: MarketData[]; + @Input() symbol: string; public days = Array(31); public defaultDateFormat = DEFAULT_DATE_FORMAT; @@ -53,7 +55,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { this.marketDataByMonth[key] = {}; } - this.marketDataByMonth[key][currentDay] = { + this.marketDataByMonth[key][ + currentDay < 10 ? `0${currentDay}` : currentDay + ] = { ...marketDataItem, day: currentDay }; @@ -66,12 +70,21 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { return isValid(date) && isBefore(date, new Date()); } - public onOpenMarketDataDetail({ date, marketPrice, symbol }: MarketData) { + public onOpenMarketDataDetail({ + day, + yearMonth + }: { + day: string; + yearMonth: string; + }) { + const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; + const dialogRef = this.dialog.open(MarketDataDetailDialog, { data: { marketPrice, - symbol, - date: format(date, DEFAULT_DATE_FORMAT) + dataSource: this.dataSource, + date: new Date(`${yearMonth}-${day}`), + symbol: this.symbol }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', width: this.deviceType === 'mobile' ? '100vw' : '50rem' diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts index a7defb817..a6f543789 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts @@ -1,5 +1,8 @@ +import { DataSource } from '@prisma/client'; + export interface MarketDataDetailDialogParams { - date: string; + dataSource: DataSource; + date: Date; marketPrice: number; symbol: string; } diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts index 4ad1ebaa3..0ce14aba6 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -1,11 +1,14 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { MarketData } from '@prisma/client'; +import { Subject, takeUntil } from 'rxjs'; import { MarketDataDetailDialogParams } from './interfaces/interfaces'; @@ -20,6 +23,8 @@ export class MarketDataDetailDialog implements OnDestroy { private unsubscribeSubject = new Subject(); public constructor( + private adminService: AdminService, + private changeDetectorRef: ChangeDetectorRef, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: MarketDataDetailDialogParams ) {} @@ -30,6 +35,21 @@ export class MarketDataDetailDialog implements OnDestroy { this.dialogRef.close(); } + public onGatherData() { + this.adminService + .gatherSymbol({ + dataSource: this.data.dataSource, + date: this.data.date, + symbol: this.data.symbol + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((marketData: MarketData) => { + this.data.marketPrice = marketData.marketPrice; + + this.changeDetectorRef.markForCheck(); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html index 65b3578fe..48bb2a7b0 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html @@ -1,15 +1,29 @@

Details for {{ data.symbol }}

-
+
Date - + + + + +
-
- - MarketPrice +
+ + Market Price +
diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts index 9a7235c88..be72b917c 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts @@ -2,6 +2,7 @@ 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 { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -15,6 +16,7 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component'; CommonModule, FormsModule, MatButtonModule, + MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss index ce1c7d599..91922978c 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss @@ -3,5 +3,21 @@ .mat-dialog-content { max-height: unset; + + .mat-form-field-appearance-outline { + ::ng-deep { + .mat-form-field-suffix { + top: -0.3rem; + } + + .mat-form-field-wrapper { + padding-bottom: 0; + } + } + + ion-icon { + font-size: 130%; + } + } } } 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 cadcc7858..15bab021f 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 @@ -47,7 +47,9 @@ diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 53d76af74..7c9f9ff04 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -1,6 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { DataSource } from '@prisma/client'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { DataSource, MarketData } from '@prisma/client'; +import { format } from 'date-fns'; @Injectable({ providedIn: 'root' @@ -18,14 +20,19 @@ export class AdminService { public gatherSymbol({ dataSource, + date, symbol }: { dataSource: DataSource; + date?: Date; symbol: string; }) { - return this.http.post( - `/api/admin/gather/${dataSource}/${symbol}`, - {} - ); + let url = `/api/admin/gather/${dataSource}/${symbol}`; + + if (date) { + url = `${url}/${format(date, DATE_FORMAT)}`; + } + + return this.http.post(url, {}); } } From 11076592d1ebbe12688b0cd0eca25aa8539a29bc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Dec 2021 17:12:24 +0100 Subject: [PATCH 009/337] Do not log coupon code (#533) --- apps/api/src/app/subscription/subscription.controller.ts | 8 ++++++-- apps/api/src/app/subscription/subscription.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 9ffb86f2d..531e798b9 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -70,7 +70,9 @@ export class SubscriptionController { value: JSON.stringify(coupons) }); - Logger.log(`Coupon with code '${couponCode}' has been redeemed`); + Logger.log( + `Subscription for user '${this.request.user.id}' has been created with coupon` + ); res.status(StatusCodes.OK); @@ -82,10 +84,12 @@ export class SubscriptionController { @Get('stripe/callback') public async stripeCallback(@Req() req, @Res() res) { - await this.subscriptionService.createSubscriptionViaStripe( + const userId = await this.subscriptionService.createSubscriptionViaStripe( req.query.checkoutSessionId ); + Logger.log(`Subscription for user '${userId}' has been created via Stripe`); + res.redirect(`${this.configurationService.get('ROOT_URL')}/account`); } diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 2c4a81b95..97e910e14 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -75,8 +75,6 @@ export class SubscriptionService { } } }); - - Logger.log(`Subscription for user '${aUserId}' has been created`); } public async createSubscriptionViaStripe(aCheckoutSessionId: string) { @@ -90,6 +88,8 @@ export class SubscriptionService { await this.stripe.customers.update(session.customer as string, { description: session.client_reference_id }); + + return session.client_reference_id; } catch (error) { Logger.error(error); } From 2aedd74480505922af65f0b842fca69a71d0a6cb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Dec 2021 17:23:29 +0100 Subject: [PATCH 010/337] Release 1.89.0 (#534) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c984066..dce01bc7e 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.89.0 - 11.12.2021 ### Added diff --git a/package.json b/package.json index 8a0576665..168d8c969 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.88.0", + "version": "1.89.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 008a2ab1236124599e5a7931c4b28a8dfcc8d753 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Dec 2021 20:58:09 +0100 Subject: [PATCH 011/337] Support arm64 (#465) * Support arm64 * Move prisma from devDependencies to dependencies (Fix "/bin/sh: prisma: not found" in docker build) * Update changelog Co-authored-by: Valentin Zickner --- CHANGELOG.md | 7 +++++++ Dockerfile | 3 ++- package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce01bc7e..0b962f1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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). +## Unreleasd + +### Fixed + +- Fixed `/bin/sh: prisma: not found` in docker build +- Added `apk` in `Dockerfile` (`python3 g++ make openssl`) + ## 1.89.0 - 11.12.2021 ### Added diff --git a/Dockerfile b/Dockerfile index 995102f2e..075b43396 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,8 @@ COPY ./package.json package.json COPY ./yarn.lock yarn.lock COPY ./prisma/schema.prisma prisma/schema.prisma -RUN yarn +RUN apk add --no-cache python3 g++ make openssl +RUN yarn install # See https://github.com/nrwl/nx/issues/6586 for further details COPY ./decorate-angular-cli.js decorate-angular-cli.js diff --git a/package.json b/package.json index 168d8c969..3dbf64da1 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", + "prisma": "3.6.0", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", @@ -161,7 +162,6 @@ "jest": "27.2.3", "jest-preset-angular": "11.0.0", "prettier": "2.3.2", - "prisma": "3.6.0", "replace-in-file": "6.2.0", "rimraf": "3.0.2", "ts-jest": "27.0.5", From ed7209fb53ca9a54d41f5c849ac3fdfda7029545 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 11 Dec 2021 21:44:54 +0100 Subject: [PATCH 012/337] Feature/remove transactions import limit (#536) * Remove default transactions import limit * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/import/import.service.ts | 14 ++++++++++---- apps/api/src/services/configuration.service.ts | 1 + .../services/interfaces/environment.interface.ts | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b962f1f3..09c8e6e48 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 ## Unreleasd +### Changed + +- Removed the default transactions import limit + ### Fixed - Fixed `/bin/sh: prisma: not found` in docker build diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 59df401bd..cf9f6f705 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1,4 +1,5 @@ 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'; @@ -6,9 +7,8 @@ import { isSameDay, parseISO } from 'date-fns'; @Injectable() export class ImportService { - private static MAX_ORDERS_TO_IMPORT = 20; - public constructor( + private readonly configurationService: ConfigurationService, private readonly dataProviderService: DataProviderService, private readonly orderService: OrderService ) {} @@ -59,8 +59,14 @@ export class ImportService { orders: Partial[]; userId: string; }) { - if (orders?.length > ImportService.MAX_ORDERS_TO_IMPORT) { - throw new Error('Too many transactions'); + if ( + orders?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT') + ) { + throw new Error( + `Too many transactions (${this.configurationService.get( + 'MAX_ORDERS_TO_IMPORT' + )} at most)` + ); } const existingOrders = await this.orderService.orders({ diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 1956a6b62..b2d9c65fb 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -27,6 +27,7 @@ export class ConfigurationService { GOOGLE_SECRET: str({ default: 'dummySecret' }), JWT_SECRET_KEY: str({}), MAX_ITEM_IN_CACHE: num({ default: 9999 }), + MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), PORT: port({ default: 3333 }), RAKUTEN_RAPID_API_KEY: str({ default: '' }), REDIS_HOST: str({ default: 'localhost' }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index e475d32ec..56f8fe822 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -18,6 +18,7 @@ export interface Environment extends CleanedEnvAccessors { GOOGLE_SECRET: string; JWT_SECRET_KEY: string; MAX_ITEM_IN_CACHE: number; + MAX_ORDERS_TO_IMPORT: number; PORT: number; RAKUTEN_RAPID_API_KEY: string; REDIS_HOST: string; From e255b76053f0629151eb508b04b3cc2d147b22ff Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Dec 2021 19:45:23 +0100 Subject: [PATCH 013/337] Feature/check currency in import validation (#538) * Check currency in import validation (avoid importing transactions in wrong currency) * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/import/import.service.ts | 6 ++++++ test/import/invalid-currency.csv | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 test/import/invalid-currency.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c8e6e48..da8dc913b 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 ## Unreleasd +### Added + +- Extended the validation in the import functionality for transactions by checking the currency of the data provider service + ### Changed - Removed the default transactions import limit diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index cf9f6f705..b17404ae9 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -104,6 +104,12 @@ export class ImportService { `orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } + + if (result[symbol].currency !== currency) { + throw new Error( + `orders.${index}.currency ("${currency}") does not match with "${result[symbol].currency}"` + ); + } } } } diff --git a/test/import/invalid-currency.csv b/test/import/invalid-currency.csv new file mode 100644 index 000000000..e04db317b --- /dev/null +++ b/test/import/invalid-currency.csv @@ -0,0 +1,2 @@ +Date,Code,Currency,Price,Quantity,Action,Fee +12/12/2021,BTC,EUR,44558.42,1,buy,0 From 7439c1bf54e89baa9492f2e6991d1b43548248a2 Mon Sep 17 00:00:00 2001 From: Rafael Augusto de Oliveira Date: Mon, 13 Dec 2021 21:25:09 +0000 Subject: [PATCH 014/337] fix(custom-cryptocurrencies): add uniswap (uni3) (#541) --- .../src/services/cryptocurrency/custom-cryptocurrencies.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index 949b455db..2e1d2e824 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -3,5 +3,6 @@ "ALGO": "Algorand", "AVAX": "Avalanche", "MATIC": "Polygon", - "SHIB": "Shiba Inu" + "SHIB": "Shiba Inu", + "UNI3": "Uniswap" } From 83ba5f3d9faecfabbbe5a3629eeba9d58d28669e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Dec 2021 22:26:16 +0100 Subject: [PATCH 015/337] Feature/add intro image for dark mode (#540) * Add intro image for dark mode * Update changelog --- CHANGELOG.md | 1 + .../src/app/pages/landing/landing-page.scss | 7 ++++++- .../src/app/pages/register/register-page.scss | 15 ++++++++++++++- apps/client/src/assets/intro-dark.jpg | Bin 0 -> 155380 bytes 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 apps/client/src/assets/intro-dark.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index da8dc913b..d09ac9beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Removed the default transactions import limit +- Improved the landing page in dark mode ### Fixed diff --git a/apps/client/src/app/pages/landing/landing-page.scss b/apps/client/src/app/pages/landing/landing-page.scss index 02dc332d9..a76dc9aac 100644 --- a/apps/client/src/app/pages/landing/landing-page.scss +++ b/apps/client/src/app/pages/landing/landing-page.scss @@ -14,7 +14,6 @@ } .intro-container { - background-color: #ffffff; margin-top: -5rem; .intro-inner-container { @@ -37,4 +36,10 @@ background-color: var(--dark-background); } } + + .intro-container { + .intro { + background-image: url('/assets/intro-dark.jpg') !important; + } + } } diff --git a/apps/client/src/app/pages/register/register-page.scss b/apps/client/src/app/pages/register/register-page.scss index c67623e11..b3c38380f 100644 --- a/apps/client/src/app/pages/register/register-page.scss +++ b/apps/client/src/app/pages/register/register-page.scss @@ -8,7 +8,6 @@ } .intro-container { - background-color: #ffffff; margin-top: -5rem; .intro-inner-container { @@ -24,3 +23,17 @@ } } } + +:host-context(.is-dark-theme) { + .button-container { + .mat-stroked-button { + background-color: var(--dark-background); + } + } + + .intro-container { + .intro { + background-image: url('/assets/intro-dark.jpg') !important; + } + } +} diff --git a/apps/client/src/assets/intro-dark.jpg b/apps/client/src/assets/intro-dark.jpg new file mode 100644 index 0000000000000000000000000000000000000000..33b0c9f0f9bfacd142337441f7632125d7fe0ac2 GIT binary patch literal 155380 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

d7Lvzr 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ⅅPkcnH^a zA4#Sg`;=`xf2?I#*9RY>1(Im!FuMPs3k^B>jcuyF2{zuWRWMT-ENq1jy<)Y0&gO`^ zv0(Pd``}ZwaOL`w@3>6Oh&lV;bdY)8+~Z97lVhzafmz4+q_Ss|M^J_o#SZR7EB>2? z1-_?N)|Uep%tsAGP^&|t5d_~TJ%o);QsDgME(xsCtASx&@6ja&YxTmmA7Nf4VA^_~ zxu9TjC1-^kGi=P>^x&?L%x?6Q>4V`Pe!O@=UW_#;q~&A%rc0n00S3{<7L%Hy>XcA; zX0ETL1>atF@!Bx%Ng3T1+tkHLE`k9|&9V6y_UDfY9rK`FQ5SK35#P3bCuqRtfXUY? zsmakD#`5%AWR--Ap0N%y7Wl&R!jxc!+A&|AI-AWk`kr(*8;&7IpTQA5hj9#QgaC9@ z?_O%)BY?bN#JT&bTzM28xBDuh+8*&eyt632f&5AOjmjj63|-|azaU-pC=g(nB&IN; z@U(N9FF+U2zDRAs6qUOZ^prV$RdEB))Yh{^FFUKAspKnLw!8R--+Me!+%x)2 zry2F4b{y7H!FO`)WJ;hkt#Qzl%Q_gNDS&rc)Z2YZA=rOdnC5X>$ z2RjiqEvjjCY`d|r7kM4r&!F8_X;A%Ie<8AHIdi;a=-K1Z)C1K+Su0_x^IGr>PAj@E z@pXOjuQHMNXcdB=U3_!xPzC>#6ZEcS$H)+Hn3y9b%lio>u!;)Zp!89+9`9R#UX)Xb z`C{cDawvN^H_av1%j{LcUEp#j*`R78r*e13s?e8hOQ$E(dJGWB`J2w~>acr&50VoB z@U+fXeq@PVS^Z7-$gn1)90I^K3EXF9D2D|oMh~@~X*_K%F}G_AUO2%JfV3_T+eSb0 zyPn*eg=kB@Bh8QF=5o3&4hCiGHM78LKF7x3zYfiHgv7y}#9HZGJwUW|Nq4iny6S_~ z#gf+|Bp_U#bA797BjEJ?wSmb@u~xu#eYb~|1E9tRTvi?HYS%rwv&j6u|Kf;V>_oqH z<8OBK+luVBN#DC>*nA0=Z&Ovy5we~S@@eCvc_TegjEvFTmOpemwLZlUj zX=vkIcR_^B+5Rdux(`D+`>O!*bi@_IgxnB?UHA~3&%Zw?mavfN2lj0;8o+Yp8yBl+ z_NLi_ox^DEr&$MyjOvOe%e3VV(`YAJZ}hZPg`hKJQai7`3g1o5?qj%L6lSs~ zXQk1qbgUa`c{9X$79TjpSWHVS(zu4Z@fQ_5`_!cI$2XhJa>p3$|L1Lh<$r#|vtQa` zIj8tvH;CI|MdH3%am8Izvp*Qs|3eoZkDU9DZ;VOlf{ym}|I5+-*TCq1Klm47e}i6w z*1W?8?$8q!+Ww2MO=|?p4oi%K7{+yzXazmH*};ovvV#J-^4u|(^CDN9TmMK)_`~q? zPt9A3({hr{wESFr@+fiPo>Vt>b53vUU4Q(TiECA2wC zYw78-*Pdp@CCTLKkm_uC~v&QDjbr+EmbpzUS zKPOx6b5}~Gm6DZjqq%}n8yY5G8a~gw=UO=wztwJ?VsP9fCojKjTc)k0F3r05XH9kx zXmrD5*ep%keko-2>|Q8dqVrtprV6ph9jr4S@0%QN$_>c7VbKW{LCWqtUExe1I>YS0 zg>Qtq)jm`FyYr`R=y;)lP(HxQk@(7%P%nkDsGk#DJPA|zD3Wk4Z8rwdtMA8r2MvMm$>k z%%*%wM;$Wz;rZpkWbBWPQe1H?esHB-I0Z>LnT8;vcarWmxb*t3{rx8(ef-73Fx$z3FbzF30fK4&nrQo zErh5`UauEJ#D<5_mB#)l1y|8C1XDR8i<4>2lyH7O6ZO`x%@#(AEdU#)x0JB>Xay4s zlJJqO&j-|MUr1m$x2FP1-IsDm1FT|2&{; z+I6M_T0(A?LX8=Bm9t$+@|YA$;IH5%82ugWJIQA-{zp>!KawWeYuLi6%z^ZiV6*Rf z#ukjuv5)k8FYc*6bQ(b=^>%c239huP1<}U>tQ5n?`_95|-3Ko33oxU}p#$UKw$QylchUz4H>NkI^iADLY zzVgVFz6l^P4)1q?J}$C&U60%Xg2m}pv${^u?JCAyM9yZEZZu6gT&;P562Dk*2UJM z0cc!k)N)sfQdXb$Et(VU`ujc$qu%OW6~(7a@*@{S@1&qx#*dITL6@G>p>r}_82@w8 z|N4ykZ$Bs<-M=P?OWFVaw3~az>0&!%I>IF!ptsI0zx<*6u=A$(cJ#l>Gwtr2ic{9`YbgyjxDmYh1=sn{2WoX+_|0YpaXha!H(t)Alz0&QQ z#v3(X{Ot>viQT!VNRGz3 zg5B;MN3$str4vN<+(K&0P$Df0HNNb~yDkBIPtqmbxRhl{($1@F4K zMvF{pG{v4IPw^zos2tgp+b9WQnfJq!^iExQrN*({y-xxX)-_yQLnOTFB{oav=QMbjd};5-bGqA4 z9wpBPFrCYJkEw3b4~v~UHP0!a;oEF<43_zQ*wgn{kAURBtbr+ptF@7|tw`1K@_7>Q z%JRE|TsL0b6Yxq|VD)w6Sp6NMU-p8!fy$f(+-9LV>y!VjV@m<3!}FoQu5vtr zc|e#P5^s=2)9N5DtW;ae?G_Jf20YA%m2I0`C`vMrsY}%rV{-}9#_VS5On7gvy$y!y zt?;l($_FJvPW(N`yU4p4i=Ty)@f&)QgocYYE$P99APZ4f6LJulp;j`E51XVHLZ*-sPd zx|^0`Fl5Y|oY`_qHNznVTi@1uR4*R*G=JUU-R2X1|gqtw~LLUGHQidX9a45n+rG_wzrvy=B#i3G3_Z z4iFB)iVhMOJ2p&HZDn8pn>R1w6pZ8xvUzJwoCNkwx`pwCO~dHUEZA~hv8_rKdy3Cw zO?-Bb(3hrF*yI#5GOhV-NLI0gNRr+`VY+-s_m(BAfpOyZ?tSY3-8y#3M1*c#f8bEl zseS9c4#54L7?3+)p%}_5FTtg|W2Z9Eo3LKZD$X+gV?sSd8RY{mH1ZJ+445ZT8ys3$ zB4n4i%DFK~g|v#BM_^sA7f=tjrkr3jGxxZEQU&db9sGKeFMhpV%8t;Kci*(eGIg># z+$d@KWNcxRk(KhnUKi>g{^V{SKII;V7y*rYijEaAEx!DmMpyWc6y;xfo5%lNPtnn( zS^2u<9Q+rY|9?LJe|t>tc2!BkRnziHYb}SQ_dbL1SJkML0{L-zsYfqQ{Lbg}KDoZU zvyzy;>K_r(p7)>X08IQO{p%LB4A;r$;rQPSC3XJ#+n~WJ!uYRLzGNWS_LY z<9+d70~d9CE1+ZHj*jzVy!?0lu_CBy{-CeortXe5{FhMlKBmGJkXbj?ch*fS5+2w< zM}x)E7~41g`Q(I7g_pejD54Fi*#zl2yo(G!#t6h+BHIeGIO=x?0|Y8-pT3g@11N_c zVfXmKz@M=~e@m&W9I(a1>oFhH4;kfkB&8Bf726Vyyh4S&+i8t5nLE30VmdfpJx^u9 z6LO$VFS?hl*c5zb#usMiah)t2lLkwy92ltDGd-hKFGOYgh!Z=~MNipcgTV;U;$9Kb zuH-I^_H(R*U$0W$1QSaA;iJK}H{fqfWU5E^Q&Tg-UU|v&AI@{$FAGCt10x{9wh4

=3dTF2`ENh){Skp5tw(eoO9ZGR}K-ixMD};pt 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 S|9koWjllmeBM|ay`hNjD= Date: Mon, 13 Dec 2021 22:31:49 +0100 Subject: [PATCH 016/337] Update changelog (#543) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d09ac9beb..0f34e25a3 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 ### Added - Extended the validation in the import functionality for transactions by checking the currency of the data provider service +- Added support for cryptocurrency _Uniswap_ ### Changed From 76dbf7827990f8b5df6deb913bf77f901230da23 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:45:55 +0100 Subject: [PATCH 017/337] implement docker build on tags (#542) Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Co-authored-by: Valentin Zickner --- .travis.yml | 31 +++++++++++++++++++++++++------ publish-docker-image.sh | 5 +++++ 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100755 publish-docker-image.sh diff --git a/.travis.yml b/.travis.yml index 57b8bdfe4..743e9308d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,28 @@ git: depth: false node_js: - 14 -before_script: - - yarn -script: - - yarn format:check - - yarn test - - yarn build:all + +services: + - docker + +cache: yarn + +if: (type = pull_request) OR (tag IS present) + +jobs: + include: + - stage: download dependencies + if: type = pull_request + script: yarn --frozen-lockfile + - stage: check formatting + if: type = pull_request + script: yarn format:check + - stage: execute tests + if: type = pull_request + script: yarn test + - stage: build + if: type = pull_request + script: yarn build:all + - stage: build and push docker images + if: tag IS present + script: ./publish-docker-image.sh diff --git a/publish-docker-image.sh b/publish-docker-image.sh new file mode 100755 index 000000000..310ffb49b --- /dev/null +++ b/publish-docker-image.sh @@ -0,0 +1,5 @@ +set -xe +echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin + +docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG . +docker push ghostfolio/ghostfolio:$TRAVIS_TAG From f953c6ea644c0680eaee597ab67d1960ffb6e83c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 14 Dec 2021 20:04:17 +0100 Subject: [PATCH 018/337] Release/1.90.0 (#544) * Update stage labels * Release 1.90.0 --- .travis.yml | 10 +++++----- CHANGELOG.md | 3 ++- package.json | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 743e9308d..ef5281c35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,18 +13,18 @@ if: (type = pull_request) OR (tag IS present) jobs: include: - - stage: download dependencies + - stage: Install dependencies if: type = pull_request script: yarn --frozen-lockfile - - stage: check formatting + - stage: Check formatting if: type = pull_request script: yarn format:check - - stage: execute tests + - stage: Execute tests if: type = pull_request script: yarn test - - stage: build + - stage: Build application if: type = pull_request script: yarn build:all - - stage: build and push docker images + - stage: Build and publish docker image if: tag IS present script: ./publish-docker-image.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f34e25a3..77927e734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,13 @@ 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). -## Unreleasd +## 1.90.0 - 14.12.2021 ### Added - Extended the validation in the import functionality for transactions by checking the currency of the data provider service - Added support for cryptocurrency _Uniswap_ +- Set up pipeline for docker build ### Changed diff --git a/package.json b/package.json index 3dbf64da1..d17d9ea97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.89.0", + "version": "1.90.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 0878febded9acde842a1a42094ec99bdc1de6665 Mon Sep 17 00:00:00 2001 From: Rafael Augusto de Oliveira Date: Wed, 15 Dec 2021 20:04:26 +0000 Subject: [PATCH 019/337] build(prisma): fix populating database when attributes have null value (#545) --- prisma/seed.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/seed.js b/prisma/seed.js index a5a789d59..b355a22f0 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -159,12 +159,12 @@ async function main() { { assetClass: 'CASH', assetSubClass: 'CRYPTOCURRENCY', - countries: null, + countries: undefined, currency: 'USD', dataSource: DataSource.YAHOO, id: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', name: 'Bitcoin USD', - sectors: null, + sectors: undefined, symbol: 'BTCUSD' }, { From 9aefe3747ed835c910ea23cf6202ecc4a5e5ac53 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:56:29 +0100 Subject: [PATCH 020/337] Feature/migrate database schema to prisma 3 (#546) * Add migration after upgrade to prisma 3 * Update changelog --- CHANGELOG.md | 6 ++ .../migration.sql | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 prisma/migrations/20211215203002_migrated_schema_to_prisma_3/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 77927e734..c89086e2f 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 + +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.90.0 - 14.12.2021 ### Added diff --git a/prisma/migrations/20211215203002_migrated_schema_to_prisma_3/migration.sql b/prisma/migrations/20211215203002_migrated_schema_to_prisma_3/migration.sql new file mode 100644 index 000000000..f8daaa5ce --- /dev/null +++ b/prisma/migrations/20211215203002_migrated_schema_to_prisma_3/migration.sql @@ -0,0 +1,59 @@ +-- DropForeignKey +ALTER TABLE "Access" DROP CONSTRAINT "Access_granteeUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "Access" DROP CONSTRAINT "Access_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Analytics" DROP CONSTRAINT "Analytics_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "AuthDevice" DROP CONSTRAINT "AuthDevice_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Order" DROP CONSTRAINT "Order_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Settings" DROP CONSTRAINT "Settings_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "Access" ADD CONSTRAINT "Access_granteeUserId_fkey" FOREIGN KEY ("granteeUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Access" ADD CONSTRAINT "Access_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Analytics" ADD CONSTRAINT "Analytics_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuthDevice" ADD CONSTRAINT "AuthDevice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Settings" ADD CONSTRAINT "Settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- RenameIndex +ALTER INDEX "MarketData.date_symbol_unique" RENAME TO "MarketData_date_symbol_key"; + +-- RenameIndex +ALTER INDEX "MarketData.symbol_index" RENAME TO "MarketData_symbol_idx"; + +-- RenameIndex +ALTER INDEX "Platform.url_unique" RENAME TO "Platform_url_key"; + +-- RenameIndex +ALTER INDEX "SymbolProfile.dataSource_symbol_unique" RENAME TO "SymbolProfile_dataSource_symbol_key"; From 155bf67f600652bd19bb9640635676c452883f21 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Dec 2021 18:44:08 +0100 Subject: [PATCH 021/337] Bugfix/fix symbol conversion from yahoo finance with hyphen (#549) * Fix symbol conversion from Yahoo Finance * Update changelog --- CHANGELOG.md | 4 ++++ .../data-provider/yahoo-finance/yahoo-finance.service.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c89086e2f..c83658fc1 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 +### Fixed + +- Fixed the symbol conversion from _Yahoo Finance_ including a hyphen + ### Todo - Apply data migration (`yarn database:migrate`) 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 ec4640d77..8b9afebee 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 @@ -240,7 +240,7 @@ export class YahooFinanceService implements DataProviderInterface { } private convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { - const symbol = aYahooFinanceSymbol.replace('-', ''); + const symbol = aYahooFinanceSymbol.replace('-USD', 'USD'); return symbol.replace('=X', ''); } From eff807dd9aa1fcd5bcf5eef1c5b6c2e1dc9d5c91 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Dec 2021 09:04:52 +0100 Subject: [PATCH 022/337] Add tests for yahoo Finance symbol conversion (#550) * Add tests for yahoo Finance symbol conversion * Refactoring --- .../cryptocurrency/cryptocurrency.service.ts | 2 +- .../yahoo-finance.service.spec.ts | 64 +++++++++++++ .../yahoo-finance/yahoo-finance.service.ts | 95 ++++++++++--------- 3 files changed, 115 insertions(+), 46 deletions(-) create mode 100644 apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts diff --git a/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts b/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts index 1ae645208..d5b9fec5b 100644 --- a/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts +++ b/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts @@ -10,7 +10,7 @@ export class CryptocurrencyService { public constructor() {} - public isCrypto(aSymbol = '') { + public isCryptocurrency(aSymbol = '') { const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3); return this.getCryptocurrencies().includes(cryptocurrencySymbol); } diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts new file mode 100644 index 000000000..414a83dd6 --- /dev/null +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts @@ -0,0 +1,64 @@ +import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; +import { YahooFinanceService } from './yahoo-finance.service'; + +jest.mock( + '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service', + () => { + return { + CryptocurrencyService: jest.fn().mockImplementation(() => { + return { + isCryptocurrency: (symbol: string) => { + switch (symbol) { + case 'BTCUSD': + return true; + case 'DOGEUSD': + return true; + case 'SOLUSD': + return true; + default: + return false; + } + } + }; + }) + }; + } +); + +describe('YahooFinanceService', () => { + let cryptocurrencyService: CryptocurrencyService; + let yahooFinanceService: YahooFinanceService; + + beforeAll(async () => { + cryptocurrencyService = new CryptocurrencyService(); + + yahooFinanceService = new YahooFinanceService(cryptocurrencyService); + }); + + it('convertFromYahooFinanceSymbol', async () => { + expect( + await yahooFinanceService.convertFromYahooFinanceSymbol('BRK-B') + ).toEqual('BRK-B'); + expect( + await yahooFinanceService.convertFromYahooFinanceSymbol('BTC-USD') + ).toEqual('BTCUSD'); + expect( + await yahooFinanceService.convertFromYahooFinanceSymbol('EURUSD=X') + ).toEqual('EURUSD'); + }); + + it('convertToYahooFinanceSymbol', async () => { + expect( + await yahooFinanceService.convertToYahooFinanceSymbol('BTCUSD') + ).toEqual('BTC-USD'); + expect( + await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD') + ).toEqual('DOGE-USD'); + expect( + await yahooFinanceService.convertToYahooFinanceSymbol('SOL1USD') + ).toEqual('SOL1-USD'); + expect( + await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF') + ).toEqual('USDCHF=X'); + }); +}); 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 8b9afebee..c9d5081d8 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,6 +1,6 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { baseCurrency, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; @@ -35,6 +35,47 @@ export class YahooFinanceService implements DataProviderInterface { return true; } + public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { + const symbol = aYahooFinanceSymbol.replace( + new RegExp(`-${baseCurrency}$`), + baseCurrency + ); + return symbol.replace('=X', ''); + } + + /** + * Converts a symbol to a Yahoo Finance symbol + * + * Currency: USDCHF -> USDCHF=X + * Cryptocurrency: BTCUSD -> BTC-USD + * DOGEUSD -> DOGE-USD + * SOL1USD -> SOL1-USD + */ + public convertToYahooFinanceSymbol(aSymbol: string) { + if (aSymbol.includes(baseCurrency) && aSymbol.length >= 6) { + if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) { + return `${aSymbol}=X`; + } else if ( + this.cryptocurrencyService.isCryptocurrency( + aSymbol + .replace(new RegExp(`-${baseCurrency}$`), baseCurrency) + .replace('1', '') + ) + ) { + // Add a dash before the last three characters + // BTCUSD -> BTC-USD + // DOGEUSD -> DOGE-USD + // SOL1USD -> SOL1-USD + return aSymbol.replace( + new RegExp(`-?${baseCurrency}$`), + `-${baseCurrency}` + ); + } + } + + return aSymbol; + } + public async get( aSymbols: string[] ): Promise<{ [symbol: string]: IDataProviderResponse }> { @@ -69,7 +110,7 @@ export class YahooFinanceService implements DataProviderInterface { exchange: this.parseExchange(value.price?.exchangeName), marketState: value.price?.marketState === 'REGULAR' || - this.cryptocurrencyService.isCrypto(symbol) + this.cryptocurrencyService.isCryptocurrency(symbol) ? MarketState.open : MarketState.closed, marketPrice: value.price?.regularMarketPrice || 0, @@ -204,8 +245,10 @@ export class YahooFinanceService implements DataProviderInterface { .filter(({ quoteType, symbol }) => { return ( (quoteType === 'CRYPTOCURRENCY' && - this.cryptocurrencyService.isCrypto( - symbol.replace(new RegExp('-USD$'), 'USD').replace('1', '') + this.cryptocurrencyService.isCryptocurrency( + symbol + .replace(new RegExp(`-${baseCurrency}$`), baseCurrency) + .replace('1', '') )) || quoteType === 'EQUITY' || quoteType === 'ETF' @@ -213,9 +256,9 @@ export class YahooFinanceService implements DataProviderInterface { }) .filter(({ quoteType, symbol }) => { if (quoteType === 'CRYPTOCURRENCY') { - // Only allow cryptocurrencies in USD to avoid having redundancy in the database. - // Trades need to be converted manually before to USD (or a UI converter needs to be developed) - return symbol.includes('USD'); + // Only allow cryptocurrencies in base currency to avoid having redundancy in the database. + // Transactions need to be converted manually to the base currency before + return symbol.includes(baseCurrency); } return true; @@ -239,44 +282,6 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { - const symbol = aYahooFinanceSymbol.replace('-USD', 'USD'); - return symbol.replace('=X', ''); - } - - /** - * Converts a symbol to a Yahoo Finance symbol - * - * Currency: USDCHF -> USDCHF=X - * Cryptocurrency: BTCUSD -> BTC-USD - * DOGEUSD -> DOGE-USD - * SOL1USD -> SOL1-USD - */ - private convertToYahooFinanceSymbol(aSymbol: string) { - if ( - (aSymbol.includes('CHF') || - aSymbol.includes('EUR') || - aSymbol.includes('USD')) && - aSymbol.length >= 6 - ) { - if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) { - return `${aSymbol}=X`; - } else if ( - this.cryptocurrencyService.isCrypto( - aSymbol.replace(new RegExp('-USD$'), 'USD').replace('1', '') - ) - ) { - // Add a dash before the last three characters - // BTCUSD -> BTC-USD - // DOGEUSD -> DOGE-USD - // SOL1USD -> SOL1-USD - return aSymbol.replace(new RegExp('-?USD$'), '-USD'); - } - } - - return aSymbol; - } - private parseAssetClass(aPrice: IYahooFinancePrice): { assetClass: AssetClass; assetSubClass: AssetSubClass; From e518bc37792d738422e58e7ae7dd0baa4d5c3d63 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Dec 2021 09:20:42 +0100 Subject: [PATCH 023/337] Feature/clean up all time high and low from performance endpoint (#551) * Remove isAllTimeHigh / isAllTimeLow * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/portfolio/portfolio.service.ts | 8 ++------ .../src/lib/interfaces/portfolio-performance.interface.ts | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83658fc1..4d1fc56bc 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 +### Changed + +- Removed the redundant all time high and all time low from the performance endpoint + ### Fixed - Fixed the symbol conversion from _Yahoo Finance_ including a hyphen diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0887175f1..b4312ed3e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -693,9 +693,7 @@ export class PortfolioService { currentGrossPerformancePercent: 0, currentNetPerformance: 0, currentNetPerformancePercent: 0, - currentValue: 0, - isAllTimeHigh: false, - isAllTimeLow: false + currentValue: 0 } }; } @@ -728,9 +726,7 @@ export class PortfolioService { currentGrossPerformancePercent, currentNetPerformance, currentNetPerformancePercent, - currentValue, - isAllTimeHigh: true, - isAllTimeLow: false + currentValue } }; } diff --git a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts index bd09c288b..3a2770786 100644 --- a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts @@ -5,6 +5,4 @@ export interface PortfolioPerformance { currentNetPerformance: number; currentNetPerformancePercent: number; currentValue: number; - isAllTimeHigh: boolean; - isAllTimeLow: boolean; } From 0806d0dc922f4967996630ccef44109e52390065 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Dec 2021 11:13:32 +0100 Subject: [PATCH 024/337] Bugfix/fix hidden statistics values (#552) * Fix hidden values (0) * Update changelog --- CHANGELOG.md | 1 + .../src/app/pages/about/about-page.html | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1fc56bc..b23a9d1f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed the symbol conversion from _Yahoo Finance_ including a hyphen +- Fixed hidden values (`0`) in the statistics section on the about page ### Todo diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index a8462f5c1..e9e9482a8 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -108,8 +108,11 @@

-

- {{ statistics?.activeUsers1d ?? '-' }} +

+ {{ statistics?.activeUsers1d || '-' }}

Active Users 
-

+

{{ statistics?.activeUsers7d ?? '-' }}

@@ -128,7 +134,10 @@
-

+

{{ statistics?.activeUsers30d ?? '-' }}

@@ -138,7 +147,7 @@
-

+

{{ statistics?.newUsers30d ?? '-' }}

@@ -148,13 +157,19 @@
-

+

{{ statistics?.gitHubContributors ?? '-' }}

Contributors on GitHub
-

+

{{ statistics?.gitHubStargazers ?? '-' }}

Stars on GitHub
From 8b9379f5ce2ec0967426cb94e1594b1890ab5077 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Dec 2021 11:41:13 +0100 Subject: [PATCH 025/337] Release 1.91.0 (#553) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23a9d1f2..f89dff771 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.91.0 - 18.12.2021 ### Changed diff --git a/package.json b/package.json index d17d9ea97..49ff72b68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.90.0", + "version": "1.91.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From aca0d77e915defb1aba07ab7ce4699a1657ccb3c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Dec 2021 16:43:34 +0100 Subject: [PATCH 026/337] Feature/add line chart to historical data view (#555) * Add line chart * Update changelog --- CHANGELOG.md | 6 ++++++ .../admin-market-data-detail.component.html | 8 +++++++- .../admin-market-data-detail.component.scss | 4 ++++ .../admin-market-data-detail.component.ts | 8 ++++++++ .../admin-market-data-detail.module.ts | 3 ++- .../components/admin-market-data/admin-market-data.html | 5 +---- .../src/app/components/admin-users/admin-users.html | 2 +- 7 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89dff771..bb5418b82 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 a line chart to the historical data view in the admin control panel + ## 1.91.0 - 18.12.2021 ### Changed 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 6aab2161b..23c20178f 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 @@ -1,4 +1,10 @@ -
+
+
{{ itemByMonth.key }}
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 b5dabd463..128c63cca 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 @@ -14,6 +14,10 @@ margin-right: 0.25rem; width: 0.5rem; + &:hover { + opacity: 0.8; + } + &.valid { background-color: var(--danger); } 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 3c6db767a..4ee2b9aea 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 @@ -8,6 +8,7 @@ import { import { MatDialog } from '@angular/material/dialog'; 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 { DeviceDetectorService } from 'ngx-device-detector'; @@ -29,6 +30,7 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { public days = Array(31); public defaultDateFormat = DEFAULT_DATE_FORMAT; public deviceType: string; + public historicalDataItems: LineChartItem[]; public marketDataByMonth: { [yearMonth: string]: { [day: string]: MarketData & { day: number } }; } = {}; @@ -45,6 +47,12 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { public ngOnInit() {} public ngOnChanges() { + this.historicalDataItems = this.marketData.map((marketDataItem) => { + return { + date: format(marketDataItem.date, DATE_FORMAT), + value: marketDataItem.marketPrice + }; + }); this.marketDataByMonth = {}; for (const marketDataItem of this.marketData) { diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts index 9ea09ab51..b51d497bf 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component'; import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/market-data-detail-dialog.module'; @@ -7,7 +8,7 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark @NgModule({ declarations: [AdminMarketDataDetailComponent], exports: [AdminMarketDataDetailComponent], - imports: [CommonModule, GfMarketDataDetailDialogModule], + imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 15bab021f..5e8083e98 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 @@ -4,7 +4,6 @@ - @@ -17,7 +16,6 @@ class="cursor-pointer mat-row" (click)="setCurrentSymbol(item.symbol)" > - - - - + diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html index 174b2e0be..2d5199ff3 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -131,7 +131,7 @@
- + Unit Price (url, {}); } + + public fetchSymbolForDate({ + dataSource, + date, + symbol + }: { + dataSource: DataSource; + date: Date; + symbol: string; + }) { + const url = `/api/symbol/${dataSource}/${symbol}/${format( + date, + DATE_FORMAT + )}`; + + return this.http.get(url); + } + + public putMarketData({ + dataSource, + date, + marketData, + symbol + }: { + dataSource: DataSource; + date: Date; + marketData: UpdateMarketDataDto; + symbol: string; + }) { + const url = `/api/admin/market-data/${dataSource}/${symbol}/${format( + date, + DATE_FORMAT + )}`; + + return this.http.put(url, marketData); + } } From 955302666e8f785951116768f0c1b670836efe78 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:45:28 +0100 Subject: [PATCH 028/337] Bugfix/improve redirection on logout (#558) * Improve redirection on logout * Update changelog --- CHANGELOG.md | 4 ++++ apps/client/src/app/app.component.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 596c07ee6..8e0d3f953 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 - Added a line chart to the historical data view in the admin control panel - Supported the update of historical data in the admin control panel +### Fixed + +- Improved the redirection on logout + ## 1.91.0 - 18.12.2021 ### Changed diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index aeec70e6b..aeb996b9d 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -89,7 +89,7 @@ export class AppComponent implements OnDestroy, OnInit { this.tokenStorageService.signOut(); this.userService.remove(); - window.location.reload(); + this.router.navigate(['/']); } public ngOnDestroy() { From 7772684413169cde32f5593dbf8395489924926a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Dec 2021 15:55:03 +0100 Subject: [PATCH 029/337] Bugfix/fix permission for system status page (#559) * Fix permission * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/pages/about/about-page.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0d3f953..885fe6cd1 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 - Improved the redirection on logout +- Fixed the permission for the system status page ## 1.91.0 - 18.12.2021 diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index e9e9482a8..25d6c2fe7 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -23,7 +23,7 @@ This instance is running Ghostfolio {{ version }} and has been last published on {{ lastPublish }}. - Check the system status at status.ghostfol.io Date: Sun, 19 Dec 2021 16:52:35 +0100 Subject: [PATCH 030/337] Replace OrderType with Type (prisma) (#560) --- .../interfaces/portfolio-order.interface.ts | 5 +-- .../portfolio/portfolio-calculator.spec.ts | 37 +++++++++---------- .../src/app/portfolio/portfolio-calculator.ts | 8 ++-- .../src/app/portfolio/portfolio.service.ts | 15 +++----- apps/api/src/models/order-type.ts | 8 ---- apps/api/src/models/order.ts | 5 +-- .../api/src/services/interfaces/interfaces.ts | 7 ++-- 7 files changed, 34 insertions(+), 51 deletions(-) delete mode 100644 apps/api/src/models/order-type.ts diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 5fd3baf8d..2466e81af 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -1,5 +1,4 @@ -import { OrderType } from '@ghostfolio/api/models/order-type'; -import { DataSource } from '@prisma/client'; +import { DataSource, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; export interface PortfolioOrder { @@ -10,6 +9,6 @@ export interface PortfolioOrder { name: string; quantity: Big; symbol: string; - type: OrderType; + type: TypeOfOrder; unitPrice: Big; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 1e3d6b576..ce8320b16 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1,4 +1,3 @@ -import { OrderType } from '@ghostfolio/api/models/order-type'; import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; @@ -155,7 +154,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('144.38'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -166,7 +165,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('147.99'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -177,7 +176,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('15'), symbol: 'VTI', - type: OrderType.Sell, + type: 'SELL', unitPrice: new Big('151.41'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -248,7 +247,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('144.38'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -259,7 +258,7 @@ describe('PortfolioCalculator', () => { name: 'Something else', quantity: new Big('10'), symbol: 'VTX', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('147.99'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -270,7 +269,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('5'), symbol: 'VTI', - type: OrderType.Sell, + type: 'SELL', unitPrice: new Big('151.41'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -360,7 +359,7 @@ describe('PortfolioCalculator', () => { name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('20'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('197.15'), fee: new Big(0) } @@ -462,7 +461,7 @@ describe('PortfolioCalculator', () => { name: 'Amazon.com, Inc.', quantity: new Big('5'), symbol: 'AMZN', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('2021.99'), fee: new Big(0) } @@ -617,7 +616,7 @@ describe('PortfolioCalculator', () => { name: 'Amazon.com, Inc.', quantity: new Big('5'), symbol: 'AMZN', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('2021.99'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -628,7 +627,7 @@ describe('PortfolioCalculator', () => { name: 'Amazon.com, Inc.', quantity: new Big('5'), symbol: 'AMZN', - type: OrderType.Sell, + type: 'SELL', unitPrice: new Big('2412.23'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2391,7 +2390,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [ name: 'Tesla, Inc.', quantity: new Big('50'), symbol: 'TSLA', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('42.97'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2402,7 +2401,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [ name: 'Bitcoin USD', quantity: new Big('0.5614682'), symbol: 'BTCUSD', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('3562.089535970158'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2413,7 +2412,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [ name: 'Amazon.com, Inc.', quantity: new Big('5'), symbol: 'AMZN', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('2021.99'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2427,7 +2426,7 @@ const ordersVTI: PortfolioOrder[] = [ name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('144.38'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2438,7 +2437,7 @@ const ordersVTI: PortfolioOrder[] = [ name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('147.99'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2449,7 +2448,7 @@ const ordersVTI: PortfolioOrder[] = [ name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('15'), symbol: 'VTI', - type: OrderType.Sell, + type: 'SELL', unitPrice: new Big('151.41'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2460,7 +2459,7 @@ const ordersVTI: PortfolioOrder[] = [ name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('177.69'), currency: 'USD', dataSource: DataSource.YAHOO, @@ -2471,7 +2470,7 @@ const ordersVTI: PortfolioOrder[] = [ name: 'Vanguard Total Stock Market Index Fund ETF Shares', quantity: new Big('10'), symbol: 'VTI', - type: OrderType.Buy, + type: 'BUY', unitPrice: new Big('203.15'), currency: 'USD', dataSource: DataSource.YAHOO, diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 0a41bcb02..d16ef4b53 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,9 +1,9 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; -import { OrderType } from '@ghostfolio/api/models/order-type'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { Logger } from '@nestjs/common'; +import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { addDays, @@ -660,14 +660,14 @@ export class PortfolioCalculator { }; } - private getFactor(type: OrderType) { + private getFactor(type: TypeOfOrder) { let factor: number; switch (type) { - case OrderType.Buy: + case 'BUY': factor = 1; break; - case OrderType.Sell: + case 'SELL': factor = -1; break; default: diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b4312ed3e..fea7c1bb1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -6,7 +6,6 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator'; -import { OrderType } from '@ghostfolio/api/models/order-type'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -21,11 +20,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { - UNKNOWN_KEY, - baseCurrency, - ghostfolioCashSymbol -} from '@ghostfolio/common/config'; +import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, @@ -413,7 +408,7 @@ export class PortfolioService { name: order.SymbolProfile?.name, quantity: new Big(order.quantity), symbol: order.symbol, - type: order.type, + type: order.type, unitPrice: new Big(order.unitPrice) })); @@ -850,8 +845,8 @@ export class PortfolioService { const fees = this.getFees(orders); const firstOrderDate = orders[0]?.date; - const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY); - const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL); + const totalBuy = this.getTotalByType(orders, currency, 'BUY'); + const totalSell = this.getTotalByType(orders, currency, 'SELL'); const committedFunds = new Big(totalBuy).sub(totalSell); @@ -990,7 +985,7 @@ export class PortfolioService { name: order.SymbolProfile?.name, quantity: new Big(order.quantity), symbol: order.symbol, - type: order.type, + type: order.type, unitPrice: new Big( this.exchangeRateDataService.toCurrency( order.unitPrice, diff --git a/apps/api/src/models/order-type.ts b/apps/api/src/models/order-type.ts deleted file mode 100644 index 4d7a425c3..000000000 --- a/apps/api/src/models/order-type.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum OrderType { - CorporateAction = 'CORPORATE_ACTION', - Bonus = 'BONUS', - Buy = 'BUY', - Dividend = 'DIVIDEND', - Sell = 'SELL', - Split = 'SPLIT' -} diff --git a/apps/api/src/models/order.ts b/apps/api/src/models/order.ts index 48928bc66..8882ebe37 100644 --- a/apps/api/src/models/order.ts +++ b/apps/api/src/models/order.ts @@ -1,8 +1,7 @@ -import { Account, SymbolProfile } from '@prisma/client'; +import { Account, SymbolProfile, Type as TypeOfOrder } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; import { IOrder } from '../services/interfaces/interfaces'; -import { OrderType } from './order-type'; export class Order { private account: Account; @@ -15,7 +14,7 @@ export class Order { private symbol: string; private symbolProfile: SymbolProfile; private total: number; - private type: OrderType; + private type: TypeOfOrder; private unitPrice: number; public constructor(data: IOrder) { diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index d4913a83b..c7d3a08f7 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -3,11 +3,10 @@ import { AssetClass, AssetSubClass, DataSource, - SymbolProfile + SymbolProfile, + Type as TypeOfOrder } from '@prisma/client'; -import { OrderType } from '../../models/order-type'; - export const MarketState = { closed: 'closed', delayed: 'delayed', @@ -24,7 +23,7 @@ export interface IOrder { quantity: number; symbol: string; symbolProfile: SymbolProfile; - type: OrderType; + type: TypeOfOrder; unitPrice: number; } From 4bf4c1a8a3a15472f872791269da457a60cf93d8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Dec 2021 16:57:22 +0100 Subject: [PATCH 031/337] Release 1.92.0 (#561) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 885fe6cd1..6cb229496 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.92.0 - 19.12.2021 ### Added diff --git a/package.json b/package.json index 49ff72b68..96bc577d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.91.0", + "version": "1.92.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1602f976f0432eecab82788d7af2ccf12272ff28 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:03:12 +0100 Subject: [PATCH 032/337] Feature/convert errors to warnings in portfolio calculator (#562) * Convert errors to warnings * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/portfolio/portfolio-calculator.ts | 8 +++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb229496..cec1875f6 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 + +- Converted errors to warnings in portfolio calculator + ## 1.92.0 - 19.12.2021 ### Added diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index d16ef4b53..f69349fa6 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -238,9 +238,7 @@ export class PortfolioCalculator { if (!marketSymbolMap[nextDate]?.[item.symbol]) { invalidSymbols.push(item.symbol); hasErrors = true; - Logger.error( - `Missing value for symbol ${item.symbol} at ${nextDate}` - ); + Logger.warn(`Missing value for symbol ${item.symbol} at ${nextDate}`); continue; } let lastInvestment: Big = new Big(0); @@ -271,7 +269,7 @@ export class PortfolioCalculator { if (!initialValue) { invalidSymbols.push(item.symbol); hasErrors = true; - Logger.error( + Logger.warn( `Missing value for symbol ${item.symbol} at ${currentDate}` ); continue; @@ -515,7 +513,7 @@ export class PortfolioCalculator { currentPosition.netPerformancePercentage.mul(currentInitialValue) ); } else if (!currentPosition.quantity.eq(0)) { - Logger.error( + Logger.warn( `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` ); hasErrors = true; From 9f545e3e2b918b9fe667406132c51ffe41ef6604 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:24:58 +0100 Subject: [PATCH 033/337] Feature/add cryptocurrency solana (#563) * Add support for Solana and clean up symbol conversion (SOL1USD) * Update changelog --- CHANGELOG.md | 4 ++++ .../services/cryptocurrency/custom-cryptocurrencies.json | 1 + .../yahoo-finance/yahoo-finance.service.spec.ts | 5 ----- .../data-provider/yahoo-finance/yahoo-finance.service.ts | 9 ++------- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec1875f6..82c20733c 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 cryptocurrency _Solana_ (`SOL-USD`) + ### Fixed - Converted errors to warnings in portfolio calculator diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index 2e1d2e824..80774fb77 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -4,5 +4,6 @@ "AVAX": "Avalanche", "MATIC": "Polygon", "SHIB": "Shiba Inu", + "SOL": "Solana", "UNI3": "Uniswap" } diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts index c07be06bf..648eb6037 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts @@ -14,8 +14,6 @@ jest.mock( return true; case 'DOGEUSD': return true; - case 'SOLUSD': - return true; default: return false; } @@ -55,9 +53,6 @@ describe('YahooFinanceService', () => { expect( await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD') ).toEqual('DOGE-USD'); - expect( - await yahooFinanceService.convertToYahooFinanceSymbol('SOL1USD') - ).toEqual('SOL1-USD'); expect( await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF') ).toEqual('USDCHF=X'); 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 8d375a10e..0717a699e 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 @@ -49,7 +49,6 @@ export class YahooFinanceService implements DataProviderInterface { * Currency: USDCHF -> USDCHF=X * Cryptocurrency: BTCUSD -> BTC-USD * DOGEUSD -> DOGE-USD - * SOL1USD -> SOL1-USD */ public convertToYahooFinanceSymbol(aSymbol: string) { if (aSymbol.includes(baseCurrency) && aSymbol.length >= 6) { @@ -57,9 +56,7 @@ export class YahooFinanceService implements DataProviderInterface { return `${aSymbol}=X`; } else if ( this.cryptocurrencyService.isCryptocurrency( - aSymbol - .replace(new RegExp(`-${baseCurrency}$`), baseCurrency) - .replace('1', '') + aSymbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency) ) ) { // Add a dash before the last three characters @@ -246,9 +243,7 @@ export class YahooFinanceService implements DataProviderInterface { return ( (quoteType === 'CRYPTOCURRENCY' && this.cryptocurrencyService.isCryptocurrency( - symbol - .replace(new RegExp(`-${baseCurrency}$`), baseCurrency) - .replace('1', '') + symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency) )) || quoteType === 'EQUITY' || quoteType === 'ETF' From ed1136999a516ffc3336581d32953921494d953c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:22:58 +0100 Subject: [PATCH 034/337] Feature/extend documentation for self hosting (#565) * Extend documentation for self-hosting * Add tag "latest" to docker image * Update changelog --- CHANGELOG.md | 1 + README.md | 24 +++++++++++++++---- ...ild-local.yml => docker-compose.build.yml} | 20 ++++++++-------- docker/docker-compose.dev.yml | 22 +++++++++++++++++ docker/docker-compose.yml | 18 +++++++------- publish-docker-image.sh | 4 ++-- 6 files changed, 64 insertions(+), 25 deletions(-) rename docker/{docker-compose-build-local.yml => docker-compose.build.yml} (100%) create mode 100644 docker/docker-compose.dev.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 82c20733c..5f4e591e4 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 ### Added - Added support for cryptocurrency _Solana_ (`SOL-USD`) +- Extended the documentation for self-hosting with the [official Ghostfolio Docker image](https://hub.docker.com/r/ghostfolio/ghostfolio) ### Fixed diff --git a/README.md b/README.md index 03f8dc8a2..653deec6a 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,27 @@ The backend is based on [NestJS](https://nestjs.com) using [PostgreSQL](https:// The frontend is built with [Angular](https://angular.io) and uses [Angular Material](https://material.angular.io) with utility classes from [Bootstrap](https://getbootstrap.com). -## Run with Docker +## Run with Docker (self-hosting) ### Prerequisites - [Docker](https://www.docker.com/products/docker-desktop) -### Setup Docker Image +### a. Run environment -Run the following commands to build and start the Docker image: +Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio): ```bash -docker-compose -f docker/docker-compose-build-local.yml build -docker-compose -f docker/docker-compose-build-local.yml up +docker-compose -f docker/docker-compose.yml up +``` + +### b. Build and run environment + +Run the following commands to build and start the Docker images: + +```bash +docker-compose -f docker/docker-compose.build.yml build +docker-compose -f docker/docker-compose.build.yml up ``` ### Setup Database @@ -112,6 +120,12 @@ Open http://localhost:3333 in your browser and accomplish these steps: 1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data 1. Click _Sign out_ and check out the _Live Demo_ +### Finalization + +1. Create a new user via _Get Started_ +1. Assign the role `ADMIN` to this user (directly in the database) +1. Delete the original _Admin_ (directly in the database) + ### Migrate Database With the following command you can keep your database schema in sync after a Ghostfolio version update: diff --git a/docker/docker-compose-build-local.yml b/docker/docker-compose.build.yml similarity index 100% rename from docker/docker-compose-build-local.yml rename to docker/docker-compose.build.yml index 39d6fcf95..1d2496f9b 100644 --- a/docker/docker-compose-build-local.yml +++ b/docker/docker-compose.build.yml @@ -1,5 +1,15 @@ version: '3.7' services: + ghostfolio: + build: ../ + env_file: + - ../.env + environment: + DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer + REDIS_HOST: 'redis' + ports: + - 3333:3333 + postgres: image: postgres:12 env_file: @@ -10,15 +20,5 @@ services: redis: image: 'redis:alpine' - ghostfolio: - build: ../ - env_file: - - ../.env - environment: - REDIS_HOST: 'redis' - DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer - ports: - - 3333:3333 - volumes: postgres: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 000000000..8e5a8ec07 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,22 @@ +version: '3.7' +services: + postgres: + image: postgres:12 + container_name: postgres + restart: unless-stopped + ports: + - 5432:5432 + env_file: + - ../.env + volumes: + - postgres:/var/lib/postgresql/data + + redis: + image: 'redis:alpine' + container_name: redis + restart: unless-stopped + ports: + - 6379:6379 + +volumes: + postgres: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8e5a8ec07..7c260f3cd 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,17 @@ version: '3.7' services: + ghostfolio: + image: ghostfolio/ghostfolio + env_file: + - ../.env + environment: + DATABASE_URL: postgresql://user:password@postgres:5432/ghostfolio-db?sslmode=prefer + REDIS_HOST: 'redis' + ports: + - 3333:3333 + postgres: image: postgres:12 - container_name: postgres - restart: unless-stopped - ports: - - 5432:5432 env_file: - ../.env volumes: @@ -13,10 +19,6 @@ services: redis: image: 'redis:alpine' - container_name: redis - restart: unless-stopped - ports: - - 6379:6379 volumes: postgres: diff --git a/publish-docker-image.sh b/publish-docker-image.sh index 310ffb49b..b554f6714 100755 --- a/publish-docker-image.sh +++ b/publish-docker-image.sh @@ -1,5 +1,5 @@ set -xe echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin -docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG . -docker push ghostfolio/ghostfolio:$TRAVIS_TAG +docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest . +docker push ghostfolio/ghostfolio --all-tags From 127abb8f4ecf3c197c65a925a3b5c7ae0c5a80e9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:24:24 +0100 Subject: [PATCH 035/337] Release 1.93.0 (#566) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4e591e4..5bbf8f58f 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.93.0 - 21.12.2021 ### Added diff --git a/package.json b/package.json index 96bc577d4..effb2e3f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.92.0", + "version": "1.93.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From fb7fb886f668d861d88c060280934aecbd77d1d8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:53:15 +0100 Subject: [PATCH 036/337] Fix anchor link (#567) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 653deec6a..161dc0ff9 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs. -If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the section [Run with Docker](#run-with-docker). +If you prefer to run Ghostfolio on your own infrastructure (self-hosting), please find further instructions in the section [Run with Docker](#run-with-docker-self-hosting). ## Why Ghostfolio? From 1a6840f1f6c0397f1ab9eac6ed45f28c73296010 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:59:01 +0100 Subject: [PATCH 037/337] Fix instruction for database setup (#568) --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 161dc0ff9..0045f699b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,14 @@ Run the following command to start the Docker images from [Docker Hub](https://h docker-compose -f docker/docker-compose.yml up ``` +#### Setup Database + +Run the following command to setup the database once Ghostfolio is running: + +```bash +docker-compose -f docker/docker-compose.yml exec ghostfolio yarn database:setup +``` + ### b. Build and run environment Run the following commands to build and start the Docker images: @@ -104,12 +112,12 @@ docker-compose -f docker/docker-compose.build.yml build docker-compose -f docker/docker-compose.build.yml up ``` -### Setup Database +#### Setup Database Run the following command to setup the database once Ghostfolio is running: ```bash -docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn database:setup +docker-compose -f docker/docker-compose.build.yml exec ghostfolio yarn database:setup ``` ### Fetch Historical Data From c65746d119447d2a4024b76a7ed6ee1164880ec2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:09:00 +0100 Subject: [PATCH 038/337] Simplify instructions for development setup (#570) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 0045f699b..1175981cb 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,7 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat ### Setup 1. Run `yarn install` -1. Run `cd docker` -1. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) -1. Run `cd -` to go back to the project root directory +1. Run `docker-compose -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) 1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data 1. Start server and client (see [_Development_](#Development)) 1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` From ffaaa14dba0e770da1df7a91d16a844c258c51f9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 24 Dec 2021 09:40:24 +0100 Subject: [PATCH 039/337] Feature/increase fear and greed index to 30 days (#571) * Increase Fear & Greed index to 30 days * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/symbol/symbol.service.ts | 10 +++++----- .../src/app/components/home-market/home-market.html | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbf8f58f..c31e65a0e 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 + +- Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 30 days + ## 1.93.0 - 21.12.2021 ### Added diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 2649ef5d0..b120d7ea2 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -8,7 +8,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data.service' import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource } from '@prisma/client'; import { format, subDays } from 'date-fns'; import { LookupItem } from './interfaces/lookup-item.interface'; @@ -36,17 +36,17 @@ export class SymbolService { let historicalData: HistoricalDataItem[]; if (includeHistoricalData) { - const days = 10; + const days = 30; const marketData = await this.marketDataService.getRange({ dateQuery: { gte: subDays(new Date(), days) }, symbols: [dataGatheringItem.symbol] }); - historicalData = marketData.map(({ date, marketPrice }) => { + historicalData = marketData.map(({ date, marketPrice: value }) => { return { - date: date.toISOString(), - value: marketPrice + value, + date: date.toISOString() }; }); } diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index c721d4c78..0ef4b5ef8 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -12,7 +12,7 @@
- Last 10 Days + Last 30 Days
Date: Fri, 24 Dec 2021 17:52:08 +0100 Subject: [PATCH 040/337] write portfolio calculator test case for symbol BALN.SW (refs #554) (#572) --- .../portfolio/portfolio-calculator.spec.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index ce8320b16..20c3503db 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -62,6 +62,19 @@ function mockGetValue(symbol: string, date: Date) { ) .toNumber() }; + case 'BALN.SW': + if (isSameDay(parseDate('2021-11-12'), date)) { + return { marketPrice: 146 }; + } else if (isSameDay(parseDate('2021-11-22'), date)) { + return { marketPrice: 142.9 }; + } else if (isSameDay(parseDate('2021-11-26'), date)) { + return { marketPrice: 139.9 }; + } else if (isSameDay(parseDate('2021-11-30'), date)) { + return { marketPrice: 136.6 }; + } else if (isSameDay(parseDate('2021-12-18'), date)) { + return { marketPrice: 143.9 }; + } + default: return { marketPrice: 0 }; } @@ -1486,6 +1499,126 @@ describe('PortfolioCalculator', () => { }) ); }); + + it('with BALN.SW', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + 'CHF' + ); + + // date,type,ticker,currency,units,price,fee + portfolioCalculator.setTransactionPoints([ + // 12.11.2021,BUY,BALN.SW,CHF,2.00,146.00,1.65 + { + date: '2021-11-12', + items: [ + { + quantity: new Big('2'), + symbol: 'BALN.SW', + investment: new Big('292'), + currency: 'CHF', + dataSource: DataSource.YAHOO, + firstBuyDate: '2021-11-12', + fee: new Big('1.65'), + transactionCount: 1 + } + ] + }, + // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) + // End Value: 142.9 * 2 = 285.8 + // Initial Value: 292 (Investment) + // Cash Flow: 0 + // HWR_n0: (285.8 - 292) / 292 = -0.021232877 + + // 22.11.2021,BUY,BALN.SW,CHF,7.00,142.90,5.75 + { + date: '2021-11-22', + items: [ + { + quantity: new Big('9'), // 7 + 2 + symbol: 'BALN.SW', + investment: new Big('1292.3'), // 142.9 * 7 + 146 * 2 + currency: 'CHF', + dataSource: DataSource.YAHOO, + firstBuyDate: '2021-11-12', + fee: new Big('7.4'), // 1.65 + 5.75 + transactionCount: 2 + } + ] + }, + // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) + // End Value: 139.9 * 9 = 1259.1 + // Initial Value: 285.8 (End Value n0) + // Cash Flow: 1000.3 + // Initial Value + Cash Flow: 285.8 + 1000.3 = 1286.1 + // HWR_n1: (1259.1 - 1286.1) / 1286.1 = -0.020993702 + + // 26.11.2021,BUY,BALN.SW,CHF,3.00,139.90,2.40 + { + date: '2021-11-26', + items: [ + { + quantity: new Big('12'), // 3 + 7 + 2 + symbol: 'BALN.SW', + investment: new Big('1712'), // 139.9 * 3 + 142.9 * 7 + 146 * 2 + currency: 'CHF', + dataSource: DataSource.YAHOO, + firstBuyDate: '2021-11-12', + fee: new Big('9.8'), // 2.40 + 1.65 + 5.75 + transactionCount: 3 + } + ] + }, + // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) + // End Value: 136.6 * 12 = 1639.2 + // Initial Value: 1259.1 (End Value n1) + // Cash Flow: 139.9 * 3 = 419.7 + // Initial Value + Cash Flow: 1259.1 + 419.7 = 1678.8 + // HWR_n2: (1639.2 - 1678.8) / 1678.8 = -0.023588277 + + // 30.11.2021,BUY,BALN.SW,CHF,2.00,136.60,1.55 + { + date: '2021-11-30', + items: [ + { + quantity: new Big('14'), // 2 + 3 + 7 + 2 + symbol: 'BALN.SW', + investment: new Big('1985.2'), // 136.6 * 2 + 139.9 * 3 + 142.9 * 7 + 146 * 2 + currency: 'CHF', + dataSource: DataSource.YAHOO, + firstBuyDate: '2021-11-12', + fee: new Big('11.35'), // 1.55 + 2.40 + 1.65 + 5.75 + transactionCount: 4 + } + ] + } + // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) + // End Value: 143.9 * 14 = 2014.6 + // Initial Value: 1639.2 (End Value n2) + // Cash Flow: 136.6 * 2 = 273.2 + // Initial Value + Cash Flow: 1639.2 + 273.2 = 1912.4 + // HWR_n3: (2014.6 - 1912.4) / 1912.4 = 0.053440703 + ]); + + // HWR_total = 1 - (HWR_n0 + 1) * (HWR_n1 + 1) * (HWR_n2 + 1) * (HWR_n3 + 1) + // HWR_total = 1 - (-0.021232877 + 1) * (-0.020993702 + 1) * (-0.023588277 + 1) * (0.053440703 + 1) = 0.014383561 + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => new Date(Date.UTC(2021, 11, 18)).getTime()); // 2021-12-18 + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2021-11-01') + ); + spy.mockRestore(); + + expect(currentPositions).toBeDefined(); + expect(currentPositions.grossPerformance).toEqual(new Big('29.4')); + expect(currentPositions.netPerformance).toEqual(new Big('18.05')); + expect(currentPositions.grossPerformancePercentage).toEqual( + new Big('-0.01438356164383561644') + ); + }); }); describe('calculate timeline', () => { From 3435b3a3482e57a1e90c7cf60b7cea559a3f1203 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 24 Dec 2021 18:21:27 +0100 Subject: [PATCH 041/337] Feature/make the csv import more flexible (#573) * Make the csv import more flexible * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/services/import-transactions.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c31e65a0e..630f115e4 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 - Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 30 days +- Made the import functionality for transactions by `csv` files more flexible ## 1.93.0 - 21.12.2021 diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index ac1b88f9d..7090a13cc 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -15,8 +15,8 @@ export class ImportTransactionsService { private static CURRENCY_KEYS = ['ccy', 'currency']; private static DATE_KEYS = ['date']; private static FEE_KEYS = ['commission', 'fee']; - private static QUANTITY_KEYS = ['qty', 'quantity', 'shares']; - private static SYMBOL_KEYS = ['code', 'symbol']; + private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units']; + private static SYMBOL_KEYS = ['code', 'symbol', 'ticker']; private static TYPE_KEYS = ['action', 'type']; private static UNIT_PRICE_KEYS = ['price', 'unitprice', 'value']; From 3b9a8fabb5312ba27ea0d455f4ab1d99e96e7cf0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 24 Dec 2021 18:42:30 +0100 Subject: [PATCH 042/337] Clean up (#574) --- .../portfolio/portfolio-calculator.spec.ts | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 20c3503db..4dd91509e 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -22,6 +22,20 @@ function mockGetValue(symbol: string, date: Date) { switch (symbol) { case 'AMZN': return { marketPrice: 2021.99 }; + case 'BALN.SW': + if (isSameDay(parseDate('2021-11-12'), date)) { + return { marketPrice: 146 }; + } else if (isSameDay(parseDate('2021-11-22'), date)) { + return { marketPrice: 142.9 }; + } else if (isSameDay(parseDate('2021-11-26'), date)) { + return { marketPrice: 139.9 }; + } else if (isSameDay(parseDate('2021-11-30'), date)) { + return { marketPrice: 136.6 }; + } else if (isSameDay(parseDate('2021-12-18'), date)) { + return { marketPrice: 143.9 }; + } + + return { marketPrice: 0 }; case 'MFA': if (isSameDay(parseDate('2010-12-31'), date)) { return { marketPrice: 1 }; @@ -45,10 +59,10 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; case 'TSLA': - if (isSameDay(parseDate('2021-07-26'), date)) { - return { marketPrice: 657.62 }; - } else if (isSameDay(parseDate('2021-01-02'), date)) { + if (isSameDay(parseDate('2021-01-02'), date)) { return { marketPrice: 666.66 }; + } else if (isSameDay(parseDate('2021-07-26'), date)) { + return { marketPrice: 657.62 }; } return { marketPrice: 0 }; @@ -62,18 +76,6 @@ function mockGetValue(symbol: string, date: Date) { ) .toNumber() }; - case 'BALN.SW': - if (isSameDay(parseDate('2021-11-12'), date)) { - return { marketPrice: 146 }; - } else if (isSameDay(parseDate('2021-11-22'), date)) { - return { marketPrice: 142.9 }; - } else if (isSameDay(parseDate('2021-11-26'), date)) { - return { marketPrice: 139.9 }; - } else if (isSameDay(parseDate('2021-11-30'), date)) { - return { marketPrice: 136.6 }; - } else if (isSameDay(parseDate('2021-12-18'), date)) { - return { marketPrice: 143.9 }; - } default: return { marketPrice: 0 }; @@ -85,20 +87,10 @@ jest.mock('./current-rate.service', () => { // eslint-disable-next-line @typescript-eslint/naming-convention CurrentRateService: jest.fn().mockImplementation(() => { return { - getValue: ({ - currency, - date, - symbol, - userCurrency - }: GetValueParams) => { + getValue: ({ date, symbol }: GetValueParams) => { return Promise.resolve(mockGetValue(symbol, date)); }, - getValues: ({ - currencies, - dateQuery, - dataGatheringItems, - userCurrency - }: GetValuesParams) => { + getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => { const result = []; if (dateQuery.lt) { for ( From cade2f6a5e232a240aac71171cc4b48b086ea271 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Dec 2021 10:29:56 +0100 Subject: [PATCH 043/337] Feature/upgrade prettier to version 2.5.1 (#575) * Upgrade prettier from version 2.3.2 to 2.5.1 * 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 630f115e4..5d85a369c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 30 days - Made the import functionality for transactions by `csv` files more flexible +- Upgraded `prettier` from version `2.3.2` to `2.5.1` ## 1.93.0 - 21.12.2021 diff --git a/package.json b/package.json index effb2e3f8..32275c818 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "import-sort-style-module": "6.0.0", "jest": "27.2.3", "jest-preset-angular": "11.0.0", - "prettier": "2.3.2", + "prettier": "2.5.1", "replace-in-file": "6.2.0", "rimraf": "3.0.2", "ts-jest": "27.0.5", diff --git a/yarn.lock b/yarn.lock index 0d5194c38..814aabc90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14380,10 +14380,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +prettier@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== prettier@^2.2.1: version "2.4.1" From 4c30212a7291dcf4852c0252b9c3bd097b0b51ff Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:18:46 +0100 Subject: [PATCH 044/337] Feature/improve data gathering (#576) * Eliminate benchmarks to gather * Optimize 7d data gathering * Update changelog --- CHANGELOG.md | 1 + .../src/services/data-gathering.service.ts | 83 +++++++++---------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d85a369c..e8cb34df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 30 days - Made the import functionality for transactions by `csv` files more flexible +- Optimized the 7d data gathering (only consider symbols with incomplete market data) - Upgraded `prettier` from version `2.3.2` to `2.5.1` ## 1.93.0 - 21.12.2021 diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 203a496d6..1ee6ee221 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -1,12 +1,11 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { PROPERTY_LAST_DATA_GATHERING, - PROPERTY_LOCKED_DATA_GATHERING, - ghostfolioFearAndGreedIndexSymbol + PROPERTY_LOCKED_DATA_GATHERING } from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource } from '@prisma/client'; import { differenceInHours, format, @@ -17,7 +16,6 @@ import { subDays } from 'date-fns'; -import { ConfigurationService } from './configuration.service'; import { DataProviderService } from './data-provider/data-provider.service'; import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface'; import { ExchangeRateDataService } from './exchange-rate-data.service'; @@ -29,7 +27,6 @@ export class DataGatheringService { private dataGatheringProgress: number; public constructor( - private readonly configurationService: ConfigurationService, @Inject('DataEnhancers') private readonly dataEnhancers: DataEnhancerInterface[], private readonly dataProviderService: DataProviderService, @@ -448,11 +445,7 @@ export class DataGatheringService { }; }); - return [ - ...this.getBenchmarksToGather(startDate), - ...currencyPairsToGather, - ...symbolProfilesToGather - ]; + return [...currencyPairsToGather, ...symbolProfilesToGather]; } public async reset() { @@ -468,23 +461,27 @@ export class DataGatheringService { }); } - private getBenchmarksToGather(startDate: Date): IDataGatheringItem[] { - const benchmarksToGather: IDataGatheringItem[] = []; - - if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - benchmarksToGather.push({ - dataSource: DataSource.RAKUTEN, - date: startDate, - symbol: ghostfolioFearAndGreedIndexSymbol - }); - } - - return benchmarksToGather; - } - private async getSymbols7D(): Promise { const startDate = subDays(resetHours(new Date()), 7); + // Only consider symbols with incomplete market data for the last + // 7 days + const symbolsToGather = ( + await this.prismaService.marketData.groupBy({ + _count: true, + by: ['symbol'], + where: { + date: { gt: startDate } + } + }) + ) + .filter((group) => { + return group._count < 6; + }) + .map((group) => { + return group.symbol; + }); + const symbolProfilesToGather = ( await this.prismaService.symbolProfile.findMany({ orderBy: [{ symbol: 'asc' }], @@ -494,12 +491,16 @@ export class DataGatheringService { symbol: true } }) - ).map((symbolProfile) => { - return { - ...symbolProfile, - date: startDate - }; - }); + ) + .filter((symbolProfile) => { + return symbolsToGather.includes(symbolProfile.symbol); + }) + .map((symbolProfile) => { + return { + ...symbolProfile, + date: startDate + }; + }); const currencyPairsToGather = this.exchangeRateDataService .getCurrencyPairs() @@ -511,30 +512,22 @@ export class DataGatheringService { }; }); - return [ - ...this.getBenchmarksToGather(startDate), - ...currencyPairsToGather, - ...symbolProfilesToGather - ]; + return [...currencyPairsToGather, ...symbolProfilesToGather]; } private async getSymbolsProfileData(): Promise { - const startDate = subDays(resetHours(new Date()), 7); - const distinctOrders = await this.prismaService.order.findMany({ distinct: ['symbol'], orderBy: [{ symbol: 'asc' }], select: { dataSource: true, symbol: true } }); - return [...this.getBenchmarksToGather(startDate), ...distinctOrders].filter( - (distinctOrder) => { - return ( - distinctOrder.dataSource !== DataSource.GHOSTFOLIO && - distinctOrder.dataSource !== DataSource.RAKUTEN - ); - } - ); + return distinctOrders.filter((distinctOrder) => { + return ( + distinctOrder.dataSource !== DataSource.GHOSTFOLIO && + distinctOrder.dataSource !== DataSource.RAKUTEN + ); + }); } private async isDataGatheringNeeded() { From 8f583709ef9547af3edf8aaa37ab3f22a0dd31d1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:23:07 +0100 Subject: [PATCH 045/337] Feature/add support for cosmos and polkadot (#577) * Add support for cryptocurrencies ATOM and DOT * Update changelog --- CHANGELOG.md | 4 ++++ .../src/services/cryptocurrency/custom-cryptocurrencies.json | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8cb34df9..9669c3d72 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 cryptocurrencies _Cosmos_ (`ATOM-USD`) and _Polkadot_ (`DOT-USD`) + ### Changed - Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 30 days diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index 80774fb77..7664e3106 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -1,7 +1,9 @@ { "1INCH": "1inch", "ALGO": "Algorand", + "ATOM": "Cosmos", "AVAX": "Avalanche", + "DOT": "Polkadot", "MATIC": "Polygon", "SHIB": "Shiba Inu", "SOL": "Solana", From c7f4825499b709b28961de46d8c9b89ef0831bc8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:45:58 +0100 Subject: [PATCH 046/337] Release 1.94.0 (#578) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9669c3d72..77526981d 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.94.0 - 25.12.2021 ### Added diff --git a/package.json b/package.json index 32275c818..b73632f81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.93.0", + "version": "1.94.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From bbc4e64cb43ef2690ff944c45570b89e4b89d16e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Dec 2021 17:08:56 +0100 Subject: [PATCH 047/337] Bugfix/filter currencies with null value (#579) * Filter currencies with null value * Update changelog --- CHANGELOG.md | 6 ++++++ .../services/exchange-rate-data.service.ts | 21 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77526981d..8b406c49f 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 + +- Filtered potential `null` currencies + ## 1.94.0 - 25.12.2021 ### Added diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index e0e0e614f..e83516e27 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -157,7 +157,12 @@ export class ExchangeRateDataService { await this.prismaService.account.findMany({ distinct: ['currency'], orderBy: [{ currency: 'asc' }], - select: { currency: true } + select: { currency: true }, + where: { + currency: { + not: null + } + } }) ).forEach((account) => { currencies.push(account.currency); @@ -167,7 +172,12 @@ export class ExchangeRateDataService { await this.prismaService.settings.findMany({ distinct: ['currency'], orderBy: [{ currency: 'asc' }], - select: { currency: true } + select: { currency: true }, + where: { + currency: { + not: null + } + } }) ).forEach((userSettings) => { currencies.push(userSettings.currency); @@ -177,7 +187,12 @@ export class ExchangeRateDataService { await this.prismaService.symbolProfile.findMany({ distinct: ['currency'], orderBy: [{ currency: 'asc' }], - select: { currency: true } + select: { currency: true }, + where: { + currency: { + not: null + } + } }) ).forEach((symbolProfile) => { currencies.push(symbolProfile.currency); From 0043b44670774eb770054c155de9f5f96d503510 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 09:15:10 +0100 Subject: [PATCH 048/337] Feature/improve data gathering for currencies (#581) * Improve data gathering for currencies, add warning if it fails * Update changelog --- CHANGELOG.md | 5 +++ .../src/services/data-gathering.service.ts | 36 ++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b406c49f..a96f9bb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a warning to the log if the data gathering fails + ### Fixed - Filtered potential `null` currencies +- Improved the 7d data gathering optimization for currencies ## 1.94.0 - 25.12.2021 diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 1ee6ee221..b4bcd3a2a 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -334,16 +334,25 @@ export class DataGatheringService { ?.marketPrice; } - try { - await this.prismaService.marketData.create({ - data: { - dataSource, - symbol, - date: currentDate, - marketPrice: lastMarketPrice - } - }); - } catch {} + if (lastMarketPrice) { + try { + await this.prismaService.marketData.create({ + data: { + dataSource, + symbol, + date: currentDate, + marketPrice: lastMarketPrice + } + }); + } catch {} + } else { + Logger.warn( + `Failed to gather data for symbol ${symbol} at ${format( + currentDate, + DATE_FORMAT + )}.` + ); + } // Count month one up for iteration currentDate = new Date( @@ -492,8 +501,8 @@ export class DataGatheringService { } }) ) - .filter((symbolProfile) => { - return symbolsToGather.includes(symbolProfile.symbol); + .filter(({ symbol }) => { + return symbolsToGather.includes(symbol); }) .map((symbolProfile) => { return { @@ -504,6 +513,9 @@ export class DataGatheringService { const currencyPairsToGather = this.exchangeRateDataService .getCurrencyPairs() + .filter(({ symbol }) => { + return symbolsToGather.includes(symbol); + }) .map(({ dataSource, symbol }) => { return { dataSource, From 9d3610331a52a329f5ed751ff5325cb6e9e8bec3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 10:07:51 +0100 Subject: [PATCH 049/337] Add guard (#582) --- apps/api/src/services/data-gathering.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index b4bcd3a2a..1098f2082 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -242,7 +242,7 @@ export class DataGatheringService { try { currentData[symbol] = await dataEnhancer.enhance({ response, - symbol: symbolMapping[dataEnhancer.getName()] ?? symbol + symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol }); } catch (error) { Logger.error(`Failed to enhance data for symbol ${symbol}`, error); From bb8b1e4f43ccd0c7e2c8c5be86744aeba86f7068 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 10:14:13 +0100 Subject: [PATCH 050/337] Release 1.95.0 (#583) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a96f9bb24..3ba3452fa 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.95.0 - 26.12.2021 ### Added diff --git a/package.json b/package.json index b73632f81..08a31be87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.94.0", + "version": "1.95.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 9725f16c81d7cbe36e9954b81ff7723315bb7371 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 14:33:18 +0100 Subject: [PATCH 051/337] Clean up schema.prisma (#584) --- prisma/schema.prisma | 3 --- 1 file changed, 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bfa575084..aaf332f91 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,3 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - datasource db { provider = "postgresql" url = env("DATABASE_URL") From 7203939c421fc426364ad86a667dadecd459c783 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 17:30:26 +0100 Subject: [PATCH 052/337] Feature/upgrade prisma to version 3.7.0 (#586) * Upgrade prisma from version 3.6.0 to 3.7.0 * Update changelog --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- yarn.lock | 36 ++++++++++++++++++------------------ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba3452fa..297c488d1 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 a warning to the log if the data gathering fails +### Changed + +- Upgraded `prisma` from version `3.6.0` to `3.7.0` + ### Fixed - Filtered potential `null` currencies diff --git a/package.json b/package.json index 08a31be87..a2c04b5f1 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", "@nrwl/angular": "13.3.0", - "@prisma/client": "3.6.0", + "@prisma/client": "3.7.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -105,7 +105,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.6.0", + "prisma": "3.7.0", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 814aabc90..386b38458 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2854,22 +2854,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.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.6.0.tgz#68a60cd4c73a369b11f72e173e86fd6789939293" - integrity sha512-ycSGY9EZGROtje0iCNsgC5Zqi/ttX2sO7BNMYaLsUMiTlf3F69ZPH+08pRo0hrDfkZzyimXYqeXJlaoYDH1w7A== +"@prisma/client@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a" + integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw== dependencies: - "@prisma/engines-version" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" + "@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" -"@prisma/engines-version@3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727": - version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#25aa447776849a774885866b998732b37ec4f4f5" - integrity sha512-vtoO2ys6mSfc8ONTWdcYztKN3GBU1tcKBj0aXObyjzSuGwHFcM/pEA0xF+n1W4/0TAJgfoPX2khNEit6g0jtNA== +"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": + version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90" + integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg== -"@prisma/engines@3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727": - version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#c68ede6aeffa9ef7743a32cfa6daf9172a4e15b3" - integrity sha512-dRClHS7DsTVchDKzeG72OaEyeDskCv91pnZ72Fftn0mp4BkUvX2LvWup65hCNzwwQm5IDd6A88APldKDnMiEMA== +"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": + version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2" + integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -14436,12 +14436,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.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.6.0.tgz#99532abc02e045e58c6133a19771bdeb28cecdbe" - integrity sha512-6SqgHS/5Rq6HtHjsWsTxlj+ySamGyCLBUQfotc2lStOjPv52IQuDVpp58GieNqc9VnfuFyHUvTZw7aQB+G2fvQ== +prisma@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2" + integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg== dependencies: - "@prisma/engines" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" + "@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" prismjs@^1.21.0, prismjs@^1.23.0, prismjs@~1.24.0: version "1.24.1" From ee397c8047b44a4382cb7c9c0bcc8b1bbad78a13 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 20:54:53 +0100 Subject: [PATCH 053/337] Bugfix/fix file type detection for import (#587) * Fix file type detection ("application/vnd.ms-excel" instead of "text/csv") * Update changelog --- CHANGELOG.md | 14 ++++++++++---- .../transactions/transactions-page.component.ts | 7 +++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297c488d1..b51725273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,22 @@ 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 `prisma` from version `3.6.0` to `3.7.0` + +### Fixed + +- Fixed the file type detection in the import functionality for transactions + ## 1.95.0 - 26.12.2021 ### Added - Added a warning to the log if the data gathering fails -### Changed - -- Upgraded `prisma` from version `3.6.0` to `3.7.0` - ### Fixed - Filtered potential `null` currencies diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 1827e156e..1178c8aea 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { const fileContent = readerEvent.target.result as string; try { - if (file.type === 'application/json') { + if (file.name.endsWith('.json')) { const content = JSON.parse(fileContent); if (!isArray(content.orders)) { @@ -203,11 +203,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.handleImportSuccess(); } catch (error) { + console.error(error); this.handleImportError({ error, orders: content.orders }); } return; - } else if (file.type === 'text/csv') { + } else if (file.name.endsWith('.csv')) { try { await this.importTransactionsService.importCsv({ fileContent, @@ -217,6 +218,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.handleImportSuccess(); } catch (error) { + console.error(error); this.handleImportError({ error: { error: { message: error?.error?.message ?? [error?.message] } @@ -230,6 +232,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { throw new Error(); } catch (error) { + console.error(error); this.handleImportError({ error: { error: { message: ['Unexpected format'] } }, orders: [] From 994275e093544ff8bec22b10a0c3928fec91c4dd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Dec 2021 21:58:56 +0100 Subject: [PATCH 054/337] Feature/upgrade angular 3rd party dependencies (#588) * Upgrade angular 3rd party dependencies * ngx-device-detector * ngx-markdown * ngx-stripe * Update changelog --- CHANGELOG.md | 3 +++ package.json | 6 +++--- yarn.lock | 60 ++++++++++++++++++++++++++++++---------------------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b51725273..e36ff4918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Upgraded `ngx-device-detector` from version `2.1.1` to `3.0.0` +- Upgraded `ngx-markdown` from version `12.0.1` to `13.0.0` +- Upgraded `ngx-stripe` from version `12.0.2` to `13.0.0` - Upgraded `prisma` from version `3.6.0` to `3.7.0` ### Fixed diff --git a/package.json b/package.json index a2c04b5f1..3efea3b72 100644 --- a/package.json +++ b/package.json @@ -97,10 +97,10 @@ "http-status-codes": "2.1.4", "ionicons": "5.5.1", "lodash": "4.17.21", - "ngx-device-detector": "2.1.1", - "ngx-markdown": "12.0.1", + "ngx-device-detector": "3.0.0", + "ngx-markdown": "13.0.0", "ngx-skeleton-loader": "2.9.1", - "ngx-stripe": "12.0.2", + "ngx-stripe": "13.0.0", "papaparse": "5.3.1", "passport": "0.4.1", "passport-google-oauth20": "2.0.0", diff --git a/yarn.lock b/yarn.lock index 386b38458..fc787ab83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6688,11 +6688,16 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.0.0, commander@^6.2.1: +commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -7799,7 +7804,7 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-toolkit@^6.5.0: +emoji-toolkit@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1" integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ== @@ -11859,12 +11864,12 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -katex@^0.13.0: - version "0.13.18" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931" - integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA== +katex@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.15.1.tgz#cf4ce2fa1257c3279cc7a7fe0c8d1fab40800893" + integrity sha512-KIk+gizli0gl1XaJlCYS8/donGMbzXYTka6BbH3AgvDJTOwyDY4hJ+YmzJ1F0y/3XzX5B9ED8AqB2Hmn2AZ0uA== dependencies: - commander "^6.0.0" + commander "^8.0.0" kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -12821,24 +12826,24 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== -ngx-device-detector@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ngx-device-detector/-/ngx-device-detector-2.1.1.tgz#a22a9477f382d02edf28786c5609878a57d2834f" - integrity sha512-eTuQLAmc2XRRbxDnO9h1QVV0piSyPjstXT5G8fo1rvXy7Ly3MAiniEM2WvTiN7FjtY/VdhEeuBmu/ErSm5cLJg== +ngx-device-detector@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ngx-device-detector/-/ngx-device-detector-3.0.0.tgz#9c5b1db66e03837d5de0e93fe4a1de93948c9c81" + integrity sha512-mzegvxnNTDkHTxh+UeWnCUgZ91/XDOcN2kj8aCupvA7wNgDc/NZ0L90feKJsc+wES7IWq0/DIIKq2F732WOkfw== dependencies: tslib "^2.0.0" -ngx-markdown@12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-12.0.1.tgz#94345e99176533c17396f93e97ff5dc172d8ebcc" - integrity sha512-vMp9SyqmVQZCX374MiCV4sRR1SIv5m3xR2HZ39b3+6/BGjAb46mb4wRXKdIxYUoPba7NYZ8GAt5moUCyVZcCyA== +ngx-markdown@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-13.0.0.tgz#07c9ef46db6827290fc533c0ee64d3856e964bfd" + integrity sha512-XIFCoqffGUHoc8mpHphVskFBHck6hUBocyGVHNBznk7dzHdy6+Ir08jECDQa6xhsoU4dTDgo9aofjK+yvzGIXw== dependencies: "@types/marked" "^2.0.0" - emoji-toolkit "^6.5.0" - katex "^0.13.0" + emoji-toolkit "^6.6.0" + katex "^0.15.1" marked "^2.0.0" - prismjs "^1.23.0" - tslib "^2.1.0" + prismjs "^1.25.0" + tslib "^2.3.0" ngx-skeleton-loader@2.9.1: version "2.9.1" @@ -12848,12 +12853,12 @@ ngx-skeleton-loader@2.9.1: perf-marks "^1.13.4" tslib "^1.10.0" -ngx-stripe@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/ngx-stripe/-/ngx-stripe-12.0.2.tgz#b250acc2a08dc96dac035fc0a67b4a8cbeca3efb" - integrity sha512-/arfIi996yv3EpzqjYsb20TUdQ9t+GVMNVIx1mdsiWcpiNjL36tO3lG45T0hyiBJNAds87Ag40Fm8PfsuHFCUw== +ngx-stripe@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/ngx-stripe/-/ngx-stripe-13.0.0.tgz#d5ed50590447aa74012de4e75ac9bcdafc68b1c8" + integrity sha512-SImKvoC/mZZrtzh2UUmxFdkqMLKX2y+BtcvMAPdHD4D7miXWEjCTZeXt8h85mcfy7y1NKKwIipH4CSr9eBzZ4w== dependencies: - tslib "^2.1.0" + tslib "^2.3.0" nice-napi@^1.0.2: version "1.0.2" @@ -14443,11 +14448,16 @@ prisma@3.7.0: dependencies: "@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" -prismjs@^1.21.0, prismjs@^1.23.0, prismjs@~1.24.0: +prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== +prismjs@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From db1d474ddfa7faed733295e2c052e23c073063b8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 27 Dec 2021 12:14:41 +0100 Subject: [PATCH 055/337] Feature/more discreet data provider warning (#589) * Upgrade http-status-codes to version 2.2.0 * Make the data provider warning more discreet * Update changelog --- CHANGELOG.md | 2 + .../src/app/portfolio/portfolio.controller.ts | 50 ++++++++----------- .../home-overview/home-overview.component.ts | 4 +- .../home-overview/home-overview.html | 14 ++---- .../portfolio-performance.component.html | 23 ++++++--- .../portfolio-performance.component.scss | 4 ++ .../portfolio-performance.component.ts | 8 ++- .../src/app/core/http-response.interceptor.ts | 23 +-------- apps/client/src/app/services/data.service.ts | 5 +- .../interfaces/portfolio-chart.interface.ts | 1 + package.json | 2 +- yarn.lock | 8 +-- 12 files changed, 65 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36ff4918..ace591773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Made the data provider warning more discreet +- Upgraded `http-status-codes` from version `2.1.4` to `2.2.0` - Upgraded `ngx-device-detector` from version `2.1.1` to `3.0.0` - Upgraded `ngx-markdown` from version `12.0.1` to `13.0.0` - Upgraded `ngx-stripe` from version `12.0.2` to `13.0.0` diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index f4578e070..879b11bb5 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -51,7 +51,7 @@ export class PortfolioController { @Get('investments') @UseGuards(AuthGuard('jwt')) public async findAll( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Res() res: Response ): Promise { if ( @@ -87,7 +87,7 @@ export class PortfolioController { @Get('chart') @UseGuards(AuthGuard('jwt')) public async getChart( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Query('range') range, @Res() res: Response ): Promise { @@ -98,18 +98,14 @@ export class PortfolioController { let chartData = historicalDataContainer.items; - let hasNullValue = false; + let hasError = false; chartData.forEach((chartDataItem) => { if (hasNotDefinedValuesInObject(chartDataItem)) { - hasNullValue = true; + hasError = true; } }); - if (hasNullValue) { - res.status(StatusCodes.ACCEPTED); - } - if ( impersonationId || this.userService.isRestrictedView(this.request.user) @@ -131,6 +127,7 @@ export class PortfolioController { } return res.json({ + hasError, chart: chartData, isAllTimeHigh: historicalDataContainer.isAllTimeHigh, isAllTimeLow: historicalDataContainer.isAllTimeLow @@ -140,7 +137,7 @@ export class PortfolioController { @Get('details') @UseGuards(AuthGuard('jwt')) public async getDetails( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Query('range') range, @Res() res: Response ): Promise { @@ -152,6 +149,8 @@ export class PortfolioController { return res.json({ accounts: {}, holdings: {} }); } + let hasError = false; + const { accounts, holdings, hasErrors } = await this.portfolioService.getDetails( impersonationId, @@ -160,7 +159,7 @@ export class PortfolioController { ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { - res.status(StatusCodes.ACCEPTED); + hasError = true; } if ( @@ -198,43 +197,38 @@ export class PortfolioController { } } - return res.json({ accounts, holdings }); + return res.json({ accounts, hasError, holdings }); } @Get('performance') @UseGuards(AuthGuard('jwt')) public async getPerformance( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Query('range') range, @Res() res: Response - ): Promise { + ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { const performanceInformation = await this.portfolioService.getPerformance( impersonationId, range ); - if (performanceInformation?.hasErrors) { - res.status(StatusCodes.ACCEPTED); - } - - let performance = performanceInformation.performance; if ( impersonationId || this.userService.isRestrictedView(this.request.user) ) { - performance = nullifyValuesInObject(performance, [ - 'currentGrossPerformance', - 'currentValue' - ]); + performanceInformation.performance = nullifyValuesInObject( + performanceInformation.performance, + ['currentGrossPerformance', 'currentValue'] + ); } - return res.json(performance); + return res.json(performanceInformation); } @Get('positions') @UseGuards(AuthGuard('jwt')) public async getPositions( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Query('range') range, @Res() res: Response ): Promise { @@ -243,10 +237,6 @@ export class PortfolioController { range ); - if (result?.hasErrors) { - res.status(StatusCodes.ACCEPTED); - } - if ( impersonationId || this.userService.isRestrictedView(this.request.user) @@ -353,7 +343,7 @@ export class PortfolioController { @Get('position/:symbol') @UseGuards(AuthGuard('jwt')) public async getPosition( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Param('symbol') symbol ): Promise { let position = await this.portfolioService.getPosition( @@ -387,7 +377,7 @@ export class PortfolioController { @Get('report') @UseGuards(AuthGuard('jwt')) public async getReport( - @Headers('impersonation-id') impersonationId, + @Headers('impersonation-id') impersonationId: string, @Res() res: Response ): Promise { if ( 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 afc77f766..393de9fe8 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 @@ -29,6 +29,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { { label: 'Max', value: 'max' } ]; public deviceType: string; + public hasError: boolean; public hasImpersonationId: boolean; public historicalDataItems: LineChartItem[]; public isAllTimeHigh: boolean; @@ -116,7 +117,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { .fetchPortfolioPerformance({ range: this.dateRange }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { - this.performance = response; + this.hasError = response.hasErrors; + this.performance = response.performance; this.isLoadingPerformance = false; this.changeDetectorRef.markForCheck(); 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 efe001161..0cfb29642 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -1,15 +1,5 @@
@@ -37,6 +27,8 @@ -
-
+
+
+ +
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss index 850cce442..398a2981a 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss @@ -1,6 +1,10 @@ :host { display: block; + .status { + font-size: 1.33rem; + } + .value-container { #value { font-variant-numeric: tabular-nums; diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 8c69eea1d..318f6e169 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -19,6 +19,8 @@ import { isNumber } from 'lodash'; }) export class PortfolioPerformanceComponent implements OnChanges, OnInit { @Input() baseCurrency: string; + @Input() deviceType: string; + @Input() hasError: boolean; @Input() isAllTimeHigh: boolean; @Input() isAllTimeLow: boolean; @Input() isLoading: boolean; @@ -44,7 +46,11 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit { this.unit = this.baseCurrency; new CountUp('value', this.performance?.currentValue, { - decimalPlaces: 2, + decimalPlaces: + this.deviceType === 'mobile' && + this.performance?.currentValue >= 100000 + ? 0 + : 2, duration: 1, separator: `'` }).start(); diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 1df29e7ec..9ac221e62 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -4,8 +4,7 @@ import { HttpEvent, HttpHandler, HttpInterceptor, - HttpRequest, - HttpResponse + HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { @@ -43,26 +42,6 @@ export class HttpResponseInterceptor implements HttpInterceptor { ): Observable> { return next.handle(request).pipe( tap((event: HttpEvent) => { - if (event instanceof HttpResponse) { - if (event.status === StatusCodes.ACCEPTED) { - if (!this.snackBarRef) { - this.snackBarRef = this.snackBar.open( - 'Sorry! Our data provider partner is experiencing a mild case of the hiccups ;(', - 'Try again?', - { duration: 6000 } - ); - - this.snackBarRef.afterDismissed().subscribe(() => { - this.snackBarRef = undefined; - }); - - this.snackBarRef.onAction().subscribe(() => { - window.location.reload(); - }); - } - } - } - return event; }), catchError((error: HttpErrorResponse) => { diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 9ba5ca669..586192db5 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -181,7 +181,10 @@ export class DataService { } public fetchPortfolioPerformance(aParams: { [param: string]: any }) { - return this.http.get('/api/portfolio/performance', { + return this.http.get<{ + hasErrors: boolean; + performance: PortfolioPerformance; + }>('/api/portfolio/performance', { params: aParams }); } diff --git a/libs/common/src/lib/interfaces/portfolio-chart.interface.ts b/libs/common/src/lib/interfaces/portfolio-chart.interface.ts index d9946746d..a696fe632 100644 --- a/libs/common/src/lib/interfaces/portfolio-chart.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-chart.interface.ts @@ -1,6 +1,7 @@ import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; export interface PortfolioChart { + hasError: boolean; isAllTimeHigh: boolean; isAllTimeLow: boolean; chart: HistoricalDataItem[]; diff --git a/package.json b/package.json index 3efea3b72..d54a466a2 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "cryptocurrencies": "7.0.0", "date-fns": "2.22.1", "envalid": "7.2.1", - "http-status-codes": "2.1.4", + "http-status-codes": "2.2.0", "ionicons": "5.5.1", "lodash": "4.17.21", "ngx-device-detector": "3.0.0", diff --git a/yarn.lock b/yarn.lock index fc787ab83..2ffd99626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10032,10 +10032,10 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -http-status-codes@2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" - integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== +http-status-codes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be" + integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng== https-browserify@^1.0.0: version "1.0.0" From fa44cee781d71e257b19e23e0dba283e2d898ee4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 27 Dec 2021 21:08:33 +0100 Subject: [PATCH 056/337] Release 1.96.0 (#590) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace591773..2f8258a27 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.96.0 - 27.12.2021 ### Changed diff --git a/package.json b/package.json index d54a466a2..5ff8a92cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.95.0", + "version": "1.96.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From ff638adf033967c746745b1276047762b07713e5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 28 Dec 2021 17:45:04 +0100 Subject: [PATCH 057/337] Feature/add transactions to position detail dialog (#591) * Add transactions to position detail dialog * Update changelog --- CHANGELOG.md | 6 +++ apps/api/src/app/order/order.controller.ts | 25 ++++----- apps/api/src/app/order/order.service.ts | 35 +++++++++---- .../portfolio-position-detail.interface.ts | 2 + .../src/app/portfolio/portfolio.controller.ts | 1 + .../src/app/portfolio/portfolio.service.ts | 3 ++ .../home-holdings/home-holdings.component.ts | 35 +++++++++++++ .../home-holdings/home-holdings.module.ts | 2 + .../position-detail-dialog.component.ts | 15 ++++-- .../position-detail-dialog.html | 14 +++++ .../position-detail-dialog.module.ts | 6 ++- .../components/position/position.component.ts | 44 +--------------- .../positions-table.component.ts | 3 +- .../positions-table/positions-table.module.ts | 2 +- .../transactions-table.component.html | 32 ++++++++++-- .../transactions-table.component.scss | 4 -- .../transactions-table.component.ts | 52 +++++-------------- .../transactions-table.module.ts | 2 - .../transactions-page.component.ts | 26 ++++++++++ apps/client/src/app/services/data.service.ts | 13 ++++- 20 files changed, 193 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8258a27..a5a8297e1 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 transactions to the position detail dialog + ## 1.96.0 - 27.12.2021 ### Changed diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 5281529b0..da1c44c95 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -66,28 +66,21 @@ export class OrderController { this.request.user.id ); - let orders = await this.orderService.orders({ - include: { - Account: { - include: { - Platform: true - } - }, - SymbolProfile: { - select: { - name: true - } - } - }, - orderBy: { date: 'desc' }, - where: { userId: impersonationUserId || this.request.user.id } + let orders = await this.orderService.getOrders({ + includeDrafts: true, + userId: impersonationUserId || this.request.user.id }); if ( impersonationUserId || this.userService.isRestrictedView(this.request.user) ) { - orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']); + orders = nullifyValuesInObjects(orders, [ + 'fee', + 'quantity', + 'unitPrice', + 'value' + ]); } return orders; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 4fabb98f1..eb58b889e 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -4,6 +4,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { DataSource, Order, Prisma } from '@prisma/client'; +import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; @Injectable() @@ -82,7 +83,7 @@ export class OrderService { }); } - public getOrders({ + public async getOrders({ includeDrafts = false, userId }: { @@ -95,15 +96,29 @@ export class OrderService { where.isDraft = false; } - return this.orders({ - where, - include: { - // eslint-disable-next-line @typescript-eslint/naming-convention - Account: true, - // eslint-disable-next-line @typescript-eslint/naming-convention - SymbolProfile: true - }, - orderBy: { date: 'asc' } + return ( + await this.orders({ + where, + include: { + // eslint-disable-next-line @typescript-eslint/naming-convention + Account: { + include: { + Platform: true + } + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + SymbolProfile: true + }, + orderBy: { date: 'asc' } + }) + ).map((order) => { + return { + ...order, + value: new Big(order.quantity) + .mul(order.unitPrice) + .plus(order.fee) + .toNumber() + }; }); } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 069230983..ddc5fbf37 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,3 +1,4 @@ +import { OrderWithAccount } from '@ghostfolio/common/types'; import { AssetClass, AssetSubClass } from '@prisma/client'; export interface PortfolioPositionDetail { @@ -16,6 +17,7 @@ export interface PortfolioPositionDetail { name: string; netPerformance: number; netPerformancePercent: number; + orders: OrderWithAccount[]; quantity: number; symbol: string; transactionCount: number; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 879b11bb5..a0e93ea0c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -360,6 +360,7 @@ export class PortfolioController { 'grossPerformance', 'investment', 'netPerformance', + 'orders', 'quantity', 'value' ]); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fea7c1bb1..4bcf9565c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -388,6 +388,7 @@ export class PortfolioService { name: undefined, netPerformance: undefined, netPerformancePercent: undefined, + orders: [], quantity: undefined, symbol: aSymbol, transactionCount: undefined, @@ -521,6 +522,7 @@ export class PortfolioService { minPrice, name, netPerformance, + orders, transactionCount, averagePrice: averagePrice.toNumber(), grossPerformancePercent: position.grossPerformancePercentage.toNumber(), @@ -578,6 +580,7 @@ export class PortfolioService { maxPrice, minPrice, name, + orders, averagePrice: 0, currency: currentData[aSymbol]?.currency, firstBuyDate: undefined, 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 e0f43ff08..b5aa0b1b7 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 @@ -1,4 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { RANGE, @@ -33,9 +36,20 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private deviceService: DeviceDetectorService, + private dialog: MatDialog, + private route: ActivatedRoute, + private router: Router, private settingsStorageService: SettingsStorageService, private userService: UserService ) { + route.queryParams + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((params) => { + if (params['positionDetailDialog'] && params['symbol']) { + this.openDialog(params['symbol']); + } + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -69,6 +83,27 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } + private openDialog(aSymbol: string): void { + const dialogRef = this.dialog.open(PositionDetailDialog, { + autoFocus: false, + data: { + baseCurrency: this.user?.settings?.baseCurrency, + deviceType: this.deviceType, + locale: this.user?.settings?.locale, + symbol: aSymbol + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } + private update() { this.dataService .fetchPositions({ range: this.dateRange }) diff --git a/apps/client/src/app/components/home-holdings/home-holdings.module.ts b/apps/client/src/app/components/home-holdings/home-holdings.module.ts index c4108673b..5d0a13ab9 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.module.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.module.ts @@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { RouterModule } from '@angular/router'; +import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module'; import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module'; import { HomeHoldingsComponent } from './home-holdings.component'; @@ -12,6 +13,7 @@ import { HomeHoldingsComponent } from './home-holdings.component'; exports: [], imports: [ CommonModule, + GfPositionDetailDialogModule, GfPositionsModule, MatButtonModule, MatCardModule, diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index f9a2127af..fd43729f3 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -3,11 +3,13 @@ import { ChangeDetectorRef, Component, Inject, - OnDestroy + OnDestroy, + OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DataService } from '@ghostfolio/client/services/data.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { OrderWithAccount } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { AssetSubClass } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; @@ -23,7 +25,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces'; templateUrl: 'position-detail-dialog.html', styleUrls: ['./position-detail-dialog.component.scss'] }) -export class PositionDetailDialog implements OnDestroy { +export class PositionDetailDialog implements OnDestroy, OnInit { public assetSubClass: AssetSubClass; public averagePrice: number; public benchmarkDataItems: LineChartItem[]; @@ -39,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy { public name: string; public netPerformance: number; public netPerformancePercent: number; + public orders: OrderWithAccount[]; public quantity: number; public quantityPrecision = 2; public symbol: string; @@ -52,9 +55,11 @@ export class PositionDetailDialog implements OnDestroy { private dataService: DataService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams - ) { + ) {} + + public ngOnInit(): void { this.dataService - .fetchPositionDetail(data.symbol) + .fetchPositionDetail(this.data.symbol) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ @@ -72,6 +77,7 @@ export class PositionDetailDialog implements OnDestroy { name, netPerformance, netPerformancePercent, + orders, quantity, symbol, transactionCount, @@ -104,6 +110,7 @@ export class PositionDetailDialog implements OnDestroy { this.name = name; this.netPerformance = netPerformance; this.netPerformancePercent = netPerformancePercent; + this.orders = orders; this.quantity = quantity; this.symbol = symbol; this.transactionCount = transactionCount; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index a20f4c892..bbf31bf77 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -124,6 +124,20 @@
+ +
(); - public constructor( - private dialog: MatDialog, - private route: ActivatedRoute, - private router: Router - ) { - route.queryParams - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((params) => { - if ( - params['positionDetailDialog'] && - params['symbol'] && - params['symbol'] === this.position?.symbol - ) { - this.openDialog(); - } - }); - } + public constructor() {} public ngOnInit() {} @@ -56,25 +35,4 @@ export class PositionComponent implements OnDestroy, OnInit { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } - - private openDialog(): void { - const dialogRef = this.dialog.open(PositionDetailDialog, { - autoFocus: false, - data: { - baseCurrency: this.baseCurrency, - deviceType: this.deviceType, - locale: this.locale, - symbol: this.position?.symbol - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } } 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 6bb8f1d0e..da6fe7f54 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 @@ -14,13 +14,12 @@ import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; +import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component'; - @Component({ selector: 'gf-positions-table', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/apps/client/src/app/components/positions-table/positions-table.module.ts b/apps/client/src/app/components/positions-table/positions-table.module.ts index 1f32b91bb..9461a8907 100644 --- a/apps/client/src/app/components/positions-table/positions-table.module.ts +++ b/apps/client/src/app/components/positions-table/positions-table.module.ts @@ -7,12 +7,12 @@ import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { RouterModule } from '@angular/router'; +import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module'; import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; import { PositionsTableComponent } from './positions-table.component'; diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.html b/apps/client/src/app/components/transactions-table/transactions-table.component.html index 99ea5c7b7..6f2626167 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.html +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.html @@ -1,4 +1,8 @@ - + + +
+ + +
# Symbol Data Source First Transaction{{ i + 1 }} {{ item.symbol }} {{ item.dataSource}} @@ -44,8 +42,7 @@
+
## User Registration From ddce8cc7f94221cdf12a5799adb86296d049d898 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Dec 2021 10:57:50 +0100 Subject: [PATCH 027/337] Feature/support update of historical data (#557) * Support update of historical data * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/admin/admin.controller.ts | 41 +++++++++++++++++-- .../src/app/admin/update-market-data.dto.ts | 6 +++ apps/api/src/app/symbol/symbol.controller.ts | 28 ++++++++++++- apps/api/src/app/symbol/symbol.service.ts | 31 ++++++++++++-- .../yahoo-finance.service.spec.ts | 1 + .../yahoo-finance/yahoo-finance.service.ts | 2 +- apps/api/src/services/market-data.service.ts | 12 ++++++ .../admin-market-data-detail.component.ts | 10 ++++- .../market-data-detail-dialog.component.ts | 25 ++++++++--- .../market-data-detail-dialog.html | 20 ++++++--- .../admin-market-data.component.ts | 7 ++++ .../admin-market-data/admin-market-data.html | 1 + .../create-or-update-transaction-dialog.html | 2 +- apps/client/src/app/services/admin.service.ts | 38 +++++++++++++++++ 15 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 apps/api/src/app/admin/update-market-data.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5418b82..596c07ee6 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 ### Added - Added a line chart to the historical data view in the admin control panel +- Supported the update of historical data in the admin control panel ## 1.91.0 - 18.12.2021 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 2c2130da3..52ec4998c 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,6 +1,6 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; -import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { AdminData, AdminMarketData, @@ -22,16 +22,18 @@ import { import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { DataSource, MarketData } from '@prisma/client'; -import { isDate, isValid } from 'date-fns'; +import { isDate } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AdminService } from './admin.service'; +import { UpdateMarketDataDto } from './update-market-data.dto'; @Controller('admin') export class AdminController { public constructor( private readonly adminService: AdminService, private readonly dataGatheringService: DataGatheringService, + private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -173,7 +175,7 @@ export class AdminController { @Get('market-data/:symbol') @UseGuards(AuthGuard('jwt')) public async getMarketDataBySymbol( - @Param('symbol') symbol + @Param('symbol') symbol: string ): Promise { if ( !hasPermission( @@ -190,6 +192,39 @@ export class AdminController { return this.adminService.getMarketDataBySymbol(symbol); } + @Put('market-data/:dataSource/:symbol/:dateString') + @UseGuards(AuthGuard('jwt')) + public async update( + @Param('dataSource') dataSource: DataSource, + @Param('dateString') dateString: string, + @Param('symbol') symbol: string, + @Body() data: UpdateMarketDataDto + ) { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + const date = new Date(dateString); + + return this.marketDataService.updateMarketData({ + data, + where: { + date_symbol: { + date, + symbol + } + } + }); + } + @Put('settings/:key') @UseGuards(AuthGuard('jwt')) public async updateProperty( diff --git a/apps/api/src/app/admin/update-market-data.dto.ts b/apps/api/src/app/admin/update-market-data.dto.ts new file mode 100644 index 000000000..79779a318 --- /dev/null +++ b/apps/api/src/app/admin/update-market-data.dto.ts @@ -0,0 +1,6 @@ +import { IsNumber } from 'class-validator'; + +export class UpdateMarketDataDto { + @IsNumber() + marketPrice: number; +} diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index a364de6bc..a5c31fe6a 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,3 +1,4 @@ +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, @@ -12,9 +13,9 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { DataSource } from '@prisma/client'; +import { DataSource, MarketData } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { isEmpty } from 'lodash'; +import { isDate, isEmpty } from 'lodash'; import { LookupItem } from './interfaces/lookup-item.interface'; import { SymbolItem } from './interfaces/symbol-item.interface'; @@ -78,4 +79,27 @@ export class SymbolController { return result; } + + @Get(':dataSource/:symbol/:dateString') + @UseGuards(AuthGuard('jwt')) + public async gatherSymbolForDate( + @Param('dataSource') dataSource: DataSource, + @Param('dateString') dateString: string, + @Param('symbol') symbol: string + ): Promise { + const date = new Date(dateString); + + if (!isDate(date)) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + + return this.symbolService.getForDate({ + dataSource, + date, + symbol + }); + } } diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index cdd13da8d..2649ef5d0 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,11 +1,15 @@ import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; +import { + IDataGatheringItem, + IDataProviderHistoricalResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; -import { subDays } from 'date-fns'; +import { DataSource, MarketData } from '@prisma/client'; +import { format, subDays } from 'date-fns'; import { LookupItem } from './interfaces/lookup-item.interface'; import { SymbolItem } from './interfaces/symbol-item.interface'; @@ -58,6 +62,27 @@ export class SymbolService { return undefined; } + public async getForDate({ + dataSource, + date, + symbol + }: { + dataSource: DataSource; + date: Date; + symbol: string; + }): Promise { + const historicalData = await this.dataProviderService.getHistoricalRaw( + [{ dataSource, symbol }], + date, + date + ); + + return { + marketPrice: + historicalData?.[symbol]?.[format(date, DATE_FORMAT)]?.marketPrice + }; + } + public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> { const results: { items: LookupItem[] } = { items: [] }; diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts index 414a83dd6..c07be06bf 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts @@ -1,4 +1,5 @@ import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; + import { YahooFinanceService } from './yahoo-finance.service'; jest.mock( 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 c9d5081d8..8d375a10e 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,6 +1,6 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { baseCurrency, UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { UNKNOWN_KEY, 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'; diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index f9c7fc003..66a1fe50a 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/apps/api/src/services/market-data.service.ts @@ -65,4 +65,16 @@ export class MarketDataService { where }); } + + public async updateMarketData(params: { + data: Prisma.MarketDataUpdateInput; + where: Prisma.MarketDataWhereUniqueInput; + }): Promise { + const { data, where } = params; + + return this.prismaService.marketData.update({ + data, + where + }); + } } 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 4ee2b9aea..fa24f5941 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 @@ -1,9 +1,11 @@ import { ChangeDetectionStrategy, Component, + EventEmitter, Input, OnChanges, - OnInit + OnInit, + Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; @@ -27,6 +29,8 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { @Input() marketData: MarketData[]; @Input() symbol: string; + @Output() marketDataChanged = new EventEmitter(); + public days = Array(31); public defaultDateFormat = DEFAULT_DATE_FORMAT; public deviceType: string; @@ -101,7 +105,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => {}); + .subscribe(({ withRefresh }) => { + this.marketDataChanged.next(withRefresh); + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts index 0ce14aba6..01b1f4718 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -7,7 +7,6 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AdminService } from '@ghostfolio/client/services/admin.service'; -import { MarketData } from '@prisma/client'; import { Subject, takeUntil } from 'rxjs'; import { MarketDataDetailDialogParams } from './interfaces/interfaces'; @@ -32,24 +31,38 @@ export class MarketDataDetailDialog implements OnDestroy { public ngOnInit() {} public onCancel(): void { - this.dialogRef.close(); + this.dialogRef.close({ withRefresh: false }); } - public onGatherData() { + public onFetchSymbolForDate() { this.adminService - .gatherSymbol({ + .fetchSymbolForDate({ dataSource: this.data.dataSource, date: this.data.date, symbol: this.data.symbol }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((marketData: MarketData) => { - this.data.marketPrice = marketData.marketPrice; + .subscribe(({ marketPrice }) => { + this.data.marketPrice = marketPrice; this.changeDetectorRef.markForCheck(); }); } + public onUpdate() { + this.adminService + .putMarketData({ + dataSource: this.data.dataSource, + date: this.data.date, + marketData: { marketPrice: this.data.marketPrice }, + symbol: this.data.symbol + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.dialogRef.close({ withRefresh: true }); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html index 48bb2a7b0..3642a9e1d 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html @@ -21,22 +21,30 @@ -
- +
+ Market Price + -
+
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 256a86f56..87ebfea28 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 @@ -68,6 +68,13 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { } } + public onMarketDataChanged(withRefresh: boolean = false) { + if (withRefresh) { + this.fetchAdminMarketData(); + this.fetchAdminMarketDataBySymbol(this.currentSymbol); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); 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 5e8083e98..b0df54364 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 @@ -47,6 +47,7 @@ [dataSource]="item.dataSource" [marketData]="marketDataDetails" [symbol]="item.symbol" + (marketDataChanged)="onMarketDataChanged($event)" >
+ Value + +
+ +
+
Account @@ -254,12 +279,13 @@ *matRowDef="let row; columns: displayedColumns" mat-row (click)=" - !row.isDraft && + hasPermissionToOpenDetails && + !row.isDraft && onOpenPositionDialog({ symbol: row.symbol }) " - [ngClass]="{ 'is-draft': row.isDraft }" + [ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }" >
diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.scss b/apps/client/src/app/components/transactions-table/transactions-table.component.scss index 965de25a8..b63124cf7 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.scss +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.scss @@ -24,10 +24,6 @@ } .mat-row { - &:not(.is-draft) { - cursor: pointer; - } - .type-badge { background-color: rgba(var(--palette-foreground-text), 0.05); border-radius: 1rem; diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.ts b/apps/client/src/app/components/transactions-table/transactions-table.component.ts index ab2f18e10..e22a0bcb7 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.ts +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.ts @@ -17,18 +17,15 @@ import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatChipInputEvent } from '@angular/material/chips'; -import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { endOfToday, format, isAfter } from 'date-fns'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component'; - const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_STRING_SEPARATOR = ','; @@ -44,9 +41,12 @@ export class TransactionsTableComponent @Input() baseCurrency: string; @Input() deviceType: string; @Input() hasPermissionToCreateOrder: boolean; + @Input() hasPermissionToFilter = true; @Input() hasPermissionToImportOrders: boolean; + @Input() hasPermissionToOpenDetails = true; @Input() locale: string; @Input() showActions: boolean; + @Input() showSymbolColumn = true; @Input() transactions: OrderWithAccount[]; @Output() export = new EventEmitter(); @@ -77,21 +77,7 @@ export class TransactionsTableComponent private allFilters: string[]; private unsubscribeSubject = new Subject(); - public constructor( - private dialog: MatDialog, - private route: ActivatedRoute, - private router: Router - ) { - this.routeQueryParams = route.queryParams - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((params) => { - if (params['positionDetailDialog'] && params['symbol']) { - this.openPositionDialog({ - symbol: params['symbol'] - }); - } - }); - + public constructor(private router: Router) { this.searchControl.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((keyword) => { @@ -150,6 +136,7 @@ export class TransactionsTableComponent 'quantity', 'unitPrice', 'fee', + 'value', 'account' ]; @@ -157,6 +144,12 @@ export class TransactionsTableComponent this.displayedColumns.push('actions'); } + if (!this.showSymbolColumn) { + this.displayedColumns = this.displayedColumns.filter((column) => { + return column !== 'symbol'; + }); + } + this.isLoading = true; if (this.transactions) { @@ -210,27 +203,6 @@ export class TransactionsTableComponent this.transactionToClone.emit(aTransaction); } - public openPositionDialog({ symbol }: { symbol: string }): void { - const dialogRef = this.dialog.open(PositionDetailDialog, { - autoFocus: false, - data: { - symbol, - baseCurrency: this.baseCurrency, - deviceType: this.deviceType, - locale: this.locale - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/transactions-table/transactions-table.module.ts b/apps/client/src/app/components/transactions-table/transactions-table.module.ts index c8bca3c62..862928127 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.module.ts +++ b/apps/client/src/app/components/transactions-table/transactions-table.module.ts @@ -14,7 +14,6 @@ import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info' import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module'; import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; import { TransactionsTableComponent } from './transactions-table.component'; @@ -24,7 +23,6 @@ import { TransactionsTableComponent } from './transactions-table.component'; imports: [ CommonModule, GfNoTransactionsInfoModule, - GfPositionDetailDialogModule, GfSymbolIconModule, GfSymbolModule, GfValueModule, diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 1178c8aea..59342f335 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -4,6 +4,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; +import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; @@ -73,6 +74,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } else { this.router.navigate(['.'], { relativeTo: this.route }); } + } else if (params['positionDetailDialog'] && params['symbol']) { + this.openPositionDialog({ + symbol: params['symbol'] + }); } }); } @@ -250,6 +255,27 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } + public openPositionDialog({ symbol }: { symbol: string }): void { + const dialogRef = this.dialog.open(PositionDetailDialog, { + autoFocus: false, + data: { + symbol, + baseCurrency: this.user?.settings?.baseCurrency, + deviceType: this.deviceType, + locale: this.user?.settings?.locale + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } + public openUpdateTransactionDialog({ accountId, currency, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 586192db5..6191fd9b1 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -212,8 +212,17 @@ export class DataService { } public fetchPositionDetail(aSymbol: string) { - return this.http.get( - `/api/portfolio/position/${aSymbol}` + return this.http.get(`/api/portfolio/position/${aSymbol}`).pipe( + map((data) => { + if (data.orders) { + for (const order of data.orders) { + order.createdAt = parseISO(order.createdAt); + order.date = parseISO(order.date); + } + } + + return data; + }) ); } From 9e1a7fc981e5de431a6af5da0891f5dee8f16bd4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 28 Dec 2021 21:12:12 +0100 Subject: [PATCH 058/337] Feature/dividend (#547) * Add dividend to order type * Support dividend in transactions table * Support dividend in transaction dialog * Extend import file with dividend * Add dividend to portfolio summary * Update changelog Co-authored-by: Fly Man --- CHANGELOG.md | 5 + apps/api/src/app/order/order.service.ts | 14 ++- .../src/app/portfolio/portfolio.controller.ts | 1 + .../src/app/portfolio/portfolio.service.ts | 107 ++++++++++++------ .../portfolio-summary.component.html | 14 +++ .../transactions-table.component.html | 8 +- .../transactions-table.component.scss | 4 + .../create-or-update-transaction-dialog.html | 7 +- .../services/import-transactions.service.ts | 13 ++- .../interfaces/portfolio-summary.interface.ts | 1 + .../migration.sql | 2 + prisma/schema.prisma | 1 + test/import/ok.csv | 1 + 13 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 prisma/migrations/20211215205808_added_dividend_to_order_type/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a8297e1..97dcf4593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the transactions to the position detail dialog +- Added support for dividend + +### Todo + +- Apply data migration (`yarn database:migrate`) ## 1.96.0 - 27.12.2021 diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index eb58b889e..1654380ae 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -3,7 +3,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource, Order, Prisma } from '@prisma/client'; +import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; @@ -85,9 +85,11 @@ export class OrderService { public async getOrders({ includeDrafts = false, + types, userId }: { includeDrafts?: boolean; + types?: TypeOfOrder[]; userId: string; }) { const where: Prisma.OrderWhereInput = { userId }; @@ -96,6 +98,16 @@ export class OrderService { where.isDraft = false; } + if (types) { + where.OR = types.map((type) => { + return { + type: { + equals: type + } + }; + }); + } + return ( await this.orders({ where, diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a0e93ea0c..ce178ee1c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -330,6 +330,7 @@ export class PortfolioController { 'currentGrossPerformance', 'currentNetPerformance', 'currentValue', + 'dividend', 'fees', 'netWorth', 'totalBuy', diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 4bcf9565c..e9be3d0a2 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -401,17 +401,21 @@ export class PortfolioService { const positionCurrency = orders[0].currency; const name = orders[0].SymbolProfile?.name ?? ''; - const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ - currency: order.currency, - dataSource: order.dataSource, - date: format(order.date, DATE_FORMAT), - fee: new Big(order.fee), - name: order.SymbolProfile?.name, - quantity: new Big(order.quantity), - symbol: order.symbol, - type: order.type, - unitPrice: new Big(order.unitPrice) - })); + const portfolioOrders: PortfolioOrder[] = orders + .filter((order) => { + return order.type === 'BUY' || order.type === 'SELL'; + }) + .map((order) => ({ + currency: order.currency, + dataSource: order.dataSource, + date: format(order.date, DATE_FORMAT), + fee: new Big(order.fee), + name: order.SymbolProfile?.name, + quantity: new Big(order.quantity), + symbol: order.symbol, + type: order.type, + unitPrice: new Big(order.unitPrice) + })); const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -729,22 +733,6 @@ export class PortfolioService { }; } - public getFees(orders: OrderWithAccount[], date = new Date(0)) { - return orders - .filter((order) => { - // Filter out all orders before given date - return isBefore(date, new Date(order.date)); - }) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - order.fee, - order.currency, - this.request.user.Settings.currency - ); - }) - .reduce((previous, current) => previous + current, 0); - } - public async getReport(impersonationId: string): Promise { const currency = this.request.user.Settings.currency; const userId = await this.getUserId(impersonationId, this.request.user.id); @@ -825,7 +813,7 @@ export class PortfolioService { new FeeRatioInitialInvestment( this.exchangeRateDataService, currentPositions.totalInvestment.toNumber(), - this.getFees(orders) + this.getFees(orders).toNumber() ) ], { baseCurrency: currency } @@ -844,8 +832,11 @@ export class PortfolioService { userId, currency ); - const orders = await this.orderService.getOrders({ userId }); - const fees = this.getFees(orders); + const orders = await this.orderService.getOrders({ + userId + }); + const dividend = this.getDividend(orders).toNumber(); + const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; const totalBuy = this.getTotalByType(orders, currency, 'BUY'); @@ -859,14 +850,17 @@ export class PortfolioService { return { ...performanceInformation.performance, + dividend, fees, firstOrderDate, netWorth, + totalBuy, + totalSell, cash: balance, committedFunds: committedFunds.toNumber(), - ordersCount: orders.length, - totalBuy: totalBuy, - totalSell: totalSell + ordersCount: orders.filter((order) => { + return order.type === 'BUY' || order.type === 'SELL'; + }).length }; } @@ -939,6 +933,47 @@ export class PortfolioService { return cashPositions; } + private getDividend(orders: OrderWithAccount[], date = new Date(0)) { + return orders + .filter((order) => { + // Filter out all orders before given date and type dividend + return ( + isBefore(date, new Date(order.date)) && + order.type === TypeOfOrder.DIVIDEND + ); + }) + .map((order) => { + return this.exchangeRateDataService.toCurrency( + new Big(order.quantity).mul(order.unitPrice).toNumber(), + order.currency, + this.request.user.Settings.currency + ); + }) + .reduce( + (previous, current) => new Big(previous).plus(current), + new Big(0) + ); + } + + private getFees(orders: OrderWithAccount[], date = new Date(0)) { + return orders + .filter((order) => { + // Filter out all orders before given date + return isBefore(date, new Date(order.date)); + }) + .map((order) => { + return this.exchangeRateDataService.toCurrency( + order.fee, + order.currency, + this.request.user.Settings.currency + ); + }) + .reduce( + (previous, current) => new Big(previous).plus(current), + new Big(0) + ); + } + private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': @@ -967,7 +1002,11 @@ export class PortfolioService { transactionPoints: TransactionPoint[]; orders: OrderWithAccount[]; }> { - const orders = await this.orderService.getOrders({ includeDrafts, userId }); + const orders = await this.orderService.getOrders({ + includeDrafts, + userId, + types: ['BUY', 'SELL'] + }); if (orders.length <= 0) { return { transactionPoints: [], orders: [] }; diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 893718044..c9c34cd03 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -169,4 +169,18 @@ >
+
+

+
+
+
Dividend
+
+ +
+
diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.html b/apps/client/src/app/components/transactions-table/transactions-table.component.html index 6f2626167..4564e6448 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.html +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.html @@ -77,11 +77,15 @@
Type - BUY - SELL + BUY + DIVIDEND + SELL
@@ -141,7 +142,7 @@ [(ngModel)]="data.transaction.unitPrice" /> - + - - - - @@ -305,7 +305,7 @@
diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.scss b/libs/ui/src/lib/activities-table/activities-table.component.scss similarity index 100% rename from apps/client/src/app/components/transactions-table/transactions-table.component.scss rename to libs/ui/src/lib/activities-table/activities-table.component.scss diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts similarity index 78% rename from apps/client/src/app/components/transactions-table/transactions-table.component.ts rename to libs/ui/src/lib/activities-table/activities-table.component.ts index e22a0bcb7..0cbde8228 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -30,30 +30,28 @@ const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_STRING_SEPARATOR = ','; @Component({ - selector: 'gf-transactions-table', changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './transactions-table.component.html', - styleUrls: ['./transactions-table.component.scss'] + selector: 'gf-activities-table', + styleUrls: ['./activities-table.component.scss'], + templateUrl: './activities-table.component.html' }) -export class TransactionsTableComponent - implements OnChanges, OnDestroy, OnInit -{ +export class ActivitiesTableComponent implements OnChanges, OnDestroy { + @Input() activities: OrderWithAccount[]; @Input() baseCurrency: string; @Input() deviceType: string; - @Input() hasPermissionToCreateOrder: boolean; + @Input() hasPermissionToCreateActivity: boolean; @Input() hasPermissionToFilter = true; - @Input() hasPermissionToImportOrders: boolean; + @Input() hasPermissionToImportActivities: boolean; @Input() hasPermissionToOpenDetails = true; @Input() locale: string; @Input() showActions: boolean; @Input() showSymbolColumn = true; - @Input() transactions: OrderWithAccount[]; + @Output() activityDeleted = new EventEmitter(); + @Output() activityToClone = new EventEmitter(); + @Output() activityToUpdate = new EventEmitter(); @Output() export = new EventEmitter(); @Output() import = new EventEmitter(); - @Output() transactionDeleted = new EventEmitter(); - @Output() transactionToClone = new EventEmitter(); - @Output() transactionToUpdate = new EventEmitter(); @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @ViewChild('searchInput') searchInput: ElementRef; @@ -124,8 +122,6 @@ export class TransactionsTableComponent this.searchControl.setValue(null); } - public ngOnInit() {} - public ngOnChanges() { this.displayedColumns = [ 'count', @@ -152,8 +148,8 @@ export class TransactionsTableComponent this.isLoading = true; - if (this.transactions) { - this.dataSource = new MatTableDataSource(this.transactions); + if (this.activities) { + this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { const dataString = this.getFilterableValues(data) .join(' ') @@ -171,13 +167,15 @@ export class TransactionsTableComponent } } - public onDeleteTransaction(aId: string) { - const confirmation = confirm( - 'Do you really want to delete this transaction?' - ); + public onCloneActivity(aActivity: OrderWithAccount) { + this.activityToClone.emit(aActivity); + } + + public onDeleteActivity(aId: string) { + const confirmation = confirm('Do you really want to delete this activity?'); if (confirmation) { - this.transactionDeleted.emit(aId); + this.activityDeleted.emit(aId); } } @@ -195,12 +193,8 @@ export class TransactionsTableComponent }); } - public onUpdateTransaction(aTransaction: OrderWithAccount) { - this.transactionToUpdate.emit(aTransaction); - } - - public onCloneTransaction(aTransaction: OrderWithAccount) { - this.transactionToClone.emit(aTransaction); + public onUpdateActivity(aActivity: OrderWithAccount) { + this.activityToUpdate.emit(aActivity); } public ngOnDestroy() { @@ -217,7 +211,7 @@ export class TransactionsTableComponent this.placeholder = lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; - this.allFilters = this.getSearchableFieldValues(this.transactions).filter( + this.allFilters = this.getSearchableFieldValues(this.activities).filter( (item) => { return !lowercaseSearchKeywords.includes(item.trim().toLowerCase()); } @@ -226,11 +220,11 @@ export class TransactionsTableComponent this.filters$.next(this.allFilters); } - private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] { + private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { const fieldValues = new Set(); - for (const transaction of transactions) { - this.getFilterableValues(transaction, fieldValues); + for (const activity of activities) { + this.getFilterableValues(activity, fieldValues); } return [...fieldValues] @@ -255,15 +249,15 @@ export class TransactionsTableComponent } private getFilterableValues( - transaction: OrderWithAccount, + activity: OrderWithAccount, fieldValues: Set = new Set() ): string[] { - fieldValues.add(transaction.currency); - fieldValues.add(transaction.symbol); - fieldValues.add(transaction.type); - fieldValues.add(transaction.Account?.name); - fieldValues.add(transaction.Account?.Platform?.name); - fieldValues.add(format(transaction.date, 'yyyy')); + fieldValues.add(activity.currency); + fieldValues.add(activity.symbol); + fieldValues.add(activity.type); + fieldValues.add(activity.Account?.name); + fieldValues.add(activity.Account?.Platform?.name); + fieldValues.add(format(activity.date, 'yyyy')); return [...fieldValues].filter((item) => { return item !== undefined; diff --git a/apps/client/src/app/components/transactions-table/transactions-table.module.ts b/libs/ui/src/lib/activities-table/activities-table.module.ts similarity index 81% rename from apps/client/src/app/components/transactions-table/transactions-table.module.ts rename to libs/ui/src/lib/activities-table/activities-table.module.ts index 862928127..56947d7e9 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.module.ts +++ b/libs/ui/src/lib/activities-table/activities-table.module.ts @@ -9,17 +9,17 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { RouterModule } from '@angular/router'; +import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; -import { TransactionsTableComponent } from './transactions-table.component'; +import { ActivitiesTableComponent } from './activities-table.component'; @NgModule({ - declarations: [TransactionsTableComponent], - exports: [TransactionsTableComponent], + declarations: [ActivitiesTableComponent], + exports: [ActivitiesTableComponent], imports: [ CommonModule, GfNoTransactionsInfoModule, @@ -37,7 +37,6 @@ import { TransactionsTableComponent } from './transactions-table.component'; ReactiveFormsModule, RouterModule ], - providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class GfTransactionsTableModule {} +export class GfActivitiesTableModule {} diff --git a/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html b/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html index bb3ce1238..9f30d437a 100644 --- a/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html +++ b/libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html @@ -5,10 +5,10 @@ - Time to add your first transaction. + Time to add your first activity.
From 15344513ceb6ce4cf0ebe32bfa66a80b279924c8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 30 Dec 2021 22:18:39 +0100 Subject: [PATCH 068/337] Feature/upgrade chart.js to version 3.7.0 (#601) * Upgrade chart.js from version 3.5.0 to 3.7.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 dc539e23e..4a34f3a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Start refactoring _transactions_ to _activities_ - Upgraded `angular` from version `13.0.2` to `13.1.1` +- Upgraded `chart.js` from version `3.5.0` to `3.7.0` - Upgraded `Nx` from version `13.3.0` to `13.4.1` ## 1.98.0 - 29.12.2021 diff --git a/package.json b/package.json index 5b3c710c1..13eda22fe 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "bootstrap": "4.6.0", "cache-manager": "3.4.3", "cache-manager-redis-store": "2.0.0", - "chart.js": "3.5.0", + "chart.js": "3.7.0", "chartjs-adapter-date-fns": "2.0.0", "chartjs-plugin-datalabels": "2.0.0", "cheerio": "1.0.0-rc.6", diff --git a/yarn.lock b/yarn.lock index 052744b48..c4f08c700 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6784,10 +6784,10 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chart.js@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.0.tgz#6eb075332d4ebbbb20a94e5a07a234052ed6c4fb" - integrity sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA== +chart.js@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.0.tgz#7a19c93035341df801d613993c2170a1fcf1d882" + integrity sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg== chartjs-adapter-date-fns@2.0.0: version "2.0.0" From d69a69ce18a4a388a38260dd93f1695a089351d3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 30 Dec 2021 22:19:08 +0100 Subject: [PATCH 069/337] Bugfix/fix exception with market state (#602) * Fix exception with market state * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/portfolio/portfolio.service.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a34f3a69..707896b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `chart.js` from version `3.5.0` to `3.7.0` - Upgraded `Nx` from version `13.3.0` to `13.4.1` +### Fixed + +- Fixed an exception with the market state caused by a failed data provider request + ## 1.98.0 - 29.12.2021 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index e9be3d0a2..9a44d2d8e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -662,7 +662,9 @@ export class PortfolioService { grossPerformancePercentage: position.grossPerformancePercentage?.toNumber() ?? null, investment: new Big(position.investment).toNumber(), - marketState: dataProviderResponses[position.symbol].marketState, + marketState: + dataProviderResponses[position.symbol]?.marketState ?? + MarketState.delayed, name: symbolProfileMap[position.symbol].name, netPerformance: position.netPerformance?.toNumber() ?? null, netPerformancePercentage: From 124bdc028d660216b0b1de2a213ca74d197a492d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:51:30 +0100 Subject: [PATCH 070/337] Bugfix/fix reload of position detail dialog (#603) * Fix reload of position detail dialog * Update changelog --- CHANGELOG.md | 1 + .../home-holdings/home-holdings.component.ts | 43 +++++++++------- .../position-detail-dialog.html | 2 +- .../positions-table.component.ts | 42 +--------------- .../allocations/allocations-page.component.ts | 49 ++++++++++++++++++- .../transactions-page.component.ts | 49 +++++++++++-------- 6 files changed, 104 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707896b00..0a9f5ebb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an exception with the market state caused by a failed data provider request +- Fixed the reload of the position detail dialog (with query parameters) ## 1.98.0 - 29.12.2021 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 13b6ffe0d..3d26b373e 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 @@ -48,7 +48,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { if (params['positionDetailDialog'] && params['symbol']) { - this.openDialog(params['symbol']); + this.openPositionDialog({ symbol: params['symbol'] }); } }); @@ -91,24 +91,31 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private openDialog(aSymbol: string): void { - const dialogRef = this.dialog.open(PositionDetailDialog, { - autoFocus: false, - data: { - baseCurrency: this.user?.settings?.baseCurrency, - deviceType: this.deviceType, - locale: this.user?.settings?.locale, - symbol: aSymbol - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() + private openPositionDialog({ symbol }: { symbol: string }) { + this.userService + .get() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); + .subscribe((user) => { + this.user = user; + + const dialogRef = this.dialog.open(PositionDetailDialog, { + autoFocus: false, + data: { + symbol, + baseCurrency: this.user?.settings?.baseCurrency, + deviceType: this.deviceType, + locale: this.user?.settings?.locale + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); }); } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 00d949263..3bd2b780b 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -12,7 +12,7 @@
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 da6fe7f54..b06a052be 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 @@ -9,16 +9,13 @@ import { Output, ViewChild } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { ActivatedRoute, Router } from '@angular/router'; -import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; +import { Router } from '@angular/router'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'gf-positions-table', @@ -48,21 +45,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { private unsubscribeSubject = new Subject(); - public constructor( - private dialog: MatDialog, - private route: ActivatedRoute, - private router: Router - ) { - this.routeQueryParams = route.queryParams - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((params) => { - if (params['positionDetailDialog'] && params['symbol']) { - this.openPositionDialog({ - symbol: params['symbol'] - }); - } - }); - } + public constructor(private router: Router) {} public ngOnInit() {} @@ -105,27 +88,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { }); } - public openPositionDialog({ symbol }: { symbol: string }): void { - const dialogRef = this.dialog.open(PositionDetailDialog, { - autoFocus: false, - data: { - symbol, - baseCurrency: this.baseCurrency, - deviceType: this.deviceType, - locale: this.locale - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); 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 482313244..678c87801 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 @@ -1,4 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; @@ -12,7 +15,7 @@ import { import { ToggleOption } from '@ghostfolio/common/types'; import { AssetClass } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ @@ -51,6 +54,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { >; }; public positionsArray: PortfolioPosition[]; + public routeQueryParams: Subscription; public sectors: { [name: string]: { name: string; value: number }; }; @@ -69,9 +73,22 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private deviceService: DeviceDetectorService, + private dialog: MatDialog, private impersonationStorageService: ImpersonationStorageService, + private route: ActivatedRoute, + private router: Router, private userService: UserService - ) {} + ) { + this.routeQueryParams = route.queryParams + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((params) => { + if (params['positionDetailDialog'] && params['symbol']) { + this.openPositionDialog({ + symbol: params['symbol'] + }); + } + }); + } /** * Initializes the controller @@ -266,4 +283,32 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private openPositionDialog({ symbol }: { symbol: string }) { + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + const dialogRef = this.dialog.open(PositionDetailDialog, { + autoFocus: false, + data: { + symbol, + baseCurrency: this.user?.settings?.baseCurrency, + deviceType: this.deviceType, + locale: this.user?.settings?.locale + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + }); + } } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 59342f335..2b9f85a0d 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -255,27 +255,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - public openPositionDialog({ symbol }: { symbol: string }): void { - const dialogRef = this.dialog.open(PositionDetailDialog, { - autoFocus: false, - data: { - symbol, - baseCurrency: this.user?.settings?.baseCurrency, - deviceType: this.deviceType, - locale: this.user?.settings?.locale - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - public openUpdateTransactionDialog({ accountId, currency, @@ -412,4 +391,32 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.router.navigate(['.'], { relativeTo: this.route }); }); } + + private openPositionDialog({ symbol }: { symbol: string }) { + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + const dialogRef = this.dialog.open(PositionDetailDialog, { + autoFocus: false, + data: { + symbol, + baseCurrency: this.user?.settings?.baseCurrency, + deviceType: this.deviceType, + locale: this.user?.settings?.locale + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + }); + } } From cebf879d6795b8d0240b2ebc0fe795dabd5c23c4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 31 Dec 2021 09:52:03 +0100 Subject: [PATCH 071/337] Feature/refactor demo user (#604) * Refactor demo user id * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/info/info.service.ts | 4 ++-- libs/common/src/lib/config.ts | 2 ++ libs/common/src/lib/helper.ts | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9f5ebb3..e80afeb21 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 - Start refactoring _transactions_ to _activities_ +- Refactored the demo user id - Upgraded `angular` from version `13.0.2` to `13.1.1` - Upgraded `chart.js` from version `3.5.0` to `3.7.0` - Upgraded `Nx` from version `13.3.0` to `13.4.1` diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 2d2ca915e..83a79f61d 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { + DEMO_USER_ID, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_STRIPE_CONFIG, @@ -23,7 +24,6 @@ import { subDays } from 'date-fns'; @Injectable() export class InfoService { private static CACHE_KEY_STATISTICS = 'STATISTICS'; - private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; public constructor( private readonly configurationService: ConfigurationService, @@ -196,7 +196,7 @@ export class InfoService { private getDemoAuthToken() { return this.jwtService.sign({ - id: InfoService.DEMO_USER_ID + id: DEMO_USER_ID }); } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 6e19ef1ff..8f2c9d8fa 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -10,6 +10,8 @@ export const defaultDateRangeOptions: ToggleOption[] = [ { label: 'Max', value: 'max' } ]; +export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; + export const ghostfolioScraperApiSymbolPrefix = '_GF_'; export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`; export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 7e23fce0b..84dba181d 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -3,8 +3,6 @@ import { getDate, getMonth, getYear, parse, subDays } from 'date-fns'; import { ghostfolioScraperApiSymbolPrefix } from './config'; -export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; - export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); } From 635f10e2d0f9581ba39bf9a6910a0136c9c13422 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 31 Dec 2021 10:21:41 +0100 Subject: [PATCH 072/337] Bugfix/hide data provider warning while loading (#605) * Hide data provider warning while loading * Update changelog --- CHANGELOG.md | 1 + .../portfolio-performance.component.html | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e80afeb21..7a5117615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Hid the data provider warning while loading - Fixed an exception with the market state caused by a failed data provider request - Fixed the reload of the position detail dialog (with query parameters) 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 9752c0445..cd61f901e 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 @@ -3,12 +3,15 @@
- +
Date: Fri, 31 Dec 2021 22:00:58 +0100 Subject: [PATCH 073/337] Bugfix/improve error handling in position api endpoint (#607) * Add guards * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/portfolio.service.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5117615..743a80c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hid the data provider warning while loading - Fixed an exception with the market state caused by a failed data provider request +- Fixed an exception in the portfolio position endpoint - Fixed the reload of the position detail dialog (with query parameters) ## 1.98.0 - 29.12.2021 diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9a44d2d8e..815900278 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -447,17 +447,17 @@ export class PortfolioService { // Convert investment, gross and net performance to currency of user const userCurrency = this.request.user.Settings.currency; const investment = this.exchangeRateDataService.toCurrency( - position.investment.toNumber(), + position.investment?.toNumber(), currency, userCurrency ); const grossPerformance = this.exchangeRateDataService.toCurrency( - position.grossPerformance.toNumber(), + position.grossPerformance?.toNumber(), currency, userCurrency ); const netPerformance = this.exchangeRateDataService.toCurrency( - position.netPerformance.toNumber(), + position.netPerformance?.toNumber(), currency, userCurrency ); From 0179823ad9036a48d492712b45db328eb536a84a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 1 Jan 2022 10:10:37 +0100 Subject: [PATCH 074/337] Feature/restructure about page (#608) * Restructure about page: introduce pages for blog and changelog * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/app-routing.module.ts | 12 +++ apps/client/src/app/core/auth.guard.ts | 2 + .../src/app/pages/about/about-page.html | 82 ++++--------------- .../src/app/pages/about/about-page.module.ts | 2 - .../changelog-page-routing.module.ts | 15 ++++ .../changelog/changelog-page.component.ts | 22 +++++ .../pages/about/changelog/changelog-page.html | 23 ++++++ .../about/changelog/changelog-page.module.ts | 19 +++++ .../pages/about/changelog/changelog-page.scss | 44 ++++++++++ .../pages/blog/blog-page-routing.module.ts | 15 ++++ .../src/app/pages/blog/blog-page.component.ts | 22 +++++ apps/client/src/app/pages/blog/blog-page.html | 49 +++++++++++ .../src/app/pages/blog/blog-page.module.ts | 13 +++ apps/client/src/app/pages/blog/blog-page.scss | 8 ++ apps/client/src/assets/sitemap.xml | 22 +++-- 16 files changed, 276 insertions(+), 75 deletions(-) create mode 100644 apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts create mode 100644 apps/client/src/app/pages/about/changelog/changelog-page.component.ts create mode 100644 apps/client/src/app/pages/about/changelog/changelog-page.html create mode 100644 apps/client/src/app/pages/about/changelog/changelog-page.module.ts create mode 100644 apps/client/src/app/pages/about/changelog/changelog-page.scss create mode 100644 apps/client/src/app/pages/blog/blog-page-routing.module.ts create mode 100644 apps/client/src/app/pages/blog/blog-page.component.ts create mode 100644 apps/client/src/app/pages/blog/blog-page.html create mode 100644 apps/client/src/app/pages/blog/blog-page.module.ts create mode 100644 apps/client/src/app/pages/blog/blog-page.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 743a80c6d..11436f14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Restructured the about page - Start refactoring _transactions_ to _activities_ - Refactored the demo user id - Upgraded `angular` from version `13.0.2` to `13.1.1` diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 314a89bdb..cd4e383cd 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -9,6 +9,13 @@ const routes: Routes = [ loadChildren: () => import('./pages/about/about-page.module').then((m) => m.AboutPageModule) }, + { + path: 'about/changelog', + loadChildren: () => + import('./pages/about/changelog/changelog-page.module').then( + (m) => m.ChangelogPageModule + ) + }, { path: 'account', loadChildren: () => @@ -33,6 +40,11 @@ const routes: Routes = [ loadChildren: () => import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule) }, + { + path: 'blog', + loadChildren: () => + import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule) + }, { path: 'de/blog/2021/07/hallo-ghostfolio', loadChildren: () => diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index c90ffbe4c..b8c629a7c 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -16,6 +16,8 @@ import { UserService } from '../services/user/user.service'; export class AuthGuard implements CanActivate { private static PUBLIC_PAGE_ROUTES = [ '/about', + '/about/changelog', + '/blog', '/de/blog', '/en/blog', '/p', diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index e68ec6f91..44c8ae9cc 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -149,73 +149,23 @@
- - -
-
-

Changelog

- - - - - -
-
-
-
-

License

- - - - - +
+ Blog +
+
diff --git a/apps/client/src/app/pages/about/about-page.module.ts b/apps/client/src/app/pages/about/about-page.module.ts index 668596413..2ca669fde 100644 --- a/apps/client/src/app/pages/about/about-page.module.ts +++ b/apps/client/src/app/pages/about/about-page.module.ts @@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; -import { MarkdownModule } from 'ngx-markdown'; import { AboutPageRoutingModule } from './about-page-routing.module'; import { AboutPageComponent } from './about-page.component'; @@ -13,7 +12,6 @@ import { AboutPageComponent } from './about-page.component'; imports: [ AboutPageRoutingModule, CommonModule, - MarkdownModule.forChild(), MatButtonModule, MatCardModule ], diff --git a/apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts b/apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts new file mode 100644 index 000000000..052f5e01f --- /dev/null +++ b/apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { ChangelogPageComponent } from './changelog-page.component'; + +const routes: Routes = [ + { path: '', component: ChangelogPageComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ChangelogPageRoutingModule {} diff --git a/apps/client/src/app/pages/about/changelog/changelog-page.component.ts b/apps/client/src/app/pages/about/changelog/changelog-page.component.ts new file mode 100644 index 000000000..a9f383b87 --- /dev/null +++ b/apps/client/src/app/pages/about/changelog/changelog-page.component.ts @@ -0,0 +1,22 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Component({ + host: { class: 'mb-5' }, + selector: 'gf-changelog-page', + styleUrls: ['./changelog-page.scss'], + templateUrl: './changelog-page.html' +}) +export class ChangelogPageComponent implements OnDestroy { + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor() {} + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/about/changelog/changelog-page.html b/apps/client/src/app/pages/about/changelog/changelog-page.html new file mode 100644 index 000000000..e75178298 --- /dev/null +++ b/apps/client/src/app/pages/about/changelog/changelog-page.html @@ -0,0 +1,23 @@ +
+
+
+

Changelog

+ + + + + +
+
+ +
+
+

License

+ + + + + +
+
+
diff --git a/apps/client/src/app/pages/about/changelog/changelog-page.module.ts b/apps/client/src/app/pages/about/changelog/changelog-page.module.ts new file mode 100644 index 000000000..ba4788c2b --- /dev/null +++ b/apps/client/src/app/pages/about/changelog/changelog-page.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { MarkdownModule } from 'ngx-markdown'; + +import { ChangelogPageRoutingModule } from './changelog-page-routing.module'; +import { ChangelogPageComponent } from './changelog-page.component'; + +@NgModule({ + declarations: [ChangelogPageComponent], + imports: [ + ChangelogPageRoutingModule, + CommonModule, + MarkdownModule.forChild(), + MatCardModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class ChangelogPageModule {} diff --git a/apps/client/src/app/pages/about/changelog/changelog-page.scss b/apps/client/src/app/pages/about/changelog/changelog-page.scss new file mode 100644 index 000000000..3fb17de78 --- /dev/null +++ b/apps/client/src/app/pages/about/changelog/changelog-page.scss @@ -0,0 +1,44 @@ +:host { + color: rgb(var(--dark-primary-text)); + display: block; + + .mat-card { + &.changelog { + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; + + &:hover { + color: rgba(var(--palette-primary-300), 1); + } + } + } + + &.changelog { + ::ng-deep { + markdown { + h1, + p { + display: none; + } + + h2 { + font-size: 18px; + + &:not(:first-of-type) { + margin-top: 2rem; + } + } + + h3 { + font-size: 15px; + } + } + } + } + } +} + +:host-context(.is-dark-theme) { + color: rgb(var(--light-primary-text)); +} diff --git a/apps/client/src/app/pages/blog/blog-page-routing.module.ts b/apps/client/src/app/pages/blog/blog-page-routing.module.ts new file mode 100644 index 000000000..1b688ff52 --- /dev/null +++ b/apps/client/src/app/pages/blog/blog-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { BlogPageComponent } from './blog-page.component'; + +const routes: Routes = [ + { path: '', component: BlogPageComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BlogPageRoutingModule {} diff --git a/apps/client/src/app/pages/blog/blog-page.component.ts b/apps/client/src/app/pages/blog/blog-page.component.ts new file mode 100644 index 000000000..c07c91ee6 --- /dev/null +++ b/apps/client/src/app/pages/blog/blog-page.component.ts @@ -0,0 +1,22 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Component({ + host: { class: 'mb-5' }, + selector: 'gf-blog-page', + styleUrls: ['./blog-page.scss'], + templateUrl: './blog-page.html' +}) +export class BlogPageComponent implements OnDestroy { + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor() {} + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html new file mode 100644 index 000000000..c97047793 --- /dev/null +++ b/apps/client/src/app/pages/blog/blog-page.html @@ -0,0 +1,49 @@ + diff --git a/apps/client/src/app/pages/blog/blog-page.module.ts b/apps/client/src/app/pages/blog/blog-page.module.ts new file mode 100644 index 000000000..d210c1aad --- /dev/null +++ b/apps/client/src/app/pages/blog/blog-page.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; + +import { BlogPageRoutingModule } from './blog-page-routing.module'; +import { BlogPageComponent } from './blog-page.component'; + +@NgModule({ + declarations: [BlogPageComponent], + imports: [BlogPageRoutingModule, CommonModule, MatCardModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BlogPageModule {} diff --git a/apps/client/src/app/pages/blog/blog-page.scss b/apps/client/src/app/pages/blog/blog-page.scss new file mode 100644 index 000000000..39eb6792e --- /dev/null +++ b/apps/client/src/app/pages/blog/blog-page.scss @@ -0,0 +1,8 @@ +:host { + color: rgb(var(--dark-primary-text)); + display: block; +} + +:host-context(.is-dark-theme) { + color: rgb(var(--light-primary-text)); +} diff --git a/apps/client/src/assets/sitemap.xml b/apps/client/src/assets/sitemap.xml index 3ee979f4e..f21832c14 100644 --- a/apps/client/src/assets/sitemap.xml +++ b/apps/client/src/assets/sitemap.xml @@ -6,30 +6,38 @@ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> https://ghostfol.io - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 https://ghostfol.io/about - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 + + + https://ghostfol.io/about/changelog + 2022-01-01T00:00:00+00:00 + + + https://ghostfol.io/blog + 2022-01-01T00:00:00+00:00 https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 https://ghostfol.io/en/blog/2021/07/hello-ghostfolio - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 https://ghostfol.io/pricing - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 https://ghostfol.io/register - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 https://ghostfol.io/resources - 2021-07-31T00:00:00+00:00 + 2022-01-01T00:00:00+00:00 From e54638a684bce4b3a0f6e16da8fd664ff1d9dbe0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 1 Jan 2022 12:09:49 +0100 Subject: [PATCH 075/337] Feature/improve analysis page (#609) * Improve analysis page (show y-axis, extend chart in relation to days in market) * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 74 +++++++++---------- .../src/app/portfolio/portfolio.service.ts | 25 ++++++- .../investment-chart.component.ts | 41 +++++++--- .../investment-chart.module.ts | 3 +- .../analysis/analysis-page.component.ts | 29 ++------ .../portfolio/analysis/analysis-page.html | 2 + apps/client/src/app/services/data.service.ts | 19 +++-- libs/common/src/lib/interfaces/index.ts | 2 + .../portfolio-investments.interface.ts | 6 ++ 10 files changed, 122 insertions(+), 80 deletions(-) create mode 100644 libs/common/src/lib/interfaces/portfolio-investments.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 11436f14f..46be289e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market - Restructured the about page - Start refactoring _transactions_ to _activities_ - Refactored the demo user id diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index ce178ee1c..44f4f1e77 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -10,12 +10,12 @@ import { baseCurrency } from '@ghostfolio/common/config'; import { PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioPerformance, PortfolioPublicDetails, PortfolioReport, PortfolioSummary } from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, @@ -48,42 +48,6 @@ export class PortfolioController { private readonly userService: UserService ) {} - @Get('investments') - @UseGuards(AuthGuard('jwt')) - public async findAll( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response - ): Promise { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && - this.request.user.subscription.type === 'Basic' - ) { - res.status(StatusCodes.FORBIDDEN); - return res.json([]); - } - - let investments = await this.portfolioService.getInvestments( - impersonationId - ); - - if ( - impersonationId || - this.userService.isRestrictedView(this.request.user) - ) { - const maxInvestment = investments.reduce( - (investment, item) => Math.max(investment, item.investment), - 1 - ); - - investments = investments.map((item) => ({ - date: item.date, - investment: item.investment / maxInvestment - })); - } - - return res.json(investments); - } - @Get('chart') @UseGuards(AuthGuard('jwt')) public async getChart( @@ -200,6 +164,42 @@ export class PortfolioController { return res.json({ accounts, hasError, holdings }); } + @Get('investments') + @UseGuards(AuthGuard('jwt')) + public async getInvestments( + @Headers('impersonation-id') impersonationId: string, + @Res() res: Response + ): Promise { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic' + ) { + res.status(StatusCodes.FORBIDDEN); + return res.json({}); + } + + let investments = await this.portfolioService.getInvestments( + impersonationId + ); + + if ( + impersonationId || + this.userService.isRestrictedView(this.request.user) + ) { + const maxInvestment = investments.reduce( + (investment, item) => Math.max(investment, item.investment), + 1 + ); + + investments = investments.map((item) => ({ + date: item.date, + investment: item.investment / maxInvestment + })); + } + + return res.json({ firstOrderDate: investments[0]?.date, investments }); + } + @Get('performance') @UseGuards(AuthGuard('jwt')) public async getPerformance( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 815900278..1902fd137 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -55,7 +55,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { isEmpty, sortBy } from 'lodash'; import { HistoricalDataContainer, @@ -150,12 +150,33 @@ export class PortfolioService { return []; } - return portfolioCalculator.getInvestments().map((item) => { + const investments = portfolioCalculator.getInvestments().map((item) => { return { date: item.date, investment: item.investment.toNumber() }; }); + + // Add investment of today + const investmentOfToday = investments.filter((investment) => { + return investment.date === format(new Date(), DATE_FORMAT); + }); + + if (investmentOfToday.length <= 0) { + const pastInvestments = investments.filter((investment) => { + return isBefore(parseDate(investment.date), new Date()); + }); + const lastInvestment = pastInvestments[pastInvestments.length - 1]; + + investments.push({ + date: format(new Date(), DATE_FORMAT), + investment: lastInvestment?.investment ?? 0 + }); + } + + return sortBy(investments, (investment) => { + return investment.date; + }); } public async getChart( diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index a8b99ea48..d21d0d9cb 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -10,6 +10,7 @@ import { ViewChild } from '@angular/core'; import { primaryColorRgb } from '@ghostfolio/common/config'; +import { parseDate } from '@ghostfolio/common/helper'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { Chart, @@ -19,7 +20,7 @@ import { PointElement, TimeScale } from 'chart.js'; -import { addMonths, isAfter, parseISO, subMonths } from 'date-fns'; +import { addDays, isAfter, parseISO, subDays } from 'date-fns'; @Component({ selector: 'gf-investment-chart', @@ -27,8 +28,10 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns'; templateUrl: './investment-chart.component.html', styleUrls: ['./investment-chart.component.scss'] }) -export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { +export class InvestmentChartComponent implements OnChanges, OnDestroy { + @Input() daysInMarket: number; @Input() investments: InvestmentItem[]; + @Input() isInPercent = false; @ViewChild('chartCanvas') chartCanvas; @@ -45,8 +48,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { ); } - public ngOnInit() {} - public ngOnChanges() { if (this.investments) { this.initialize(); @@ -61,19 +62,25 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { this.isLoading = true; if (this.investments?.length > 0) { - // Extend chart by three months (before) + // Extend chart by 5% of days in market (before) const firstItem = this.investments[0]; this.investments.unshift({ ...firstItem, - date: subMonths(parseISO(firstItem.date), 3).toISOString(), + date: subDays( + parseISO(firstItem.date), + this.daysInMarket * 0.05 || 90 + ).toISOString(), investment: 0 }); - // Extend chart by three months (after) + // Extend chart by 5% of days in market (after) const lastItem = this.investments[this.investments.length - 1]; this.investments.push({ ...lastItem, - date: addMonths(new Date(), 3).toISOString() + date: addDays( + parseDate(lastItem.date), + this.daysInMarket * 0.05 || 90 + ).toISOString() }); } @@ -136,12 +143,26 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { } }, y: { - display: false, + display: !this.isInPercent, grid: { display: false }, ticks: { - display: false + display: true, + callback: (tickValue, index, ticks) => { + if (index === 0 || index === ticks.length - 1) { + // Only print last and first legend entry + if (typeof tickValue === 'number') { + return tickValue.toFixed(2); + } + + return tickValue; + } + + return ''; + }, + mirror: true, + z: 1 } } } diff --git a/apps/client/src/app/components/investment-chart/investment-chart.module.ts b/apps/client/src/app/components/investment-chart/investment-chart.module.ts index faef8596e..2af174b1c 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.module.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.module.ts @@ -7,7 +7,6 @@ import { InvestmentChartComponent } from './investment-chart.component'; @NgModule({ declarations: [InvestmentChartComponent], exports: [InvestmentChartComponent], - imports: [CommonModule, NgxSkeletonLoaderModule], - providers: [] + imports: [CommonModule, NgxSkeletonLoaderModule] }) export class GfInvestmentChartModule {} diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index e2ed6065f..1c9b872fe 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; -import { ToggleOption } from '@ghostfolio/common/types'; +import { differenceInDays } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -16,28 +16,10 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './analysis-page.html' }) export class AnalysisPageComponent implements OnDestroy, OnInit { - public accounts: { - [symbol: string]: Pick & { value: number }; - }; - public continents: { - [code: string]: { name: string; value: number }; - }; - public countries: { - [code: string]: { name: string; value: number }; - }; + public daysInMarket: number; public deviceType: string; public hasImpersonationId: boolean; - public period = 'current'; - public periodOptions: ToggleOption[] = [ - { label: 'Initial', value: 'original' }, - { label: 'Current', value: 'current' } - ]; public investments: InvestmentItem[]; - public portfolioPositions: { [symbol: string]: PortfolioPosition }; - public positions: { [symbol: string]: any }; - public sectors: { - [name: string]: { name: string; value: number }; - }; public user: User; private unsubscribeSubject = new Subject(); @@ -69,8 +51,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.dataService .fetchInvestments() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.investments = response; + .subscribe(({ firstOrderDate, investments }) => { + this.daysInMarket = differenceInDays(new Date(), firstOrderDate); + this.investments = investments; this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 66cda46c7..4d396d6cd 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -11,6 +11,8 @@ diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6191fd9b1..d975c4cb5 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -5,7 +5,6 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; -import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; @@ -23,13 +22,13 @@ import { InfoItem, PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioPerformance, PortfolioPublicDetails, PortfolioReport, PortfolioSummary, User } from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { DataSource, Order as OrderModel } from '@prisma/client'; @@ -124,6 +123,18 @@ export class DataService { return info; } + public fetchInvestments(): Observable { + return this.http.get('/api/portfolio/investments').pipe( + map((response) => { + if (response.firstOrderDate) { + response.firstOrderDate = parseISO(response.firstOrderDate); + } + + return response; + }) + ); + } + public fetchSymbolItem({ dataSource, includeHistoricalData = false, @@ -170,10 +181,6 @@ export class DataService { ); } - public fetchInvestments() { - return this.http.get('/api/portfolio/investments'); - } - public fetchPortfolioDetails(aParams: { [param: string]: any }) { return this.http.get('/api/portfolio/details', { params: aParams diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d9bcc3a8b..d88a572ce 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { Export } from './export.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioDetails } from './portfolio-details.interface'; +import { PortfolioInvestments } from './portfolio-investments.interface'; import { PortfolioItem } from './portfolio-item.interface'; import { PortfolioOverview } from './portfolio-overview.interface'; import { PortfolioPerformance } from './portfolio-performance.interface'; @@ -33,6 +34,7 @@ export { InfoItem, PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioItem, PortfolioOverview, PortfolioPerformance, diff --git a/libs/common/src/lib/interfaces/portfolio-investments.interface.ts b/libs/common/src/lib/interfaces/portfolio-investments.interface.ts new file mode 100644 index 000000000..06e91fbd2 --- /dev/null +++ b/libs/common/src/lib/interfaces/portfolio-investments.interface.ts @@ -0,0 +1,6 @@ +import { InvestmentItem } from './investment-item.interface'; + +export interface PortfolioInvestments { + firstOrderDate: Date; + investments: InvestmentItem[]; +} From 1e526852a7573086073069d26851282d991da4fe Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 1 Jan 2022 13:55:53 +0100 Subject: [PATCH 076/337] Bugfix/fix mapping for russia in trackinsight data enhancer (#610) * Fix mapping for Russia * Update changelog --- CHANGELOG.md | 1 + .../data-enhancer/trackinsight/trackinsight.service.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46be289e2..01065cac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an exception with the market state caused by a failed data provider request - Fixed an exception in the portfolio position endpoint - Fixed the reload of the position detail dialog (with query parameters) +- Fixed the missing mapping for Russia in the data enhancer for symbol profile data via _Trackinsight_ ## 1.98.0 - 29.12.2021 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 d7dfb3b42..a469e57a5 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 @@ -7,6 +7,9 @@ const getJSON = bent('json'); export class TrackinsightDataEnhancerService implements DataEnhancerInterface { private static baseUrl = 'https://data.trackinsight.com/holdings'; private static countries = require('countries-list/dist/countries.json'); + private static countriesMapping = { + 'Russian Federation': 'Russia' + }; private static sectorsMapping = { 'Consumer Discretionary': 'Consumer Cyclical', 'Consumer Defensive': 'Consumer Staples', @@ -45,7 +48,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { for (const [key, country] of Object.entries( TrackinsightDataEnhancerService.countries )) { - if (country.name === name) { + if ( + country.name === name || + country.name === + TrackinsightDataEnhancerService.countriesMapping[name] + ) { countryCode = key; break; } From 9ac67b0af22f629dd0625aa75a1eb8b35e05ae6e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 1 Jan 2022 16:18:18 +0100 Subject: [PATCH 077/337] Feature/expose profile data gathering by symbol endpoint (#611) * Expose profile data gathering by symbol endpoint * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/admin/admin.controller.ts | 23 +++++++++++++++++++ .../admin-market-data.component.ts | 13 +++++++++++ .../admin-market-data/admin-market-data.html | 7 ++++++ apps/client/src/app/services/admin.service.ts | 13 +++++++++++ 5 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01065cac5..105aa05ad 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 + +- Exposed the profile data gathering by symbol as an endpoint + ### Changed - Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index ab20df270..e24522444 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -96,6 +96,29 @@ export class AdminController { return; } + @Post('gather/profile-data/:dataSource/:symbol') + @UseGuards(AuthGuard('jwt')) + public async gatherProfileDataForSymbol( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + this.dataGatheringService.gatherProfileData([{ dataSource, symbol }]); + + return; + } + @Post('gather/:dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async gatherSymbol( 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 87ebfea28..a8d96233a 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 @@ -43,6 +43,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } + public onGatherProfileDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .gatherProfileDataBySymbol({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + public onGatherSymbol({ dataSource, symbol 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 b0df54364..3410e99ed 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 @@ -38,6 +38,13 @@ > Gather Data +
diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 66488f4ac..b10d3fed0 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -20,6 +20,19 @@ export class AdminService { return this.http.post(`/api/admin/gather/profile-data`, {}); } + public gatherProfileDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.http.post( + `/api/admin/gather/profile-data/${dataSource}/${symbol}`, + {} + ); + } + public gatherSymbol({ dataSource, date, From 313d2a2f79a95f3e2c21e61625b227c7cca5a07d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 1 Jan 2022 16:40:55 +0100 Subject: [PATCH 078/337] Release 1.99.0 (#612) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105aa05ad..b2a1752da 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.99.0 - 01.01.2022 ### Added diff --git a/package.json b/package.json index 13eda22fe..02ea8c378 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.98.0", + "version": "1.99.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From aca37a27f9d2def594056c55d87010e5188cca84 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 2 Jan 2022 13:29:45 +0100 Subject: [PATCH 079/337] Feature/add top performers to analysis page (#613) * Add Top 3 / Bottom 3 performers * Update changelog --- CHANGELOG.md | 6 ++ .../analysis/analysis-page.component.ts | 25 +++++- .../portfolio/analysis/analysis-page.html | 80 ++++++++++++++++++- .../analysis/analysis-page.module.ts | 6 +- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a1752da..06b2cf246 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 _Top 3_ and _Bottom 3_ performers to the analysis page + ## 1.99.0 - 01.01.2022 ### Added diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 1c9b872fe..9cf798a18 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -2,9 +2,10 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { Position, User } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { differenceInDays } from 'date-fns'; +import { sortBy } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -16,10 +17,12 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './analysis-page.html' }) export class AnalysisPageComponent implements OnDestroy, OnInit { + public bottom3: Position[]; public daysInMarket: number; public deviceType: string; public hasImpersonationId: boolean; public investments: InvestmentItem[]; + public top3: Position[]; public user: User; private unsubscribeSubject = new Subject(); @@ -58,6 +61,26 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); + this.dataService + .fetchPositions({ range: 'max' }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ positions }) => { + const positionsSorted = sortBy( + positions, + 'netPerformancePercentage' + ).reverse(); + + this.top3 = positionsSorted.slice(0, 3); + + if (positions?.length > 3) { + this.bottom3 = positionsSorted.slice(-3).reverse(); + } else { + this.bottom3 = []; + } + + this.changeDetectorRef.markForCheck(); + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 4d396d6cd..361110d76 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -5,7 +5,7 @@ TimelineInvestment Timeline @@ -19,4 +19,82 @@
+ +
+
+ + + Top 3 + + +
+
+ {{ i + 1 }}. {{ position.name }} +
+
+ +
+
+
+ +
+
+
+
+
+ + + Bottom 3 + + +
+
+ {{ i + 1 }}. {{ position.name }} +
+
+ +
+
+
+ +
+
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts index 53e9bb283..ec22a3704 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { AnalysisPageRoutingModule } from './analysis-page-routing.module'; import { AnalysisPageComponent } from './analysis-page.component'; @@ -13,7 +15,9 @@ import { AnalysisPageComponent } from './analysis-page.component'; AnalysisPageRoutingModule, CommonModule, GfInvestmentChartModule, - MatCardModule + GfValueModule, + MatCardModule, + NgxSkeletonLoaderModule ], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] From 69c9e259b1605ff9aa2a96a44d3b8aec4d2fb5a9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 3 Jan 2022 21:31:55 +0100 Subject: [PATCH 080/337] Bugfix/fix routing of create activity dialog (#615) * Fix routing of create activity dialog * Update changelog --- CHANGELOG.md | 4 ++++ .../home-holdings/home-holdings.html | 1 + .../home-overview/home-overview.component.ts | 7 ++++++ .../home-overview/home-overview.html | 2 +- .../positions-table.component.html | 7 +++++- .../positions-table.component.ts | 1 + .../positions/positions.component.html | 5 +++- .../positions/positions.component.ts | 1 + .../app/components/rules/rules.component.html | 5 +++- .../app/components/rules/rules.component.ts | 1 + .../allocations/allocations-page.component.ts | 7 ++++++ .../allocations/allocations-page.html | 1 + .../portfolio/report/report-page.component.ts | 24 +++++++++++++++++-- .../pages/portfolio/report/report-page.html | 15 +++++++++--- .../transactions-page.component.ts | 2 +- 15 files changed, 73 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b2cf246..573fd1161 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 _Top 3_ and _Bottom 3_ performers to the analysis page +### Fixed + +- Fixed the routing of the create activity dialog + ## 1.99.0 - 01.01.2022 ### Added 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 ccc2ed73f..881c0eec5 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -14,6 +14,7 @@
diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index 69f09678c..97ab56d4c 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -123,7 +123,12 @@ }" > -
+
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 b06a052be..0c44be05d 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 @@ -26,6 +26,7 @@ import { Subject, Subscription } from 'rxjs'; export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { @Input() baseCurrency: string; @Input() deviceType: string; + @Input() hasPermissionToCreateOrder: boolean; @Input() locale: string; @Input() positions: PortfolioPosition[]; diff --git a/apps/client/src/app/components/positions/positions.component.html b/apps/client/src/app/components/positions/positions.component.html index 61ed865f7..1a6f44270 100644 --- a/apps/client/src/app/components/positions/positions.component.html +++ b/apps/client/src/app/components/positions/positions.component.html @@ -23,7 +23,10 @@ [range]="range" > -
+
diff --git a/apps/client/src/app/components/positions/positions.component.ts b/apps/client/src/app/components/positions/positions.component.ts index bd0540eff..bbd7dcf37 100644 --- a/apps/client/src/app/components/positions/positions.component.ts +++ b/apps/client/src/app/components/positions/positions.component.ts @@ -17,6 +17,7 @@ import { Position } from '@ghostfolio/common/interfaces'; export class PositionsComponent implements OnChanges, OnInit { @Input() baseCurrency: string; @Input() deviceType: string; + @Input() hasPermissionToCreateOrder: boolean; @Input() locale: string; @Input() positions: Position[]; @Input() range: string; diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index ba193f495..a049e6134 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -1,7 +1,10 @@
- + diff --git a/apps/client/src/app/components/rules/rules.component.ts b/apps/client/src/app/components/rules/rules.component.ts index 0043be1ab..e5429bdcc 100644 --- a/apps/client/src/app/components/rules/rules.component.ts +++ b/apps/client/src/app/components/rules/rules.component.ts @@ -8,6 +8,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; styleUrls: ['./rules.component.scss'] }) export class RulesComponent { + @Input() hasPermissionToCreateOrder: boolean; @Input() rules: PortfolioReportRule; public constructor() {} 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 678c87801..5ab8ba1b5 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 @@ -12,6 +12,7 @@ import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ToggleOption } from '@ghostfolio/common/types'; import { AssetClass } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -36,6 +37,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; public deviceType: string; public hasImpersonationId: boolean; + public hasPermissionToCreateOrder: boolean; public period = 'current'; public periodOptions: ToggleOption[] = [ { label: 'Initial', value: 'original' }, @@ -120,6 +122,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.hasPermissionToCreateOrder = hasPermission( + this.user.permissions, + permissions.createOrder + ); + this.changeDetectorRef.markForCheck(); } }); 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 b5207257c..a210c4cdb 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -197,6 +197,7 @@ diff --git a/apps/client/src/app/pages/portfolio/report/report-page.component.ts b/apps/client/src/app/pages/portfolio/report/report-page.component.ts index c4a1ce2fb..057923d09 100644 --- a/apps/client/src/app/pages/portfolio/report/report-page.component.ts +++ b/apps/client/src/app/pages/portfolio/report/report-page.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -14,6 +16,8 @@ export class ReportPageComponent implements OnDestroy, OnInit { public accountClusterRiskRules: PortfolioReportRule[]; public currencyClusterRiskRules: PortfolioReportRule[]; public feeRules: PortfolioReportRule[]; + public hasPermissionToCreateOrder: boolean; + public user: User; private unsubscribeSubject = new Subject(); @@ -22,7 +26,8 @@ export class ReportPageComponent implements OnDestroy, OnInit { */ public constructor( private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService + private dataService: DataService, + private userService: UserService ) {} /** @@ -41,6 +46,21 @@ export class ReportPageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.hasPermissionToCreateOrder = hasPermission( + this.user.permissions, + permissions.createOrder + ); + + this.changeDetectorRef.markForCheck(); + } + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/portfolio/report/report-page.html b/apps/client/src/app/pages/portfolio/report/report-page.html index 6b093119b..759fb3345 100644 --- a/apps/client/src/app/pages/portfolio/report/report-page.html +++ b/apps/client/src/app/pages/portfolio/report/report-page.html @@ -15,15 +15,24 @@

Currency Cluster Risks

- +

Account Cluster Risks

- +

Fees

- +
diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 2b9f85a0d..b6a1ee647 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -62,7 +62,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['createDialog'] && this.hasPermissionToCreateOrder) { + if (params['createDialog']) { this.openCreateTransactionDialog(); } else if (params['editDialog']) { if (this.transactions) { From 5882b7914ded9fcc3981e91172456d911ad1c588 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 5 Jan 2022 20:22:59 +0100 Subject: [PATCH 081/337] Feature/add first months in open source blog post (#616) * Add blog post * Update changelog --- CHANGELOG.md | 2 + apps/client/src/app/app-routing.module.ts | 7 + .../hallo-ghostfolio-page.html | 107 +++++----- .../hallo-ghostfolio-page.module.ts | 2 - .../hello-ghostfolio-page.html | 76 +++---- .../hello-ghostfolio-page.module.ts | 2 - ...nths-in-open-source-page-routing.module.ts | 19 ++ ...st-months-in-open-source-page.component.ts | 9 + .../first-months-in-open-source-page.html | 185 ++++++++++++++++++ ...first-months-in-open-source-page.module.ts | 13 ++ .../first-months-in-open-source-page.scss | 3 + apps/client/src/app/pages/blog/blog-page.html | 20 ++ apps/client/src/assets/sitemap.xml | 4 + apps/client/src/styles.scss | 2 +- 14 files changed, 356 insertions(+), 95 deletions(-) create mode 100644 apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page-routing.module.ts create mode 100644 apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts create mode 100644 apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html create mode 100644 apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module.ts create mode 100644 apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 573fd1161..25317c6e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the _Top 3_ and _Bottom 3_ performers to the analysis page +- Added a blog post ### Fixed - Fixed the routing of the create activity dialog +- Fixed the link color in the blog posts ## 1.99.0 - 01.01.2022 diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index cd4e383cd..8fde79cd7 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -59,6 +59,13 @@ const routes: Routes = [ './pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module' ).then((m) => m.HelloGhostfolioPageModule) }, + { + path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source', + loadChildren: () => + import( + './pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module' + ).then((m) => m.FirstMonthsInOpenSourcePageModule) + }, { path: 'home', loadChildren: () => diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html index cea9ae9da..9e0aadf22 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html @@ -1,6 +1,6 @@
-
+

Hallo Ghostfolio 👋

@@ -141,58 +141,59 @@
    -
  • - Aktie - Altersvorsorge - Anlage - App - Cryptocurrency - ETF - Feedback - Fintech - Ghostfolio - Investition - Open Source - OSS - Portfolio - Software - Strategie - Trading - TypeScript - Vermögen - Wealth Management +
  • + Aktie +
  • +
  • + Altersvorsorge +
  • +
  • + Anlage +
  • +
  • + App +
  • +
  • + Cryptocurrency +
  • +
  • + Feedback +
  • +
  • + Fintech +
  • +
  • + Ghostfolio +
  • +
  • + Investition +
  • +
  • + Open Source +
  • +
  • + OSS +
  • +
  • + Portfolio +
  • +
  • + Software +
  • +
  • + Strategie +
  • +
  • + Trading +
  • +
  • + TypeScript +
  • +
  • + Vermögen +
  • +
  • + Wealth Management
diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module.ts b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module.ts index 203ab2ccc..75eef2a55 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module.ts +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module.ts @@ -7,9 +7,7 @@ import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component' @NgModule({ declarations: [HalloGhostfolioPageComponent], - exports: [], imports: [CommonModule, HalloGhostfolioPageRoutingModule, RouterModule], - providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class HalloGhostfolioPageModule {} diff --git a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html index d83deb55b..c7260bd23 100644 --- a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html +++ b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html @@ -1,6 +1,6 @@
-
+

Hello Ghostfolio 👋

@@ -136,42 +136,44 @@
    -
  • - Cryptocurrency - ETF - Fintech - Ghostfolio - Investment - Open Source - OSS - Portfolio - Software - Stock - Strategy - Wealth - Wealth Management +
  • + Cryptocurrency +
  • +
  • + ETF +
  • +
  • + Fintech +
  • +
  • + Ghostfolio +
  • +
  • + Investment +
  • +
  • + Open Source +
  • +
  • + OSS +
  • +
  • + Portfolio +
  • +
  • + Software +
  • +
  • + Stock +
  • +
  • + Strategy +
  • +
  • + Wealth +
  • +
  • + Wealth Management
diff --git a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module.ts b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module.ts index c9b7d82f5..a885ef14f 100644 --- a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module.ts +++ b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module.ts @@ -7,9 +7,7 @@ import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component' @NgModule({ declarations: [HelloGhostfolioPageComponent], - exports: [], imports: [CommonModule, HelloGhostfolioPageRoutingModule, RouterModule], - providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class HelloGhostfolioPageModule {} diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page-routing.module.ts b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page-routing.module.ts new file mode 100644 index 000000000..96f8ab532 --- /dev/null +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page-routing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component'; + +const routes: Routes = [ + { + path: '', + component: FirstMonthsInOpenSourcePageComponent, + canActivate: [AuthGuard] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class FirstMonthsInOpenSourceRoutingModule {} diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts new file mode 100644 index 000000000..84e5aae45 --- /dev/null +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + host: { class: 'mb-5' }, + selector: 'gf-first-months-in-open-source-page', + styleUrls: ['./first-months-in-open-source-page.scss'], + templateUrl: './first-months-in-open-source-page.html' +}) +export class FirstMonthsInOpenSourcePageComponent {} diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html new file mode 100644 index 000000000..71ea7489f --- /dev/null +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html @@ -0,0 +1,185 @@ +
+
+
+
+
+

+ 👻 Ghostfolio – + First months in Open Source +

+
05.01.2022
+
+
+

+ In this article I would like to recap the first months running the + open source project Ghostfolio, a + web-based personal finance management software. +

+
+
+

From 1* to 100 stars on GitHub

+

+ When I decided to + publish + the project as + open source software + (OSS), I did not know what exactly to expect. In the worst case, + nobody would care. And in the best case, the repository would be + overrun with contributions. The truth is probably somewhere in + between. +

+

+ In the beginning, it felt quite weird to develop in public where + anyone can observe the progress. Stupid mistakes remain visible + forever. But this feeling, fortunately, quickly settled. I believe + the benefits like all the learning clearly outweigh the drawbacks + when you just do it. +

+

+ At the end of 2021, Ghostfolio reached an important milestone: + 100 stars + on GitHub. This is really exciting with almost no marketing. I am a + technical founder, so I prefer writing code over anything else. But + there is so much more to make this project happen: writing + documentation, maintaining bug reports and feature requests, + supporting users and managing the community, keeping the SaaS + running, etc. +

+

+ Reaching 100 stars will not only attract very early adopters, but + also the early adopters. At the same time, the demands and + expectations are also increasing. +

+
+
+

What is new?

+

+ During the last months, Ghostfolio has transformed from a one man + project into a prospering wealth management application with 9 + contributors and counting. User feedback has directly shaped the + direction of the product development. +

+

These are some selected key features:

+
    +
  • + Simplified self-hosting with an + official Ghostfolio docker image + on Docker Hub +
  • +
  • Improved import for activities (transactions and dividend)
  • +
  • Enriched market data for ETFs (region and industries)
  • +
+
+
+

What is coming?

+

Here is a brief overview of what I am planning in 2022.

+

+ The goal remains to offer a simple and solid software to manage + personal finances. Thus, the main focus is on the core + functionality. +

+

+ My personal goal is to reach break-even with the Saas offering (Ghostfolio Premium) and regularly report about the progress and my learnings on this + exciting journey. +

+

+ I have already started to build a + community + of users. In the future, I would like to involve more contributors + to further extend the functionality of Ghostfolio (e.g. with new + reports). Get in touch with me by email at + hi@ghostfol.io or on Twitter + @ghostfolio_ if you + are interested, I’m happy to discuss ideas. +

+

+ I would like to say thank you for all your feedback and support + during the last months. +

+

+ Have a great start into the new year and happy investing
+ Thomas from Ghostfolio +

+
+
+

* Pro Tip: add the first star to your own open source project

+
+
+
    +
  • + BuildInPublic +
  • +
  • + Community +
  • +
  • + Cryptocurrency +
  • +
  • + Docker +
  • +
  • + ETF +
  • +
  • + Fintech +
  • +
  • + Ghostfolio +
  • +
  • + Image +
  • +
  • + Investment +
  • +
  • + Open Source +
  • +
  • + OSS +
  • +
  • + Portfolio +
  • +
  • + Progress +
  • +
  • + SaaS +
  • +
  • + Software +
  • +
  • + Stock +
  • +
  • + Strategy +
  • +
  • + Wealth +
  • +
  • + Wealth Management +
  • +
+
+
+
+
+
diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module.ts b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module.ts new file mode 100644 index 000000000..e44bd0ddb --- /dev/null +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { FirstMonthsInOpenSourceRoutingModule } from './first-months-in-open-source-page-routing.module'; +import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component'; + +@NgModule({ + declarations: [FirstMonthsInOpenSourcePageComponent], + imports: [CommonModule, FirstMonthsInOpenSourceRoutingModule, RouterModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class FirstMonthsInOpenSourcePageModule {} diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.scss b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html index c97047793..840f19903 100644 --- a/apps/client/src/app/pages/blog/blog-page.html +++ b/apps/client/src/app/pages/blog/blog-page.html @@ -5,6 +5,26 @@
+
https://ghostfol.io/en/blog/2021/07/hello-ghostfolio 2022-01-01T00:00:00+00:00 + + https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source + 2022-01-05T00:00:00+00:00 + https://ghostfol.io/pricing 2022-01-01T00:00:00+00:00 diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 5cc4d43c2..eb162f9fa 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -38,7 +38,7 @@ body { .blog { a { - color: rgba(var(--palette-primary-500), 1); + color: rgba(var(--palette-primary-500), 1) !important; font-weight: 500; &:hover { From 3e16041c16831cb1c3ce80d5b73b20d5f3ffd063 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 5 Jan 2022 20:24:34 +0100 Subject: [PATCH 082/337] Release 1.100.0 (#617) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25317c6e0..8126d7417 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.100.0 - 05.01.2022 ### Added diff --git a/package.json b/package.json index 02ea8c378..b3de40d0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.99.0", + "version": "1.100.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 07de8f87fcb36b3746849ccf24391b857f86ee67 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 7 Jan 2022 08:09:12 +0100 Subject: [PATCH 083/337] Set market prices explicitly (#618) * Set market prices explicitly * Set comments explicitly --- .../portfolio/portfolio-calculator.spec.ts | 362 +++++++++++++----- 1 file changed, 275 insertions(+), 87 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 4dd91509e..1de2a1d15 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1,10 +1,11 @@ -import { parseDate, resetHours } from '@ghostfolio/common/helper'; +import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; import { addDays, differenceInCalendarDays, endOfDay, + format, isBefore, isSameDay } from 'date-fns'; @@ -67,15 +68,202 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; case 'VTI': - return { - marketPrice: new Big('144.38') - .plus( - new Big('0.08').mul( - differenceInCalendarDays(date, parseDate('2019-02-01')) - ) - ) - .toNumber() - }; + switch (format(date, DATE_FORMAT)) { + case '2019-01-01': + return { marketPrice: 144.38 }; + case '2019-02-01': + return { marketPrice: 144.38 }; + case '2019-03-01': + return { marketPrice: 146.62 }; + case '2019-04-01': + return { marketPrice: 149.1 }; + case '2019-05-01': + return { marketPrice: 151.5 }; + case '2019-06-01': + return { marketPrice: 153.98 }; + case '2019-07-01': + return { marketPrice: 156.38 }; + case '2019-08-01': + return { marketPrice: 158.86 }; + case '2019-08-03': + return { marketPrice: 159.02 }; + case '2019-09-01': + return { marketPrice: 161.34 }; + case '2019-10-01': + return { marketPrice: 163.74 }; + case '2019-11-01': + return { marketPrice: 166.22 }; + case '2019-12-01': + return { marketPrice: 168.62 }; + case '2020-01-01': + return { marketPrice: 171.1 }; + case '2020-02-01': + return { marketPrice: 173.58 }; + case '2020-02-02': + return { marketPrice: 173.66 }; + case '2020-03-01': + return { marketPrice: 175.9 }; + case '2020-04-01': + return { marketPrice: 178.38 }; + case '2020-05-01': + return { marketPrice: 180.78 }; + case '2020-06-01': + return { marketPrice: 183.26 }; + case '2020-07-01': + return { marketPrice: 185.66 }; + case '2020-08-01': + return { marketPrice: 188.14 }; + case '2020-08-02': + return { marketPrice: 188.22 }; + case '2020-08-03': + return { marketPrice: 188.3 }; + case '2020-09-01': + return { marketPrice: 190.62 }; + case '2020-10-01': + return { marketPrice: 193.02 }; + case '2020-11-01': + return { marketPrice: 195.5 }; + case '2020-12-01': + return { marketPrice: 197.9 }; + case '2021-01-01': + return { marketPrice: 200.38 }; + case '2021-02-01': + return { marketPrice: 202.86 }; + case '2021-03-01': + return { marketPrice: 205.1 }; + case '2021-04-01': + return { marketPrice: 207.58 }; + case '2021-05-01': + return { marketPrice: 209.98 }; + case '2021-06-01': + return { marketPrice: 212.46 }; + case '2021-06-02': + return { marketPrice: 212.54 }; + case '2021-06-03': + return { marketPrice: 212.62 }; + case '2021-06-04': + return { marketPrice: 212.7 }; + case '2021-06-05': + return { marketPrice: 212.78 }; + case '2021-06-06': + return { marketPrice: 212.86 }; + case '2021-06-07': + return { marketPrice: 212.94 }; + case '2021-06-08': + return { marketPrice: 213.02 }; + case '2021-06-09': + return { marketPrice: 213.1 }; + case '2021-06-10': + return { marketPrice: 213.18 }; + case '2021-06-11': + return { marketPrice: 213.26 }; + case '2021-06-12': + return { marketPrice: 213.34 }; + case '2021-06-13': + return { marketPrice: 213.42 }; + case '2021-06-14': + return { marketPrice: 213.5 }; + case '2021-06-15': + return { marketPrice: 213.58 }; + case '2021-06-16': + return { marketPrice: 213.66 }; + case '2021-06-17': + return { marketPrice: 213.74 }; + case '2021-06-18': + return { marketPrice: 213.82 }; + case '2021-06-19': + return { marketPrice: 213.9 }; + case '2021-06-20': + return { marketPrice: 213.98 }; + case '2021-06-21': + return { marketPrice: 214.06 }; + case '2021-06-22': + return { marketPrice: 214.14 }; + case '2021-06-23': + return { marketPrice: 214.22 }; + case '2021-06-24': + return { marketPrice: 214.3 }; + case '2021-06-25': + return { marketPrice: 214.38 }; + case '2021-06-26': + return { marketPrice: 214.46 }; + case '2021-06-27': + return { marketPrice: 214.54 }; + case '2021-06-28': + return { marketPrice: 214.62 }; + case '2021-06-29': + return { marketPrice: 214.7 }; + case '2021-06-30': + return { marketPrice: 214.78 }; + case '2021-07-01': + return { marketPrice: 214.86 }; + case '2021-07-02': + return { marketPrice: 214.94 }; + case '2021-07-03': + return { marketPrice: 215.02 }; + case '2021-07-04': + return { marketPrice: 215.1 }; + case '2021-07-05': + return { marketPrice: 215.18 }; + case '2021-07-06': + return { marketPrice: 215.26 }; + case '2021-07-07': + return { marketPrice: 215.34 }; + case '2021-07-08': + return { marketPrice: 215.42 }; + case '2021-07-09': + return { marketPrice: 215.5 }; + case '2021-07-10': + return { marketPrice: 215.58 }; + case '2021-07-11': + return { marketPrice: 215.66 }; + case '2021-07-12': + return { marketPrice: 215.74 }; + case '2021-07-13': + return { marketPrice: 215.82 }; + case '2021-07-14': + return { marketPrice: 215.9 }; + case '2021-07-15': + return { marketPrice: 215.98 }; + case '2021-07-16': + return { marketPrice: 216.06 }; + case '2021-07-17': + return { marketPrice: 216.14 }; + case '2021-07-18': + return { marketPrice: 216.22 }; + case '2021-07-19': + return { marketPrice: 216.3 }; + case '2021-07-20': + return { marketPrice: 216.38 }; + case '2021-07-21': + return { marketPrice: 216.46 }; + case '2021-07-22': + return { marketPrice: 216.54 }; + case '2021-07-23': + return { marketPrice: 216.62 }; + case '2021-07-24': + return { marketPrice: 216.7 }; + case '2021-07-25': + return { marketPrice: 216.78 }; + case '2021-07-26': + return { marketPrice: 216.86 }; + case '2021-07-27': + return { marketPrice: 216.94 }; + case '2021-07-28': + return { marketPrice: 217.02 }; + case '2021-07-29': + return { marketPrice: 217.1 }; + case '2021-07-30': + return { marketPrice: 217.18 }; + case '2021-07-31': + return { marketPrice: 217.26 }; + case '2021-08-01': + return { marketPrice: 217.34 }; + case '2020-10-24': + return { marketPrice: 194.86 }; + default: + return { marketPrice: 0 }; + } default: return { marketPrice: 0 }; @@ -1645,14 +1833,14 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('498.3'), netPerformance: new Big('498.3'), investment: new Big('2923.7'), - value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + value: new Big('3422') // 20 * 171.1 }, { date: '2021-01-01', grossPerformance: new Big('349.35'), netPerformance: new Big('349.35'), investment: new Big('652.55'), - value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + value: new Big('1001.9') // 5 * 200.38 } ]); }); @@ -1765,14 +1953,14 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('498.3'), netPerformance: new Big('398.3'), // 100 fees investment: new Big('2923.7'), - value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + value: new Big('3422') // 20 * 171.1 }, { date: '2021-01-01', grossPerformance: new Big('349.35'), netPerformance: new Big('199.35'), // 150 fees investment: new Big('652.55'), - value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + value: new Big('1001.9') // 5 * 200.38 } ]); }); @@ -1808,203 +1996,203 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('0'), netPerformance: new Big('0'), investment: new Big('1443.8'), - value: new Big('1443.8') // 10 * (144.38 + days=0 * 0.08) + value: new Big('1443.8') // 10 * 144.38 }, { date: '2019-03-01', grossPerformance: new Big('22.4'), netPerformance: new Big('22.4'), investment: new Big('1443.8'), - value: new Big('1466.2') // 10 * (144.38 + days=28 * 0.08) + value: new Big('1466.2') // 10 * 146.62 }, { date: '2019-04-01', grossPerformance: new Big('47.2'), netPerformance: new Big('47.2'), investment: new Big('1443.8'), - value: new Big('1491') // 10 * (144.38 + days=59 * 0.08) + value: new Big('1491') // 10 * 149.1 }, { date: '2019-05-01', grossPerformance: new Big('71.2'), netPerformance: new Big('71.2'), investment: new Big('1443.8'), - value: new Big('1515') // 10 * (144.38 + days=89 * 0.08) + value: new Big('1515') // 10 * 151.5 }, { date: '2019-06-01', grossPerformance: new Big('96'), netPerformance: new Big('96'), investment: new Big('1443.8'), - value: new Big('1539.8') // 10 * (144.38 + days=120 * 0.08) + value: new Big('1539.8') // 10 * 153.98 }, { date: '2019-07-01', grossPerformance: new Big('120'), netPerformance: new Big('120'), investment: new Big('1443.8'), - value: new Big('1563.8') // 10 * (144.38 + days=150 * 0.08) + value: new Big('1563.8') // 10 * 156.38 }, { date: '2019-08-01', grossPerformance: new Big('144.8'), netPerformance: new Big('144.8'), investment: new Big('1443.8'), - value: new Big('1588.6') // 10 * (144.38 + days=181 * 0.08) + value: new Big('1588.6') // 10 * 158.86 }, { date: '2019-09-01', grossPerformance: new Big('303.1'), netPerformance: new Big('303.1'), investment: new Big('2923.7'), - value: new Big('3226.8') // 20 * (144.38 + days=212 * 0.08) + value: new Big('3226.8') // 20 * 161.34 }, { date: '2019-10-01', grossPerformance: new Big('351.1'), netPerformance: new Big('351.1'), investment: new Big('2923.7'), - value: new Big('3274.8') // 20 * (144.38 + days=242 * 0.08) + value: new Big('3274.8') // 20 * 163.74 }, { date: '2019-11-01', grossPerformance: new Big('400.7'), netPerformance: new Big('400.7'), investment: new Big('2923.7'), - value: new Big('3324.4') // 20 * (144.38 + days=273 * 0.08) + value: new Big('3324.4') // 20 * 166.22 }, { date: '2019-12-01', grossPerformance: new Big('448.7'), netPerformance: new Big('448.7'), investment: new Big('2923.7'), - value: new Big('3372.4') // 20 * (144.38 + days=303 * 0.08) + value: new Big('3372.4') // 20 * 168.62 }, { date: '2020-01-01', grossPerformance: new Big('498.3'), netPerformance: new Big('498.3'), investment: new Big('2923.7'), - value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + value: new Big('3422') // 20 * 171.1 }, { date: '2020-02-01', grossPerformance: new Big('547.9'), netPerformance: new Big('547.9'), investment: new Big('2923.7'), - value: new Big('3471.6') // 20 * (144.38 + days=365 * 0.08) + value: new Big('3471.6') // 20 * 173.58 }, { date: '2020-03-01', grossPerformance: new Big('226.95'), netPerformance: new Big('226.95'), investment: new Big('652.55'), - value: new Big('879.5') // 5 * (144.38 + days=394 * 0.08) + value: new Big('879.5') // 5 * 175.9 }, { date: '2020-04-01', grossPerformance: new Big('239.35'), netPerformance: new Big('239.35'), investment: new Big('652.55'), - value: new Big('891.9') // 5 * (144.38 + days=425 * 0.08) + value: new Big('891.9') // 5 * 178.38 }, { date: '2020-05-01', grossPerformance: new Big('251.35'), netPerformance: new Big('251.35'), investment: new Big('652.55'), - value: new Big('903.9') // 5 * (144.38 + days=455 * 0.08) + value: new Big('903.9') // 5 * 180.78 }, { date: '2020-06-01', grossPerformance: new Big('263.75'), netPerformance: new Big('263.75'), investment: new Big('652.55'), - value: new Big('916.3') // 5 * (144.38 + days=486 * 0.08) + value: new Big('916.3') // 5 * 183.26 }, { date: '2020-07-01', grossPerformance: new Big('275.75'), netPerformance: new Big('275.75'), investment: new Big('652.55'), - value: new Big('928.3') // 5 * (144.38 + days=516 * 0.08) + value: new Big('928.3') // 5 * 185.66 }, { date: '2020-08-01', grossPerformance: new Big('288.15'), netPerformance: new Big('288.15'), investment: new Big('652.55'), - value: new Big('940.7') // 5 * (144.38 + days=547 * 0.08) + value: new Big('940.7') // 5 * 188.14 }, { date: '2020-09-01', grossPerformance: new Big('300.55'), netPerformance: new Big('300.55'), investment: new Big('652.55'), - value: new Big('953.1') // 5 * (144.38 + days=578 * 0.08) + value: new Big('953.1') // 5 * 190.62 }, { date: '2020-10-01', grossPerformance: new Big('312.55'), netPerformance: new Big('312.55'), investment: new Big('652.55'), - value: new Big('965.1') // 5 * (144.38 + days=608 * 0.08) + value: new Big('965.1') // 5 * 193.02 }, { date: '2020-11-01', grossPerformance: new Big('324.95'), netPerformance: new Big('324.95'), investment: new Big('652.55'), - value: new Big('977.5') // 5 * (144.38 + days=639 * 0.08) + value: new Big('977.5') // 5 * 195.5 }, { date: '2020-12-01', grossPerformance: new Big('336.95'), netPerformance: new Big('336.95'), investment: new Big('652.55'), - value: new Big('989.5') // 5 * (144.38 + days=669 * 0.08) + value: new Big('989.5') // 5 * 197.9 }, { date: '2021-01-01', grossPerformance: new Big('349.35'), netPerformance: new Big('349.35'), investment: new Big('652.55'), - value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + value: new Big('1001.9') // 5 * 200.38 }, { date: '2021-02-01', grossPerformance: new Big('358.85'), netPerformance: new Big('358.85'), investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) + value: new Big('3042.9') // 15 * 202.86 }, { date: '2021-03-01', grossPerformance: new Big('392.45'), netPerformance: new Big('392.45'), investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) + value: new Big('3076.5') // 15 * 205.1 }, { date: '2021-04-01', grossPerformance: new Big('429.65'), netPerformance: new Big('429.65'), investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) + value: new Big('3113.7') // 15 * 207.58 }, { date: '2021-05-01', grossPerformance: new Big('465.65'), netPerformance: new Big('465.65'), investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) + value: new Big('3149.7') // 15 * 209.98 }, { date: '2021-06-01', grossPerformance: new Big('502.85'), netPerformance: new Big('502.85'), investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) + value: new Big('3186.9') // 15 * 212.46 } ]); @@ -2047,49 +2235,49 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('498.3'), netPerformance: new Big('498.3'), investment: new Big('2923.7'), - value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + value: new Big('3422') // 20 * 171.1 }, { date: '2021-01-01', grossPerformance: new Big('349.35'), netPerformance: new Big('349.35'), investment: new Big('652.55'), - value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + value: new Big('1001.9') // 5 * 200.38 }, { date: '2021-02-01', grossPerformance: new Big('358.85'), netPerformance: new Big('358.85'), investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) + value: new Big('3042.9') // 15 * 202.86 }, { date: '2021-03-01', grossPerformance: new Big('392.45'), netPerformance: new Big('392.45'), investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) + value: new Big('3076.5') // 15 * 205.1 }, { date: '2021-04-01', grossPerformance: new Big('429.65'), netPerformance: new Big('429.65'), investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) + value: new Big('3113.7') // 15 * 207.58 }, { date: '2021-05-01', grossPerformance: new Big('465.65'), netPerformance: new Big('465.65'), investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) + value: new Big('3149.7') // 15 * 209.98 }, { date: '2021-06-01', grossPerformance: new Big('502.85'), netPerformance: new Big('502.85'), investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) + value: new Big('3186.9') // 15 * 212.46 } ]); }); @@ -2134,252 +2322,252 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('498.3'), netPerformance: new Big('498.3'), investment: new Big('2923.7'), - value: new Big('3422') // 20 * (144.38 + days=335 * 0.08) + value: new Big('3422') // 20 * 171.1 }, { date: '2021-01-01', grossPerformance: new Big('349.35'), netPerformance: new Big('349.35'), investment: new Big('652.55'), - value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08) + value: new Big('1001.9') // 5 * 200.38 }, { date: '2021-02-01', grossPerformance: new Big('358.85'), netPerformance: new Big('358.85'), investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08) + value: new Big('3042.9') // 15 * 202.86 }, { date: '2021-03-01', grossPerformance: new Big('392.45'), netPerformance: new Big('392.45'), investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08) + value: new Big('3076.5') // 15 * 205.1 }, { date: '2021-04-01', grossPerformance: new Big('429.65'), netPerformance: new Big('429.65'), investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08) + value: new Big('3113.7') // 15 * 207.58 }, { date: '2021-05-01', grossPerformance: new Big('465.65'), netPerformance: new Big('465.65'), investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08) + value: new Big('3149.7') // 15 * 209.98 }, { date: '2021-06-01', grossPerformance: new Big('502.85'), netPerformance: new Big('502.85'), investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) + value: new Big('3186.9') // 15 * 212.46 }, { date: '2021-06-02', grossPerformance: new Big('504.05'), netPerformance: new Big('504.05'), investment: new Big('2684.05'), - value: new Big('3188.1') // 15 * (144.38 + days=852 * 0.08) / +1.2 + value: new Big('3188.1') // 15 * 212.54 }, { date: '2021-06-03', grossPerformance: new Big('505.25'), netPerformance: new Big('505.25'), investment: new Big('2684.05'), - value: new Big('3189.3') // +1.2 + value: new Big('3189.3') // 15 * 212.62 }, { date: '2021-06-04', grossPerformance: new Big('506.45'), netPerformance: new Big('506.45'), investment: new Big('2684.05'), - value: new Big('3190.5') // +1.2 + value: new Big('3190.5') // 15 * 212.7 }, { date: '2021-06-05', grossPerformance: new Big('507.65'), netPerformance: new Big('507.65'), investment: new Big('2684.05'), - value: new Big('3191.7') // +1.2 + value: new Big('3191.7') // 15 * 212.78 }, { date: '2021-06-06', grossPerformance: new Big('508.85'), netPerformance: new Big('508.85'), investment: new Big('2684.05'), - value: new Big('3192.9') // +1.2 + value: new Big('3192.9') // 15 * 212.86 }, { date: '2021-06-07', grossPerformance: new Big('510.05'), netPerformance: new Big('510.05'), investment: new Big('2684.05'), - value: new Big('3194.1') // +1.2 + value: new Big('3194.1') // 15 * 212.94 }, { date: '2021-06-08', grossPerformance: new Big('511.25'), netPerformance: new Big('511.25'), investment: new Big('2684.05'), - value: new Big('3195.3') // +1.2 + value: new Big('3195.3') // 15 * 213.02 }, { date: '2021-06-09', grossPerformance: new Big('512.45'), netPerformance: new Big('512.45'), investment: new Big('2684.05'), - value: new Big('3196.5') // +1.2 + value: new Big('3196.5') // 15 * 213.1 }, { date: '2021-06-10', grossPerformance: new Big('513.65'), netPerformance: new Big('513.65'), investment: new Big('2684.05'), - value: new Big('3197.7') // +1.2 + value: new Big('3197.7') // 15 * 213.18 }, { date: '2021-06-11', grossPerformance: new Big('514.85'), netPerformance: new Big('514.85'), investment: new Big('2684.05'), - value: new Big('3198.9') // +1.2 + value: new Big('3198.9') // 15 * 213.26 }, { date: '2021-06-12', grossPerformance: new Big('516.05'), netPerformance: new Big('516.05'), investment: new Big('2684.05'), - value: new Big('3200.1') // +1.2 + value: new Big('3200.1') // 15 * 213.34 }, { date: '2021-06-13', grossPerformance: new Big('517.25'), netPerformance: new Big('517.25'), investment: new Big('2684.05'), - value: new Big('3201.3') // +1.2 + value: new Big('3201.3') // 15 * 213.42 }, { date: '2021-06-14', grossPerformance: new Big('518.45'), netPerformance: new Big('518.45'), investment: new Big('2684.05'), - value: new Big('3202.5') // +1.2 + value: new Big('3202.5') // 15 * 213.5 }, { date: '2021-06-15', grossPerformance: new Big('519.65'), netPerformance: new Big('519.65'), investment: new Big('2684.05'), - value: new Big('3203.7') // +1.2 + value: new Big('3203.7') // 15 * 213.58 }, { date: '2021-06-16', grossPerformance: new Big('520.85'), netPerformance: new Big('520.85'), investment: new Big('2684.05'), - value: new Big('3204.9') // +1.2 + value: new Big('3204.9') // 15 * 213.66 }, { date: '2021-06-17', grossPerformance: new Big('522.05'), netPerformance: new Big('522.05'), investment: new Big('2684.05'), - value: new Big('3206.1') // +1.2 + value: new Big('3206.1') // 15 * 213.74 }, { date: '2021-06-18', grossPerformance: new Big('523.25'), netPerformance: new Big('523.25'), investment: new Big('2684.05'), - value: new Big('3207.3') // +1.2 + value: new Big('3207.3') // 15 * 213.82 }, { date: '2021-06-19', grossPerformance: new Big('524.45'), netPerformance: new Big('524.45'), investment: new Big('2684.05'), - value: new Big('3208.5') // +1.2 + value: new Big('3208.5') // 15 * 213.9 }, { date: '2021-06-20', grossPerformance: new Big('525.65'), netPerformance: new Big('525.65'), investment: new Big('2684.05'), - value: new Big('3209.7') // +1.2 + value: new Big('3209.7') // 15 * 213.98 }, { date: '2021-06-21', grossPerformance: new Big('526.85'), netPerformance: new Big('526.85'), investment: new Big('2684.05'), - value: new Big('3210.9') // +1.2 + value: new Big('3210.9') // 15 * 214.06 }, { date: '2021-06-22', grossPerformance: new Big('528.05'), netPerformance: new Big('528.05'), investment: new Big('2684.05'), - value: new Big('3212.1') // +1.2 + value: new Big('3212.1') // 15 * 214.14 }, { date: '2021-06-23', grossPerformance: new Big('529.25'), netPerformance: new Big('529.25'), investment: new Big('2684.05'), - value: new Big('3213.3') // +1.2 + value: new Big('3213.3') // 15 * 214.22 }, { date: '2021-06-24', grossPerformance: new Big('530.45'), netPerformance: new Big('530.45'), investment: new Big('2684.05'), - value: new Big('3214.5') // +1.2 + value: new Big('3214.5') // 15 * 214.3 }, { date: '2021-06-25', grossPerformance: new Big('531.65'), netPerformance: new Big('531.65'), investment: new Big('2684.05'), - value: new Big('3215.7') // +1.2 + value: new Big('3215.7') // 15 * 214.38 }, { date: '2021-06-26', grossPerformance: new Big('532.85'), netPerformance: new Big('532.85'), investment: new Big('2684.05'), - value: new Big('3216.9') // +1.2 + value: new Big('3216.9') // 15 * 214.46 }, { date: '2021-06-27', grossPerformance: new Big('534.05'), netPerformance: new Big('534.05'), investment: new Big('2684.05'), - value: new Big('3218.1') // +1.2 + value: new Big('3218.1') // 15 * 214.54 }, { date: '2021-06-28', grossPerformance: new Big('535.25'), netPerformance: new Big('535.25'), investment: new Big('2684.05'), - value: new Big('3219.3') // +1.2 + value: new Big('3219.3') // 15 * 214.62 }, { date: '2021-06-29', grossPerformance: new Big('536.45'), netPerformance: new Big('536.45'), investment: new Big('2684.05'), - value: new Big('3220.5') // +1.2 + value: new Big('3220.5') // 15 * 214.7 }, { date: '2021-06-30', grossPerformance: new Big('537.65'), netPerformance: new Big('537.65'), investment: new Big('2684.05'), - value: new Big('3221.7') // +1.2 + value: new Big('3221.7') // 15 * 214.78 } ]) ); @@ -2442,7 +2630,7 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big('267.2'), netPerformance: new Big('267.2'), investment: new Big('11553.75'), - value: new Big('11820.95') // 10 * (144.38 + days=334 * 0.08) + 5 * 2021.99 + value: new Big('11820.95') // 10 * 171.1 + 5 * 2021.99 } ]); }); From 0168c1c4e8d186f5c38fd2860a081a9d9ff5c78d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Jan 2022 09:37:54 +0100 Subject: [PATCH 084/337] Feature/exclude url pattern of shared portfolios in robots.txt (#619) * Exclude shared portfolios * Update changelog --- CHANGELOG.md | 6 ++++++ apps/client/src/assets/robots.txt | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8126d7417..395fdc199 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 + +- Excluded the url pattern of shared portfolios in the `robots.txt` file + ## 1.100.0 - 05.01.2022 ### Added diff --git a/apps/client/src/assets/robots.txt b/apps/client/src/assets/robots.txt index c78f791d5..87cf3208f 100644 --- a/apps/client/src/assets/robots.txt +++ b/apps/client/src/assets/robots.txt @@ -1,4 +1,5 @@ User-agent: * Allow: / +Disallow: /p/* Sitemap: https://ghostfol.io/sitemap.xml From 075431d868e964fbfe750bb3253d0c6238c6ed38 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Jan 2022 18:19:25 +0100 Subject: [PATCH 085/337] Feature/add google sheets as data source (#620) * Add google sheets as data source * Update changelog --- CHANGELOG.md | 8 + apps/api/src/app/symbol/symbol.controller.ts | 5 +- apps/api/src/app/symbol/symbol.service.ts | 26 --- .../api/src/services/configuration.service.ts | 4 + .../alpha-vantage/alpha-vantage.service.ts | 6 +- .../data-provider/data-provider.module.ts | 5 + .../data-provider/data-provider.service.ts | 6 +- .../ghostfolio-scraper-api.service.ts | 46 ++++- .../google-sheets/google-sheets.service.ts | 172 ++++++++++++++++++ .../interfaces/data-provider.interface.ts | 9 +- .../rakuten-rapid-api.service.ts | 10 +- .../yahoo-finance/yahoo-finance.service.ts | 10 +- .../interfaces/environment.interface.ts | 4 + package.json | 2 + .../migration.sql | 2 + prisma/schema.prisma | 1 + yarn.lock | 130 ++++++++++++- 17 files changed, 383 insertions(+), 63 deletions(-) create mode 100644 apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts create mode 100644 prisma/migrations/20220108083624_added_google_sheets_to_data_source/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 395fdc199..7f3c5eda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added `GOOGLE_SHEETS` as a new data source type + ### Changed - Excluded the url pattern of shared portfolios in the `robots.txt` file +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.100.0 - 05.01.2022 ### Added diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index a5c31fe6a..bbe582cea 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { isDate, isEmpty } from 'lodash'; @@ -37,8 +37,7 @@ export class SymbolController { @Query() { query = '' } ): Promise<{ items: LookupItem[] }> { try { - const encodedQuery = encodeURIComponent(query.toLowerCase()); - return this.symbolService.lookup(encodedQuery); + return this.symbolService.lookup(query.toLowerCase()); } catch { throw new HttpException( getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index b120d7ea2..8c95ce947 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -93,32 +93,6 @@ export class SymbolService { try { const { items } = await this.dataProviderService.search(aQuery); results.items = items; - - // Add custom symbols - const ghostfolioSymbolProfiles = - await this.prismaService.symbolProfile.findMany({ - select: { - currency: true, - dataSource: true, - name: true, - symbol: true - }, - where: { - AND: [ - { - dataSource: DataSource.GHOSTFOLIO, - name: { - startsWith: aQuery - } - } - ] - } - }); - - for (const ghostfolioSymbolProfile of ghostfolioSymbolProfiles) { - results.items.push(ghostfolioSymbolProfile); - } - return results; } catch (error) { Logger.error(error); diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index b2d9c65fb..0c358f099 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -13,6 +13,7 @@ export class ConfigurationService { ACCESS_TOKEN_SALT: str(), ALPHA_VANTAGE_API_KEY: str({ default: '' }), CACHE_TTL: num({ default: 1 }), + DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }), DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }), ENABLE_FEATURE_BLOG: bool({ default: false }), ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), @@ -25,6 +26,9 @@ export class ConfigurationService { ENABLE_FEATURE_SYSTEM_MESSAGE: bool({ default: false }), GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }), GOOGLE_SECRET: str({ default: 'dummySecret' }), + GOOGLE_SHEETS_ACCOUNT: str({ default: '' }), + GOOGLE_SHEETS_ID: str({ default: '' }), + GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), JWT_SECRET_KEY: str({}), MAX_ITEM_IN_CACHE: num({ default: 9999 }), MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), 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 625bbc627..838f1ae6e 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 @@ -88,13 +88,13 @@ export class AlphaVantageService implements DataProviderInterface { return DataSource.ALPHA_VANTAGE; } - public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { - const result = await this.alphaVantage.data.search(aSymbol); + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { + const result = await this.alphaVantage.data.search(aQuery); return { items: result?.bestMatches?.map((bestMatch) => { return { - dataSource: DataSource.ALPHA_VANTAGE, + dataSource: this.getName(), name: bestMatch['2. name'], symbol: bestMatch['1. symbol'] }; diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index 37afb3abf..c05570932 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -1,6 +1,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; +import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; @@ -21,12 +22,14 @@ import { DataProviderService } from './data-provider.service'; AlphaVantageService, DataProviderService, GhostfolioScraperApiService, + GoogleSheetsService, RakutenRapidApiService, YahooFinanceService, { inject: [ AlphaVantageService, GhostfolioScraperApiService, + GoogleSheetsService, RakutenRapidApiService, YahooFinanceService ], @@ -34,11 +37,13 @@ import { DataProviderService } from './data-provider.service'; useFactory: ( alphaVantageService, ghostfolioScraperApiService, + googleSheetsService, rakutenRapidApiService, yahooFinanceService ) => [ alphaVantageService, ghostfolioScraperApiService, + googleSheetsService, rakutenRapidApiService, yahooFinanceService ] 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 15a8a6efb..98237af8b 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -149,13 +149,13 @@ export class DataProviderService { return result; } - public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const promises: Promise<{ items: LookupItem[] }>[] = []; let lookupItems: LookupItem[] = []; for (const dataSource of this.configurationService.get('DATA_SOURCES')) { promises.push( - this.getDataProvider(DataSource[dataSource]).search(aSymbol) + this.getDataProvider(DataSource[dataSource]).search(aQuery) ); } @@ -176,7 +176,7 @@ export class DataProviderService { } public getPrimaryDataSource(): DataSource { - return DataSource[this.configurationService.get('DATA_SOURCES')[0]]; + return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; } private getDataProvider(providerName: DataSource) { 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 77d406fdb..a34f7cf92 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 @@ -1,4 +1,10 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { @@ -13,13 +19,6 @@ import * as bent from 'bent'; import * as cheerio from 'cheerio'; import { format } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; -import { DataProviderInterface } from '../interfaces/data-provider.interface'; - @Injectable() export class GhostfolioScraperApiService implements DataProviderInterface { private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g; @@ -59,7 +58,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { [symbol]: { marketPrice, currency: symbolProfile?.currency, - dataSource: DataSource.GHOSTFOLIO, + dataSource: this.getName(), marketState: MarketState.delayed } }; @@ -116,8 +115,35 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return DataSource.GHOSTFOLIO; } - public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { - return { items: [] }; + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { + const items = await this.prismaService.symbolProfile.findMany({ + select: { + currency: true, + dataSource: true, + name: true, + symbol: true + }, + where: { + OR: [ + { + dataSource: this.getName(), + name: { + mode: 'insensitive', + startsWith: aQuery + } + }, + { + dataSource: this.getName(), + symbol: { + mode: 'insensitive', + startsWith: aQuery + } + } + ] + } + }); + + return { items }; } private extractNumberFromString(aString: string): number { 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 new file mode 100644 index 000000000..81b369279 --- /dev/null +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -0,0 +1,172 @@ +import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; +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 { format } from 'date-fns'; +import { GoogleSpreadsheet } from 'google-spreadsheet'; + +@Injectable() +export class GoogleSheetsService implements DataProviderInterface { + public constructor( + private readonly configurationService: ConfigurationService, + private readonly prismaService: PrismaService, + private readonly symbolProfileService: SymbolProfileService + ) {} + + public canHandle(symbol: string) { + return true; + } + + public async get( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const [symbol] = aSymbols; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [symbol] + ); + + const sheet = await this.getSheet({ + sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), + symbol + }); + const marketPrice = parseFloat( + (await sheet.getCellByA1('B1').value) as string + ); + + return { + [symbol]: { + marketPrice, + currency: symbolProfile?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + } + }; + } catch (error) { + Logger.error(error); + } + + return {}; + } + + public async getHistorical( + aSymbols: string[], + aGranularity: Granularity = 'day', + from: Date, + to: Date + ): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const [symbol] = aSymbols; + + const sheet = await this.getSheet({ + symbol, + sheetId: this.configurationService.get('GOOGLE_SHEETS_ID') + }); + + const rows = await sheet.getRows(); + + const historicalData: { + [date: string]: IDataProviderHistoricalResponse; + } = {}; + + rows + .filter((row, index) => { + return index >= 1; + }) + .forEach((row) => { + const date = new Date(row._rawData[0]); + const close = parseFloat(row._rawData[1]); + + historicalData[format(date, DATE_FORMAT)] = { marketPrice: close }; + }); + + return { + [symbol]: historicalData + }; + } catch (error) { + Logger.error(error); + } + + return {}; + } + + public getName(): DataSource { + return DataSource.GOOGLE_SHEETS; + } + + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { + const items = await this.prismaService.symbolProfile.findMany({ + select: { + currency: true, + dataSource: true, + name: true, + symbol: true + }, + where: { + OR: [ + { + dataSource: this.getName(), + name: { + mode: 'insensitive', + startsWith: aQuery + } + }, + { + dataSource: this.getName(), + symbol: { + mode: 'insensitive', + startsWith: aQuery + } + } + ] + } + }); + + return { items }; + } + + private async getSheet({ + sheetId, + symbol + }: { + sheetId: string; + symbol: string; + }) { + const doc = new GoogleSpreadsheet(sheetId); + + await doc.useServiceAccountAuth({ + client_email: this.configurationService.get('GOOGLE_SHEETS_ACCOUNT'), + private_key: this.configurationService + .get('GOOGLE_SHEETS_PRIVATE_KEY') + .replace(/\\n/g, '\n') + }); + + await doc.loadInfo(); + + const sheet = doc.sheetsByTitle[symbol]; + + await sheet.loadCells(); + + return sheet; + } +} 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 5f99c8614..c5cf4c330 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 @@ -1,11 +1,10 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; -import { Granularity } from '@ghostfolio/common/types'; -import { DataSource } from '@prisma/client'; - import { IDataProviderHistoricalResponse, IDataProviderResponse -} from '../../interfaces/interfaces'; +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { Granularity } from '@ghostfolio/common/types'; +import { DataSource } from '@prisma/client'; export interface DataProviderInterface { canHandle(symbol: string): boolean; @@ -23,5 +22,5 @@ export interface DataProviderInterface { getName(): DataSource; - search(aSymbol: string): Promise<{ items: LookupItem[] }>; + search(aQuery: string): Promise<{ items: LookupItem[] }>; } 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 dea52e74d..bdfc147dd 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 @@ -45,7 +45,7 @@ export class RakutenRapidApiService implements DataProviderInterface { return { [ghostfolioFearAndGreedIndexSymbol]: { currency: undefined, - dataSource: DataSource.RAKUTEN, + dataSource: this.getName(), marketPrice: fgi.now.value, marketState: MarketState.open, name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME @@ -85,7 +85,7 @@ export class RakutenRapidApiService implements DataProviderInterface { await this.prismaService.marketData.create({ data: { symbol, - dataSource: DataSource.RAKUTEN, + dataSource: this.getName(), date: subWeeks(getToday(), 1), marketPrice: fgi.oneWeekAgo.value } @@ -94,7 +94,7 @@ export class RakutenRapidApiService implements DataProviderInterface { await this.prismaService.marketData.create({ data: { symbol, - dataSource: DataSource.RAKUTEN, + dataSource: this.getName(), date: subMonths(getToday(), 1), marketPrice: fgi.oneMonthAgo.value } @@ -103,7 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface { await this.prismaService.marketData.create({ data: { symbol, - dataSource: DataSource.RAKUTEN, + dataSource: this.getName(), date: subYears(getToday(), 1), marketPrice: fgi.oneYearAgo.value } @@ -129,7 +129,7 @@ export class RakutenRapidApiService implements DataProviderInterface { return DataSource.RAKUTEN; } - public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } 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 0717a699e..2b4fe8f92 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 @@ -103,7 +103,7 @@ export class YahooFinanceService implements DataProviderInterface { assetClass, assetSubClass, currency: value.price?.currency, - dataSource: DataSource.YAHOO, + dataSource: this.getName(), exchange: this.parseExchange(value.price?.exchangeName), marketState: value.price?.marketState === 'REGULAR' || @@ -221,12 +221,14 @@ export class YahooFinanceService implements DataProviderInterface { return DataSource.YAHOO; } - public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items: LookupItem[] = []; try { const get = bent( - `${this.yahooFinanceHostname}/v1/finance/search?q=${aSymbol}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`, + `${this.yahooFinanceHostname}/v1/finance/search?q=${encodeURIComponent( + aQuery + )}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`, 'GET', 'json', 200 @@ -268,7 +270,7 @@ export class YahooFinanceService implements DataProviderInterface { items.push({ symbol, currency: value.currency, - dataSource: DataSource.YAHOO, + dataSource: this.getName(), name: value.name }); } diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 56f8fe822..86b049546 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors { ACCESS_TOKEN_SALT: string; ALPHA_VANTAGE_API_KEY: string; CACHE_TTL: number; + DATA_SOURCE_PRIMARY: string; DATA_SOURCES: string | string[]; // string is not correct, error in envalid? ENABLE_FEATURE_BLOG: boolean; ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean; @@ -16,6 +17,9 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_SYSTEM_MESSAGE: boolean; GOOGLE_CLIENT_ID: string; GOOGLE_SECRET: string; + GOOGLE_SHEETS_ACCOUNT: string; + GOOGLE_SHEETS_ID: string; + GOOGLE_SHEETS_PRIVATE_KEY: string; JWT_SECRET_KEY: string; MAX_ITEM_IN_CACHE: number; MAX_ORDERS_TO_IMPORT: number; diff --git a/package.json b/package.json index b3de40d0e..3d1a24d97 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "cryptocurrencies": "7.0.0", "date-fns": "2.22.1", "envalid": "7.2.1", + "google-spreadsheet": "3.2.0", "http-status-codes": "2.2.0", "ionicons": "5.5.1", "lodash": "4.17.21", @@ -143,6 +144,7 @@ "@types/big.js": "6.1.2", "@types/cache-manager": "3.4.2", "@types/color": "3.0.2", + "@types/google-spreadsheet": "3.1.5", "@types/jest": "27.0.2", "@types/lodash": "4.14.174", "@types/node": "14.14.33", diff --git a/prisma/migrations/20220108083624_added_google_sheets_to_data_source/migration.sql b/prisma/migrations/20220108083624_added_google_sheets_to_data_source/migration.sql new file mode 100644 index 000000000..f8923af93 --- /dev/null +++ b/prisma/migrations/20220108083624_added_google_sheets_to_data_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DataSource" ADD VALUE 'GOOGLE_SHEETS'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0d7a313c..0c89f8ec5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -184,6 +184,7 @@ enum AssetSubClass { enum DataSource { ALPHA_VANTAGE GHOSTFOLIO + GOOGLE_SHEETS RAKUTEN YAHOO } diff --git a/yarn.lock b/yarn.lock index c4f08c700..bce12e97b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4487,6 +4487,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/google-spreadsheet@3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@types/google-spreadsheet/-/google-spreadsheet-3.1.5.tgz#2bdc6f9f5372551e0506cb6ef3f562adcf44fc2e" + integrity sha512-7N+mDtZ1pmya2RRFPPl4KYc2TRgiqCNBLUZfyrKfER+u751JgCO+C24/LzF70UmUm/zhHUbzRZ5mtfaxekQ1ZQ== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -5284,6 +5289,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -5774,7 +5786,7 @@ array.prototype.map@^1.0.3: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.5" -arrify@^2.0.1: +arrify@^2.0.0, arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== @@ -5903,6 +5915,13 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axobject-query@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -6124,7 +6143,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -6190,6 +6209,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bignumber.js@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" + integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw== + bignumber.js@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" @@ -8233,7 +8257,7 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -9064,6 +9088,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter-asyncresource@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" @@ -9233,7 +9262,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -9324,6 +9353,11 @@ fast-safe-stringify@2.1.1: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-text-encoding@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -9587,6 +9621,11 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== +follow-redirects@^1.14.0: + version "1.14.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" + integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== + follow-redirects@^1.14.4: version "1.14.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" @@ -9831,6 +9870,25 @@ gauge@^4.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +gaxios@^4.0.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.2.tgz#845827c2dc25a0213c8ab4155c7a28910f5be83f" + integrity sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.1" + +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -10086,11 +10144,51 @@ globby@^9.0.0, globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" +google-auth-library@^6.1.3: + version "6.1.6" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.6.tgz#deacdcdb883d9ed6bac78bb5d79a078877fdf572" + integrity sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.2.tgz#c3d61c2da8e10843ff830fdb0d2059046238c1d4" + integrity sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A== + dependencies: + node-forge "^0.10.0" + +google-spreadsheet@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/google-spreadsheet/-/google-spreadsheet-3.2.0.tgz#ce8aa75c15705aa950ad52b091a6fc4d33dcb329" + integrity sha512-z7XMaqb+26rdo8p51r5O03u8aPLAPzn5YhOXYJPcf2hdMVr0dUbIARgdkRdmGiBeoV/QoU/7VNhq1MMCLZv3kQ== + dependencies: + axios "^0.21.4" + google-auth-library "^6.1.3" + lodash "^4.17.21" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +gtoken@^5.0.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.1.tgz#c1c2598a826f2b5df7c6bb53d7be6cf6d50c3c78" + integrity sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.0.3" + jws "^4.0.0" + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -12175,6 +12273,13 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -12295,6 +12400,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jwk-to-pem@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz#151310bcfbcf731adc5ad9f379cbc8b395742906" @@ -12312,6 +12426,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + karma-source-map-support@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" From a83441b3bae121e116f927e1f220b942b2a4f82e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Jan 2022 18:21:33 +0100 Subject: [PATCH 086/337] Release 1.101.0 (#621) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f3c5eda3..6983303d2 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.101.0 - 08.01.2022 ### Added diff --git a/package.json b/package.json index 3d1a24d97..26f25ec79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.100.0", + "version": "1.101.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From b2b3fde80e209b5b8fbd079ed8f0acedaa6f15bb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:23:47 +0100 Subject: [PATCH 087/337] Bugfix/support multiple accounts with the same name (#623) * Support multiple accounts with the same name * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/portfolio/portfolio.service.ts | 14 ++++++++------ .../allocations/allocations-page.component.ts | 4 ++-- .../lib/interfaces/portfolio-details.interface.ts | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6983303d2..2cceba33c 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 + +- Fixed the support for multiple accounts with the same name + ## 1.101.0 - 08.01.2022 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 1902fd137..9ed19810f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -107,7 +107,7 @@ export class PortfolioService { account.currency, userCurrency ), - value: details.accounts[account.name]?.current ?? 0 + value: details.accounts[account.id]?.current ?? 0 }; delete result.Order; @@ -1091,10 +1091,11 @@ export class PortfolioService { account.currency, userCurrency ); - accounts[account.name] = { + accounts[account.id] = { balance: convertedBalance, currency: account.currency, current: convertedBalance, + name: account.name, original: convertedBalance }; @@ -1108,16 +1109,17 @@ export class PortfolioService { originalValueOfSymbol *= -1; } - if (accounts[order.Account?.name || UNKNOWN_KEY]?.current) { - accounts[order.Account?.name || UNKNOWN_KEY].current += + if (accounts[order.Account?.id || UNKNOWN_KEY]?.current) { + accounts[order.Account?.id || UNKNOWN_KEY].current += currentValueOfSymbol; - accounts[order.Account?.name || UNKNOWN_KEY].original += + accounts[order.Account?.id || UNKNOWN_KEY].original += originalValueOfSymbol; } else { - accounts[order.Account?.name || UNKNOWN_KEY] = { + accounts[order.Account?.id || UNKNOWN_KEY] = { balance: 0, currency: order.Account?.currency, current: currentValueOfSymbol, + name: account.name, original: originalValueOfSymbol }; } 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 5ab8ba1b5..c19af7fb4 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 @@ -162,10 +162,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } }; - for (const [name, { current, original }] of Object.entries( + for (const [id, { current, name, original }] of Object.entries( this.portfolioDetails.accounts )) { - this.accounts[name] = { + this.accounts[id] = { name, value: aPeriod === 'original' ? original : current }; diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index 2cbf1c6fa..17430438b 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -2,10 +2,11 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces'; export interface PortfolioDetails { accounts: { - [name: string]: { + [id: string]: { balance: number; currency: string; current: number; + name: string; original: number; }; }; From 7df53896f3745829e541fbabb0329a2a234f1a95 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 11 Jan 2022 19:49:45 +0100 Subject: [PATCH 088/337] Feature/start eliminating data source from order (#622) * Start eliminating dataSource from order * Update changelog --- CHANGELOG.md | 4 ++ .../portfolio/current-rate.service.spec.ts | 13 ------ .../src/app/portfolio/current-rate.service.ts | 41 ------------------- .../interfaces/get-value-params.interface.ts | 6 --- .../portfolio/portfolio-calculator.spec.ts | 13 +----- .../src/app/portfolio/portfolio.service.ts | 4 +- 6 files changed, 7 insertions(+), 74 deletions(-) delete mode 100644 apps/api/src/app/portfolio/interfaces/get-value-params.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cceba33c..ef67e69f1 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 +### Changed + +- Start eliminating `dataSource` from activity + ### Fixed - Fixed the support for multiple accounts with the same name diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index bb9aa78b6..ca45483e1 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -85,19 +85,6 @@ describe('CurrentRateService', () => { ); }); - it('getValue', async () => { - expect( - await currentRateService.getValue({ - currency: 'USD', - date: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)), - symbol: 'AMZN', - userCurrency: 'CHF' - }) - ).toMatchObject({ - marketPrice: 1847.839966 - }); - }); - it('getValues', async () => { expect( await currentRateService.getValues({ diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 3c7a6bde5..fac041837 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -7,7 +7,6 @@ import { isBefore, isToday } from 'date-fns'; import { flatten } from 'lodash'; import { GetValueObject } from './interfaces/get-value-object.interface'; -import { GetValueParams } from './interfaces/get-value-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface'; @Injectable() @@ -18,46 +17,6 @@ export class CurrentRateService { private readonly marketDataService: MarketDataService ) {} - public async getValue({ - currency, - date, - symbol, - userCurrency - }: GetValueParams): Promise { - if (isToday(date)) { - const dataProviderResult = await this.dataProviderService.get([ - { - symbol, - dataSource: this.dataProviderService.getPrimaryDataSource() - } - ]); - return { - symbol, - date: resetHours(date), - marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0 - }; - } - - const marketData = await this.marketDataService.get({ - date, - symbol - }); - - if (marketData) { - return { - date: marketData.date, - marketPrice: this.exchangeRateDataService.toCurrency( - marketData.marketPrice, - currency, - userCurrency - ), - symbol: marketData.symbol - }; - } - - throw new Error(`Value not found for ${symbol} at ${resetHours(date)}`); - } - public async getValues({ currencies, dataGatheringItems, diff --git a/apps/api/src/app/portfolio/interfaces/get-value-params.interface.ts b/apps/api/src/app/portfolio/interfaces/get-value-params.interface.ts deleted file mode 100644 index a9b934271..000000000 --- a/apps/api/src/app/portfolio/interfaces/get-value-params.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface GetValueParams { - currency: string; - date: Date; - symbol: string; - userCurrency: string; -} diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 1de2a1d15..74c45f026 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1,17 +1,9 @@ import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; -import { - addDays, - differenceInCalendarDays, - endOfDay, - format, - isBefore, - isSameDay -} from 'date-fns'; +import { addDays, endOfDay, format, isBefore, isSameDay } from 'date-fns'; import { CurrentRateService } from './current-rate.service'; -import { GetValueParams } from './interfaces/get-value-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { TimelinePeriod } from './interfaces/timeline-period.interface'; @@ -275,9 +267,6 @@ jest.mock('./current-rate.service', () => { // eslint-disable-next-line @typescript-eslint/naming-convention CurrentRateService: jest.fn().mockImplementation(() => { return { - getValue: ({ date, symbol }: GetValueParams) => { - return Promise.resolve(mockGetValue(symbol, date)); - }, getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => { const result = []; if (dateQuery.lt) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9ed19810f..15170dcef 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -428,7 +428,7 @@ export class PortfolioService { }) .map((order) => ({ currency: order.currency, - dataSource: order.dataSource, + dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big(order.fee), name: order.SymbolProfile?.name, @@ -1038,7 +1038,7 @@ export class PortfolioService { const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, - dataSource: order.dataSource, + dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, date: format(order.date, DATE_FORMAT), fee: new Big( this.exchangeRateDataService.toCurrency( From d3707bbb87f5403a088a45d5fbd5d02a97054acc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 11 Jan 2022 19:50:22 +0100 Subject: [PATCH 089/337] Bugfix/fix preselected default account in create activity dialog (#624) * Fix preselected default account * Update changelog --- CHANGELOG.md | 1 + .../transactions-page.component.ts | 107 ++++++++++-------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef67e69f1..8fe934494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed the support for multiple accounts with the same name +- Fixed the preselected default account of the create activity dialog ## 1.101.0 - 08.01.2022 diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index b6a1ee647..3ccdb0921 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -106,20 +106,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { - this.user = state.user; - - this.defaultAccountId = this.user?.accounts.find((account) => { - return account.isDefault; - })?.id; - - this.hasPermissionToCreateOrder = hasPermission( - this.user.permissions, - permissions.createOrder - ); - this.hasPermissionToDeleteOrder = hasPermission( - this.user.permissions, - permissions.deleteOrder - ); + this.updateUser(state.user); this.changeDetectorRef.markForCheck(); } @@ -352,43 +339,50 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } private openCreateTransactionDialog(aTransaction?: OrderModel): void { - const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { - data: { - accounts: this.user?.accounts?.filter((account) => { - return account.accountType === 'SECURITIES'; - }), - transaction: { - accountId: aTransaction?.accountId ?? this.defaultAccountId, - currency: aTransaction?.currency ?? null, - dataSource: aTransaction?.dataSource ?? null, - date: new Date(), - fee: 0, - quantity: null, - symbol: aTransaction?.symbol ?? null, - type: aTransaction?.type ?? 'BUY', - unitPrice: null - }, - user: this.user - }, - height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() + this.userService + .get() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const transaction: CreateOrderDto = data?.transaction; + .subscribe((user) => { + this.updateUser(user); - if (transaction) { - this.dataService.postOrder(transaction).subscribe({ - next: () => { - this.fetchOrders(); + const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { + data: { + accounts: this.user?.accounts?.filter((account) => { + return account.accountType === 'SECURITIES'; + }), + transaction: { + accountId: aTransaction?.accountId ?? this.defaultAccountId, + currency: aTransaction?.currency ?? null, + dataSource: aTransaction?.dataSource ?? null, + date: new Date(), + fee: 0, + quantity: null, + symbol: aTransaction?.symbol ?? null, + type: aTransaction?.type ?? 'BUY', + unitPrice: null + }, + user: this.user + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data: any) => { + const transaction: CreateOrderDto = data?.transaction; + + if (transaction) { + this.dataService.postOrder(transaction).subscribe({ + next: () => { + this.fetchOrders(); + } + }); } - }); - } - this.router.navigate(['.'], { relativeTo: this.route }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); }); } @@ -397,7 +391,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .get() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((user) => { - this.user = user; + this.updateUser(user); const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, @@ -419,4 +413,21 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); }); } + + private updateUser(aUser: User) { + this.user = aUser; + + this.defaultAccountId = this.user?.accounts.find((account) => { + return account.isDefault; + })?.id; + + this.hasPermissionToCreateOrder = hasPermission( + this.user.permissions, + permissions.createOrder + ); + this.hasPermissionToDeleteOrder = hasPermission( + this.user.permissions, + permissions.deleteOrder + ); + } } From 9bbb856f663389d93b316a8ce4885ae67f945d9a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 11 Jan 2022 19:53:23 +0100 Subject: [PATCH 090/337] Release 1.102.0 (#625) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe934494..bbec30610 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.102.0 - 11.01.2022 ### Changed diff --git a/package.json b/package.json index 26f25ec79..579c419c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.101.0", + "version": "1.102.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From d6b78f3457144c0f0c3dd5c834ca7411df9caf4a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 13 Jan 2022 19:07:23 +0100 Subject: [PATCH 091/337] Feature/add links to statistics section (#626) * Add links and clean up style * Update changelog --- CHANGELOG.md | 6 +++ .../src/app/pages/about/about-page.html | 47 ++++++++++++++----- .../src/app/pages/about/about-page.scss | 34 +------------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbec30610..c25f19f1a 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 + +- Added links to the statistics section on the about page + ## 1.102.0 - 11.01.2022 ### Changed diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index 44c8ae9cc..ca3375652 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -132,16 +132,35 @@
-

{{ statistics?.gitHubContributors ?? '-' }}

-
Contributors on GitHub
+ +

+ {{ statistics?.gitHubContributors ?? '-' }} +

+
Contributors on GitHub
+
-

{{ statistics?.gitHubStargazers ?? '-' }}

-
Stars on GitHub
+ +

{{ statistics?.gitHubStargazers ?? '-' }}

+
Stars on GitHub
+
@@ -150,22 +169,28 @@
diff --git a/apps/client/src/app/pages/about/about-page.scss b/apps/client/src/app/pages/about/about-page.scss index 2e4517709..498665eff 100644 --- a/apps/client/src/app/pages/about/about-page.scss +++ b/apps/client/src/app/pages/about/about-page.scss @@ -2,13 +2,8 @@ color: rgb(var(--dark-primary-text)); display: block; - a { - color: rgb(var(--dark-primary-text)); - } - .mat-card { - &.about-container, - &.changelog { + &.about-container { a { color: rgba(var(--palette-primary-500), 1); font-weight: 500; @@ -19,29 +14,6 @@ } } - &.changelog { - ::ng-deep { - markdown { - h1, - p { - display: none; - } - - h2 { - font-size: 18px; - - &:not(:first-of-type) { - margin-top: 2rem; - } - } - - h3 { - font-size: 15px; - } - } - } - } - .independent-and-bootstrapped-logo { background-image: url('/assets/bootstrapped-dark.svg'); background-position: center; @@ -57,10 +29,6 @@ :host-context(.is-dark-theme) { color: rgb(var(--light-primary-text)); - a { - color: rgb(var(--light-primary-text)); - } - .mat-card { .independent-and-bootstrapped-logo { background-image: url('/assets/bootstrapped-light.svg'); From e344c43a5aa3b559234a245002803548b83f0067 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 13 Jan 2022 20:25:21 +0100 Subject: [PATCH 092/337] Bugfix/fix currency of value in position detail dialog (#627) * Fix currency * Update changelog --- CHANGELOG.md | 4 ++++ .../position-detail-dialog/position-detail-dialog.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c25f19f1a..c36baa819 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 links to the statistics section on the about page +### Fixed + +- Fixed the currency of the value in the position detail dialog + ## 1.102.0 - 11.01.2022 ### Changed diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 3bd2b780b..00d949263 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -12,7 +12,7 @@
From 92e502e1c26d481318684b8ff2daf16c3f82fe47 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 13 Jan 2022 20:33:31 +0100 Subject: [PATCH 093/337] Release 1.103.0 (#628) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c36baa819..10b0c4d2e 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.103.0 - 13.01.2022 ### Changed diff --git a/package.json b/package.json index 579c419c8..4ce006446 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.102.0", + "version": "1.103.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1a4109ebaa1d1ea08b93d993a268f359d469eb00 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 16 Jan 2022 13:46:00 +0100 Subject: [PATCH 094/337] Bugfix/fix fallback to load currencies directly from data provider (#629) * Fix fallback * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/services/exchange-rate-data.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10b0c4d2e..387ebb8ba 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 + +- Fixed the fallback to load currencies directly from the data provider + ## 1.103.0 - 13.01.2022 ### Changed diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index e83516e27..f77f7ef79 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -58,9 +58,9 @@ export class ExchangeRateDataService { getYesterday() ); - if (isEmpty(result)) { + if (Object.keys(result).length !== this.currencyPairs.length) { // Load currencies directly from data provider as a fallback - // if historical data is not yet available + // if historical data is not fully available const historicalData = await this.dataProviderService.get( this.currencyPairs.map(({ dataSource, symbol }) => { return { dataSource, symbol }; From 0a8d159f78250f02c051b20859707db218519eca Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 16 Jan 2022 15:31:56 +0100 Subject: [PATCH 095/337] Bugfix/fix missing symbol profile data connection in import (#630) * Fix missing symbol profile data connection in import * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/import/import.service.ts | 24 +++++++++++++++++----- apps/api/src/app/order/order.controller.ts | 10 ++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 387ebb8ba..9db53d78e 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 ### Fixed - Fixed the fallback to load currencies directly from the data provider +- Fixed the missing symbol profile data connection in the import functionality for activities ## 1.103.0 - 13.01.2022 diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index b17404ae9..6bf936137 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -34,11 +34,6 @@ export class ImportService { unitPrice } of orders) { await this.orderService.createOrder({ - Account: { - connect: { - id_userId: { userId, id: accountId } - } - }, currency, dataSource, fee, @@ -46,7 +41,26 @@ export class ImportService { symbol, type, unitPrice, + Account: { + connect: { + id_userId: { userId, id: accountId } + } + }, date: parseISO((date)), + SymbolProfile: { + connectOrCreate: { + create: { + dataSource, + symbol + }, + where: { + dataSource_symbol: { + dataSource, + symbol + } + } + } + }, User: { connect: { id: userId } } }); } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index da1c44c95..052138e62 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -116,23 +116,23 @@ export class OrderController { return this.orderService.createOrder({ ...data, + date, Account: { connect: { id_userId: { id: accountId, userId: this.request.user.id } } }, - date, SymbolProfile: { connectOrCreate: { + create: { + dataSource: data.dataSource, + symbol: data.symbol + }, where: { dataSource_symbol: { dataSource: data.dataSource, symbol: data.symbol } - }, - create: { - dataSource: data.dataSource, - symbol: data.symbol } } }, From 651b4bcff7572594fde11f65be66598866b410a9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 16 Jan 2022 15:45:28 +0100 Subject: [PATCH 096/337] Release 1.104.0 (#631) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db53d78e..40b44b9d2 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.104.0 - 16.01.2022 ### Fixed diff --git a/package.json b/package.json index 4ce006446..0b730c36d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.103.0", + "version": "1.104.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 556be61fffe3a03cf9d09d8d36fa96097c60e4b6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 19 Jan 2022 21:28:15 +0100 Subject: [PATCH 097/337] Bugfix/fix unresolved account names in reports (#636) * Fix unresolved account names * Update changelog --- CHANGELOG.md | 6 +++++ .../current-investment.ts | 12 +++++----- .../initial-investment.ts | 22 +++++++++---------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b44b9d2..c1ff0c73d 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 + +- Fixed the unresolved account names in the _X-ray_ section + ## 1.104.0 - 16.01.2022 ### Fixed diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index bff51aabe..3893efd44 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -25,17 +25,17 @@ export class AccountClusterRiskCurrentInvestment extends Rule { }; } = {}; - for (const account of Object.keys(this.accounts)) { - accounts[account] = { - name: account, - investment: this.accounts[account].current + for (const [accountId, account] of Object.entries(this.accounts)) { + accounts[accountId] = { + name: account.name, + investment: account.current }; } let maxItem; let totalInvestment = 0; - Object.values(accounts).forEach((account) => { + for (const account of Object.values(accounts)) { if (!maxItem) { maxItem = account; } @@ -47,7 +47,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule { if (account.investment > maxItem?.investment) { maxItem = account; } - }); + } const maxInvestmentRatio = maxItem.investment / totalInvestment; diff --git a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts index 13da575dd..0eed098cc 100644 --- a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts @@ -19,35 +19,35 @@ export class AccountClusterRiskInitialInvestment extends Rule { } public evaluate(ruleSettings?: Settings) { - const platforms: { + const accounts: { [symbol: string]: Pick & { investment: number; }; } = {}; - for (const account of Object.keys(this.accounts)) { - platforms[account] = { - name: account, - investment: this.accounts[account].original + for (const [accountId, account] of Object.entries(this.accounts)) { + accounts[accountId] = { + name: account.name, + investment: account.original }; } let maxItem; let totalInvestment = 0; - Object.values(platforms).forEach((platform) => { + for (const account of Object.values(accounts)) { if (!maxItem) { - maxItem = platform; + maxItem = account; } // Calculate total investment - totalInvestment += platform.investment; + totalInvestment += account.investment; // Find maximum - if (platform.investment > maxItem?.investment) { - maxItem = platform; + if (account.investment > maxItem?.investment) { + maxItem = account; } - }); + } const maxInvestmentRatio = maxItem.investment / totalInvestment; From 38f2930ec68e979f815331a776e9a713781dab00 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 20 Jan 2022 21:34:23 +0100 Subject: [PATCH 098/337] Feature/improve data provider service (#637) * Improve data provider service * Update changelog --- CHANGELOG.md | 9 ++++ .../data-provider/data-provider.service.ts | 29 ++++++++----- .../google-sheets/google-sheets.service.ts | 41 +++++++++++-------- .../admin-market-data/admin-market-data.html | 4 +- 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1ff0c73d..2ca1317fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for fetching multiple symbols in the `GOOGLE_SHEETS` data provider + +### Changed + +- Improved the data provider with grouping by data source and thereby reducing the number of requests + ### Fixed - Fixed the unresolved account names in the _X-ray_ section +- Fixed the date conversion in the `GOOGLE_SHEETS` data provider ## 1.104.0 - 16.01.2022 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 98237af8b..f051927f9 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -12,7 +12,7 @@ import { Granularity } from '@ghostfolio/common/types'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource, MarketData } from '@prisma/client'; import { format, isValid } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { groupBy, isEmpty } from 'lodash'; @Injectable() export class DataProviderService { @@ -30,18 +30,27 @@ export class DataProviderService { [symbol: string]: IDataProviderResponse; } = {}; - for (const item of items) { - const dataProvider = this.getDataProvider(item.dataSource); - response[item.symbol] = (await dataProvider.get([item.symbol]))[ - item.symbol - ]; - } + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); const promises = []; - for (const symbol of Object.keys(response)) { - const promise = Promise.resolve(response[symbol]); + + 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((currentResponse) => (response[symbol] = currentResponse)) + promise.then((result) => { + for (const [symbol, dataProviderResponse] of Object.entries(result)) { + response[symbol] = dataProviderResponse; + } + }) ); } 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 81b369279..fff9db21e 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 @@ -8,7 +8,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 } from '@ghostfolio/common/helper'; +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'; @@ -35,27 +35,36 @@ export class GoogleSheetsService implements DataProviderInterface { } try { - const [symbol] = aSymbols; - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( - [symbol] + 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 + symbol: 'Overview' }); - const marketPrice = parseFloat( - (await sheet.getCellByA1('B1').value) as string - ); - return { - [symbol]: { - marketPrice, - currency: symbolProfile?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed + 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); } @@ -94,7 +103,7 @@ export class GoogleSheetsService implements DataProviderInterface { return index >= 1; }) .forEach((row) => { - const date = new Date(row._rawData[0]); + const date = parseDate(row._rawData[0]); const close = parseFloat(row._rawData[1]); historicalData[format(date, DATE_FORMAT)] = { marketPrice: close }; 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 3410e99ed..50d6790a5 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 @@ -16,8 +16,8 @@ class="cursor-pointer mat-row" (click)="setCurrentSymbol(item.symbol)" > - {{ item.symbol }} - {{ item.dataSource}} + {{ item.symbol }} + {{ item.dataSource }} {{ (item.date | date: defaultDateFormat) ?? '' }} From 1c6050d3e388acf4473cacd328c8e14f99d72c42 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 20 Jan 2022 21:35:56 +0100 Subject: [PATCH 099/337] Release 1.105.0 (#638) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca1317fc..441bf491e 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.105.0 - 20.01.2022 ### Added diff --git a/package.json b/package.json index 0b730c36d..c7bdc14eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.104.0", + "version": "1.105.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 5607c6bb52084a4911b5a20896286a410c2b0f6a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 21 Jan 2022 20:07:56 +0100 Subject: [PATCH 100/337] Update blog url (#639) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1175981cb..ed545ed4e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Open Source Wealth Management Software made for Humans

- Live Demo | Ghostfolio Premium | Blog | Slack | Twitter + Live Demo | Ghostfolio Premium | Blog | Slack | Twitter

From 3261e3ee59a5b49a2d549c9b9b6109dbade0c44b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 21 Jan 2022 20:30:41 +0100 Subject: [PATCH 101/337] Feature/upgrade stripe dependencies (#641) * Upgrade stripe dependencies * Update changelog --- CHANGELOG.md | 6 ++++++ package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441bf491e..0a0d0ca5c 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 _Stripe_ dependencies + ## 1.105.0 - 20.01.2022 ### Added diff --git a/package.json b/package.json index c7bdc14eb..9fec7f0c2 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", - "@stripe/stripe-js": "1.15.0", + "@stripe/stripe-js": "1.22.0", "@types/papaparse": "5.2.6", "alphavantage": "2.2.0", "angular-material-css-vars": "3.0.0", @@ -110,7 +110,7 @@ "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", - "stripe": "8.156.0", + "stripe": "8.199.0", "svgmap": "2.6.0", "tslib": "2.0.0", "uuid": "8.3.2", diff --git a/yarn.lock b/yarn.lock index bce12e97b..38d7112da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4347,10 +4347,10 @@ resolve-from "^5.0.0" store2 "^2.12.0" -"@stripe/stripe-js@1.15.0": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.15.0.tgz#86178cfbe66151910b09b03595e60048ab4c698e" - integrity sha512-KQsNPc+uVQkc8dewwz1A6uHOWeU2cWoZyNIbsx5mtmperr5TPxw4u8M20WOa22n6zmIOh/zLdzEe8DYK/0IjBw== +"@stripe/stripe-js@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.22.0.tgz#9d3d2f0a1ce81f185ec477fd7cc67544b2b2a00c" + integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg== "@tootallnate/once@1": version "1.1.2" @@ -16952,10 +16952,10 @@ strip-json-comments@^2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -stripe@8.156.0: - version "8.156.0" - resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.156.0.tgz#040de551df88d71ef670a8c8d4df114c3fa6eb4b" - integrity sha512-q+bixlhaxnSI/Htk/iB1i5LhuZ557hL0pFgECBxQNhso1elxIsOsPOIXEuo3tSLJEb8CJSB7t/+Fyq6KP69tAQ== +stripe@8.199.0: + version "8.199.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.199.0.tgz#dcd109f16ff0c33da638a0d154c966d0f20c73d1" + integrity sha512-Bc5Zfp6eOOCdde9x5NPrAczeGSKuNwemzjsfGJXWtpbUfQXgJujzTGgkhx2YuzamqakDYJkTgf9w7Ry2uY8QNA== dependencies: "@types/node" ">=8.1.0" qs "^6.6.0" From ba05f5ba3092739e6b7f13805d63325ab6950e2e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 22 Jan 2022 09:36:58 +0100 Subject: [PATCH 102/337] Feature/upgrade prisma to version 3.8.1 (#640) * Upgrade prisma to version 3.8.1 * 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 0a0d0ca5c..c000eb252 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 _Stripe_ dependencies +- Upgraded `prisma` from version `3.7.0` to `3.8.1` ## 1.105.0 - 20.01.2022 diff --git a/package.json b/package.json index 9fec7f0c2..c34cbc96e 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", "@nrwl/angular": "13.4.1", - "@prisma/client": "3.7.0", + "@prisma/client": "3.8.1", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -106,7 +106,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.7.0", + "prisma": "3.8.1", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 38d7112da..814879352 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3349,22 +3349,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.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a" - integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw== +"@prisma/client@3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0" + integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ== dependencies: - "@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + "@prisma/engines-version" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" -"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": - version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90" - integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg== +"@prisma/engines-version@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": + version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4c8d9744b5e54650a8ba5fde0a711399d6adba24" + integrity sha512-G2JH6yWt6ixGKmsRmVgaQYahfwMopim0u/XLIZUo2o/mZ5jdu7+BL+2V5lZr7XiG1axhyrpvlyqE/c0OgYSl3g== -"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": - version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2" - integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg== +"@prisma/engines@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": + version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4479099b99f6a082ce5843ee7208943ccedd127f" + integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -15025,12 +15025,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.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2" - integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg== +prisma@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873" + integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA== dependencies: - "@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" + "@prisma/engines" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1" From 9d907b5eb592c8a0dc50bcbd0a267ebe64144308 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 22 Jan 2022 09:38:01 +0100 Subject: [PATCH 103/337] Bugfix/improve the redirection on logout (#642) * Improve logout * Update changelog --- CHANGELOG.md | 4 ++++ apps/client/src/app/app.component.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c000eb252..d61e7b508 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 - Upgraded _Stripe_ dependencies - Upgraded `prisma` from version `3.7.0` to `3.8.1` +### Fixed + +- Improved the redirection on logout + ## 1.105.0 - 20.01.2022 ### Added diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index aeb996b9d..7f6774f52 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -89,7 +89,7 @@ export class AppComponent implements OnDestroy, OnInit { this.tokenStorageService.signOut(); this.userService.remove(); - this.router.navigate(['/']); + document.location.href = '/'; } public ngOnDestroy() { From 585f99e4df83a222b99a6e0ef9f46651bf99ff49 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 23 Jan 2022 11:39:30 +0100 Subject: [PATCH 104/337] Feature/add summary row to activities table (#645) * Add summary row to activities table * Update changelog --- CHANGELOG.md | 4 ++ apps/api/src/app/import/import.module.ts | 5 +- .../order/interfaces/activities.interface.ts | 10 ++++ apps/api/src/app/order/order.controller.ts | 15 ++++-- apps/api/src/app/order/order.module.ts | 2 + apps/api/src/app/order/order.service.ts | 25 +++++++-- .../src/app/portfolio/portfolio.service.ts | 22 ++++---- .../accounts-table.component.html | 2 +- .../transactions-page.component.ts | 13 ++--- .../transactions/transactions-page.html | 2 +- apps/client/src/app/services/data.service.ts | 15 +++--- .../activities-table.component.html | 54 ++++++++++++++++++- .../activities-table.component.scss | 19 +++++++ .../activities-table.component.ts | 46 ++++++++++++++-- 14 files changed, 192 insertions(+), 42 deletions(-) create mode 100644 apps/api/src/app/order/interfaces/activities.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d61e7b508..40bfd330b 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 the footer row with total fees and total value to the activities table + ### Changed - Upgraded _Stripe_ dependencies diff --git a/apps/api/src/app/import/import.module.ts b/apps/api/src/app/import/import.module.ts index c7533980d..35781e499 100644 --- a/apps/api/src/app/import/import.module.ts +++ b/apps/api/src/app/import/import.module.ts @@ -1,5 +1,5 @@ import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; -import { OrderService } from '@ghostfolio/api/app/order/order.service'; +import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; @@ -15,10 +15,11 @@ import { ImportService } from './import.service'; ConfigurationModule, DataGatheringModule, DataProviderModule, + OrderModule, PrismaModule, RedisCacheModule ], controllers: [ImportController], - providers: [CacheService, ImportService, OrderService] + providers: [CacheService, ImportService] }) export class ImportModule {} diff --git a/apps/api/src/app/order/interfaces/activities.interface.ts b/apps/api/src/app/order/interfaces/activities.interface.ts new file mode 100644 index 000000000..e14adce0b --- /dev/null +++ b/apps/api/src/app/order/interfaces/activities.interface.ts @@ -0,0 +1,10 @@ +import { OrderWithAccount } from '@ghostfolio/common/types'; + +export interface Activities { + activities: Activity[]; +} + +export interface Activity extends OrderWithAccount { + feeInBaseCurrency: number; + valueInBaseCurrency: number; +} diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 052138e62..eaf01a4f0 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -23,6 +23,7 @@ import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { CreateOrderDto } from './create-order.dto'; +import { Activities } from './interfaces/activities.interface'; import { OrderService } from './order.service'; import { UpdateOrderDto } from './update-order.dto'; @@ -59,14 +60,16 @@ export class OrderController { @UseGuards(AuthGuard('jwt')) public async getAllOrders( @Headers('impersonation-id') impersonationId - ): Promise { + ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId( impersonationId, this.request.user.id ); + const userCurrency = this.request.user.Settings.currency; - let orders = await this.orderService.getOrders({ + let activities = await this.orderService.getOrders({ + userCurrency, includeDrafts: true, userId: impersonationUserId || this.request.user.id }); @@ -75,15 +78,17 @@ export class OrderController { impersonationUserId || this.userService.isRestrictedView(this.request.user) ) { - orders = nullifyValuesInObjects(orders, [ + activities = nullifyValuesInObjects(activities, [ 'fee', + 'feeInBaseCurrency', 'quantity', 'unitPrice', - 'value' + 'value', + 'valueInBaseCurrency' ]); } - return orders; + return { activities }; } @Get(':id') diff --git a/apps/api/src/app/order/order.module.ts b/apps/api/src/app/order/order.module.ts index 92a503d68..3e6f6fca8 100644 --- a/apps/api/src/app/order/order.module.ts +++ b/apps/api/src/app/order/order.module.ts @@ -4,6 +4,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { Module } from '@nestjs/common'; @@ -16,6 +17,7 @@ import { OrderService } from './order.service'; ConfigurationModule, DataGatheringModule, DataProviderModule, + ExchangeRateDataModule, ImpersonationModule, PrismaModule, RedisCacheModule, diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 1654380ae..af386e209 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -1,5 +1,6 @@ import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -7,10 +8,13 @@ import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; +import { Activity } from './interfaces/activities.interface'; + @Injectable() export class OrderService { public constructor( private readonly cacheService: CacheService, + private readonly exchangeRateDataService: ExchangeRateDataService, private readonly dataGatheringService: DataGatheringService, private readonly prismaService: PrismaService ) {} @@ -86,12 +90,14 @@ export class OrderService { public async getOrders({ includeDrafts = false, types, + userCurrency, userId }: { includeDrafts?: boolean; types?: TypeOfOrder[]; + userCurrency: string; userId: string; - }) { + }): Promise { const where: Prisma.OrderWhereInput = { userId }; if (includeDrafts === false) { @@ -124,12 +130,21 @@ export class OrderService { orderBy: { date: 'asc' } }) ).map((order) => { + const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); + return { ...order, - value: new Big(order.quantity) - .mul(order.unitPrice) - .plus(order.fee) - .toNumber() + value, + feeInBaseCurrency: this.exchangeRateDataService.toCurrency( + order.fee, + order.currency, + userCurrency + ), + valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + value, + order.currency, + userCurrency + ) }; }); } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 15170dcef..fbbd5db0f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -388,11 +388,12 @@ export class PortfolioService { aImpersonationId: string, aSymbol: string ): Promise { + const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const orders = (await this.orderService.getOrders({ userId })).filter( - (order) => order.symbol === aSymbol - ); + const orders = ( + await this.orderService.getOrders({ userCurrency, userId }) + ).filter((order) => order.symbol === aSymbol); if (orders.length <= 0) { return { @@ -846,24 +847,25 @@ export class PortfolioService { } public async getSummary(aImpersonationId: string): Promise { - const currency = this.request.user.Settings.currency; + const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); const performanceInformation = await this.getPerformance(aImpersonationId); const { balance } = await this.accountService.getCashDetails( userId, - currency + userCurrency ); const orders = await this.orderService.getOrders({ + userCurrency, userId }); const dividend = this.getDividend(orders).toNumber(); const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; - const totalBuy = this.getTotalByType(orders, currency, 'BUY'); - const totalSell = this.getTotalByType(orders, currency, 'SELL'); + const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); + const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); const committedFunds = new Big(totalBuy).sub(totalSell); @@ -895,8 +897,8 @@ export class PortfolioService { }: { cashDetails: CashDetails; investment: Big; - value: Big; userCurrency: string; + value: Big; }) { const cashPositions = {}; @@ -1025,8 +1027,11 @@ export class PortfolioService { transactionPoints: TransactionPoint[]; orders: OrderWithAccount[]; }> { + const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; + const orders = await this.orderService.getOrders({ includeDrafts, + userCurrency, userId, types: ['BUY', 'SELL'] }); @@ -1035,7 +1040,6 @@ export class PortfolioService { return { transactionPoints: [], orders: [] }; } - const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, 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 1871e7f3c..f08ea8430 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 @@ -15,7 +15,7 @@ >(Default) - Total + Total diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 3ccdb0921..8606cd395 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -28,6 +29,7 @@ import { ImportTransactionDialog } from './import-transaction-dialog/import-tran templateUrl: './transactions-page.html' }) export class TransactionsPageComponent implements OnDestroy, OnInit { + public activities: Activity[]; public defaultAccountId: string; public deviceType: string; public hasImpersonationId: boolean; @@ -35,7 +37,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { public hasPermissionToDeleteOrder: boolean; public hasPermissionToImportOrders: boolean; public routeQueryParams: Subscription; - public transactions: OrderModel[]; public user: User; private primaryDataSource: DataSource; @@ -65,8 +66,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { if (params['createDialog']) { this.openCreateTransactionDialog(); } else if (params['editDialog']) { - if (this.transactions) { - const transaction = this.transactions.find(({ id }) => { + if (this.activities) { + const transaction = this.activities.find(({ id }) => { return id === params['transactionId']; }); @@ -119,10 +120,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.dataService .fetchOrders() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.transactions = response; + .subscribe(({ activities }) => { + this.activities = activities; - if (this.hasPermissionToCreateOrder && this.transactions?.length <= 0) { + if (this.hasPermissionToCreateOrder && this.activities?.length <= 0) { this.router.navigate([], { queryParams: { createDialog: true } }); } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index 6674025fe..db365c2d9 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -3,7 +3,7 @@

+ Total @@ -93,6 +99,7 @@ {{ element.type }}
+ @@ -107,6 +114,7 @@ >
+ @@ -122,6 +130,9 @@ {{ element.currency }} + + {{ baseCurrency }} + @@ -143,6 +154,11 @@ >
+ @@ -164,6 +180,11 @@ >
+ @@ -176,7 +197,7 @@ > Fee - +
+ +
+ +
+
@@ -197,7 +227,7 @@ > Value - +
+ +
+ +
+
@@ -223,6 +262,11 @@ {{ element.Account?.name }}
+ @@ -276,6 +320,7 @@ + @@ -291,6 +336,11 @@ " [ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }" > + ; @ViewChild(MatSort) sort: MatSort; - public dataSource: MatTableDataSource = - new MatTableDataSource(); + public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat = DEFAULT_DATE_FORMAT; public displayedColumns = []; public endOfToday = endOfToday(); @@ -71,6 +72,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public searchControl = new FormControl(); public searchKeywords: string[] = []; public separatorKeysCodes: number[] = [ENTER, COMMA]; + public totalFees: number; + public totalValue: number; private allFilters: string[]; private unsubscribeSubject = new Subject(); @@ -218,6 +221,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { ); this.filters$.next(this.allFilters); + + this.totalFees = this.getTotalFees(); + this.totalValue = this.getTotalValue(); } private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { @@ -263,4 +269,36 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return item !== undefined; }); } + + private getTotalFees() { + let totalFees = new Big(0); + + for (const activity of this.dataSource.filteredData) { + if (isNumber(activity.feeInBaseCurrency)) { + totalFees = totalFees.plus(activity.feeInBaseCurrency); + } else { + return null; + } + } + + return totalFees.toNumber(); + } + + private getTotalValue() { + let totalValue = new Big(0); + + for (const activity of this.dataSource.filteredData) { + if (isNumber(activity.valueInBaseCurrency)) { + if (activity.type === 'BUY') { + totalValue = totalValue.plus(activity.valueInBaseCurrency); + } else if (activity.type === 'SELL') { + totalValue = totalValue.minus(activity.valueInBaseCurrency); + } + } else { + return null; + } + } + + return totalValue.toNumber(); + } } From 9c086edffe3ec9c6c1c15a8bf1c8bfc96ef840f9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 23 Jan 2022 17:02:12 +0100 Subject: [PATCH 105/337] Feature/extend historical data view in admin control (#646) * Extend market data view * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/admin/admin.service.ts | 68 +++++++++++++++++-- .../admin-market-data/admin-market-data.html | 6 +- .../home-overview/home-overview.html | 2 +- .../interfaces/admin-market-data.interface.ts | 5 ++ libs/common/src/lib/interfaces/index.ts | 6 +- 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40bfd330b..cae550a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extended the historical data view in the admin control panel - Upgraded _Stripe_ dependencies - Upgraded `prisma` from version `3.7.0` to `3.8.1` diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 3ea3d8f22..4df3ea09f 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -9,7 +9,8 @@ import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, - AdminMarketDataDetails + AdminMarketDataDetails, + AdminMarketDataItem } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { Property } from '@prisma/client'; @@ -56,12 +57,67 @@ export class AdminService { } public async getMarketData(): Promise { - return { - marketData: await ( - await this.dataGatheringService.getSymbolsMax() - ).map((symbol) => { - return symbol; + const marketData = await this.prismaService.marketData.groupBy({ + _count: true, + by: ['dataSource', 'symbol'] + }); + + const currencyPairsToGather: AdminMarketDataItem[] = + this.exchangeRateDataService + .getCurrencyPairs() + .map(({ dataSource, symbol }) => { + const marketDataItemCount = + marketData.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; + + return { + dataSource, + marketDataItemCount, + symbol + }; + }); + + const symbolProfilesToGather: AdminMarketDataItem[] = ( + await this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }], + select: { + _count: { + select: { Order: true } + }, + dataSource: true, + Order: { + orderBy: [{ date: 'asc' }], + select: { date: true }, + take: 1 + }, + scraperConfiguration: true, + symbol: true + } }) + ).map((symbolProfile) => { + const marketDataItemCount = + marketData.find((marketDataItem) => { + return ( + marketDataItem.dataSource === symbolProfile.dataSource && + marketDataItem.symbol === symbolProfile.symbol + ); + })?._count ?? 0; + + return { + marketDataItemCount, + activityCount: symbolProfile._count.Order, + dataSource: symbolProfile.dataSource, + date: symbolProfile.Order?.[0]?.date, + symbol: symbolProfile.symbol + }; + }); + + return { + marketData: [...currencyPairsToGather, ...symbolProfilesToGather] }; } 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 50d6790a5..d1e45bfc9 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 @@ -6,7 +6,9 @@ Symbol Data Source - First Transaction + First Activity + Activity Count + Historical Data @@ -21,6 +23,8 @@ {{ (item.date | date: defaultDateFormat) ?? '' }} + {{ item.activityCount }} + {{ item.marketDataItemCount }}
From b464fefc574028cf73740bcc363deb07cebcc907 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 24 Jan 2022 21:43:37 +0100 Subject: [PATCH 110/337] Release 1.107.0 (#650) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12dbd19fa..8f6647cb8 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.107.0 - 24.01.2022 ### Added diff --git a/package.json b/package.json index bae60aba1..da41a0858 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.106.0", + "version": "1.107.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 5d3bbb8f30fe54c32042a5dc963ebea92d1ee371 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 27 Jan 2022 20:56:20 +0100 Subject: [PATCH 111/337] Feature/improve annualized performance (#651) * Improve annualized performance calculation * Update changelog --- CHANGELOG.md | 6 ++ .../interfaces/current-positions.interface.ts | 2 +- .../portfolio-calculator-new.spec.ts | 73 +++++++++++++++++++ .../app/portfolio/portfolio-calculator-new.ts | 24 +----- .../app/portfolio/portfolio.service-new.ts | 22 ++++-- .../src/app/portfolio/portfolio.service.ts | 2 + .../portfolio-performance.interface.ts | 2 +- 7 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6647cb8..329f43a3c 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 + +- Improved the annualized performance in the new calculation engine + ## 1.107.0 - 24.01.2022 ### Added 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 a02d2a6a6..29550b43a 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -6,7 +6,7 @@ export interface CurrentPositions { positions: TimelinePosition[]; grossPerformance: Big; grossPerformancePercentage: Big; - netAnnualizedPerformance: Big; + netAnnualizedPerformance?: Big; netPerformance: Big; netPerformancePercentage: Big; currentValue: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts new file mode 100644 index 000000000..72e3091f1 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts @@ -0,0 +1,73 @@ +import Big from 'big.js'; + +import { CurrentRateService } from './current-rate.service'; +import { PortfolioCalculatorNew } from './portfolio-calculator-new'; + +describe('PortfolioCalculatorNew', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('annualized performance percentage', () => { + const portfolioCalculatorNew = new PortfolioCalculatorNew({ + currentRateService, + currency: 'USD', + orders: [] + }); + + it('Get annualized performance', async () => { + expect( + portfolioCalculatorNew + .getAnnualizedPerformancePercent({ + daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + expect( + portfolioCalculatorNew + .getAnnualizedPerformancePercent({ + daysInMarket: 0, + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + /** + * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html + */ + expect( + portfolioCalculatorNew + .getAnnualizedPerformancePercent({ + daysInMarket: 65, // < 1 year + netPerformancePercent: new Big(0.1025) + }) + .toNumber() + ).toBeCloseTo(0.729705); + + expect( + portfolioCalculatorNew + .getAnnualizedPerformancePercent({ + daysInMarket: 365, // 1 year + netPerformancePercent: new Big(0.05) + }) + .toNumber() + ).toBeCloseTo(0.05); + + /** + * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation + */ + expect( + portfolioCalculatorNew + .getAnnualizedPerformancePercent({ + daysInMarket: 575, // > 1 year + netPerformancePercent: new Big(0.2374) + }) + .toNumber() + ).toBeCloseTo(0.145); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 1f812a154..3ebad797c 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -10,7 +10,6 @@ import { addMilliseconds, addMonths, addYears, - differenceInDays, endOfDay, format, isAfter, @@ -130,7 +129,10 @@ export class PortfolioCalculatorNew { netPerformancePercent: Big; }): Big { if (isNumber(daysInMarket) && daysInMarket > 0) { - return netPerformancePercent.mul(daysInMarket).div(365); + const exponent = new Big(365).div(daysInMarket).toNumber(); + return new Big( + Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) + ).minus(1); } return new Big(0); @@ -151,7 +153,6 @@ export class PortfolioCalculatorNew { hasErrors: false, grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - netAnnualizedPerformance: new Big(0), netPerformance: new Big(0), netPerformancePercentage: new Big(0), positions: [], @@ -632,10 +633,6 @@ export class PortfolioCalculatorNew { let netPerformance = new Big(0); let netPerformancePercentage = new Big(0); let completeInitialValue = new Big(0); - let netAnnualizedPerformance = new Big(0); - - // use Date.now() to use the mock for today - const today = new Date(Date.now()); for (const currentPosition of positions) { if (currentPosition.marketPrice) { @@ -664,16 +661,6 @@ export class PortfolioCalculatorNew { grossPerformancePercentage = grossPerformancePercentage.plus( currentPosition.grossPerformancePercentage.mul(currentInitialValue) ); - - netAnnualizedPerformance = netAnnualizedPerformance.plus( - this.getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays( - today, - parseDate(currentPosition.firstBuyDate) - ), - netPerformancePercent: currentPosition.netPerformancePercentage - }).mul(currentInitialValue) - ); netPerformancePercentage = netPerformancePercentage.plus( currentPosition.netPerformancePercentage.mul(currentInitialValue) ); @@ -690,8 +677,6 @@ export class PortfolioCalculatorNew { grossPerformancePercentage.div(completeInitialValue); netPerformancePercentage = netPerformancePercentage.div(completeInitialValue); - netAnnualizedPerformance = - netAnnualizedPerformance.div(completeInitialValue); } return { @@ -699,7 +684,6 @@ export class PortfolioCalculatorNew { grossPerformance, grossPerformancePercentage, hasErrors, - netAnnualizedPerformance, netPerformance, netPerformancePercentage, totalInvestment diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index da6bd44aa..8396dd249 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -42,6 +42,7 @@ import { REQUEST } from '@nestjs/core'; import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { + differenceInDays, endOfToday, format, isAfter, @@ -480,7 +481,6 @@ export class PortfolioServiceNew { } = position; // Convert investment, gross and net performance to currency of user - const userCurrency = this.request.user.Settings.currency; const investment = this.exchangeRateDataService.toCurrency( position.investment?.toNumber(), currency, @@ -736,7 +736,6 @@ export class PortfolioServiceNew { return { hasErrors: false, performance: { - annualizedPerformancePercent: 0, currentGrossPerformance: 0, currentGrossPerformancePercent: 0, currentNetPerformance: 0, @@ -755,8 +754,6 @@ export class PortfolioServiceNew { ); const hasErrors = currentPositions.hasErrors; - const annualizedPerformancePercent = - currentPositions.netAnnualizedPerformance.toNumber(); const currentValue = currentPositions.currentValue.toNumber(); const currentGrossPerformance = currentPositions.grossPerformance.toNumber(); @@ -769,7 +766,6 @@ export class PortfolioServiceNew { return { hasErrors: currentPositions.hasErrors || hasErrors, performance: { - annualizedPerformancePercent, currentGrossPerformance, currentGrossPerformancePercent, currentNetPerformance, @@ -898,8 +894,24 @@ export class PortfolioServiceNew { .plus(performanceInformation.performance.currentValue) .toNumber(); + const daysInMarket = differenceInDays(new Date(), firstOrderDate); + + const annualizedPerformancePercent = new PortfolioCalculatorNew({ + currency: userCurrency, + currentRateService: this.currentRateService, + orders: [] + }) + .getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent: new Big( + performanceInformation.performance.currentNetPerformancePercent + ) + }) + ?.toNumber(); + return { ...performanceInformation.performance, + annualizedPerformancePercent, dividend, fees, firstOrderDate, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fbbd5db0f..f54f4fe95 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -881,6 +881,8 @@ export class PortfolioService { netWorth, totalBuy, totalSell, + annualizedPerformancePercent: + performanceInformation.performance.annualizedPerformancePercent, cash: balance, committedFunds: committedFunds.toNumber(), ordersCount: orders.filter((order) => { diff --git a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts index 3a2770786..670d69018 100644 --- a/libs/common/src/lib/interfaces/portfolio-performance.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-performance.interface.ts @@ -1,5 +1,5 @@ export interface PortfolioPerformance { - annualizedPerformancePercent: number; + annualizedPerformancePercent?: number; currentGrossPerformance: number; currentGrossPerformancePercent: number; currentNetPerformance: number; From 65e151151b639de6a149d8fce70385b92abbb16c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:01:38 +0100 Subject: [PATCH 112/337] Feature/increase fear and greed index to 90 days (#652) * Increase fear and greed index to 90 days * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/symbol/symbol.controller.ts | 13 ++----------- apps/api/src/app/symbol/symbol.service.ts | 14 ++++++-------- .../home-market/home-market.component.ts | 3 ++- .../app/components/home-market/home-market.html | 12 ++---------- apps/client/src/app/services/data.service.ts | 14 ++++++++++---- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329f43a3c..76e404802 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 - Improved the annualized performance in the new calculation engine +- Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 90 days ## 1.107.0 - 24.01.2022 diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index bbe582cea..d81ba5ca8 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,17 +1,12 @@ import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; -import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, - DefaultValuePipe, Get, HttpException, - Inject, Param, - ParseBoolPipe, Query, UseGuards } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -23,10 +18,7 @@ import { SymbolService } from './symbol.service'; @Controller('symbol') export class SymbolController { - public constructor( - private readonly symbolService: SymbolService, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} + public constructor(private readonly symbolService: SymbolService) {} /** * Must be before /:symbol @@ -54,8 +46,7 @@ export class SymbolController { public async getSymbolData( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string, - @Query('includeHistoricalData', new DefaultValuePipe(false), ParseBoolPipe) - includeHistoricalData: boolean + @Query('includeHistoricalData') includeHistoricalData?: number ): Promise { if (!DataSource[dataSource]) { throw new HttpException( diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 8c95ce947..37b1c5864 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -5,7 +5,6 @@ import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; @@ -18,25 +17,24 @@ import { SymbolItem } from './interfaces/symbol-item.interface'; export class SymbolService { public constructor( private readonly dataProviderService: DataProviderService, - private readonly marketDataService: MarketDataService, - private readonly prismaService: PrismaService + private readonly marketDataService: MarketDataService ) {} public async get({ dataGatheringItem, - includeHistoricalData = false + includeHistoricalData }: { dataGatheringItem: IDataGatheringItem; - includeHistoricalData?: boolean; + includeHistoricalData?: number; }): Promise { const response = await this.dataProviderService.get([dataGatheringItem]); const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {}; if (dataGatheringItem.dataSource && marketPrice) { - let historicalData: HistoricalDataItem[]; + let historicalData: HistoricalDataItem[] = []; - if (includeHistoricalData) { - const days = 30; + if (includeHistoricalData > 0) { + const days = includeHistoricalData; const marketData = await this.marketDataService.getRange({ dateQuery: { gte: subDays(new Date(), days) }, diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index 87a86814d..37b31676f 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -20,6 +20,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit { public hasPermissionToAccessFearAndGreedIndex: boolean; public historicalData: HistoricalDataItem[]; public isLoading = true; + public readonly numberOfDays = 90; public user: User; private unsubscribeSubject = new Subject(); @@ -49,7 +50,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit { this.dataService .fetchSymbolItem({ dataSource: DataSource.RAKUTEN, - includeHistoricalData: true, + includeHistoricalData: this.numberOfDays, symbol: ghostfolioFearAndGreedIndexSymbol }) .pipe(takeUntil(this.unsubscribeSubject)) diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index 0ef4b5ef8..f3d8315dd 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -1,18 +1,10 @@
- Last 30 Days + Last {{ numberOfDays }} Days
(`/api/symbol/${dataSource}/${symbol}`, { - params: { includeHistoricalData } + params }); } From 9676f96e97e2e9fb9b89c1ceb7cb3536bbe4b07a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:33:59 +0100 Subject: [PATCH 113/337] Release 1.108.0 (#653) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e404802..7624821c0 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.108.0 - 27.01.2022 ### Changed diff --git a/package.json b/package.json index da41a0858..01f8f938e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.107.0", + "version": "1.108.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 035d8ad9eb899d3277987c21baca5b5bce02c3eb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 29 Jan 2022 09:15:11 +0100 Subject: [PATCH 114/337] Update copyright (#655) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed545ed4e..260aca9be 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,6 @@ Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Sl ## License -© 2021 [Ghostfolio](https://ghostfol.io) +© 2022 [Ghostfolio](https://ghostfol.io) Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html). From 62885ea890396ee0ac09ffdce338c3f04e70197b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 29 Jan 2022 16:51:37 +0100 Subject: [PATCH 115/337] Feature/improve consistent use of symbol with data source (#656) * Improve the consistent use of symbol with dataSource * Update changelog --- CHANGELOG.md | 6 ++ .../src/app/portfolio/portfolio.controller.ts | 6 +- .../app/portfolio/portfolio.service-new.ts | 6 +- .../src/app/portfolio/portfolio.service.ts | 7 +- .../home-holdings/home-holdings.component.ts | 21 ++++- .../interfaces/interfaces.ts | 6 -- .../performance-chart-dialog.component.scss | 12 --- .../performance-chart-dialog.component.ts | 94 ------------------- .../performance-chart-dialog.html | 27 ------ .../performance-chart-dialog.module.ts | 28 ------ .../interfaces/interfaces.ts | 3 + .../position-detail-dialog.component.ts | 5 +- .../position/position.component.html | 6 +- .../positions-table.component.html | 2 +- .../positions-table.component.ts | 12 ++- .../allocations/allocations-page.component.ts | 18 +++- .../transactions-page.component.ts | 16 +++- apps/client/src/app/services/data.service.ts | 30 +++--- .../portfolio-position.interface.ts | 3 +- .../src/lib/interfaces/position.interface.ts | 3 +- .../activities-table.component.html | 1 + .../activities-table.component.ts | 11 ++- 22 files changed, 122 insertions(+), 201 deletions(-) delete mode 100644 apps/client/src/app/components/performance-chart-dialog/interfaces/interfaces.ts delete mode 100644 apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.scss delete mode 100644 apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts delete mode 100644 apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html delete mode 100644 apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7624821c0..a392b5e83 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 + +- Improved the consistent use of `symbol` in combination with `dataSource` + ## 1.108.0 - 27.01.2022 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a6d291ad2..0cb0fb61b 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -30,6 +30,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { DataSource } from '@prisma/client'; import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -337,15 +338,16 @@ export class PortfolioController { return summary; } - @Get('position/:symbol') + @Get('position/:dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async getPosition( @Headers('impersonation-id') impersonationId: string, + @Param('dataSource') dataSource, @Param('symbol') symbol ): Promise { let position = await this.portfolioServiceStrategy .get() - .getPosition(impersonationId, symbol); + .getPosition(dataSource, impersonationId, symbol); if (position) { if ( diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 8396dd249..d2aec3683 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -357,6 +357,7 @@ export class PortfolioServiceNew { assetSubClass: symbolProfile.assetSubClass, countries: symbolProfile.countries, currency: item.currency, + dataSource: symbolProfile.dataSource, exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: @@ -397,6 +398,7 @@ export class PortfolioServiceNew { } public async getPosition( + aDataSource: DataSource, aImpersonationId: string, aSymbol: string ): Promise { @@ -405,7 +407,9 @@ export class PortfolioServiceNew { const orders = ( await this.orderService.getOrders({ userCurrency, userId }) - ).filter((order) => order.symbol === aSymbol); + ).filter((order) => { + return order.dataSource === aDataSource && order.symbol === aSymbol; + }); if (orders.length <= 0) { return { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f54f4fe95..9e4135a03 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -345,6 +345,7 @@ export class PortfolioService { assetSubClass: symbolProfile.assetSubClass, countries: symbolProfile.countries, currency: item.currency, + dataSource: symbolProfile.dataSource, exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: @@ -385,6 +386,7 @@ export class PortfolioService { } public async getPosition( + aDataSource: DataSource, aImpersonationId: string, aSymbol: string ): Promise { @@ -393,7 +395,9 @@ export class PortfolioService { const orders = ( await this.orderService.getOrders({ userCurrency, userId }) - ).filter((order) => order.symbol === aSymbol); + ).filter((order) => { + return order.dataSource === aDataSource && order.symbol === aSymbol; + }); if (orders.length <= 0) { return { @@ -467,7 +471,6 @@ export class PortfolioService { } = position; // Convert investment, gross and net performance to currency of user - const userCurrency = this.request.user.Settings.currency; const investment = this.exchangeRateDataService.toCurrency( position.investment?.toNumber(), currency, 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 3d26b373e..c7e964372 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 @@ -12,6 +12,7 @@ import { defaultDateRangeOptions } from '@ghostfolio/common/config'; import { Position, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; +import { DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -47,8 +48,15 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['positionDetailDialog'] && params['symbol']) { - this.openPositionDialog({ symbol: params['symbol'] }); + if ( + params['dataSource'] && + params['positionDetailDialog'] && + params['symbol'] + ) { + this.openPositionDialog({ + dataSource: params['dataSource'], + symbol: params['symbol'] + }); } }); @@ -91,7 +99,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private openPositionDialog({ symbol }: { symbol: string }) { + private openPositionDialog({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { this.userService .get() .pipe(takeUntil(this.unsubscribeSubject)) @@ -101,6 +115,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, data: { + dataSource, symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, diff --git a/apps/client/src/app/components/performance-chart-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/performance-chart-dialog/interfaces/interfaces.ts deleted file mode 100644 index 4795d68cf..000000000 --- a/apps/client/src/app/components/performance-chart-dialog/interfaces/interfaces.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; - -export interface PositionDetailDialogParams { - deviceType: string; - historicalDataItems: LineChartItem[]; -} diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.scss b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.scss deleted file mode 100644 index 1786afd7a..000000000 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -:host { - display: block; - - .mat-dialog-content { - max-height: unset; - - gf-line-chart { - aspect-ratio: 16 / 9; - margin: 0 -1rem; - } - } -} diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts deleted file mode 100644 index 301b867ca..000000000 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Inject -} from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DataService } from '@ghostfolio/client/services/data.service'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; -import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; -import { isToday, parse } from 'date-fns'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -import { PositionDetailDialogParams } from './interfaces/interfaces'; - -@Component({ - selector: 'gf-performance-chart-dialog', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: 'performance-chart-dialog.html', - styleUrls: ['./performance-chart-dialog.component.scss'] -}) -export class PerformanceChartDialog { - public benchmarkDataItems: LineChartItem[]; - public benchmarkSymbol = 'VOO'; - public currency: string; - public firstBuyDate: string; - public marketPrice: number; - public historicalDataItems: LineChartItem[]; - - private unsubscribeSubject = new Subject(); - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams - ) { - this.dataService - .fetchPositionDetail(this.benchmarkSymbol) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ currency, firstBuyDate, historicalData, marketPrice }) => { - this.benchmarkDataItems = []; - this.currency = currency; - this.firstBuyDate = firstBuyDate; - this.historicalDataItems = []; - this.marketPrice = marketPrice; - - let coefficient = 1; - - this.historicalDataItems = this.data.historicalDataItems; - - this.historicalDataItems?.forEach((historicalDataItem) => { - const benchmarkItem = historicalData.find((item) => { - return item.date === historicalDataItem.date; - }); - - if (benchmarkItem) { - if (coefficient === 1) { - coefficient = historicalDataItem.value / benchmarkItem.value || 1; - } - - this.benchmarkDataItems.push({ - date: historicalDataItem.date, - value: benchmarkItem.value * coefficient - }); - } else if ( - isToday(parse(historicalDataItem.date, DATE_FORMAT, new Date())) - ) { - this.benchmarkDataItems.push({ - date: historicalDataItem.date, - value: marketPrice * coefficient - }); - } else { - this.benchmarkDataItems.push({ - date: historicalDataItem.date, - value: undefined - }); - } - }); - - this.changeDetectorRef.markForCheck(); - }); - } - - public onClose(): void { - this.dialogRef.close(); - } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } -} diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html deleted file mode 100644 index dff26c202..000000000 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.html +++ /dev/null @@ -1,27 +0,0 @@ - - -
-
- -
-
- - diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.module.ts b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.module.ts deleted file mode 100644 index 7accddf7c..000000000 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule } from '@angular/material/dialog'; -import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; -import { GfValueModule } from '@ghostfolio/ui/value'; -import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; - -import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module'; -import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module'; -import { PerformanceChartDialog } from './performance-chart-dialog.component'; - -@NgModule({ - declarations: [PerformanceChartDialog], - exports: [], - imports: [ - CommonModule, - GfDialogFooterModule, - GfDialogHeaderModule, - GfLineChartModule, - GfValueModule, - MatButtonModule, - MatDialogModule, - NgxSkeletonLoaderModule - ], - providers: [] -}) -export class GfPerformanceChartDialogModule {} diff --git a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts index 28a99f7b6..daac4065a 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts @@ -1,5 +1,8 @@ +import { DataSource } from '@prisma/client'; + export interface PositionDetailDialogParams { baseCurrency: string; + dataSource: DataSource; deviceType: string; locale: string; symbol: string; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index fd43729f3..1802619a0 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -59,7 +59,10 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public ngOnInit(): void { this.dataService - .fetchPositionDetail(this.data.symbol) + .fetchPositionDetail({ + dataSource: this.data.dataSource, + symbol: this.data.symbol + }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index bb85859e3..8d254bb98 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -3,7 +3,11 @@
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 0c44be05d..d338244da 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 @@ -14,7 +14,7 @@ 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, Order as OrderModel } from '@prisma/client'; +import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; @Component({ @@ -75,9 +75,15 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { this.dataSource.filter = filterValue.trim().toLowerCase(); }*/ - public onOpenPositionDialog({ symbol }: { symbol: string }): void { + public onOpenPositionDialog({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): void { this.router.navigate([], { - queryParams: { positionDetailDialog: true, symbol } + 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 c19af7fb4..f89297403 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 } from '@prisma/client'; +import { AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -84,8 +84,13 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['positionDetailDialog'] && params['symbol']) { + if ( + params['dataSource'] && + params['positionDetailDialog'] && + params['symbol'] + ) { this.openPositionDialog({ + dataSource: params['dataSource'], symbol: params['symbol'] }); } @@ -291,7 +296,13 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private openPositionDialog({ symbol }: { symbol: string }) { + private openPositionDialog({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { this.userService .get() .pipe(takeUntil(this.unsubscribeSubject)) @@ -301,6 +312,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, data: { + dataSource, symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 8606cd395..5f252caea 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -75,8 +75,13 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } else { this.router.navigate(['.'], { relativeTo: this.route }); } - } else if (params['positionDetailDialog'] && params['symbol']) { + } else if ( + params['dataSource'] && + params['positionDetailDialog'] && + params['symbol'] + ) { this.openPositionDialog({ + dataSource: params['dataSource'], symbol: params['symbol'] }); } @@ -387,7 +392,13 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - private openPositionDialog({ symbol }: { symbol: string }) { + private openPositionDialog({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { this.userService .get() .pipe(takeUntil(this.unsubscribeSubject)) @@ -397,6 +408,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, data: { + dataSource, symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 9dd51ace5..22160b180 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -225,19 +225,27 @@ export class DataService { ); } - public fetchPositionDetail(aSymbol: string) { - return this.http.get(`/api/portfolio/position/${aSymbol}`).pipe( - map((data) => { - if (data.orders) { - for (const order of data.orders) { - order.createdAt = parseISO(order.createdAt); - order.date = parseISO(order.date); + public fetchPositionDetail({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.http + .get(`/api/portfolio/position/${dataSource}/${symbol}`) + .pipe( + map((data) => { + if (data.orders) { + for (const order of data.orders) { + order.createdAt = parseISO(order.createdAt); + order.date = parseISO(order.date); + } } - } - return data; - }) - ); + return data; + }) + ); } public loginAnonymous(accessToken: string) { diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index ab86e0582..1145d98ff 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,5 +1,5 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; -import { AssetClass, AssetSubClass } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { Country } from './country.interface'; import { Sector } from './sector.interface'; @@ -11,6 +11,7 @@ export interface PortfolioPosition { assetSubClass?: AssetSubClass | 'CASH'; countries: Country[]; currency: string; + dataSource: DataSource; exchange?: string; grossPerformance: number; grossPerformancePercent: number; diff --git a/libs/common/src/lib/interfaces/position.interface.ts b/libs/common/src/lib/interfaces/position.interface.ts index d3ef64eb1..a5e3eb93c 100644 --- a/libs/common/src/lib/interfaces/position.interface.ts +++ b/libs/common/src/lib/interfaces/position.interface.ts @@ -1,10 +1,11 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; -import { AssetClass } from '@prisma/client'; +import { AssetClass, DataSource } from '@prisma/client'; export interface Position { assetClass: AssetClass; averagePrice: number; currency: string; + dataSource: DataSource; firstBuyDate: string; grossPerformance?: number; grossPerformancePercentage?: number; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index ab84ee08e..5af6013bb 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -327,6 +327,7 @@ hasPermissionToOpenDetails && !row.isDraft && onOpenPositionDialog({ + dataSource: row.dataSource, symbol: row.symbol }) " diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index f76e2ddb8..3d459bb9f 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -22,6 +22,7 @@ import { Router } from '@angular/router'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { OrderWithAccount } from '@ghostfolio/common/types'; +import { DataSource } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, format, isAfter } from 'date-fns'; import { isNumber } from 'lodash'; @@ -190,9 +191,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.import.emit(); } - public onOpenPositionDialog({ symbol }: { symbol: string }): void { + public onOpenPositionDialog({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): void { this.router.navigate([], { - queryParams: { positionDetailDialog: true, symbol } + queryParams: { dataSource, symbol, positionDetailDialog: true } }); } From 919b20197f49ebfcb67d7aa3b4b22396e9199ffa Mon Sep 17 00:00:00 2001 From: Ronald Konjer Date: Sat, 29 Jan 2022 17:27:33 +0100 Subject: [PATCH 116/337] import csv with account name or id (#654) * import csv with account id --- apps/api/src/app/export/export.service.ts | 1 + .../transactions-page.component.ts | 7 +- .../services/import-transactions.service.ts | 70 +++++++++++++------ package.json | 1 + 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 5fd88d4c7..784b34d5e 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -11,6 +11,7 @@ export class ExportService { const orders = await this.prismaService.order.findMany({ orderBy: { date: 'desc' }, select: { + accountId: true, currency: true, dataSource: true, date: true, diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 5f252caea..977cc720b 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -195,8 +195,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { try { await this.importTransactionsService.importJson({ - content: content.orders, - defaultAccountId: this.defaultAccountId + content: content.orders }); this.handleImportSuccess(); @@ -210,8 +209,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { try { await this.importTransactionsService.importCsv({ fileContent, - defaultAccountId: this.defaultAccountId, - primaryDataSource: this.primaryDataSource + primaryDataSource: this.primaryDataSource, + user: this.user }); this.handleImportSuccess(); diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index d3baa13a4..4708a5779 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -3,15 +3,17 @@ import { Injectable } from '@angular/core'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { DataSource, Type } from '@prisma/client'; import { parse } from 'date-fns'; -import { isNumber } from 'lodash'; import { parse as csvToJson } from 'papaparse'; +import { isNumber } from 'lodash'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; +import { User } from '@ghostfolio/common/interfaces'; @Injectable({ providedIn: 'root' }) export class ImportTransactionsService { + private static ACCOUNT_ID = ['account', 'accountid']; private static CURRENCY_KEYS = ['ccy', 'currency']; private static DATE_KEYS = ['date']; private static FEE_KEYS = ['commission', 'fee']; @@ -23,25 +25,29 @@ export class ImportTransactionsService { public constructor(private http: HttpClient) {} public async importCsv({ - defaultAccountId, fileContent, - primaryDataSource + primaryDataSource, + user }: { - defaultAccountId: string; fileContent: string; primaryDataSource: DataSource; + user: User; }) { - const content = csvToJson(fileContent, { + let content: any[] = []; + + csvToJson(fileContent, { dynamicTyping: true, header: true, - skipEmptyLines: true - }).data; + skipEmptyLines: true, + complete: (parsedData) => { + content = parsedData.data.filter((item) => item['date'] != null); + } + }); const orders: CreateOrderDto[] = []; - for (const [index, item] of content.entries()) { orders.push({ - accountId: defaultAccountId, + accountId: this.parseAccount({ content, index, item, user }), currency: this.parseCurrency({ content, index, item }), dataSource: primaryDataSource, date: this.parseDate({ content, index, item }), @@ -53,21 +59,13 @@ export class ImportTransactionsService { }); } - await this.importJson({ defaultAccountId, content: orders }); + await this.importJson({ content: orders }); } - public importJson({ - content, - defaultAccountId - }: { - content: CreateOrderDto[]; - defaultAccountId: string; - }): Promise { + public importJson({ content }: { content: CreateOrderDto[] }): Promise { return new Promise((resolve, reject) => { this.postImport({ - orders: content.map((order) => { - return { ...order, accountId: defaultAccountId }; - }) + orders: content }) .pipe( catchError((error) => { @@ -90,6 +88,38 @@ export class ImportTransactionsService { }, {}); } + private parseAccount({ + content, + index, + item, + user + }: { + content: any[]; + index: number; + item: any; + user: User; + }) { + item = this.lowercaseKeys(item); + for (const key of ImportTransactionsService.ACCOUNT_ID) { + if (item[key]) { + let accountid = user.accounts.find((account) => { + return ( + account.name.toLowerCase() === item[key].toLowerCase() || + account.id == item[key] + ); + })?.id; + if (!accountid) { + accountid = user?.accounts.find((account) => { + return account.isDefault; + })?.id; + } + return accountid; + } + } + + throw { message: `orders.${index}.account is not valid`, orders: content }; + } + private parseCurrency({ content, index, diff --git a/package.json b/package.json index 01f8f938e..4ce1cd3e9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "affected:test": "nx affected:test", "angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng", "build:all": "ng build --configuration production api && ng build --configuration production client && yarn replace-placeholders-in-build", + "build:dev": "nx build api && nx build client && yarn replace-placeholders-in-build", "build:storybook": "nx run ui:build-storybook", "clean": "rimraf dist", "database:format-schema": "prisma format", From 9d6977e3f7b0a14556979cb96f4565a3053404d9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Feb 2022 10:58:34 +0100 Subject: [PATCH 117/337] Feature/support cryptocurrency mina protocol (#659) * Support Mina Protocol (MINA-USD) * Update changelog --- CHANGELOG.md | 2 ++ .../src/services/cryptocurrency/custom-cryptocurrencies.json | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a392b5e83..db15af81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Added support for cryptocurrency _Mina Protocol_ (`MINA-USD`) + ### Changed - Improved the consistent use of `symbol` in combination with `dataSource` diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index 7664e3106..eff896ed6 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -5,6 +5,7 @@ "AVAX": "Avalanche", "DOT": "Polkadot", "MATIC": "Polygon", + "MINA": "Mina Protocol", "SHIB": "Shiba Inu", "SOL": "Solana", "UNI3": "Uniswap" From b8ad6d6662bfaef095d9672ce553e03842b1f9cc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Feb 2022 19:12:00 +0100 Subject: [PATCH 118/337] Feature/improve import (#657) * Improve import * Update changelog Co-Authored-By: Ronald Konjer --- CHANGELOG.md | 9 ++- apps/api/src/app/import/import.service.ts | 12 ++-- apps/api/src/app/info/info.service.ts | 3 - apps/api/src/app/order/create-order.dto.ts | 10 ++- apps/api/src/app/order/order.controller.ts | 15 +---- apps/api/src/app/order/order.module.ts | 3 +- apps/api/src/app/order/order.service.ts | 29 +++++++- .../transactions-page.component.ts | 7 +- .../services/import-transactions.service.ts | 66 +++++++++---------- .../src/lib/interfaces/info-item.interface.ts | 3 - 10 files changed, 87 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db15af81a..39f2a2b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Added support for cryptocurrency _Mina Protocol_ (`MINA-USD`) +### Added + +- Added support for the (optional) `accountId` in the import functionality for activities +- Added support for the (optional) `dataSource` in the import functionality for activities +- Added support for the cryptocurrency _Mina Protocol_ (`MINA-USD`) ### Changed - Improved the consistent use of `symbol` in combination with `dataSource` +- Removed the primary data source from the client ## 1.108.0 - 27.01.2022 @@ -208,7 +213,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added support for cryptocurrency _Solana_ (`SOL-USD`) +- Added support for the cryptocurrency _Solana_ (`SOL-USD`) - Extended the documentation for self-hosting with the [official Ghostfolio Docker image](https://hub.docker.com/r/ghostfolio/ghostfolio) ### Fixed diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 6bf936137..92ffcca2c 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -20,6 +20,11 @@ export class ImportService { orders: Partial[]; userId: string; }): Promise { + for (const order of orders) { + order.dataSource = + order.dataSource ?? this.dataProviderService.getPrimaryDataSource(); + } + await this.validateOrders({ orders, userId }); for (const { @@ -34,6 +39,7 @@ export class ImportService { unitPrice } of orders) { await this.orderService.createOrder({ + accountId, currency, dataSource, fee, @@ -41,11 +47,7 @@ export class ImportService { symbol, type, unitPrice, - Account: { - connect: { - id_userId: { userId, id: accountId } - } - }, + userId, date: parseISO((date)), SymbolProfile: { connectOrCreate: { diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 83a79f61d..cba659b99 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -1,7 +1,6 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; -import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; @@ -27,7 +26,6 @@ export class InfoService { public constructor( private readonly configurationService: ConfigurationService, - private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly dataGatheringService: DataGatheringService, private readonly jwtService: JwtService, @@ -92,7 +90,6 @@ export class InfoService { currencies: this.exchangeRateDataService.getCurrencies(), demoAuthToken: this.getDemoAuthToken(), lastDataGathering: await this.getLastDataGathering(), - primaryDataSource: this.dataProviderService.getPrimaryDataSource(), statistics: await this.getStatistics(), subscriptions: await this.getSubscriptions() }; diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index bd52578e2..b826166b0 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -1,14 +1,22 @@ import { DataSource, Type } from '@prisma/client'; -import { IsEnum, IsISO8601, IsNumber, IsString } from 'class-validator'; +import { + IsEnum, + IsISO8601, + IsNumber, + IsOptional, + IsString +} from 'class-validator'; export class CreateOrderDto { @IsString() + @IsOptional() accountId: string; @IsString() currency: string; @IsEnum(DataSource, { each: true }) + @IsOptional() dataSource: DataSource; @IsISO8601() diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index eaf01a4f0..3d315a849 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -114,19 +114,9 @@ export class OrderController { ); } - const date = parseISO(data.date); - - const accountId = data.accountId; - delete data.accountId; - return this.orderService.createOrder({ ...data, - date, - Account: { - connect: { - id_userId: { id: accountId, userId: this.request.user.id } - } - }, + date: parseISO(data.date), SymbolProfile: { connectOrCreate: { create: { @@ -141,7 +131,8 @@ export class OrderController { } } }, - User: { connect: { id: this.request.user.id } } + User: { connect: { id: this.request.user.id } }, + userId: this.request.user.id }); } diff --git a/apps/api/src/app/order/order.module.ts b/apps/api/src/app/order/order.module.ts index 3e6f6fca8..3f896dc5e 100644 --- a/apps/api/src/app/order/order.module.ts +++ b/apps/api/src/app/order/order.module.ts @@ -1,3 +1,4 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; @@ -24,7 +25,7 @@ import { OrderService } from './order.service'; UserModule ], controllers: [OrderController], - providers: [CacheService, OrderService], + providers: [AccountService, CacheService, OrderService], exports: [OrderService] }) export class OrderModule {} diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index af386e209..2a5e9f230 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -1,3 +1,4 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; @@ -13,6 +14,7 @@ import { Activity } from './interfaces/activities.interface'; @Injectable() export class OrderService { public constructor( + private readonly accountService: AccountService, private readonly cacheService: CacheService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly dataGatheringService: DataGatheringService, @@ -47,7 +49,24 @@ export class OrderService { }); } - public async createOrder(data: Prisma.OrderCreateInput): Promise { + public async createOrder( + data: Prisma.OrderCreateInput & { accountId?: string; userId: string } + ): Promise { + const defaultAccount = ( + await this.accountService.getAccounts(data.userId) + ).find((account) => { + return account.isDefault === true; + }); + + const Account = { + connect: { + id_userId: { + userId: data.userId, + id: data.accountId ?? defaultAccount?.id + } + } + }; + const isDraft = isAfter(data.date as Date, endOfToday()); // Convert the symbol to uppercase to avoid case-sensitive duplicates @@ -70,9 +89,15 @@ export class OrderService { await this.cacheService.flush(); + delete data.accountId; + delete data.userId; + + const orderData: Prisma.OrderCreateInput = data; + return this.prismaService.order.create({ data: { - ...data, + ...orderData, + Account, isDraft, symbol } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 977cc720b..8172ed080 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -39,7 +39,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { public routeQueryParams: Subscription; public user: User; - private primaryDataSource: DataSource; private unsubscribeSubject = new Subject(); /** @@ -57,9 +56,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { private snackBar: MatSnackBar, private userService: UserService ) { - const { primaryDataSource } = this.dataService.fetchInfo(); - this.primaryDataSource = primaryDataSource; - this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { @@ -209,8 +205,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { try { await this.importTransactionsService.importCsv({ fileContent, - primaryDataSource: this.primaryDataSource, - user: this.user + userAccounts: this.user.accounts }); this.handleImportSuccess(); diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index 4708a5779..b39d4ac65 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -1,20 +1,20 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; -import { DataSource, Type } from '@prisma/client'; +import { Account, DataSource, Type } from '@prisma/client'; import { parse } from 'date-fns'; -import { parse as csvToJson } from 'papaparse'; import { isNumber } from 'lodash'; +import { parse as csvToJson } from 'papaparse'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { User } from '@ghostfolio/common/interfaces'; @Injectable({ providedIn: 'root' }) export class ImportTransactionsService { - private static ACCOUNT_ID = ['account', 'accountid']; + private static ACCOUNT_KEYS = ['account', 'accountid']; private static CURRENCY_KEYS = ['ccy', 'currency']; + private static DATA_SOURCE_KEYS = ['datasource']; private static DATE_KEYS = ['date']; private static FEE_KEYS = ['commission', 'fee']; private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units']; @@ -26,30 +26,23 @@ export class ImportTransactionsService { public async importCsv({ fileContent, - primaryDataSource, - user + userAccounts }: { fileContent: string; - primaryDataSource: DataSource; - user: User; + userAccounts: Account[]; }) { - let content: any[] = []; - - csvToJson(fileContent, { + const content = csvToJson(fileContent, { dynamicTyping: true, header: true, - skipEmptyLines: true, - complete: (parsedData) => { - content = parsedData.data.filter((item) => item['date'] != null); - } - }); + skipEmptyLines: true + }).data; const orders: CreateOrderDto[] = []; for (const [index, item] of content.entries()) { orders.push({ - accountId: this.parseAccount({ content, index, item, user }), + accountId: this.parseAccount({ item, userAccounts }), currency: this.parseCurrency({ content, index, item }), - dataSource: primaryDataSource, + dataSource: this.parseDataSource({ item }), date: this.parseDate({ content, index, item }), fee: this.parseFee({ content, index, item }), quantity: this.parseQuantity({ content, index, item }), @@ -89,35 +82,26 @@ export class ImportTransactionsService { } private parseAccount({ - content, - index, item, - user + userAccounts }: { - content: any[]; - index: number; item: any; - user: User; + userAccounts: Account[]; }) { item = this.lowercaseKeys(item); - for (const key of ImportTransactionsService.ACCOUNT_ID) { + + for (const key of ImportTransactionsService.ACCOUNT_KEYS) { if (item[key]) { - let accountid = user.accounts.find((account) => { + return userAccounts.find((account) => { return ( - account.name.toLowerCase() === item[key].toLowerCase() || - account.id == item[key] + account.id === item[key] || + account.name.toLowerCase() === item[key].toLowerCase() ); })?.id; - if (!accountid) { - accountid = user?.accounts.find((account) => { - return account.isDefault; - })?.id; - } - return accountid; } } - throw { message: `orders.${index}.account is not valid`, orders: content }; + return undefined; } private parseCurrency({ @@ -140,6 +124,18 @@ export class ImportTransactionsService { throw { message: `orders.${index}.currency is not valid`, orders: content }; } + private parseDataSource({ item }: { item: any }) { + item = this.lowercaseKeys(item); + + for (const key of ImportTransactionsService.DATA_SOURCE_KEYS) { + if (item[key]) { + return DataSource[item[key].toUpperCase()]; + } + } + + return undefined; + } + private parseDate({ content, index, diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 7ac5f3e9e..5ccf2602e 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,5 +1,3 @@ -import { DataSource } from '@prisma/client'; - import { Statistics } from './statistics.interface'; import { Subscription } from './subscription.interface'; @@ -10,7 +8,6 @@ export interface InfoItem { isReadOnlyMode?: boolean; lastDataGathering?: Date; platforms: { id: string; name: string }[]; - primaryDataSource: DataSource; statistics: Statistics; stripePublicKey?: string; subscriptions: Subscription[]; From 155c08d665d5a9dd6e06cf0dba96362a30ef4dd6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Feb 2022 20:35:25 +0100 Subject: [PATCH 119/337] Transform data source (#658) * Transform data source * Update changelog --- CHANGELOG.md | 5 ++ apps/api/src/app/order/order.controller.ts | 19 ++--- .../src/app/portfolio/portfolio.controller.ts | 79 ++++++++++--------- .../subscription/subscription.controller.ts | 14 ++-- apps/api/src/app/symbol/symbol.controller.ts | 8 +- ...form-data-source-in-request.interceptor.ts | 45 +++++++++++ ...orm-data-source-in-response.interceptor.ts | 76 ++++++++++++++++++ 7 files changed, 185 insertions(+), 61 deletions(-) create mode 100644 apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts create mode 100644 apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f2a2b04..83df4164d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for the (optional) `accountId` in the import functionality for activities - Added support for the (optional) `dataSource` in the import functionality for activities +- Added support for the data source transformation - Added support for the cryptocurrency _Mina Protocol_ (`MINA-USD`) ### Changed @@ -18,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the consistent use of `symbol` in combination with `dataSource` - Removed the primary data source from the client +### Removed + +- Removed the unused endpoint `GET api/order/:id` + ## 1.108.0 - 27.01.2022 ### Changed diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 3d315a849..58a043b82 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,5 +1,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -14,7 +16,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -58,6 +61,7 @@ export class OrderController { @Get() @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAllOrders( @Headers('impersonation-id') impersonationId ): Promise { @@ -91,19 +95,9 @@ export class OrderController { return { activities }; } - @Get(':id') - @UseGuards(AuthGuard('jwt')) - public async getOrderById(@Param('id') id: string): Promise { - return this.orderService.order({ - id_userId: { - id, - userId: this.request.user.id - } - }); - } - @Post() @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async createOrder(@Body() data: CreateOrderDto): Promise { if ( !hasPermission(this.request.user.permissions, permissions.createOrder) @@ -138,6 +132,7 @@ export class OrderController { @Put(':id') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { if ( !hasPermission(this.request.user.permissions, permissions.updateOrder) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 0cb0fb61b..6a2351a49 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -4,9 +4,12 @@ import { hasNotDefinedValuesInObject, nullifyValuesInObject } from '@ghostfolio/api/helper/object.helper'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { baseCurrency } from '@ghostfolio/common/config'; +import { parseDate } from '@ghostfolio/common/helper'; import { PortfolioChart, PortfolioDetails, @@ -25,13 +28,11 @@ import { Inject, Param, Query, - Res, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { DataSource } from '@prisma/client'; -import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; @@ -53,8 +54,7 @@ export class PortfolioController { @UseGuards(AuthGuard('jwt')) public async getChart( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise { const historicalDataContainer = await this.portfolioServiceStrategy .get() @@ -90,27 +90,29 @@ export class PortfolioController { }); } - return res.json({ + return { hasError, chart: chartData, isAllTimeHigh: historicalDataContainer.isAllTimeHigh, isAllTimeLow: historicalDataContainer.isAllTimeLow - }); + }; } @Get('details') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response - ): Promise { + @Query('range') range + ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({ accounts: {}, holdings: {} }); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } let hasError = false; @@ -159,21 +161,22 @@ export class PortfolioController { } } - return res.json({ accounts, hasError, holdings }); + return { accounts, hasError, holdings }; } @Get('investments') @UseGuards(AuthGuard('jwt')) public async getInvestments( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response + @Headers('impersonation-id') impersonationId: string ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({}); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } let investments = await this.portfolioServiceStrategy @@ -195,15 +198,14 @@ export class PortfolioController { })); } - return res.json({ firstOrderDate: investments[0]?.date, investments }); + return { firstOrderDate: parseDate(investments[0]?.date), investments }; } @Get('performance') @UseGuards(AuthGuard('jwt')) public async getPerformance( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { const performanceInformation = await this.portfolioServiceStrategy .get() @@ -219,15 +221,15 @@ export class PortfolioController { ); } - return res.json(performanceInformation); + return performanceInformation; } @Get('positions') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPositions( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise { const result = await this.portfolioServiceStrategy .get() @@ -247,13 +249,12 @@ export class PortfolioController { }); } - return res.json(result); + return result; } @Get('public/:accessId') public async getPublic( - @Param('accessId') accessId, - @Res() res: Response + @Param('accessId') accessId ): Promise { const access = await this.accessService.access({ id: accessId }); const user = await this.userService.user({ @@ -261,8 +262,10 @@ export class PortfolioController { }); if (!access) { - res.status(StatusCodes.NOT_FOUND); - return res.json({ accounts: {}, holdings: {} }); + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); } let hasDetails = true; @@ -305,7 +308,7 @@ export class PortfolioController { } } - return res.json(portfolioPublicDetails); + return portfolioPublicDetails; } @Get('summary') @@ -339,6 +342,7 @@ export class PortfolioController { } @Get('position/:dataSource/:symbol') + @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseGuards(AuthGuard('jwt')) public async getPosition( @Headers('impersonation-id') impersonationId: string, @@ -376,21 +380,18 @@ export class PortfolioController { @Get('report') @UseGuards(AuthGuard('jwt')) public async getReport( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response + @Headers('impersonation-id') impersonationId: string ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({ rules: [] }); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } - return ( - res.json( - await this.portfolioServiceStrategy.get().getReport(impersonationId) - ) - ); + return await this.portfolioServiceStrategy.get().getReport(impersonationId); } } diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 531e798b9..1f68c8f72 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -7,6 +7,7 @@ import { Body, Controller, Get, + HttpCode, HttpException, Inject, Logger, @@ -17,7 +18,6 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { SubscriptionService } from './subscription.service'; @@ -32,11 +32,9 @@ export class SubscriptionController { ) {} @Post('redeem-coupon') + @HttpCode(StatusCodes.OK) @UseGuards(AuthGuard('jwt')) - public async redeemCoupon( - @Body() { couponCode }: { couponCode: string }, - @Res() res: Response - ) { + public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) { if (!this.request.user) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -74,12 +72,10 @@ export class SubscriptionController { `Subscription for user '${this.request.user.id}' has been created with coupon` ); - res.status(StatusCodes.OK); - - return res.json({ + return { message: getReasonPhrase(StatusCodes.OK), statusCode: StatusCodes.OK - }); + }; } @Get('stripe/callback') diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index d81ba5ca8..5b3c0f030 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,3 +1,5 @@ +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Controller, @@ -5,7 +7,8 @@ import { HttpException, Param, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; @@ -25,6 +28,7 @@ export class SymbolController { */ @Get('lookup') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async lookupSymbol( @Query() { query = '' } ): Promise<{ items: LookupItem[] }> { @@ -43,6 +47,8 @@ export class SymbolController { */ @Get(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getSymbolData( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string, diff --git a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts new file mode 100644 index 000000000..27bbe047b --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -0,0 +1,45 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { ConfigurationService } from '../services/configuration.service'; + +@Injectable() +export class TransformDataSourceInRequestInterceptor + implements NestInterceptor +{ + public constructor( + private readonly configurationService: ConfigurationService + ) {} + + public intercept( + context: ExecutionContext, + next: CallHandler + ): Observable { + const http = context.switchToHttp(); + const request = http.getRequest(); + + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) { + if (request.body.dataSource) { + request.body.dataSource = this.decodeDataSource( + request.body.dataSource + ); + } + + if (request.params.dataSource) { + request.params.dataSource = this.decodeDataSource( + request.params.dataSource + ); + } + } + + return next.handle(); + } + + private decodeDataSource(encodeDataSource: string) { + return Buffer.from(encodeDataSource, 'hex').toString(); + } +} 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 new file mode 100644 index 000000000..4f7d641ac --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -0,0 +1,76 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor +} from '@nestjs/common'; +import { DataSource } from '@prisma/client'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ConfigurationService } from '../services/configuration.service'; + +@Injectable() +export class TransformDataSourceInResponseInterceptor + implements NestInterceptor +{ + public constructor( + private readonly configurationService: ConfigurationService + ) {} + + public intercept( + context: ExecutionContext, + next: CallHandler + ): Observable { + return next.handle().pipe( + map((data: any) => { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true + ) { + if (data.activities) { + data.activities.map((activity) => { + activity.SymbolProfile.dataSource = this.encodeDataSource( + activity.SymbolProfile.dataSource + ); + activity.dataSource = this.encodeDataSource(activity.dataSource); + return activity; + }); + } + + if (data.dataSource) { + data.dataSource = this.encodeDataSource(data.dataSource); + } + + if (data.holdings) { + for (const symbol of Object.keys(data.holdings)) { + if (data.holdings[symbol].dataSource) { + data.holdings[symbol].dataSource = this.encodeDataSource( + data.holdings[symbol].dataSource + ); + } + } + } + + if (data.items) { + data.items.map((item) => { + item.dataSource = this.encodeDataSource(item.dataSource); + return item; + }); + } + + if (data.positions) { + data.positions.map((position) => { + position.dataSource = this.encodeDataSource(position.dataSource); + return position; + }); + } + } + + return data; + }) + ); + } + + private encodeDataSource(aDataSource: DataSource) { + return Buffer.from(aDataSource, 'utf-8').toString('hex'); + } +} From 8bd9330accf26ef3564195e4d66ed3d371af4c51 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 1 Feb 2022 20:35:44 +0100 Subject: [PATCH 120/337] Feature/improve usability of create or edit transaction dialog (#661) * Move the fee to the bottom * Update changelog --- CHANGELOG.md | 1 + .../create-or-update-transaction-dialog.html | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83df4164d..88a6d7f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the usability of the form in the create or edit transaction dialog - Improved the consistent use of `symbol` in combination with `dataSource` - Removed the primary data source from the client diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html index f2dbd54f8..11f7415fb 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -107,18 +107,6 @@
-
- - Fee - - -
Quantity @@ -141,6 +129,7 @@ type="number" [(ngModel)]="data.transaction.unitPrice" /> + {{ data.transaction.currency }}
+
+ + Fee + + {{ data.transaction.currency }} + +
Date: Tue, 1 Feb 2022 21:05:14 +0100 Subject: [PATCH 121/337] Release 1.109.0 (#662) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a6d7f6b..897f18081 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.109.0 - 01.02.2022 ### Added diff --git a/package.json b/package.json index 4ce1cd3e9..0c1896012 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.108.0", + "version": "1.109.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 161cb82820e61cc5ca83d74eec347455c9837574 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Feb 2022 20:07:33 +0100 Subject: [PATCH 122/337] Bugfix/fix data source of fear and greed index (#663) * Encode data source * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/info/info.service.ts | 6 ++++++ ...sform-data-source-in-request.interceptor.ts | 13 +++---------- ...form-data-source-in-response.interceptor.ts | 18 +++++++----------- .../home-market/home-market.component.ts | 7 ++++--- apps/client/src/app/services/data.service.ts | 2 +- libs/common/src/lib/helper.ts | 9 +++++++++ .../src/lib/interfaces/info-item.interface.ts | 1 + 8 files changed, 37 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 897f18081..f268e4db3 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 + +-Fixed the data source of the _Fear & Greed Index_ (market mood) + ## 1.109.0 - 01.02.2022 ### Added diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index cba659b99..203dc6dc4 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -11,12 +11,14 @@ import { PROPERTY_STRIPE_CONFIG, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; +import { encodeDataSource } from '@ghostfolio/common/helper'; import { InfoItem } from '@ghostfolio/common/interfaces'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface'; import { permissions } from '@ghostfolio/common/permissions'; import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { DataSource } from '@prisma/client'; import * as bent from 'bent'; import { subDays } from 'date-fns'; @@ -49,6 +51,10 @@ export class InfoService { globalPermissions.push(permissions.enableBlog); } + if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + info.fearAndGreedDataSource = encodeDataSource(DataSource.RAKUTEN); + } + if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { globalPermissions.push(permissions.enableImport); } diff --git a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts index 27bbe047b..9cf85cdfd 100644 --- a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -1,3 +1,4 @@ +import { decodeDataSource } from '@ghostfolio/common/helper'; import { CallHandler, ExecutionContext, @@ -24,22 +25,14 @@ export class TransformDataSourceInRequestInterceptor if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) { if (request.body.dataSource) { - request.body.dataSource = this.decodeDataSource( - request.body.dataSource - ); + request.body.dataSource = decodeDataSource(request.body.dataSource); } if (request.params.dataSource) { - request.params.dataSource = this.decodeDataSource( - request.params.dataSource - ); + request.params.dataSource = decodeDataSource(request.params.dataSource); } } return next.handle(); } - - private decodeDataSource(encodeDataSource: string) { - return Buffer.from(encodeDataSource, 'hex').toString(); - } } 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 4f7d641ac..5e41f5da2 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 @@ -1,10 +1,10 @@ +import { encodeDataSource } from '@ghostfolio/common/helper'; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ConfigurationService } from '../services/configuration.service'; @@ -28,22 +28,22 @@ export class TransformDataSourceInResponseInterceptor ) { if (data.activities) { data.activities.map((activity) => { - activity.SymbolProfile.dataSource = this.encodeDataSource( + activity.SymbolProfile.dataSource = encodeDataSource( activity.SymbolProfile.dataSource ); - activity.dataSource = this.encodeDataSource(activity.dataSource); + activity.dataSource = encodeDataSource(activity.dataSource); return activity; }); } if (data.dataSource) { - data.dataSource = this.encodeDataSource(data.dataSource); + data.dataSource = encodeDataSource(data.dataSource); } if (data.holdings) { for (const symbol of Object.keys(data.holdings)) { if (data.holdings[symbol].dataSource) { - data.holdings[symbol].dataSource = this.encodeDataSource( + data.holdings[symbol].dataSource = encodeDataSource( data.holdings[symbol].dataSource ); } @@ -52,14 +52,14 @@ export class TransformDataSourceInResponseInterceptor if (data.items) { data.items.map((item) => { - item.dataSource = this.encodeDataSource(item.dataSource); + item.dataSource = encodeDataSource(item.dataSource); return item; }); } if (data.positions) { data.positions.map((position) => { - position.dataSource = this.encodeDataSource(position.dataSource); + position.dataSource = encodeDataSource(position.dataSource); return position; }); } @@ -69,8 +69,4 @@ export class TransformDataSourceInResponseInterceptor }) ); } - - private encodeDataSource(aDataSource: DataSource) { - return Buffer.from(aDataSource, 'utf-8').toString('hex'); - } } diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index 37b31676f..12ef7b765 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -4,9 +4,8 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { resetHours } from '@ghostfolio/common/helper'; -import { User } from '@ghostfolio/common/interfaces'; +import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { DataSource } from '@prisma/client'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -19,6 +18,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit { public fearAndGreedIndex: number; public hasPermissionToAccessFearAndGreedIndex: boolean; public historicalData: HistoricalDataItem[]; + public info: InfoItem; public isLoading = true; public readonly numberOfDays = 90; public user: User; @@ -33,6 +33,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit { private dataService: DataService, private userService: UserService ) { + this.info = this.dataService.fetchInfo(); this.isLoading = true; this.userService.stateChanged @@ -49,7 +50,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit { if (this.hasPermissionToAccessFearAndGreedIndex) { this.dataService .fetchSymbolItem({ - dataSource: DataSource.RAKUTEN, + dataSource: this.info.fearAndGreedDataSource, includeHistoricalData: this.numberOfDays, symbol: ghostfolioFearAndGreedIndexSymbol }) diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 22160b180..8d614a545 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -141,7 +141,7 @@ export class DataService { includeHistoricalData, symbol }: { - dataSource: DataSource; + dataSource: DataSource | string; includeHistoricalData?: number; symbol: string; }) { diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 84dba181d..4fac26654 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -1,4 +1,5 @@ import * as currencies from '@dinero.js/currencies'; +import { DataSource } from '@prisma/client'; import { getDate, getMonth, getYear, parse, subDays } from 'date-fns'; import { ghostfolioScraperApiSymbolPrefix } from './config'; @@ -7,6 +8,14 @@ export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); } +export function decodeDataSource(encodedDataSource: string) { + return Buffer.from(encodedDataSource, 'hex').toString(); +} + +export function encodeDataSource(aDataSource: DataSource) { + return Buffer.from(aDataSource, 'utf-8').toString('hex'); +} + export function getBackgroundColor() { return getCssVariable( window.matchMedia('(prefers-color-scheme: dark)').matches diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 5ccf2602e..443bb061a 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -4,6 +4,7 @@ import { Subscription } from './subscription.interface'; export interface InfoItem { currencies: string[]; demoAuthToken: string; + fearAndGreedDataSource?: string; globalPermissions: string[]; isReadOnlyMode?: boolean; lastDataGathering?: Date; From 5000e9c79bd9cb550ae7b0aa65e877b593ba31fb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Feb 2022 20:29:19 +0100 Subject: [PATCH 123/337] Feature/update database schema of order (#664) * Add schema migrations * Update changelog --- CHANGELOG.md | 4 ++++ .../migration.sql | 2 ++ .../migration.sql | 2 ++ .../migration.sql | 8 ++++++++ prisma/schema.prisma | 20 +++++++++---------- 5 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20220202192001_changed_data_source_to_optional_in_order/migration.sql create mode 100644 prisma/migrations/20220202192041_changed_symbol_to_optional_in_order/migration.sql create mode 100644 prisma/migrations/20220202192216_changed_symbol_profile_to_required_in_order/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index f268e4db3..12e1a3e0b 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 -Fixed the data source of the _Fear & Greed Index_ (market mood) +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.109.0 - 01.02.2022 ### Added diff --git a/prisma/migrations/20220202192001_changed_data_source_to_optional_in_order/migration.sql b/prisma/migrations/20220202192001_changed_data_source_to_optional_in_order/migration.sql new file mode 100644 index 000000000..56d3d0442 --- /dev/null +++ b/prisma/migrations/20220202192001_changed_data_source_to_optional_in_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" ALTER COLUMN "dataSource" DROP NOT NULL; diff --git a/prisma/migrations/20220202192041_changed_symbol_to_optional_in_order/migration.sql b/prisma/migrations/20220202192041_changed_symbol_to_optional_in_order/migration.sql new file mode 100644 index 000000000..4a077e004 --- /dev/null +++ b/prisma/migrations/20220202192041_changed_symbol_to_optional_in_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" ALTER COLUMN "symbol" DROP NOT NULL; diff --git a/prisma/migrations/20220202192216_changed_symbol_profile_to_required_in_order/migration.sql b/prisma/migrations/20220202192216_changed_symbol_profile_to_required_in_order/migration.sql new file mode 100644 index 000000000..6caf8c08f --- /dev/null +++ b/prisma/migrations/20220202192216_changed_symbol_profile_to_required_in_order/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Order" DROP CONSTRAINT "Order_symbolProfileId_fkey"; + +-- AlterTable +ALTER TABLE "Order" ALTER COLUMN "symbolProfileId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_symbolProfileId_fkey" FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c89f8ec5..2625c0fcd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -70,24 +70,24 @@ model MarketData { } model Order { - Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId]) + Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId]) accountId String? accountUserId String? - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) currency String? - dataSource DataSource + dataSource DataSource? date DateTime fee Float - id String @default(uuid()) - isDraft Boolean @default(false) + id String @default(uuid()) + isDraft Boolean @default(false) quantity Float - symbol String - SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id]) - symbolProfileId String? + symbol String? + SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) + symbolProfileId String type Type unitPrice Float - updatedAt DateTime @updatedAt - User User @relation(fields: [userId], references: [id]) + updatedAt DateTime @updatedAt + User User @relation(fields: [userId], references: [id]) userId String @@id([id, userId]) From 526a6b20308105f3ae89964c9f0a2fff1d768cd7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Feb 2022 20:31:23 +0100 Subject: [PATCH 124/337] Release 1.110.0 (#665) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e1a3e0b..e870ace5a 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.110.0 - 02.02.2022 ### Fixed diff --git a/package.json b/package.json index 0c1896012..196c48821 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.109.0", + "version": "1.110.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 16360c0c672d57916c8ce1c0acfd164670d9cbfe Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Feb 2022 22:06:34 +0100 Subject: [PATCH 125/337] Feature/minor code cleanup (#667) * Sort imports * Update changelog --- CHANGELOG.md | 2 +- .../transform-data-source-in-request.interceptor.ts | 1 + .../transform-data-source-in-response.interceptor.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e870ace5a..3c2ca72ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed --Fixed the data source of the _Fear & Greed Index_ (market mood) +- Fixed the data source of the _Fear & Greed Index_ (market mood) ### Todo diff --git a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts index 9cf85cdfd..aa9952473 100644 --- a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -6,6 +6,7 @@ import { NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; + import { ConfigurationService } from '../services/configuration.service'; @Injectable() 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 5e41f5da2..59e6c0e20 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 @@ -7,6 +7,7 @@ import { } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; + import { ConfigurationService } from '../services/configuration.service'; @Injectable() From 5d8bde5a700aa7e4214a49d3aa47a258db63a38e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Feb 2022 19:21:55 +0100 Subject: [PATCH 126/337] Feature/access data source and symbol from symbol profile (#668) * Access dataSource and symbol from SymbolProfile * Update changelog --- CHANGELOG.md | 6 +++++ apps/api/src/app/export/export.service.ts | 27 +++++++++++++++++-- .../app/portfolio/portfolio.service-new.ts | 7 +++-- .../src/app/portfolio/portfolio.service.ts | 7 +++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2ca72ec..d7f02a4bc 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 + +- Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) + ## 1.110.0 - 02.02.2022 ### Fixed diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 784b34d5e..30b1ed082 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -17,7 +17,7 @@ export class ExportService { date: true, fee: true, quantity: true, - symbol: true, + SymbolProfile: true, type: true, unitPrice: true }, @@ -26,7 +26,30 @@ export class ExportService { return { meta: { date: new Date().toISOString(), version: environment.version }, - orders + orders: orders.map( + ({ + accountId, + currency, + date, + fee, + quantity, + SymbolProfile, + type, + unitPrice + }) => { + return { + accountId, + currency, + date, + fee, + quantity, + type, + unitPrice, + dataSource: SymbolProfile.dataSource, + symbol: SymbolProfile.symbol + }; + } + ) }; } } diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index d2aec3683..64d4ea0a0 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -407,8 +407,11 @@ export class PortfolioServiceNew { const orders = ( await this.orderService.getOrders({ userCurrency, userId }) - ).filter((order) => { - return order.dataSource === aDataSource && order.symbol === aSymbol; + ).filter(({ SymbolProfile }) => { + return ( + SymbolProfile.dataSource === aDataSource && + SymbolProfile.symbol === aSymbol + ); }); if (orders.length <= 0) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9e4135a03..5cd06dc57 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -395,8 +395,11 @@ export class PortfolioService { const orders = ( await this.orderService.getOrders({ userCurrency, userId }) - ).filter((order) => { - return order.dataSource === aDataSource && order.symbol === aSymbol; + ).filter(({ SymbolProfile }) => { + return ( + SymbolProfile.dataSource === aDataSource && + SymbolProfile.symbol === aSymbol + ); }); if (orders.length <= 0) { From dc424a86ec069f8d334bcb3132925d7c15e26d15 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Feb 2022 20:56:39 +0100 Subject: [PATCH 127/337] Feature/support deleting symbol profile data (#669) * Add support for deleting symbol profile data * Update changelog --- CHANGELOG.md | 4 ++ apps/api/src/app/admin/admin.controller.ts | 27 ++++++++- apps/api/src/app/admin/admin.module.ts | 4 +- apps/api/src/app/admin/admin.service.ts | 30 ++++++++-- apps/api/src/services/market-data.service.ts | 15 +++++ .../src/services/symbol-profile.service.ts | 16 ++++- .../admin-market-data.component.ts | 59 ++++++++++++++----- .../admin-market-data/admin-market-data.html | 12 +++- apps/client/src/app/services/admin.service.ts | 35 ++++++++++- apps/client/src/app/services/data.service.ts | 14 ----- 10 files changed, 174 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f02a4bc..ca1310641 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 deleting symbol profile data in the admin control panel + ### Changed - Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e24522444..8ce4def5e 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -11,6 +11,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, + Delete, Get, HttpException, Inject, @@ -195,9 +196,10 @@ export class AdminController { return this.adminService.getMarketData(); } - @Get('market-data/:symbol') + @Get('market-data/:dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async getMarketDataBySymbol( + @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { if ( @@ -212,7 +214,7 @@ export class AdminController { ); } - return this.adminService.getMarketDataBySymbol(symbol); + return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); } @Put('market-data/:dataSource/:symbol/:dateString') @@ -248,6 +250,27 @@ export class AdminController { }); } + @Delete('profile-data/:dataSource/:symbol') + @UseGuards(AuthGuard('jwt')) + public async deleteProfileData( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.adminService.deleteProfileData({ dataSource, symbol }); + } + @Put('settings/:key') @UseGuards(AuthGuard('jwt')) public async updateProperty( diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index b8e96fa04..8e83236e4 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { Module } from '@nestjs/common'; import { AdminController } from './admin.controller'; @@ -20,7 +21,8 @@ import { AdminService } from './admin.service'; MarketDataModule, PrismaModule, PropertyModule, - SubscriptionModule + SubscriptionModule, + SymbolProfileModule ], controllers: [AdminController], providers: [AdminService], diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 4df3ea09f..9c1de05b0 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -5,6 +5,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { AdminData, @@ -13,7 +14,7 @@ import { AdminMarketDataItem } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; -import { Property } from '@prisma/client'; +import { DataSource, Property } from '@prisma/client'; import { differenceInDays } from 'date-fns'; @Injectable() @@ -25,9 +26,21 @@ export class AdminService { private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, - private readonly subscriptionService: SubscriptionService + private readonly subscriptionService: SubscriptionService, + private readonly symbolProfileService: SymbolProfileService ) {} + public async deleteProfileData({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + await this.marketDataService.deleteMany({ dataSource, symbol }); + await this.symbolProfileService.delete({ dataSource, symbol }); + } + public async get(): Promise { return { dataGatheringProgress: @@ -121,16 +134,21 @@ export class AdminService { }; } - public async getMarketDataBySymbol( - aSymbol: string - ): Promise { + public async getMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): Promise { return { marketData: await this.marketDataService.marketDataItems({ orderBy: { date: 'asc' }, where: { - symbol: aSymbol + dataSource, + symbol } }) }; diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index 0e0a26883..582ee2593 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/apps/api/src/services/market-data.service.ts @@ -9,6 +9,21 @@ 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; + }) { + return this.prismaService.marketData.deleteMany({ + where: { + dataSource, + symbol + } + }); + } + public async get({ date, symbol diff --git a/apps/api/src/services/symbol-profile.service.ts b/apps/api/src/services/symbol-profile.service.ts index 0a95fd6da..0b2857338 100644 --- a/apps/api/src/services/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile.service.ts @@ -4,14 +4,26 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Injectable } from '@nestjs/common'; -import { Prisma, SymbolProfile } from '@prisma/client'; +import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { continents, countries } from 'countries-list'; import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface'; @Injectable() export class SymbolProfileService { - constructor(private readonly prismaService: PrismaService) {} + public constructor(private readonly prismaService: PrismaService) {} + + public async delete({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.prismaService.symbolProfile.delete({ + where: { dataSource_symbol: { dataSource, symbol } } + }); + } public async getSymbolProfiles( symbols: string[] 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 a8d96233a..5dd3a3fd2 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 @@ -20,6 +20,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-market-data.html' }) export class AdminMarketDataComponent implements OnDestroy, OnInit { + public currentDataSource: DataSource; public currentSymbol: string; public defaultDateFormat = DEFAULT_DATE_FORMAT; public marketData: AdminMarketDataItem[] = []; @@ -43,6 +44,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } + public onDeleteProfileData({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .deleteProfileData({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + public onGatherProfileDataBySymbol({ dataSource, symbol @@ -69,22 +83,33 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { .subscribe(() => {}); } - public setCurrentSymbol(aSymbol: string) { + public onMarketDataChanged(withRefresh: boolean = false) { + if (withRefresh) { + this.fetchAdminMarketData(); + this.fetchAdminMarketDataBySymbol({ + dataSource: this.currentDataSource, + symbol: this.currentSymbol + }); + } + } + + public setCurrentProfile({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { this.marketDataDetails = []; - if (this.currentSymbol === aSymbol) { + if (this.currentSymbol === symbol) { + this.currentDataSource = undefined; this.currentSymbol = ''; } else { - this.currentSymbol = aSymbol; + this.currentDataSource = dataSource; + this.currentSymbol = symbol; - this.fetchAdminMarketDataBySymbol(this.currentSymbol); - } - } - - public onMarketDataChanged(withRefresh: boolean = false) { - if (withRefresh) { - this.fetchAdminMarketData(); - this.fetchAdminMarketDataBySymbol(this.currentSymbol); + this.fetchAdminMarketDataBySymbol({ dataSource, symbol }); } } @@ -104,9 +129,15 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { }); } - private fetchAdminMarketDataBySymbol(aSymbol: string) { - this.dataService - .fetchAdminMarketDataBySymbol(aSymbol) + private fetchAdminMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .fetchAdminMarketDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ marketData }) => { this.marketDataDetails = marketData; 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 d1e45bfc9..3ff7435b4 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 @@ -16,7 +16,7 @@ {{ item.symbol }} {{ item.dataSource }} @@ -49,11 +49,19 @@ > Gather Profile Data + - + ( + `/api/admin/profile-data/${dataSource}/${symbol}` + ); + } + + public fetchAdminMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): Observable { + return this.http + .get(`/api/admin/market-data/${dataSource}/${symbol}`) + .pipe( + map((data) => { + for (const item of data.marketData) { + item.date = parseISO(item.date); + } + return data; + }) + ); + } + public gatherMax() { return this.http.post(`/api/admin/gather/max`, {}); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 8d614a545..bf03b3f5f 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -18,7 +18,6 @@ import { Accounts, AdminData, AdminMarketData, - AdminMarketDataDetails, Export, InfoItem, PortfolioChart, @@ -69,19 +68,6 @@ export class DataService { return this.http.get('/api/admin/market-data'); } - public fetchAdminMarketDataBySymbol( - aSymbol: string - ): Observable { - return this.http.get(`/api/admin/market-data/${aSymbol}`).pipe( - map((data) => { - for (const item of data.marketData) { - item.date = parseISO(item.date); - } - return data; - }) - ); - } - public deleteAccess(aId: string) { return this.http.delete(`/api/access/${aId}`); } From f36999691208ce74689d572310a0f0cb729585b1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Feb 2022 20:58:59 +0100 Subject: [PATCH 128/337] Bugfix/fix symbol selection of 7d data gathering (#670) * Fix symbol selection of 7d data gathering * Update changelog --- CHANGELOG.md | 4 +++ .../src/services/data-gathering.service.ts | 28 +++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1310641..245b719d9 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 deleting symbol profile data in the admin control panel +### Fixed + +- Fixed the symbol selection of the 7d data gathering + ### Changed - Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 1098f2082..8fe2c4835 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -473,9 +473,18 @@ export class DataGatheringService { private async getSymbols7D(): Promise { const startDate = subDays(resetHours(new Date()), 7); + const symbolProfiles = await this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }], + select: { + dataSource: true, + scraperConfiguration: true, + symbol: true + } + }); + // Only consider symbols with incomplete market data for the last // 7 days - const symbolsToGather = ( + const symbolsNotToGather = ( await this.prismaService.marketData.groupBy({ _count: true, by: ['symbol'], @@ -485,24 +494,15 @@ export class DataGatheringService { }) ) .filter((group) => { - return group._count < 6; + return group._count >= 6; }) .map((group) => { return group.symbol; }); - const symbolProfilesToGather = ( - await this.prismaService.symbolProfile.findMany({ - orderBy: [{ symbol: 'asc' }], - select: { - dataSource: true, - scraperConfiguration: true, - symbol: true - } - }) - ) + const symbolProfilesToGather = symbolProfiles .filter(({ symbol }) => { - return symbolsToGather.includes(symbol); + return !symbolsNotToGather.includes(symbol); }) .map((symbolProfile) => { return { @@ -514,7 +514,7 @@ export class DataGatheringService { const currencyPairsToGather = this.exchangeRateDataService .getCurrencyPairs() .filter(({ symbol }) => { - return symbolsToGather.includes(symbol); + return !symbolsNotToGather.includes(symbol); }) .map(({ dataSource, symbol }) => { return { From 48f6b8d3537c56190e021503cca30a80d7740ab5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Feb 2022 21:00:53 +0100 Subject: [PATCH 129/337] Release 1.111.0 (#671) --- CHANGELOG.md | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 245b719d9..e27515571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,20 +5,20 @@ 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.111.0 - 03.02.2022 ### Added - Added support for deleting symbol profile data in the admin control panel -### Fixed - -- Fixed the symbol selection of the 7d data gathering - ### Changed - Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) +### Fixed + +- Fixed the symbol selection of the 7d data gathering + ## 1.110.0 - 02.02.2022 ### Fixed diff --git a/package.json b/package.json index 196c48821..033097f9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.110.0", + "version": "1.111.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 67d40333f624175cdaa2e96f2cf08b77343c57a5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Feb 2022 10:17:09 +0100 Subject: [PATCH 130/337] Move currency column (#674) --- .../app/components/accounts-table/accounts-table.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ed0b9b298..eda979eaa 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 @@ -46,9 +46,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { public ngOnChanges() { this.displayedColumns = [ 'account', - 'currency', 'platform', 'transactions', + 'currency', 'balance', 'value' ]; From 48b524de5aecaa091819e2b725a03e3208a357fe Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Feb 2022 20:26:10 +0100 Subject: [PATCH 131/337] Feature/add export functionality to position detail dialog (#672) * Add export functionality to the position detail dialog * Respect filters in activities export * Update changelog --- CHANGELOG.md | 10 ++++++ apps/api/src/app/export/export.controller.ts | 16 ++++++++-- apps/api/src/app/export/export.service.ts | 17 ++++++++-- .../home-holdings/home-holdings.component.ts | 11 +++++++ .../interfaces/interfaces.ts | 1 + .../position-detail-dialog.component.ts | 22 ++++++++++++- .../position-detail-dialog.html | 2 ++ .../allocations/allocations-page.component.ts | 1 + .../transactions-page.component.ts | 31 ++++++------------- .../transactions/transactions-page.html | 3 +- apps/client/src/app/services/data.service.ts | 12 +++++-- libs/common/src/lib/helper.ts | 14 +++++++++ .../activities-table.component.html | 5 +++ .../activities-table.component.ts | 20 +++++++----- 14 files changed, 127 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27515571..679bb42fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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 export functionality to the position detail dialog + +### Changed + +- Improved the export functionality for activities (respect filtering) + ## 1.111.0 - 03.02.2022 ### Added diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index ca318ce81..3617ebe24 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -1,6 +1,13 @@ import { Export } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; -import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Headers, + Inject, + Query, + UseGuards +} from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -15,8 +22,11 @@ export class ExportController { @Get() @UseGuards(AuthGuard('jwt')) - public async export(): Promise { - return await this.exportService.export({ + public async export( + @Query('activityIds') activityIds?: string[] + ): Promise { + return this.exportService.export({ + activityIds, userId: this.request.user.id }); } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 30b1ed082..301f13cea 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -7,8 +7,14 @@ import { Injectable } from '@nestjs/common'; export class ExportService { public constructor(private readonly prismaService: PrismaService) {} - public async export({ userId }: { userId: string }): Promise { - const orders = await this.prismaService.order.findMany({ + public async export({ + activityIds, + userId + }: { + activityIds?: string[]; + userId: string; + }): Promise { + let orders = await this.prismaService.order.findMany({ orderBy: { date: 'desc' }, select: { accountId: true, @@ -16,6 +22,7 @@ export class ExportService { dataSource: true, date: true, fee: true, + id: true, quantity: true, SymbolProfile: true, type: true, @@ -24,6 +31,12 @@ export class ExportService { where: { userId } }); + if (activityIds) { + orders = orders.filter((order) => { + return activityIds.includes(order.id); + }); + } + return { meta: { date: new Date().toISOString(), version: environment.version }, orders: orders.map( 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 c7e964372..50c452ea8 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 @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { RANGE, SettingsStorageService @@ -26,6 +27,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public dateRange: DateRange; public dateRangeOptions = defaultDateRangeOptions; public deviceType: string; + public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public positions: Position[]; public user: User; @@ -40,6 +42,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, private settingsStorageService: SettingsStorageService, @@ -82,6 +85,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + this.dateRange = this.settingsStorageService.getSetting(RANGE) || 'max'; @@ -119,6 +129,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts index daac4065a..791c2b46a 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts @@ -4,6 +4,7 @@ export interface PositionDetailDialogParams { baseCurrency: string; dataSource: DataSource; deviceType: string; + hasImpersonationId: boolean; locale: string; symbol: string; } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 1802619a0..02563afba 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { AssetSubClass } from '@prisma/client'; @@ -185,6 +185,26 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.dialogRef.close(); } + public onExport() { + this.dataService + .fetchExport( + this.orders.map((order) => { + return order.id; + }) + ) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + downloadAsFile( + data, + `ghostfolio-export-${this.symbol}-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.json`, + 'text/plain' + ); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 00d949263..db8f78bc3 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -131,12 +131,14 @@ [baseCurrency]="data.baseCurrency" [deviceType]="data.deviceType" [hasPermissionToCreateActivity]="false" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilter]="false" [hasPermissionToImportActivities]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" [showActions]="false" [showSymbolColumn]="false" + (export)="onExport()" >
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 f89297403..82a7fdcc4 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 @@ -316,6 +316,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 8172ed080..abd99042d 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -10,6 +10,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { downloadAsFile } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataSource, Order as OrderModel } from '@prisma/client'; @@ -90,11 +91,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { public ngOnInit() { const { globalPermissions } = this.dataService.fetchInfo(); - this.hasPermissionToImportOrders = hasPermission( - globalPermissions, - permissions.enableImport - ); - this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.impersonationStorageService @@ -102,6 +98,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; + + this.hasPermissionToImportOrders = + hasPermission(globalPermissions, permissions.enableImport) && + !this.hasImpersonationId; }); this.userService.stateChanged @@ -147,12 +147,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - public onExport() { + public onExport(activityIds?: string[]) { this.dataService - .fetchExport() + .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - this.downloadAsFile( + downloadAsFile( data, `ghostfolio-export-${format( parseISO(data.meta.date), @@ -303,20 +303,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private downloadAsFile( - aContent: unknown, - aFileName: string, - aContentType: string - ) { - const a = document.createElement('a'); - const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { - type: aContentType - }); - a.href = URL.createObjectURL(file); - a.download = aFileName; - a.click(); - } - private handleImportError({ error, orders }: { error: any; orders: any[] }) { this.snackBar.dismiss(); @@ -406,6 +392,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index db365c2d9..0df0171b9 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -7,13 +7,14 @@ [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToImportActivities]="hasPermissionToImportOrders" [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView" (activityDeleted)="onDeleteTransaction($event)" (activityToClone)="onCloneTransaction($event)" (activityToUpdate)="onUpdateTransaction($event)" - (export)="onExport()" + (export)="onExport($event)" (import)="onImport()" >
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index bf03b3f5f..fac56a1f2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -94,8 +94,16 @@ export class DataService { }); } - public fetchExport() { - return this.http.get('/api/export'); + public fetchExport(activityIds?: string[]) { + let params = new HttpParams(); + + if (activityIds) { + params = params.append('activityIds', activityIds.join(',')); + } + + return this.http.get('/api/export', { + params + }); } public fetchInfo(): InfoItem { diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 4fac26654..dbfc787f3 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -12,6 +12,20 @@ export function decodeDataSource(encodedDataSource: string) { return Buffer.from(encodedDataSource, 'hex').toString(); } +export function downloadAsFile( + aContent: unknown, + aFileName: string, + aContentType: string +) { + const a = document.createElement('a'); + const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { + type: aContentType + }); + a.href = URL.createObjectURL(file); + a.download = aFileName; + a.click(); +} + export function encodeDataSource(aDataSource: DataSource) { return Buffer.from(aDataSource, 'utf-8').toString('hex'); } 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 5af6013bb..31475941f 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -268,6 +268,9 @@
- + diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts index da122b585..b4b15dfdb 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts @@ -1,9 +1,10 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { User } from '@ghostfolio/common/interfaces'; -import { Account, Order } from '@prisma/client'; +import { Account } from '@prisma/client'; export interface CreateOrUpdateTransactionDialogParams { accountId: string; accounts: Account[]; - transaction: Order; + activity: Activity; user: User; } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 5d165c40f..a40d44a4f 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -132,8 +132,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - public onCloneTransaction(aTransaction: OrderModel) { - this.openCreateTransactionDialog(aTransaction); + public onCloneTransaction(aActivity: Activity) { + this.openCreateTransactionDialog(aActivity); } public onDeleteTransaction(aId: string) { @@ -242,35 +242,13 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - public openUpdateTransactionDialog({ - accountId, - currency, - dataSource, - date, - fee, - id, - quantity, - symbol, - type, - unitPrice - }: OrderModel): void { + public openUpdateTransactionDialog(activity: Activity): void { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { data: { + activity, accounts: this.user?.accounts?.filter((account) => { return account.accountType === 'SECURITIES'; }), - transaction: { - accountId, - currency, - dataSource, - date, - fee, - id, - quantity, - symbol, - type, - unitPrice - }, user: this.user }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', @@ -281,7 +259,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data: any) => { - const transaction: UpdateOrderDto = data?.transaction; + const transaction: UpdateOrderDto = data?.activity; if (transaction) { this.dataService @@ -324,7 +302,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - private openCreateTransactionDialog(aTransaction?: OrderModel): void { + private openCreateTransactionDialog(aActivity?: Activity): void { this.userService .get() .pipe(takeUntil(this.unsubscribeSubject)) @@ -336,15 +314,14 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { accounts: this.user?.accounts?.filter((account) => { return account.accountType === 'SECURITIES'; }), - transaction: { - accountId: aTransaction?.accountId ?? this.defaultAccountId, - currency: aTransaction?.currency ?? null, - dataSource: aTransaction?.dataSource ?? null, + activity: { + ...aActivity, + accountId: aActivity?.accountId ?? this.defaultAccountId, date: new Date(), + id: null, fee: 0, quantity: null, - symbol: aTransaction?.symbol ?? null, - type: aTransaction?.type ?? 'BUY', + type: aActivity?.type ?? 'BUY', unitPrice: null }, user: this.user @@ -357,7 +334,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data: any) => { - const transaction: CreateOrderDto = data?.transaction; + const transaction: CreateOrderDto = data?.activity; if (transaction) { this.dataService.postOrder(transaction).subscribe({ diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 8b75473d1..ff6e624d0 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -6,7 +6,7 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; import { DataSource, MarketData } from '@prisma/client'; import { format, parseISO } from 'date-fns'; -import { map, Observable } from 'rxjs'; +import { Observable, map } from 'rxjs'; @Injectable({ providedIn: 'root' diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index b39d4ac65..8fe829d55 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -245,6 +245,8 @@ export class ImportTransactionsService { return Type.BUY; case 'dividend': return Type.DIVIDEND; + case 'item': + return Type.ITEM; case 'sell': return Type.SELL; default: diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 10025f694..7ffec5d4d 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -7,6 +7,7 @@ export interface PortfolioSummary extends PortfolioPerformance { committedFunds: number; fees: number; firstOrderDate: Date; + items: number; netWorth: number; ordersCount: number; totalBuy: number; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 5fd343a0a..4cb943f55 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -87,15 +87,21 @@ [ngClass]="{ buy: element.type === 'BUY', dividend: element.type === 'DIVIDEND', + item: element.type === 'ITEM', sell: element.type === 'SELL' }" > + + {{ element.type }}
@@ -109,7 +115,12 @@
- {{ element.symbol | gfSymbol }} + + {{ element.SymbolProfile.name }} + + + {{ element.SymbolProfile.symbol | gfSymbol }} + Draft @@ -349,13 +360,15 @@ (click)=" hasPermissionToOpenDetails && !row.isDraft && + row.type !== 'ITEM' && onOpenPositionDialog({ dataSource: row.dataSource, - symbol: row.symbol + symbol: row.SymbolProfile.symbol }) " [ngClass]="{ - 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft + 'cursor-pointer': + hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM' }" > = this.filters$.asObservable(); public isAfter = isAfter; public isLoading = true; + public isUUID = isUUID; public placeholder = ''; public routeQueryParams: Subscription; public searchControl = new FormControl(); @@ -271,11 +273,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { activity: OrderWithAccount, fieldValues: Set = new Set() ): string[] { - fieldValues.add(activity.currency); - fieldValues.add(activity.symbol); - fieldValues.add(activity.type); fieldValues.add(activity.Account?.name); fieldValues.add(activity.Account?.Platform?.name); + fieldValues.add(activity.SymbolProfile.currency); + + if (!isUUID(activity.SymbolProfile.symbol)) { + fieldValues.add(activity.SymbolProfile.symbol); + } + + fieldValues.add(activity.type); fieldValues.add(format(activity.date, 'yyyy')); return [...fieldValues].filter((item) => { @@ -302,7 +308,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { for (const activity of this.dataSource.filteredData) { if (isNumber(activity.valueInBaseCurrency)) { - if (activity.type === 'BUY') { + if (activity.type === 'BUY' || activity.type === 'ITEM') { totalValue = totalValue.plus(activity.valueInBaseCurrency); } else if (activity.type === 'SELL') { totalValue = totalValue.minus(activity.valueInBaseCurrency); diff --git a/prisma/migrations/20220209194930_added_manual_to_data_source/migration.sql b/prisma/migrations/20220209194930_added_manual_to_data_source/migration.sql new file mode 100644 index 000000000..3b8e61250 --- /dev/null +++ b/prisma/migrations/20220209194930_added_manual_to_data_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DataSource" ADD VALUE 'MANUAL'; diff --git a/prisma/migrations/20220209195038_added_item_to_order_type/migration.sql b/prisma/migrations/20220209195038_added_item_to_order_type/migration.sql new file mode 100644 index 000000000..5275e96d8 --- /dev/null +++ b/prisma/migrations/20220209195038_added_item_to_order_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Type" ADD VALUE 'ITEM'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 53467d7b7..f0a1a173b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -185,6 +185,7 @@ enum DataSource { ALPHA_VANTAGE GHOSTFOLIO GOOGLE_SHEETS + MANUAL RAKUTEN YAHOO } @@ -208,5 +209,6 @@ enum Role { enum Type { BUY DIVIDEND + ITEM SELL } diff --git a/test/import/ok.csv b/test/import/ok.csv index 0f67f2d4f..ddefbc93f 100644 --- a/test/import/ok.csv +++ b/test/import/ok.csv @@ -1,3 +1,4 @@ Date,Code,Currency,Price,Quantity,Action,Fee 17/11/2021,MSFT,USD,0.62,5,dividend,0.00 16/09/2021,MSFT,USD,298.580,5,buy,19.00 +01/01/2022,Penthouse Apartment,USD,500000.0,1,item,0.00 From e355847f40303f70d4eeedb597913c15d8d3bc6d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Feb 2022 10:33:58 +0100 Subject: [PATCH 146/337] Release 1.114.0 (#688) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2389ec01e..c8bfe0a3b 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.114.0 - 10.02.2022 ### Added diff --git a/package.json b/package.json index f87e1116a..789eb8cd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.113.0", + "version": "1.114.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From dcc7ef89fe1141a420b674bcf391c5a4935406b7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Feb 2022 11:16:25 +0100 Subject: [PATCH 147/337] Release/1.114.1 (#689) * Fix creation of wealth items * Release 1.114.1 --- CHANGELOG.md | 6 ++++++ .../create-or-update-transaction-dialog.component.ts | 8 +++++--- package.json | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bfe0a3b..ccd6e9aa6 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.114.1 - 10.02.2022 + +### Fixed + +- Fixed the creation of (wealth) items + ## 1.114.0 - 10.02.2022 ### Added 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 05c02bcb4..40c38bb95 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 @@ -216,9 +216,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { dataSource: this.activityForm.controls['dataSource'].value, fee: this.activityForm.controls['fee'].value, quantity: this.activityForm.controls['quantity'].value, - symbol: isUUID(this.activityForm.controls['searchSymbol'].value.symbol) - ? this.activityForm.controls['name'].value - : this.activityForm.controls['searchSymbol'].value.symbol, + symbol: + this.activityForm.controls['searchSymbol'].value.symbol === undefined || + isUUID(this.activityForm.controls['searchSymbol'].value.symbol) + ? this.activityForm.controls['name'].value + : this.activityForm.controls['searchSymbol'].value.symbol, type: this.activityForm.controls['type'].value, unitPrice: this.activityForm.controls['unitPrice'].value }; diff --git a/package.json b/package.json index 789eb8cd2..045d14443 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.114.0", + "version": "1.114.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 90ad22cccfd04e7682a834c7bebd2d55d9134d99 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 11 Feb 2022 10:00:49 +0100 Subject: [PATCH 148/337] Add import and export (#690) --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 64c8cfd3a..7927faa6a 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,13 @@ If you prefer to run Ghostfolio on your own infrastructure (self-hosting), pleas Ghostfolio is for you if you are... - 💼 trading stocks, ETFs or cryptocurrencies on multiple platforms - - 🏦 pursuing a buy & hold strategy - - 🎯 interested in getting insights of your portfolio composition - - 👻 valuing privacy and data ownership - - 🧘 into minimalism - - 🧺 caring about diversifying your financial resources - - 🆓 interested in financial independence - - 🙅 saying no to spreadsheets in 2021 - - 😎 still reading this list ## Features @@ -65,6 +57,7 @@ Ghostfolio is for you if you are... - ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `YTD`, `1Y`, `5Y`, `Max` - ✅ Various charts - ✅ Static analysis to identify potential risks in your portfolio +- ✅ Import and export transactions - ✅ Dark Mode - ✅ Zen Mode - ✅ Mobile-first design From 9344dcd26e4f960f66babbda0eaf334516d000bf Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 11 Feb 2022 10:01:15 +0100 Subject: [PATCH 149/337] Feature/upgrade nx to 13.8.1 (#691) * Upgrade angular, nx and storybook dependencies * Update changelog --- CHANGELOG.md | 8 + angular.json | 6 +- package.json | 60 +- yarn.lock | 4249 ++++++++++++++++++++++++++------------------------ 4 files changed, 2280 insertions(+), 2043 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd6e9aa6..97df251e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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 `angular` from version `13.1.2` to `13.2.3` +- Upgraded `Nx` from version `13.4.1` to `13.8.1` +- Upgraded `storybook` from version `6.4.9` to `6.4.18` + ## 1.114.1 - 10.02.2022 ### Fixed diff --git a/angular.json b/angular.json index c2f3109e6..29d3a50ed 100644 --- a/angular.json +++ b/angular.json @@ -264,7 +264,8 @@ "port": 4400, "config": { "configFolder": "libs/ui/.storybook" - } + }, + "projectBuildConfig": "ui:build-storybook" }, "configurations": { "ci": { @@ -280,7 +281,8 @@ "outputPath": "dist/storybook/ui", "config": { "configFolder": "libs/ui/.storybook" - } + }, + "projectBuildConfig": "ui:build-storybook" }, "configurations": { "ci": { diff --git a/package.json b/package.json index 045d14443..10d767ebc 100644 --- a/package.json +++ b/package.json @@ -49,16 +49,16 @@ "workspace-generator": "nx workspace-generator" }, "dependencies": { - "@angular/animations": "13.1.1", - "@angular/cdk": "13.1.1", - "@angular/common": "13.1.1", - "@angular/compiler": "13.1.1", - "@angular/core": "13.1.1", - "@angular/forms": "13.1.1", - "@angular/material": "13.1.1", - "@angular/platform-browser": "13.1.1", - "@angular/platform-browser-dynamic": "13.1.1", - "@angular/router": "13.1.1", + "@angular/animations": "13.2.2", + "@angular/cdk": "13.2.2", + "@angular/common": "13.2.2", + "@angular/compiler": "13.2.2", + "@angular/core": "13.2.2", + "@angular/forms": "13.2.2", + "@angular/material": "13.2.2", + "@angular/platform-browser": "13.2.2", + "@angular/platform-browser-dynamic": "13.2.2", + "@angular/router": "13.2.2", "@codewithdan/observable-store": "2.2.11", "@dinero.js/currencies": "2.0.0-alpha.8", "@nestjs/common": "8.2.3", @@ -69,7 +69,7 @@ "@nestjs/platform-express": "8.2.3", "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", - "@nrwl/angular": "13.4.1", + "@nrwl/angular": "13.8.1", "@prisma/client": "3.9.1", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", @@ -119,29 +119,29 @@ "zone.js": "0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "13.1.2", + "@angular-devkit/build-angular": "13.2.3", "@angular-eslint/eslint-plugin": "13.0.1", "@angular-eslint/eslint-plugin-template": "13.0.1", "@angular-eslint/template-parser": "13.0.1", - "@angular/cli": "13.1.2", - "@angular/compiler-cli": "13.1.1", - "@angular/language-service": "13.1.1", - "@angular/localize": "13.1.1", + "@angular/cli": "13.2.3", + "@angular/compiler-cli": "13.2.2", + "@angular/language-service": "13.2.2", + "@angular/localize": "13.2.2", "@nestjs/schematics": "8.0.5", "@nestjs/testing": "8.2.3", - "@nrwl/cli": "13.4.1", - "@nrwl/cypress": "13.4.1", - "@nrwl/eslint-plugin-nx": "13.4.1", - "@nrwl/jest": "13.4.1", - "@nrwl/nest": "13.4.1", - "@nrwl/node": "13.4.1", - "@nrwl/storybook": "13.4.1", - "@nrwl/tao": "13.4.1", - "@nrwl/workspace": "13.4.1", - "@storybook/addon-essentials": "6.4.9", - "@storybook/angular": "6.4.9", - "@storybook/builder-webpack5": "6.4.9", - "@storybook/manager-webpack5": "6.4.9", + "@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", + "@storybook/addon-essentials": "6.4.18", + "@storybook/angular": "6.4.18", + "@storybook/builder-webpack5": "6.4.18", + "@storybook/manager-webpack5": "6.4.18", "@types/big.js": "6.1.2", "@types/cache-manager": "3.4.2", "@types/color": "3.0.2", @@ -169,7 +169,7 @@ "rimraf": "3.0.2", "ts-jest": "27.0.5", "ts-node": "9.1.1", - "typescript": "4.5.4" + "typescript": "4.5.5" }, "engines": { "node": ">=14" diff --git a/yarn.lock b/yarn.lock index 6d75f42a8..8671fd039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,53 +2,53 @@ # yarn lockfile v1 -"@ampproject/remapping@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-1.0.2.tgz#a7ebbadb71517dd63298420868f27d98fe230a0a" - integrity sha512-SncaVxs+E3EdoA9xJgHfWPxZfowAgeIsd71VpqCKP6KNKm6s7zSqqvUc70UpKUFsrV3dAmy6qxHoIj5NG+3DiA== +"@ampproject/remapping@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-1.1.1.tgz#e220d0a5288b07afd6392a584d15921839e9da32" + integrity sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw== dependencies: - "@jridgewell/resolve-uri" "1.0.0" + "@jridgewell/resolve-uri" "^3.0.3" sourcemap-codec "1.4.8" -"@angular-devkit/architect@0.1301.2": - version "0.1301.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1301.2.tgz#a646862b7ef388e4912473c14d336dde94cfc517" - integrity sha512-v8e6OF80Ezo5MTHtFcq1AZJH+Wq+hN9pMZ1iLGkODIfKIW9zx6aPhx0JY0b7sZkfNVL8ay8JA8f339eBMnOE9A== +"@angular-devkit/architect@0.1302.3": + version "0.1302.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1302.3.tgz#0e71cc2d737be6828d4072f53872aa2b17940870" + integrity sha512-0m8jMKrFfIqsYt33zTUwSmyekyfuS67hna08RQ6USjzWQSE3z4S8ulCUARSjM6AzdMblX+whfy56nJUpT17NSA== dependencies: - "@angular-devkit/core" "13.1.2" + "@angular-devkit/core" "13.2.3" rxjs "6.6.7" -"@angular-devkit/build-angular@13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-13.1.2.tgz#77004c925aced5ff9993c42cc098aaf47e06ec76" - integrity sha512-0FeDqfjWJjgIU42T3136RNYb7Yv2as6Z8rAnfUlX6RjRGZf98+6ZQZ80yREgrLkm7L8G1qWJc1sn3NyVMDwf9A== - dependencies: - "@ampproject/remapping" "1.0.2" - "@angular-devkit/architect" "0.1301.2" - "@angular-devkit/build-webpack" "0.1301.2" - "@angular-devkit/core" "13.1.2" - "@babel/core" "7.16.0" - "@babel/generator" "7.16.0" - "@babel/helper-annotate-as-pure" "7.16.0" - "@babel/plugin-proposal-async-generator-functions" "7.16.4" - "@babel/plugin-transform-async-to-generator" "7.16.0" - "@babel/plugin-transform-runtime" "7.16.4" - "@babel/preset-env" "7.16.4" - "@babel/runtime" "7.16.3" - "@babel/template" "7.16.0" +"@angular-devkit/build-angular@13.2.3": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-13.2.3.tgz#03d9bb0f3a620a9125c2794e801c7071a9fea7e7" + integrity sha512-cZ2gRcMRgW3t1WCeP+2D/wmr2M+BR/RICAh0wL9irIdypWAzIFt3Z2+2R/HmgAAxoEkdUMIfB9AnkYmwRVgFeA== + dependencies: + "@ampproject/remapping" "1.1.1" + "@angular-devkit/architect" "0.1302.3" + "@angular-devkit/build-webpack" "0.1302.3" + "@angular-devkit/core" "13.2.3" + "@babel/core" "7.16.12" + "@babel/generator" "7.16.8" + "@babel/helper-annotate-as-pure" "7.16.7" + "@babel/plugin-proposal-async-generator-functions" "7.16.8" + "@babel/plugin-transform-async-to-generator" "7.16.8" + "@babel/plugin-transform-runtime" "7.16.10" + "@babel/preset-env" "7.16.11" + "@babel/runtime" "7.16.7" + "@babel/template" "7.16.7" "@discoveryjs/json-ext" "0.5.6" - "@ngtools/webpack" "13.1.2" + "@ngtools/webpack" "13.2.3" ansi-colors "4.1.1" babel-loader "8.2.3" babel-plugin-istanbul "6.1.1" browserslist "^4.9.1" cacache "15.3.0" circular-dependency-plugin "5.2.2" - copy-webpack-plugin "10.0.0" - core-js "3.19.3" - critters "0.0.15" + copy-webpack-plugin "10.2.1" + core-js "3.20.3" + critters "0.0.16" css-loader "6.5.1" - esbuild-wasm "0.14.2" + esbuild-wasm "0.14.14" glob "7.2.0" https-proxy-agent "5.0.0" inquirer "8.2.0" @@ -56,46 +56,46 @@ karma-source-map-support "1.4.0" less "4.1.2" less-loader "10.2.0" - license-webpack-plugin "4.0.0" + license-webpack-plugin "4.0.1" loader-utils "3.2.0" - mini-css-extract-plugin "2.4.5" + mini-css-extract-plugin "2.5.3" minimatch "3.0.4" open "8.4.0" ora "5.4.1" parse5-html-rewriting-stream "6.0.1" - piscina "3.1.0" - postcss "8.4.4" + piscina "3.2.0" + postcss "8.4.5" postcss-import "14.0.2" postcss-loader "6.2.1" - postcss-preset-env "6.7.0" + postcss-preset-env "7.2.3" regenerator-runtime "0.13.9" - resolve-url-loader "4.0.0" + resolve-url-loader "5.0.0" rxjs "6.6.7" - sass "1.44.0" + sass "1.49.0" sass-loader "12.4.0" semver "7.3.5" - source-map-loader "3.0.0" + source-map-loader "3.0.1" source-map-support "0.5.21" - stylus "0.55.0" + stylus "0.56.0" stylus-loader "6.2.0" terser "5.10.0" text-table "0.2.0" tree-kill "1.2.2" tslib "2.3.1" - webpack "5.65.0" - webpack-dev-middleware "5.2.2" - webpack-dev-server "4.6.0" + webpack "5.67.0" + webpack-dev-middleware "5.3.0" + webpack-dev-server "4.7.3" webpack-merge "5.8.0" - webpack-subresource-integrity "5.0.0" + webpack-subresource-integrity "5.1.0" optionalDependencies: - esbuild "0.14.2" + esbuild "0.14.14" -"@angular-devkit/build-webpack@0.1301.2": - version "0.1301.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1301.2.tgz#e1035aefc696232497d5c3024308b3b0175be109" - integrity sha512-Xk0k0tMcLOy2HI1/YrfWeLUrtKvk7/E7fhG3XoozT/pXBQgiZGoPuCt34HNPDkx3WNSedzvh5DNv8kPlILfjIw== +"@angular-devkit/build-webpack@0.1302.3": + version "0.1302.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1302.3.tgz#a1b43f0d61048e91d25f43e2ec14c1545b070655" + integrity sha512-+JYH1lWU0UOjaWYxpoR2VLsdcb6nG9Gv+M1gH+kT0r2sAKOFaHnrksbOvca3EhDoaMa2b9LSGEE0OcSHWnN+eQ== dependencies: - "@angular-devkit/architect" "0.1301.2" + "@angular-devkit/architect" "0.1302.3" rxjs "6.6.7" "@angular-devkit/core@13.0.2": @@ -110,12 +110,12 @@ rxjs "6.6.7" source-map "0.7.3" -"@angular-devkit/core@13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.1.2.tgz#7ff959aaff4206daa141d6139aed06947bf74ad1" - integrity sha512-uXVesIRiCL/Nv+RSV8JM4j8IoZiGCGnqV2FOJ1hvH7DPxIjhjPMdG/B54xMydZpeASW3ofuxeORyAXxFIBm8Zg== +"@angular-devkit/core@13.2.3": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.2.3.tgz#a5770c7a5e50d1b09e5a41ccb7cf0af140a9e168" + integrity sha512-/47RA8qmWzeS60xSdaprIn1MiSv0Iw83t0M9/ENH7irFS5vMAq62NCcwiWXH59pZmvvLbF+7xy/RgYUZLr4nHQ== dependencies: - ajv "8.8.2" + ajv "8.9.0" ajv-formats "2.1.1" fast-json-stable-stringify "2.1.0" magic-string "0.25.7" @@ -133,12 +133,12 @@ ora "5.4.1" rxjs "6.6.7" -"@angular-devkit/schematics@13.1.2", "@angular-devkit/schematics@~13.1.0": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.1.2.tgz#4e6d25e1b2a3360f5a7ef434615ed895ce0bb8de" - integrity sha512-ayYbHGU8QpMGx8ZyhKOBupz+Zfv/2H1pNQErahYV3qg7hA9hfjTGmNmDQ4iw0fiT04NajjUxuomlKsCsg7oXDw== +"@angular-devkit/schematics@13.2.3", "@angular-devkit/schematics@~13.2.0": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.2.3.tgz#c2acb68ba798f4ab115bee73f078d98f5b20d21c" + integrity sha512-+dyC4iKV0huvpjiuz4uyjLNK3FsCIp/Ghv5lXvhG6yok/dCAubsJItJOxi6G16aVCzG/E9zbsDfm9fNMyVOkgQ== dependencies: - "@angular-devkit/core" "13.1.2" + "@angular-devkit/core" "13.2.3" jsonc-parser "3.0.0" magic-string "0.25.7" ora "5.4.1" @@ -183,31 +183,31 @@ "@angular-eslint/bundled-angular-compiler" "13.0.1" "@typescript-eslint/experimental-utils" "5.3.0" -"@angular/animations@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-13.1.1.tgz#13adfd4d8c2fbf36b87b1b6714ed5121267ea092" - integrity sha512-6ECC9Dn5gmV4U1cz1pRJ2p5lo0BET2CjG1RbhTaZR8lOsoMsmlV/JdBAp8eyYTiGii3MLS6Q2P/hN/YG2SRGQQ== +"@angular/animations@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-13.2.2.tgz#b6af6962a721d73a3832d6f4891dc644bee50ed7" + integrity sha512-qX8LAMuCJaueHBVyuwKtqunx96G0Dr26k7y5Z03VTcscYst4Ib4V2d4i5dwn3HS82DehFdO86cm3Hi2PqE/qww== dependencies: tslib "^2.3.0" -"@angular/cdk@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-13.1.1.tgz#bfc1050df357a26bda03410d821ae05826dcf88e" - integrity sha512-66PyWg+zKdxTe3b1pc1RduT8hsMs/hJ0aD0JX0pSEWVq7O0OJWJ5f0z+Mk03T9tAERA3NK1GifcKEDq5k7R2Zw== +"@angular/cdk@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-13.2.2.tgz#4d41b1da6e92e3bc80d28aabe2049fa7ca40f5b1" + integrity sha512-cT5DIaz+NI9IGb3X61Wh26+L6zdRcOXT1BP37iRbK2Qa2qM8/0VNeK6hrBBIblyoHKR/WUmRlS8XYf6mmArpZw== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^5.0.0" -"@angular/cli@13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-13.1.2.tgz#e83f593dd78020a328f1bc94b88cfab6267fde4e" - integrity sha512-jEsQWzHgODFpppWGb49jfqlN8YYhphsKY3MPHlrjmd05qWgKItUGSgA46hSoDqjaJKVUN9koUnJBFCc9utERYA== +"@angular/cli@13.2.3": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-13.2.3.tgz#4a1694025bf0ac589aeaec17a6ba560647cf07b0" + integrity sha512-QsakxpdQuO67u4fQNuOASqabYUO9gJb/5CpUGpWbuBzru0/9CMEF1CtXoF4EoDiwa5sJMirz3SJMKhtzFlv1cQ== dependencies: - "@angular-devkit/architect" "0.1301.2" - "@angular-devkit/core" "13.1.2" - "@angular-devkit/schematics" "13.1.2" - "@schematics/angular" "13.1.2" + "@angular-devkit/architect" "0.1302.3" + "@angular-devkit/core" "13.2.3" + "@angular-devkit/schematics" "13.2.3" + "@schematics/angular" "13.2.3" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" debug "4.3.3" @@ -218,26 +218,25 @@ npm-pick-manifest "6.1.1" open "8.4.0" ora "5.4.1" - pacote "12.0.2" - resolve "1.20.0" + pacote "12.0.3" + resolve "1.22.0" semver "7.3.5" symbol-observable "4.0.0" uuid "8.3.2" -"@angular/common@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-13.1.1.tgz#e8b659d6376d6764cd2516a4c6d604aafe24cb88" - integrity sha512-FQwRZ1XgTH2PbPjBmq2jAZzETVNX9yWQt21MuNGtokC7V4eS0NYlFIDbhy3UPWCzRgd3+P7P4+HdX15VxCjf9g== +"@angular/common@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-13.2.2.tgz#3c787a53acf69633902e804c141c39a3455d85d3" + integrity sha512-56C/bheNLKtTCyQUZCiYtKbBIZN9jj6rjFILPtJCGls3cBCxp7t9tIdoLiQG/wVQRmaxdj1ioLT+sCWz7mLtQw== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-13.1.1.tgz#b01114eb6256085f086e95bdfe832f5c5f447730" - integrity sha512-ycdXN2urBZepbXn2xx1oxF1i6g0Dq/Rb8ySQeELdL9qr6hiZF9fkvIwd91d8uhFG2PvoM4O8/U/3x4yA2bXzew== +"@angular/compiler-cli@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-13.2.2.tgz#f2c4c207fdbcc274c7a153b3506c3b8f99fc6f59" + integrity sha512-tuOIcEEKVIht+mKrj0rtX3I8gc+ByPjzpCZhFQRggxM6xbKJIToO1zERbEGKrZ+sUJ6BB5KLvscDy+Pddy3b8w== dependencies: "@babel/core" "^7.8.6" - canonical-path "1.0.0" chokidar "^3.0.0" convert-source-map "^1.5.1" dependency-graph "^0.11.0" @@ -248,10 +247,10 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.1.1.tgz#56d1889fbe837ebfe595287cc5aa188cea9be615" - integrity sha512-WS+BB4h2LOBAGQ+P+RcKDw43Z7yAB5m1RY2/MAI+qI339V97WlWEQXxSMvBhCuzJnww1SSZfHMADaB54Jdjx2g== +"@angular/compiler@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.2.2.tgz#89999576906f4d65b58ecff38666c71809ddee45" + integrity sha512-XXQtB0/e7pR2LPrHmpEiTU72SX4xxHGy91vYWIj1JCjSn0fYF7vtHzSJPXDvkbnkNow/PXXzJJYaU1ctdMZPcA== dependencies: tslib "^2.3.0" @@ -260,10 +259,10 @@ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== -"@angular/core@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-13.1.1.tgz#bc01b1d7e1d21749a595b0ae8cab5b8f51fb7dbc" - integrity sha512-oLGKgzUbHqte/q7EokOJWUiXAtBjwuZM6c9Or2a7WDJNeImQilxk5qy91RPSbP8FhOBysebqAayrfiCYexlShg== +"@angular/core@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-13.2.2.tgz#30d2cd6ed3d74d90071c135def7163b12137eb7f" + integrity sha512-zpctw0BxIVOsRFnckchK15SD1L8tzhf5GzwIDaM6+VylDQj1uYkm8mvAjJTQZyUuApomoFet2Rfj7XQPV+cNSQ== dependencies: tslib "^2.3.0" @@ -272,52 +271,52 @@ resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== -"@angular/forms@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-13.1.1.tgz#d298ea9324929521c5fb7d4f8c0892bdfbe5e4b6" - integrity sha512-wtYzRHPv4mf1Vsi4GEal5qcI2wjqUW+lu8Fsd2Aoe8NqkwtY3fq+iWEP/4pnvmH0RlC+3QbNNV/01D5UKolvgg== +"@angular/forms@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-13.2.2.tgz#cdc4f249a85160eab003104d7050f8797a2038a7" + integrity sha512-T61W4Ay9X9qhxjc6lLqpNFeHrGKwg2mqdsZ3zIm/c7oKo37mgl9TB5kkrtnS+205r3N2hF4ICnGFZ4a/egUP/g== dependencies: tslib "^2.3.0" -"@angular/language-service@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-13.1.1.tgz#1e0fcf07a8cf1600ac5dc45837b05b83465fd1a6" - integrity sha512-ilMwR7tv/nANTj5nkEY2/F2VtERi2BFJJEBlfzWrD9yt73pPhPg84o4GPeax07jydBwN0tYOK8jlioCm3MckQg== +"@angular/language-service@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-13.2.2.tgz#4de8742dc7603a331cb39be065f5a30d0bdc0fad" + integrity sha512-2P5+wRsbHgpI2rVeFwnsLWxyntUiw8kG9Tqh5BkVDqtQovbYtzFiaMkf5TFz/g938JBBgeRQzvXr1kQhEidAWQ== -"@angular/localize@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-13.1.1.tgz#0e86fa8c511f4e2c84b11839ccd1050c4d98ff08" - integrity sha512-R3DJaDDl+IMo0JbNtU4bgVGT4poavHGvg8E2q9xausn0jtkKY8MLa231a1MWlrc/lLVhX1M0sEr+X3CG/lRqhw== +"@angular/localize@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-13.2.2.tgz#a19ee864e362baf1d538d56b9021e7d71d7293c9" + integrity sha512-W/deDIxAFlv0euq7hxYPRCvj2DtDTsid5mRqyjEaVr+eYaLPnD8EElZuKuQEDeo7rWkCjljEpBrFsXwC5FZ8cw== dependencies: "@babel/core" "7.8.6" glob "7.2.0" yargs "^17.2.1" -"@angular/material@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-13.1.1.tgz#4d2d5a1ea6527b282beb26de6491eb3a221fab2a" - integrity sha512-kKWZBhnzuBYAVO1nrkqEaVTCJ2onEWs+tzAJDIlmbo9USiQyVCnFXx+rs86m4kRUxeAAZ9mcW5BGJr6oy5ClCA== +"@angular/material@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-13.2.2.tgz#d673f68b0c9357ada2ee82ffeaf11f10f5d49b27" + integrity sha512-YAjPp2+/wuEOPfkAxdRVdbWHiK4P3DgMZa9qP/NizN2lTXNrftEfD09ZlPIFMZRnnExezJ2LnO7eyELpc1VSKg== dependencies: tslib "^2.3.0" -"@angular/platform-browser-dynamic@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.1.1.tgz#452c9b1a61998400674f6ee03bc46326ae1295a4" - integrity sha512-ujHJMhJk93hjLx/SQ67y7xiGh2UDL+toVi3OlorWvnYGgPR26ufyL+J73BA+RAKHSP2WPiXU+/87vSz8r+BEgA== +"@angular/platform-browser-dynamic@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.2.2.tgz#9a50fb79b96b62c13e65c996727542c5174e9246" + integrity sha512-lj6xwat0StLp+ROFqXU62upwHQhlxaQi0djhrS+DGKUK0Xu9bkBeaSCfBFgS78jPm1SwL8Xztu9/vuDAHLRrqw== dependencies: tslib "^2.3.0" -"@angular/platform-browser@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-13.1.1.tgz#d9687beec9c9af63097b1bcb91920bda6d1ea0b2" - integrity sha512-jk9MGwnaVc98wmw5dRBicduI/a8dHtUzaAi1dV003fUWldS9a5FBuj/ym7DJubaD5Njl8l79SFbjrP9aAsqM5A== +"@angular/platform-browser@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-13.2.2.tgz#e603c32c3550970211b004df3e227e0d2b5088cd" + integrity sha512-M7gWC8fFCPc/CRcHCzqe/j7WzwAUMeKt9vwlK633XnesHBoqZdYgbb3YHHc6WPVU0YI09Nb/Hm5sezEKmjUmPg== dependencies: tslib "^2.3.0" -"@angular/router@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-13.1.1.tgz#656919d3c186f46310a0825d62bbc712c20890d7" - integrity sha512-rlz5BBgNX+G2vVu2Gb5avx3LL08i7R/xZO7zPwh0HhXz/Vp8XFlWwaqAGb6Hgat772K2uCxF1/JBLQCUBY2MNQ== +"@angular/router@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-13.2.2.tgz#ba637da8963e5905520622f02357c4eda2f3da68" + integrity sha512-dt2b9/kGJAkmOqUmUD3aKlp4pGpdqLwB0zmhUYF3ktNEcQaPf4ZjWT/4jhy09gFL+TKOHG5OQW9GxBbhWI4bSg== dependencies: tslib "^2.3.0" @@ -326,13 +325,6 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== -"@babel/code-frame@7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" @@ -347,6 +339,13 @@ dependencies: "@babel/highlight" "^7.16.0" +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.7", "@babel/compat-data@^7.15.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" @@ -357,6 +356,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== +"@babel/compat-data@^7.16.8": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" + integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -379,20 +383,20 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@7.16.0", "@babel/core@^7.12.3": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" +"@babel/core@7.16.12": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" + integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.10" + "@babel/types" "^7.16.8" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -442,12 +446,33 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@7.16.0", "@babel/generator@^7.16.0", "@babel/generator@^7.8.6": +"@babel/core@^7.12.3": version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" + integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== dependencies: + "@babel/code-frame" "^7.16.0" + "@babel/generator" "^7.16.0" + "@babel/helper-compilation-targets" "^7.16.0" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helpers" "^7.16.0" + "@babel/parser" "^7.16.0" + "@babel/template" "^7.16.0" + "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/generator@7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" + integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== + dependencies: + "@babel/types" "^7.16.8" jsesc "^2.5.1" source-map "^0.5.0" @@ -460,21 +485,30 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.5.tgz#26e1192eb8f78e0a3acaf3eede3c6fc96d22bedf" - integrity sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA== +"@babel/generator@^7.16.0", "@babel/generator@^7.8.6": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" + integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== dependencies: "@babel/types" "^7.16.0" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@7.16.0", "@babel/helper-annotate-as-pure@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" - integrity sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg== +"@babel/generator@^7.16.8", "@babel/generator@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e" + integrity sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw== dependencies: - "@babel/types" "^7.16.0" + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@7.16.7", "@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" "@babel/helper-annotate-as-pure@^7.14.5", "@babel/helper-annotate-as-pure@^7.15.4": version "7.15.4" @@ -491,13 +525,13 @@ "@babel/helper-explode-assignable-expression" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.5.tgz#a8429d064dce8207194b8bf05a70a9ea828746af" - integrity sha512-3JEA9G5dmmnIWdzaT9d0NmFRgYnWUThLsDaL7982H0XqqWr56lRrsmwheXFMjR+TMl7QMBb6mzy9kvgr1lRLUA== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" + integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== dependencies: - "@babel/helper-explode-assignable-expression" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/helper-explode-assignable-expression" "^7.16.7" + "@babel/types" "^7.16.7" "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.4": version "7.15.4" @@ -509,7 +543,7 @@ browserslist "^4.16.6" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.16.0", "@babel/helper-compilation-targets@^7.16.3": +"@babel/helper-compilation-targets@^7.16.0": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== @@ -519,6 +553,16 @@ browserslist "^4.17.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" + integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== + dependencies: + "@babel/compat-data" "^7.16.4" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz#7f977c17bd12a5fba363cb19bea090394bf37d2e" @@ -531,18 +575,18 @@ "@babel/helper-replace-supers" "^7.15.4" "@babel/helper-split-export-declaration" "^7.15.4" -"@babel/helper-create-class-features-plugin@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.5.tgz#5d1bcd096792c1ebec6249eebc6358eec55d0cad" - integrity sha512-NEohnYA7mkB8L5JhU7BLwcBdU3j83IziR9aseMueWGeAjblbul3zzb8UvJ3a1zuBiqCMObzCJHFqKIQE6hTVmg== +"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7": + version "7.17.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz#9699f14a88833a7e055ce57dcd3ffdcd25186b21" + integrity sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-environment-visitor" "^7.16.5" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-member-expression-to-functions" "^7.16.5" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.5" - "@babel/helper-split-export-declaration" "^7.16.0" + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-create-regexp-features-plugin@^7.14.5": version "7.14.5" @@ -552,13 +596,13 @@ "@babel/helper-annotate-as-pure" "^7.14.5" regexpu-core "^4.7.1" -"@babel/helper-create-regexp-features-plugin@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz#06b2348ce37fccc4f5e18dcd8d75053f2a7c44ff" - integrity sha512-3DyG0zAFAZKcOp7aVr33ddwkxJ0Z0Jr5V99y3I690eYLpukJsJvAbzTy1ewoCqsML8SbIrjH14Jc/nSQ4TvNPA== +"@babel/helper-create-regexp-features-plugin@^7.16.7": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" + integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - regexpu-core "^4.7.1" + "@babel/helper-annotate-as-pure" "^7.16.7" + regexpu-core "^5.0.1" "@babel/helper-define-polyfill-provider@^0.1.5": version "0.1.5" @@ -602,12 +646,26 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.5.tgz#f6a7f38b3c6d8b07c88faea083c46c09ef5451b8" - integrity sha512-ODQyc5AnxmZWm/R2W7fzhamOk1ey8gSguo5SGvF0zcB3uUzRpTRmM/jmLSm9bDMyPlvbyJ+PwPEK0BWIoZ9wjg== +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== dependencies: - "@babel/types" "^7.16.0" + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" "@babel/helper-explode-assignable-expression@^7.15.4": version "7.15.4" @@ -616,12 +674,12 @@ dependencies: "@babel/types" "^7.15.4" -"@babel/helper-explode-assignable-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" - integrity sha512-Hk2SLxC9ZbcOhLpg/yMznzJ11W++lg5GMbxt1ev6TXUiJB0N42KPC+7w8a+eWGuqDnUYuwStJoZHM7RgmIOaGQ== +"@babel/helper-explode-assignable-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" + integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== dependencies: - "@babel/types" "^7.16.0" + "@babel/types" "^7.16.7" "@babel/helper-function-name@^7.14.5", "@babel/helper-function-name@^7.15.4": version "7.15.4" @@ -641,6 +699,15 @@ "@babel/template" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + "@babel/helper-get-function-arity@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" @@ -655,6 +722,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-hoist-variables@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df" @@ -669,6 +743,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-member-expression-to-functions@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef" @@ -683,12 +764,12 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-member-expression-to-functions@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.5.tgz#1bc9f7e87354e86f8879c67b316cb03d3dc2caab" - integrity sha512-7fecSXq7ZrLE+TWshbGT+HyCLkxloWNhTbU2QM1NTI/tDqyf0oZiMcEfYtDuUDCo528EOlt39G1rftea4bRZIw== +"@babel/helper-member-expression-to-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" + integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== dependencies: - "@babel/types" "^7.16.0" + "@babel/types" "^7.16.7" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.15.4": version "7.15.4" @@ -704,6 +785,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz#962cc629a7f7f9a082dd62d0307fa75fe8788d7c" @@ -732,19 +820,19 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/helper-module-transforms@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.5.tgz#530ebf6ea87b500f60840578515adda2af470a29" - integrity sha512-CkvMxgV4ZyyioElFwcuWnDCcNIeyqTkCm9BxXZi73RR1ozqlpboqsbGUNvRTflgZtFbbJ1v5Emvm+lkjMYY/LQ== +"@babel/helper-module-transforms@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" + integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== dependencies: - "@babel/helper-environment-visitor" "^7.16.5" - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-simple-access" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.5" - "@babel/types" "^7.16.0" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" "@babel/helper-optimise-call-expression@^7.15.4": version "7.15.4" @@ -760,6 +848,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" @@ -770,10 +865,10 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== -"@babel/helper-plugin-utils@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.5.tgz#afe37a45f39fce44a3d50a7958129ea5b1a5c074" - integrity sha512-59KHWHXxVA9K4HNF4sbHCf+eJeFe0Te/ZFGqBT4OjXhrwvA04sGfaEGsVTdsjoszq0YTP49RC9UKe5g8uN2RwQ== +"@babel/helper-plugin-utils@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== "@babel/helper-remap-async-to-generator@^7.14.5", "@babel/helper-remap-async-to-generator@^7.15.4": version "7.15.4" @@ -784,23 +879,14 @@ "@babel/helper-wrap-function" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.5.tgz#e706646dc4018942acb4b29f7e185bc246d65ac3" - integrity sha512-X+aAJldyxrOmN9v3FKp+Hu1NO69VWgYgDGq6YDykwRPzxs5f2N+X988CBXS7EQahDU+Vpet5QYMqLk+nsp+Qxw== +"@babel/helper-remap-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" + integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-wrap-function" "^7.16.5" - "@babel/types" "^7.16.0" - -"@babel/helper-remap-async-to-generator@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e" - integrity sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-wrap-function" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-wrap-function" "^7.16.8" + "@babel/types" "^7.16.8" "@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.4": version "7.15.4" @@ -822,16 +908,16 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/helper-replace-supers@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.5.tgz#96d3988bd0ab0a2d22c88c6198c3d3234ca25326" - integrity sha512-ao3seGVa/FZCMCCNDuBcqnBFSbdr8N2EW35mzojx3TwfIbdPmNK+JV6+2d5bR0Z71W5ocLnQp9en/cTF7pBJiQ== +"@babel/helper-replace-supers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" + integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== dependencies: - "@babel/helper-environment-visitor" "^7.16.5" - "@babel/helper-member-expression-to-functions" "^7.16.5" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/traverse" "^7.16.5" - "@babel/types" "^7.16.0" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" "@babel/helper-simple-access@^7.15.4": version "7.15.4" @@ -847,6 +933,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-simple-access@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" + integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers@^7.14.5", "@babel/helper-skip-transparent-expression-wrappers@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb" @@ -875,6 +968,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": version "7.14.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" @@ -885,11 +985,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + "@babel/helper-wrap-function@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz#6f754b2446cfaf3d612523e6ab8d79c27c3a3de7" @@ -900,25 +1010,15 @@ "@babel/traverse" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/helper-wrap-function@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c" - integrity sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g== +"@babel/helper-wrap-function@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" + integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-wrap-function@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.5.tgz#0158fca6f6d0889c3fee8a6ed6e5e07b9b54e41f" - integrity sha512-2J2pmLBqUqVdJw78U0KPNdeE2qeuIyKoG4mKV7wAq3mc4jJG282UgjZw4ZYDnqiWQuS3Y3IYdF/AQ6CpyBV3VA== - dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.5" - "@babel/types" "^7.16.0" + "@babel/helper-function-name" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.8" + "@babel/types" "^7.16.8" "@babel/helpers@^7.12.5", "@babel/helpers@^7.15.4": version "7.15.4" @@ -938,7 +1038,16 @@ "@babel/traverse" "^7.16.3" "@babel/types" "^7.16.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": +"@babel/helpers@^7.16.7": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" + integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.0" + "@babel/types" "^7.17.0" + +"@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== @@ -956,6 +1065,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.0.0-beta.54", "@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5", "@babel/parser@^7.7.2": version "7.15.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.5.tgz#d33a58ca69facc05b26adfe4abebfed56c1c2dac" @@ -966,17 +1084,17 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== -"@babel/parser@^7.16.5": - version "7.16.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314" - integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ== +"@babel/parser@^7.16.12", "@babel/parser@^7.16.7", "@babel/parser@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" + integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2": - version "7.16.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz#2977fca9b212db153c195674e57cfab807733183" - integrity sha512-h37CvpLSf8gb2lIJ2CgC3t+EjFbi0t8qS7LCS1xcJIlEXE4czlofwaW7W1HA8zpgOCzI9C1nmoqNR1zWkk0pQg== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" + integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4": version "7.15.4" @@ -987,22 +1105,22 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4" "@babel/plugin-proposal-optional-chaining" "^7.14.5" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.0.tgz#358972eaab006f5eb0826183b0c93cbcaf13e1e2" - integrity sha512-4tcFwwicpWTrpl9qjf7UsoosaArgImF85AxqCRZlgc3IQDvkUHjJpruXAL58Wmj+T6fypWTC/BakfEkwIL/pwA== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" + integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" -"@babel/plugin-proposal-async-generator-functions@7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz#e606eb6015fec6fa5978c940f315eae4e300b081" - integrity sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg== +"@babel/plugin-proposal-async-generator-functions@7.16.8", "@babel/plugin-proposal-async-generator-functions@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" + integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.4" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-proposal-async-generator-functions@^7.15.4": @@ -1014,15 +1132,6 @@ "@babel/helper-remap-async-to-generator" "^7.15.4" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-proposal-async-generator-functions@^7.16.4": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.5.tgz#fd3bd7e0d98404a3d4cbca15a72d533f8c9a2f67" - integrity sha512-C/FX+3HNLV6sz7AqbTQqEo1L9/kfrKjxcVtgyBCmvIgOjvuBVUWooDoi7trsLxOzCEo5FccjRvKHkfDsJFZlfA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-remap-async-to-generator" "^7.16.5" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-proposal-class-properties@^7.12.1", "@babel/plugin-proposal-class-properties@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz#40d1ee140c5b1e31a350f4f5eed945096559b42e" @@ -1031,13 +1140,13 @@ "@babel/helper-create-class-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-class-properties@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.5.tgz#3269f44b89122110f6339806e05d43d84106468a" - integrity sha512-pJD3HjgRv83s5dv1sTnDbZOaTjghKEz8KUn1Kbh2eAIRhGuyQ1XSeI4xVXU3UlIEVA3DAyIdxqT1eRn7Wcn55A== +"@babel/plugin-proposal-class-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" + integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-proposal-class-static-block@^7.15.4": version "7.15.4" @@ -1048,13 +1157,13 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-proposal-class-static-block@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.5.tgz#df58ab015a7d3b0963aafc8f20792dcd834952a9" - integrity sha512-EEFzuLZcm/rNJ8Q5krK+FRKdVkd6FjfzT9tuSZql9sQn64K0hHA2KLJ0DqVot9/iV6+SsuadC5yI39zWnm+nmQ== +"@babel/plugin-proposal-class-static-block@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz#712357570b612106ef5426d13dc433ce0f200c2a" + integrity sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-proposal-decorators@^7.12.12": @@ -1074,12 +1183,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-proposal-dynamic-import@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.5.tgz#2e0d19d5702db4dcb9bc846200ca02f2e9d60e9e" - integrity sha512-P05/SJZTTvHz79LNYTF8ff5xXge0kk5sIIWAypcWgX4BTRUgyHc8wRxJ/Hk+mU0KXldgOOslKaeqnhthcDJCJQ== +"@babel/plugin-proposal-dynamic-import@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" + integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-proposal-export-default-from@^7.12.1": @@ -1098,12 +1207,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-export-namespace-from@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.5.tgz#3b4dd28378d1da2fea33e97b9f25d1c2f5bf1ac9" - integrity sha512-i+sltzEShH1vsVydvNaTRsgvq2vZsfyrd7K7vPLUU/KgS0D5yZMe6uipM0+izminnkKrEfdUnz7CxMRb6oHZWw== +"@babel/plugin-proposal-export-namespace-from@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" + integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-proposal-json-strings@^7.14.5": @@ -1114,12 +1223,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.5.tgz#1e726930fca139caab6b084d232a9270d9d16f9c" - integrity sha512-QQJueTFa0y9E4qHANqIvMsuxM/qcLQmKttBACtPCQzGUEizsXDACGonlPiSwynHfOa3vNw0FPMVvQzbuXwh4SQ== +"@babel/plugin-proposal-json-strings@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" + integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-proposal-logical-assignment-operators@^7.14.5": @@ -1130,12 +1239,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-logical-assignment-operators@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.5.tgz#df1f2e4b5a0ec07abf061d2c18e53abc237d3ef5" - integrity sha512-xqibl7ISO2vjuQM+MzR3rkd0zfNWltk7n9QhaD8ghMmMceVguYrNDt7MikRyj4J4v3QehpnrU8RYLnC7z/gZLA== +"@babel/plugin-proposal-logical-assignment-operators@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" + integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5": @@ -1146,12 +1255,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.5.tgz#652555bfeeeee2d2104058c6225dc6f75e2d0f07" - integrity sha512-YwMsTp/oOviSBhrjwi0vzCUycseCYwoXnLiXIL3YNjHSMBHicGTz7GjVU/IGgz4DtOEXBdCNG72pvCX22ehfqg== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" + integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-proposal-numeric-separator@^7.14.5": @@ -1162,12 +1271,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-numeric-separator@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.5.tgz#edcb6379b6cf4570be64c45965d8da7a2debf039" - integrity sha512-DvB9l/TcsCRvsIV9v4jxR/jVP45cslTVC0PMVHvaJhhNuhn2Y1SOhCSFlPK777qLB5wb8rVDaNoqMTyOqtY5Iw== +"@babel/plugin-proposal-numeric-separator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" + integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@7.12.1": @@ -1190,16 +1299,16 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.14.5" -"@babel/plugin-proposal-object-rest-spread@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.5.tgz#f30f80dacf7bc1404bf67f99c8d9c01665e830ad" - integrity sha512-UEd6KpChoyPhCoE840KRHOlGhEZFutdPDMGj+0I56yuTTOaT51GzmnEl/0uT41fB/vD2nT+Pci2KjezyE3HmUw== +"@babel/plugin-proposal-object-rest-spread@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz#94593ef1ddf37021a25bdcb5754c4a8d534b01d8" + integrity sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA== dependencies: "@babel/compat-data" "^7.16.4" - "@babel/helper-compilation-targets" "^7.16.3" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.16.5" + "@babel/plugin-transform-parameters" "^7.16.7" "@babel/plugin-proposal-optional-catch-binding@^7.14.5": version "7.14.5" @@ -1209,12 +1318,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-proposal-optional-catch-binding@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.5.tgz#1a5405765cf589a11a33a1fd75b2baef7d48b74e" - integrity sha512-ihCMxY1Iljmx4bWy/PIMJGXN4NS4oUj1MKynwO07kiKms23pNvIn1DMB92DNB2R0EA882sw0VXIelYGdtF7xEQ== +"@babel/plugin-proposal-optional-catch-binding@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" + integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-proposal-optional-chaining@^7.12.7", "@babel/plugin-proposal-optional-chaining@^7.14.5": @@ -1226,12 +1335,12 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.5.tgz#a5fa61056194d5059366c0009cb9a9e66ed75c1f" - integrity sha512-kzdHgnaXRonttiTfKYnSVafbWngPPr2qKw9BWYBESl91W54e+9R5pP70LtWxV56g0f05f/SQrwHYkfvbwcdQ/A== +"@babel/plugin-proposal-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" + integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" @@ -1243,13 +1352,13 @@ "@babel/helper-create-class-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-private-methods@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.5.tgz#2086f7d78c1b0c712d49b5c3fbc2d1ca21a7ee12" - integrity sha512-+yFMO4BGT3sgzXo+lrq7orX5mAZt57DwUK6seqII6AcJnJOIhBJ8pzKH47/ql/d426uQ7YhN8DpUFirQzqYSUA== +"@babel/plugin-proposal-private-methods@^7.16.11": + version "7.16.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" + integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-create-class-features-plugin" "^7.16.10" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-proposal-private-property-in-object@^7.15.4": version "7.15.4" @@ -1261,14 +1370,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-proposal-private-property-in-object@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.5.tgz#a42d4b56005db3d405b12841309dbca647e7a21b" - integrity sha512-+YGh5Wbw0NH3y/E5YMu6ci5qTDmAEVNoZ3I54aB6nVEOZ5BQ7QJlwKq5pYVucQilMByGn/bvX0af+uNaPRCabA== +"@babel/plugin-proposal-private-property-in-object@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" + integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-create-class-features-plugin" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-proposal-unicode-property-regex@^7.14.5", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": @@ -1279,13 +1388,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-unicode-property-regex@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.5.tgz#35fe753afa7c572f322bd068ff3377bde0f37080" - integrity sha512-s5sKtlKQyFSatt781HQwv1hoM5BQ9qRH30r+dK56OLDsHmV74mzwJNX7R1yMuE7VZKG5O6q/gmOGSAO6ikTudg== +"@babel/plugin-proposal-unicode-property-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" + integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -1441,21 +1550,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-arrow-functions@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.5.tgz#04c18944dd55397b521d9d7511e791acea7acf2d" - integrity sha512-8bTHiiZyMOyfZFULjsCnYOWG059FVMes0iljEHSfARhNgFfpsqE92OrCffv3veSw9rwMkYcFe9bj0ZoXU2IGtQ== +"@babel/plugin-transform-arrow-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" + integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-async-to-generator@7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz#df12637f9630ddfa0ef9d7a11bc414d629d38604" - integrity sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw== +"@babel/plugin-transform-async-to-generator@7.16.8", "@babel/plugin-transform-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" + integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.0" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" "@babel/plugin-transform-async-to-generator@^7.14.5": version "7.14.5" @@ -1466,15 +1575,6 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-remap-async-to-generator" "^7.14.5" -"@babel/plugin-transform-async-to-generator@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.5.tgz#89c9b501e65bb14c4579a6ce9563f859de9b34e4" - integrity sha512-TMXgfioJnkXU+XRoj7P2ED7rUm5jbnDWwlCuFVTpQboMfbSya5WrmubNBAMlk7KXvywpo8rd8WuYZkis1o2H8w== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-remap-async-to-generator" "^7.16.5" - "@babel/plugin-transform-block-scoped-functions@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz#e48641d999d4bc157a67ef336aeb54bc44fd3ad4" @@ -1482,12 +1582,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-block-scoped-functions@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.5.tgz#af087494e1c387574260b7ee9b58cdb5a4e9b0b0" - integrity sha512-BxmIyKLjUGksJ99+hJyL/HIxLIGnLKtw772zYDER7UuycDZ+Xvzs98ZQw6NGgM2ss4/hlFAaGiZmMNKvValEjw== +"@babel/plugin-transform-block-scoped-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" + integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-block-scoping@^7.12.12", "@babel/plugin-transform-block-scoping@^7.15.3": version "7.15.3" @@ -1496,12 +1596,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-block-scoping@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.5.tgz#b91f254fe53e210eabe4dd0c40f71c0ed253c5e7" - integrity sha512-JxjSPNZSiOtmxjX7PBRBeRJTUKTyJ607YUYeT0QJCNdsedOe+/rXITjP08eG8xUpsLfPirgzdCFN+h0w6RI+pQ== +"@babel/plugin-transform-block-scoping@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" + integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.15.4": version "7.15.4" @@ -1516,18 +1616,18 @@ "@babel/helper-split-export-declaration" "^7.15.4" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.5.tgz#6acf2ec7adb50fb2f3194dcd2909dbd056dcf216" - integrity sha512-DzJ1vYf/7TaCYy57J3SJ9rV+JEuvmlnvvyvYKFbk5u46oQbBvuB9/0w+YsVsxkOv8zVWKpDmUoj4T5ILHoXevA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-environment-visitor" "^7.16.5" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-replace-supers" "^7.16.5" - "@babel/helper-split-export-declaration" "^7.16.0" +"@babel/plugin-transform-classes@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" + integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.14.5": @@ -1537,12 +1637,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-computed-properties@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.5.tgz#2af91ebf0cceccfcc701281ada7cfba40a9b322a" - integrity sha512-n1+O7xtU5lSLraRzX88CNcpl7vtGdPakKzww74bVwpAIRgz9JVLJJpOLb0uYqcOaXVM0TL6X0RVeIJGD2CnCkg== +"@babel/plugin-transform-computed-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" + integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.14.7": version "7.14.7" @@ -1551,12 +1651,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-destructuring@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.5.tgz#89ebc87499ac4a81b897af53bb5d3eed261bd568" - integrity sha512-GuRVAsjq+c9YPK6NeTkRLWyQskDC099XkBSVO+6QzbnOnH2d/4mBVXYStaPrZD3dFRfg00I6BFJ9Atsjfs8mlg== +"@babel/plugin-transform-destructuring@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz#ca9588ae2d63978a4c29d3f33282d8603f618e23" + integrity sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-dotall-regex@^7.14.5", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.14.5" @@ -1566,13 +1666,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-dotall-regex@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.5.tgz#b40739c00b6686820653536d6d143e311de67936" - integrity sha512-iQiEMt8Q4/5aRGHpGVK2Zc7a6mx7qEAO7qehgSug3SDImnuMzgmm/wtJALXaz25zUj1PmnNHtShjFgk4PDx4nw== +"@babel/plugin-transform-dotall-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" + integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-duplicate-keys@^7.14.5": version "7.14.5" @@ -1581,12 +1681,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-duplicate-keys@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.5.tgz#2450f2742325412b746d7d005227f5e8973b512a" - integrity sha512-81tijpDg2a6I1Yhj4aWY1l3O1J4Cg/Pd7LfvuaH2VVInAkXtzibz9+zSPdUM1WvuUi128ksstAP0hM5w48vQgg== +"@babel/plugin-transform-duplicate-keys@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" + integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-exponentiation-operator@^7.14.5": version "7.14.5" @@ -1596,13 +1696,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-exponentiation-operator@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.5.tgz#36e261fa1ab643cfaf30eeab38e00ed1a76081e2" - integrity sha512-12rba2HwemQPa7BLIKCzm1pT2/RuQHtSFHdNl41cFiC6oi4tcrp7gjB07pxQvFpcADojQywSjblQth6gJyE6CA== +"@babel/plugin-transform-exponentiation-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" + integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-for-of@^7.12.1", "@babel/plugin-transform-for-of@^7.15.4": version "7.15.4" @@ -1611,12 +1711,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-for-of@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.5.tgz#9b544059c6ca11d565457c0ff1f08e13ce225261" - integrity sha512-+DpCAJFPAvViR17PIMi9x2AE34dll5wNlXO43wagAX2YcRGgEVHCNFC4azG85b4YyyFarvkc/iD5NPrz4Oneqw== +"@babel/plugin-transform-for-of@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" + integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-function-name@^7.14.5": version "7.14.5" @@ -1626,13 +1726,14 @@ "@babel/helper-function-name" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-function-name@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.5.tgz#6896ebb6a5538a75d6a4086a277752f655a7bd15" - integrity sha512-Fuec/KPSpVLbGo6z1RPw4EE1X+z9gZk1uQmnYy7v4xr4TO9p41v1AoUuXEtyqAI7H+xNJYSICzRqZBhDEkd3kQ== +"@babel/plugin-transform-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" + integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-literals@^7.14.5": version "7.14.5" @@ -1641,12 +1742,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-literals@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.5.tgz#af392b90e3edb2bd6dc316844cbfd6b9e009d320" - integrity sha512-B1j9C/IfvshnPcklsc93AVLTrNVa69iSqztylZH6qnmiAsDDOmmjEYqOm3Ts2lGSgTSywnBNiqC949VdD0/gfw== +"@babel/plugin-transform-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" + integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-member-expression-literals@^7.14.5": version "7.14.5" @@ -1655,12 +1756,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-member-expression-literals@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.5.tgz#4bd6ecdc11932361631097b779ca5c7570146dd5" - integrity sha512-d57i3vPHWgIde/9Y8W/xSFUndhvhZN5Wu2TjRrN1MVz5KzdUihKnfDVlfP1U7mS5DNj/WHHhaE4/tTi4hIyHwQ== +"@babel/plugin-transform-member-expression-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" + integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-modules-amd@^7.14.5": version "7.14.5" @@ -1671,13 +1772,13 @@ "@babel/helper-plugin-utils" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-amd@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.5.tgz#92c0a3e83f642cb7e75fada9ab497c12c2616527" - integrity sha512-oHI15S/hdJuSCfnwIz+4lm6wu/wBn7oJ8+QrkzPPwSFGXk8kgdI/AIKcbR/XnD1nQVMg/i6eNaXpszbGuwYDRQ== +"@babel/plugin-transform-modules-amd@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" + integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== dependencies: - "@babel/helper-module-transforms" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-commonjs@^7.15.4": @@ -1690,14 +1791,14 @@ "@babel/helper-simple-access" "^7.15.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.5.tgz#4ee03b089536f076b2773196529d27c32b9d7bde" - integrity sha512-ABhUkxvoQyqhCWyb8xXtfwqNMJD7tx+irIRnUh6lmyFud7Jln1WzONXKlax1fg/ey178EXbs4bSGNd6PngO+SQ== +"@babel/plugin-transform-modules-commonjs@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" + integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== dependencies: - "@babel/helper-module-transforms" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-simple-access" "^7.16.0" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-systemjs@^7.15.4": @@ -1711,15 +1812,15 @@ "@babel/helper-validator-identifier" "^7.14.9" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.5.tgz#07078ba2e3cc94fbdd06836e355c246e98ad006b" - integrity sha512-53gmLdScNN28XpjEVIm7LbWnD/b/TpbwKbLk6KV4KqC9WyU6rq1jnNmVG6UgAdQZVVGZVoik3DqHNxk4/EvrjA== +"@babel/plugin-transform-modules-systemjs@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz#887cefaef88e684d29558c2b13ee0563e287c2d7" + integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== dependencies: - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-validator-identifier" "^7.15.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.14.5": @@ -1730,13 +1831,13 @@ "@babel/helper-module-transforms" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-modules-umd@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.5.tgz#caa9c53d636fb4e3c99fd35a4c9ba5e5cd7e002e" - integrity sha512-qTFnpxHMoenNHkS3VoWRdwrcJ3FhX567GvDA3hRZKF0Dj8Fmg0UzySZp3AP2mShl/bzcywb/UWAMQIjA1bhXvw== +"@babel/plugin-transform-modules-umd@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" + integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== dependencies: - "@babel/helper-module-transforms" "^7.16.5" - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-named-capturing-groups-regex@^7.14.9": version "7.14.9" @@ -1745,12 +1846,12 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.14.5" -"@babel/plugin-transform-named-capturing-groups-regex@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.5.tgz#4afd8cdee377ce3568f4e8a9ee67539b69886a3c" - integrity sha512-/wqGDgvFUeKELW6ex6QB7dLVRkd5ehjw34tpXu1nhKC0sFfmaLabIswnpf8JgDyV2NeDmZiwoOb0rAmxciNfjA== +"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" + integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" + "@babel/helper-create-regexp-features-plugin" "^7.16.7" "@babel/plugin-transform-new-target@^7.14.5": version "7.14.5" @@ -1759,12 +1860,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-new-target@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.5.tgz#759ea9d6fbbc20796056a5d89d13977626384416" - integrity sha512-ZaIrnXF08ZC8jnKR4/5g7YakGVL6go6V9ql6Jl3ecO8PQaQqFE74CuM384kezju7Z9nGCCA20BqZaR1tJ/WvHg== +"@babel/plugin-transform-new-target@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" + integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-object-super@^7.14.5": version "7.14.5" @@ -1774,13 +1875,13 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-replace-supers" "^7.14.5" -"@babel/plugin-transform-object-super@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.5.tgz#8ccd9a1bcd3e7732ff8aa1702d067d8cd70ce380" - integrity sha512-tded+yZEXuxt9Jdtkc1RraW1zMF/GalVxaVVxh41IYwirdRgyAxxxCKZ9XB7LxZqmsjfjALxupNE1MIz9KH+Zg== +"@babel/plugin-transform-object-super@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" + integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" - "@babel/helper-replace-supers" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" "@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.14.5", "@babel/plugin-transform-parameters@^7.15.4": version "7.15.4" @@ -1789,12 +1890,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-parameters@^7.16.3", "@babel/plugin-transform-parameters@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.5.tgz#4fc74b18a89638bd90aeec44a11793ecbe031dde" - integrity sha512-B3O6AL5oPop1jAVg8CV+haeUte9oFuY85zu0jwnRNZZi3tVAbJriu5tag/oaO2kGaQM/7q7aGPBlTI5/sr9enA== +"@babel/plugin-transform-parameters@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" + integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-property-literals@^7.14.5": version "7.14.5" @@ -1803,12 +1904,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.5.tgz#58f1465a7202a2bb2e6b003905212dd7a79abe3f" - integrity sha512-+IRcVW71VdF9pEH/2R/Apab4a19LVvdVsr/gEeotH00vSDVlKD+XgfSIw+cgGWsjDB/ziqGv/pGoQZBIiQVXHg== +"@babel/plugin-transform-property-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" + integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-display-name@^7.14.5": version "7.15.1" @@ -1850,10 +1951,10 @@ dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-regenerator@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.5.tgz#704cc6d8dd3dd4758267621ab7b36375238cef13" - integrity sha512-2z+it2eVWU8TtQQRauvGUqZwLy4+7rTfo6wO4npr+fvvN1SW30ZF3O/ZRCNmTuu4F5MIP8OJhXAhRV5QMJOuYg== +"@babel/plugin-transform-regenerator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" + integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== dependencies: regenerator-transform "^0.14.2" @@ -1864,22 +1965,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-reserved-words@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.5.tgz#db95e98799675e193dc2b47d3e72a7c0651d0c30" - integrity sha512-aIB16u8lNcf7drkhXJRoggOxSTUAuihTSTfAcpynowGJOZiGf+Yvi7RuTwFzVYSYPmWyARsPqUGoZWWWxLiknw== +"@babel/plugin-transform-reserved-words@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" + integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-runtime@7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.4.tgz#f9ba3c7034d429c581e1bd41b4952f3db3c2c7e8" - integrity sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A== +"@babel/plugin-transform-runtime@7.16.10": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.10.tgz#53d9fd3496daedce1dd99639097fa5d14f4c7c2c" + integrity sha512-9nwTiqETv2G7xI4RvXHNfpGdr8pAA+Q/YtN3yLK7OoK7n9OibVm/xymJ838a9A6E/IciOLPj82lZk0fW6O4O7w== dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.4.0" + babel-plugin-polyfill-corejs3 "^0.5.0" babel-plugin-polyfill-regenerator "^0.3.0" semver "^6.3.0" @@ -1890,12 +1991,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-shorthand-properties@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.5.tgz#ccb60b1a23b799f5b9a14d97c5bc81025ffd96d7" - integrity sha512-ZbuWVcY+MAXJuuW7qDoCwoxDUNClfZxoo7/4swVbOW1s/qYLOMHlm9YRWMsxMFuLs44eXsv4op1vAaBaBaDMVg== +"@babel/plugin-transform-shorthand-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.14.6": version "7.14.6" @@ -1905,12 +2006,12 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" -"@babel/plugin-transform-spread@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.5.tgz#912b06cff482c233025d3e69cf56d3e8fa166c29" - integrity sha512-5d6l/cnG7Lw4tGHEoga4xSkYp1euP7LAtrah1h1PgJ3JY7yNsjybsxQAnVK4JbtReZ/8z6ASVmd3QhYYKLaKZw== +"@babel/plugin-transform-spread@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" + integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-transform-sticky-regex@^7.14.5": @@ -1920,12 +2021,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-sticky-regex@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.5.tgz#593579bb2b5a8adfbe02cb43823275d9098f75f9" - integrity sha512-usYsuO1ID2LXxzuUxifgWtJemP7wL2uZtyrTVM4PKqsmJycdS4U4mGovL5xXkfUheds10Dd2PjoQLXw6zCsCbg== +"@babel/plugin-transform-sticky-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" + integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-template-literals@^7.12.1", "@babel/plugin-transform-template-literals@^7.14.5": version "7.14.5" @@ -1934,12 +2035,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-template-literals@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.5.tgz#343651385fd9923f5aa2275ca352c5d9183e1773" - integrity sha512-gnyKy9RyFhkovex4BjKWL3BVYzUDG6zC0gba7VMLbQoDuqMfJ1SDXs8k/XK41Mmt1Hyp4qNAvGFb9hKzdCqBRQ== +"@babel/plugin-transform-template-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" + integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-typeof-symbol@^7.14.5": version "7.14.5" @@ -1948,12 +2049,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-typeof-symbol@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.5.tgz#a1d1bf2c71573fe30965d0e4cd6a3291202e20ed" - integrity sha512-ldxCkW180qbrvyCVDzAUZqB0TAeF8W/vGJoRcaf75awm6By+PxfJKvuqVAnq8N9wz5Xa6mSpM19OfVKKVmGHSQ== +"@babel/plugin-transform-typeof-symbol@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" + integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-typescript@^7.15.0": version "7.15.4" @@ -1971,12 +2072,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-unicode-escapes@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.5.tgz#80507c225af49b4f4ee647e2a0ce53d2eeff9e85" - integrity sha512-shiCBHTIIChGLdyojsKQjoAyB8MBwat25lKM7MJjbe1hE0bgIppD+LX9afr41lLHOhqceqeWl4FkLp+Bgn9o1Q== +"@babel/plugin-transform-unicode-escapes@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" + integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== dependencies: - "@babel/helper-plugin-utils" "^7.16.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-unicode-regex@^7.14.5": version "7.14.5" @@ -1986,40 +2087,40 @@ "@babel/helper-create-regexp-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-unicode-regex@^7.16.0": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.5.tgz#ac84d6a1def947d71ffb832426aa53b83d7ed49e" - integrity sha512-GTJ4IW012tiPEMMubd7sD07iU9O/LOo8Q/oU4xNhcaq0Xn8+6TcUQaHtC8YxySo1T+ErQ8RaWogIEeFhKGNPzw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.16.5" - -"@babel/preset-env@7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.4.tgz#4f6ec33b2a3fe72d6bfdcdf3859500232563a2e3" - integrity sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-compilation-targets" "^7.16.3" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.2" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-async-generator-functions" "^7.16.4" - "@babel/plugin-proposal-class-properties" "^7.16.0" - "@babel/plugin-proposal-class-static-block" "^7.16.0" - "@babel/plugin-proposal-dynamic-import" "^7.16.0" - "@babel/plugin-proposal-export-namespace-from" "^7.16.0" - "@babel/plugin-proposal-json-strings" "^7.16.0" - "@babel/plugin-proposal-logical-assignment-operators" "^7.16.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.0" - "@babel/plugin-proposal-numeric-separator" "^7.16.0" - "@babel/plugin-proposal-object-rest-spread" "^7.16.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-private-methods" "^7.16.0" - "@babel/plugin-proposal-private-property-in-object" "^7.16.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.16.0" +"@babel/plugin-transform-unicode-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" + integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/preset-env@7.16.11": + version "7.16.11" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" + integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== + dependencies: + "@babel/compat-data" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-async-generator-functions" "^7.16.8" + "@babel/plugin-proposal-class-properties" "^7.16.7" + "@babel/plugin-proposal-class-static-block" "^7.16.7" + "@babel/plugin-proposal-dynamic-import" "^7.16.7" + "@babel/plugin-proposal-export-namespace-from" "^7.16.7" + "@babel/plugin-proposal-json-strings" "^7.16.7" + "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" + "@babel/plugin-proposal-numeric-separator" "^7.16.7" + "@babel/plugin-proposal-object-rest-spread" "^7.16.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-private-methods" "^7.16.11" + "@babel/plugin-proposal-private-property-in-object" "^7.16.7" + "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" @@ -2034,44 +2135,44 @@ "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.16.0" - "@babel/plugin-transform-async-to-generator" "^7.16.0" - "@babel/plugin-transform-block-scoped-functions" "^7.16.0" - "@babel/plugin-transform-block-scoping" "^7.16.0" - "@babel/plugin-transform-classes" "^7.16.0" - "@babel/plugin-transform-computed-properties" "^7.16.0" - "@babel/plugin-transform-destructuring" "^7.16.0" - "@babel/plugin-transform-dotall-regex" "^7.16.0" - "@babel/plugin-transform-duplicate-keys" "^7.16.0" - "@babel/plugin-transform-exponentiation-operator" "^7.16.0" - "@babel/plugin-transform-for-of" "^7.16.0" - "@babel/plugin-transform-function-name" "^7.16.0" - "@babel/plugin-transform-literals" "^7.16.0" - "@babel/plugin-transform-member-expression-literals" "^7.16.0" - "@babel/plugin-transform-modules-amd" "^7.16.0" - "@babel/plugin-transform-modules-commonjs" "^7.16.0" - "@babel/plugin-transform-modules-systemjs" "^7.16.0" - "@babel/plugin-transform-modules-umd" "^7.16.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.0" - "@babel/plugin-transform-new-target" "^7.16.0" - "@babel/plugin-transform-object-super" "^7.16.0" - "@babel/plugin-transform-parameters" "^7.16.3" - "@babel/plugin-transform-property-literals" "^7.16.0" - "@babel/plugin-transform-regenerator" "^7.16.0" - "@babel/plugin-transform-reserved-words" "^7.16.0" - "@babel/plugin-transform-shorthand-properties" "^7.16.0" - "@babel/plugin-transform-spread" "^7.16.0" - "@babel/plugin-transform-sticky-regex" "^7.16.0" - "@babel/plugin-transform-template-literals" "^7.16.0" - "@babel/plugin-transform-typeof-symbol" "^7.16.0" - "@babel/plugin-transform-unicode-escapes" "^7.16.0" - "@babel/plugin-transform-unicode-regex" "^7.16.0" + "@babel/plugin-transform-arrow-functions" "^7.16.7" + "@babel/plugin-transform-async-to-generator" "^7.16.8" + "@babel/plugin-transform-block-scoped-functions" "^7.16.7" + "@babel/plugin-transform-block-scoping" "^7.16.7" + "@babel/plugin-transform-classes" "^7.16.7" + "@babel/plugin-transform-computed-properties" "^7.16.7" + "@babel/plugin-transform-destructuring" "^7.16.7" + "@babel/plugin-transform-dotall-regex" "^7.16.7" + "@babel/plugin-transform-duplicate-keys" "^7.16.7" + "@babel/plugin-transform-exponentiation-operator" "^7.16.7" + "@babel/plugin-transform-for-of" "^7.16.7" + "@babel/plugin-transform-function-name" "^7.16.7" + "@babel/plugin-transform-literals" "^7.16.7" + "@babel/plugin-transform-member-expression-literals" "^7.16.7" + "@babel/plugin-transform-modules-amd" "^7.16.7" + "@babel/plugin-transform-modules-commonjs" "^7.16.8" + "@babel/plugin-transform-modules-systemjs" "^7.16.7" + "@babel/plugin-transform-modules-umd" "^7.16.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" + "@babel/plugin-transform-new-target" "^7.16.7" + "@babel/plugin-transform-object-super" "^7.16.7" + "@babel/plugin-transform-parameters" "^7.16.7" + "@babel/plugin-transform-property-literals" "^7.16.7" + "@babel/plugin-transform-regenerator" "^7.16.7" + "@babel/plugin-transform-reserved-words" "^7.16.7" + "@babel/plugin-transform-shorthand-properties" "^7.16.7" + "@babel/plugin-transform-spread" "^7.16.7" + "@babel/plugin-transform-sticky-regex" "^7.16.7" + "@babel/plugin-transform-template-literals" "^7.16.7" + "@babel/plugin-transform-typeof-symbol" "^7.16.7" + "@babel/plugin-transform-unicode-escapes" "^7.16.7" + "@babel/plugin-transform-unicode-regex" "^7.16.7" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.16.0" + "@babel/types" "^7.16.8" babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.4.0" + babel-plugin-polyfill-corejs3 "^0.5.0" babel-plugin-polyfill-regenerator "^0.3.0" - core-js-compat "^3.19.1" + core-js-compat "^3.20.2" semver "^6.3.0" "@babel/preset-env@^7.12.11": @@ -2215,10 +2316,10 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@7.16.3", "@babel/runtime@^7.7.6": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" - integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== +"@babel/runtime@7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== dependencies: regenerator-runtime "^0.13.4" @@ -2229,14 +2330,21 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@7.16.0", "@babel/template@^7.16.0", "@babel/template@^7.8.6": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" - integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== +"@babel/runtime@^7.7.6": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" + regenerator-runtime "^0.13.4" + +"@babel/template@7.16.7", "@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" "@babel/template@^7.12.7", "@babel/template@^7.15.4", "@babel/template@^7.3.3": version "7.15.4" @@ -2247,6 +2355,15 @@ "@babel/parser" "^7.15.4" "@babel/types" "^7.15.4" +"@babel/template@^7.16.0", "@babel/template@^7.8.6": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" + integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== + dependencies: + "@babel/code-frame" "^7.16.0" + "@babel/parser" "^7.16.0" + "@babel/types" "^7.16.0" + "@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.1.0", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.7.2": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" @@ -2277,19 +2394,19 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.16.5": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.5.tgz#d7d400a8229c714a59b87624fc67b0f1fbd4b2b3" - integrity sha512-FOCODAzqUMROikDYLYxl4nmwiLlu85rNqBML/A5hKRVXG2LV8d0iMqgPzdYTcIpjZEBB7D6UDU9vxRZiriASdQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.5" - "@babel/helper-environment-visitor" "^7.16.5" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.5" - "@babel/types" "^7.16.0" +"@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.0.tgz#3143e5066796408ccc880a33ecd3184f3e75cd30" + integrity sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.0" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.0" + "@babel/types" "^7.17.0" debug "^4.1.0" globals "^11.1.0" @@ -2309,6 +2426,14 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -2332,11 +2457,6 @@ resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.11.tgz#f5a168e86a2fa185a50ca40a1e838aa5e5fb007d" integrity sha512-6CfqLJUqV0SwS4yE+9vciqxHUJ6CqIptSXXzFw80MonCDoVJvCJ/xhKfs7VZqJ4jDtEu/7ILvovFtZdLg9fiAg== -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== - "@ctrl/tinycolor@^3.4.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f" @@ -2835,10 +2955,10 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jridgewell/resolve-uri@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz#3fdf5798f0b49e90155896f6291df186eac06c83" - integrity sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA== +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72" + integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg== "@mdx-js/loader@^1.6.22": version "1.6.22" @@ -2985,10 +3105,10 @@ optional "0.1.4" tslib "2.3.1" -"@ngtools/webpack@13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-13.1.2.tgz#58d8bfe8b3d4ee3b5aa1ceb3f7911b77410c6c6b" - integrity sha512-F/KraxCCUjSn5nWVEQSuyVfnoE9j/bTcpIb+6e38/Hq/saPfsUoNiRjWlTAxCD44vHbMuVkJ/ZRZT6hdICAslw== +"@ngtools/webpack@13.2.3": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-13.2.3.tgz#a4434eb5e2ec3cc04d7f714f20d5203114563f33" + integrity sha512-wooUZiV92QyoeFxkhqIwH/cfiAAAn+l8fEEuaaEIfJtpjpbShvvlboEVsqb28soeGiFJfLcmsZM3mUFgsG4QBQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -3076,20 +3196,19 @@ node-gyp "^8.2.0" read-package-json-fast "^2.0.1" -"@nrwl/angular@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-13.4.1.tgz#651dd2dd029a382929a86718425bf0d76f1b81b6" - integrity sha512-EUuL2nydXia10SHc6hrThViL0dH2ea5qmfDv/W5EfGYG0Ng3UMM9cHogyuVlcoCmEBqK42Hwvb7t3JeWJ6+V+g== - dependencies: - "@angular-devkit/schematics" "~13.1.0" - "@nrwl/cypress" "13.4.1" - "@nrwl/devkit" "13.4.1" - "@nrwl/jest" "13.4.1" - "@nrwl/linter" "13.4.1" - "@nrwl/storybook" "13.4.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== + 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" "@phenomnomnominal/tsquery" "4.1.1" - "@schematics/angular" "~13.1.0" - find-parent-dir "^0.3.1" + "@schematics/angular" "~13.2.0" ignore "^5.0.4" jasmine-marbles "~0.8.4" rxjs-for-await "0.0.2" @@ -3099,70 +3218,74 @@ tslib "^2.3.0" webpack-merge "5.7.3" -"@nrwl/cli@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-13.4.1.tgz#f01e3442ce9c22d8d4fc4b55285000561de27f3c" - integrity sha512-U0ik9wrumTxg+l0f17xajOLgKPQH2hFFPiDlegOupef7mR3JBxrEzadkTXjRH7wXSOWBdzYyVYsfx+6UwS9Ilw== +"@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== dependencies: - "@nrwl/tao" "13.4.1" + "@nrwl/tao" "13.8.1" chalk "4.1.0" enquirer "~2.3.6" v8-compile-cache "2.3.0" - yargs "15.4.1" yargs-parser "20.0.0" -"@nrwl/cypress@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-13.4.1.tgz#2f1005363343c1778b3c22926c4303616482c446" - integrity sha512-+H4oSmtYhH/zyEGVYVEc6kw96hhdjIfajfGW1Hkver+yqtsXLuD8gfxC+LT8gK1VWcR+seMOS5DzdHzToVKuOA== +"@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== dependencies: "@cypress/webpack-preprocessor" "^5.9.1" - "@nrwl/devkit" "13.4.1" - "@nrwl/linter" "13.4.1" - "@nrwl/workspace" "13.4.1" + "@nrwl/devkit" "13.8.1" + "@nrwl/linter" "13.8.1" + "@nrwl/workspace" "13.8.1" chalk "4.1.0" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" rxjs "^6.5.4" ts-loader "^9.2.6" tsconfig-paths "^3.9.0" - tsconfig-paths-webpack-plugin "3.4.1" + tsconfig-paths-webpack-plugin "3.5.2" tslib "^2.3.0" webpack-node-externals "^3.0.0" - yargs-parser "20.0.0" -"@nrwl/devkit@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-13.4.1.tgz#32395f506be9e2adabf95febf774609168b21449" - integrity sha512-dk/moNFriAFnAFro9EQILJWkYQsgkRI1rIou2uCkbrhUHeFZ1nZECjBNdBj/hocz76O24xH7+A8HneacXlKZHg== +"@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== dependencies: - "@nrwl/tao" "13.4.1" + "@nrwl/tao" "13.8.1" 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.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-13.4.1.tgz#5180e1f083e380ae1b14b913d4c693e930a351a1" - integrity sha512-oId9nHYd52JT3soTXtrqt7OtXk/AY3Jph2ZnPaFesFbv/0Oe6twGL+7WmG/5RQIauJ9pj6owfUqM+fGznZVHVw== +"@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== dependencies: - "@nrwl/devkit" "13.4.1" - "@nrwl/workspace" "13.4.1" - "@typescript-eslint/experimental-utils" "~5.3.0" + "@nrwl/devkit" "13.8.1" + "@nrwl/workspace" "13.8.1" + "@swc-node/register" "^1.4.2" + "@typescript-eslint/experimental-utils" "~5.10.0" + chalk "4.1.0" confusing-browser-globals "^1.0.9" - ts-node "^9.1.1" 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.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-13.4.1.tgz#174fba0700b93c30943e53c5b301b34216c84b8a" - integrity sha512-wV5RNDhM7FYvE1+xbla6wVP5VSonPA4os8GLJYF+fKwAYf8lcUwFx2SIlRvFEqZF/wyQzZYR1D6filGaXGlU5w== +"@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== dependencies: "@jest/reporters" "27.2.2" "@jest/test-result" "27.2.2" - "@nrwl/devkit" "13.4.1" + "@nrwl/devkit" "13.8.1" chalk "4.1.0" identity-obj-proxy "3.0.0" jest-config "27.2.2" @@ -3172,78 +3295,76 @@ rxjs "^6.5.4" tslib "^2.3.0" -"@nrwl/linter@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-13.4.1.tgz#af2a6677ef79d44f2ef6ee007b03be575e290cc1" - integrity sha512-Ufn/TtHpAuvSbS7vRDCcHN+etMEKoAnoLNOifhvA6Iw4k2B8pw3mxPuZHLUKG3j/+l+O+YIZdPxhzsr0z2O/Rw== +"@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== dependencies: - "@nrwl/devkit" "13.4.1" - "@nrwl/jest" "13.4.1" + "@nrwl/devkit" "13.8.1" + "@nrwl/jest" "13.8.1" "@phenomnomnominal/tsquery" "4.1.1" - eslint "8.2.0" - glob "7.1.4" - minimatch "3.0.4" tmp "~0.2.1" tslib "^2.3.0" -"@nrwl/nest@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-13.4.1.tgz#68929fee9f49e37bfb4d2bab8d912b70925eb270" - integrity sha512-KcsXLtOz34nKWT2d05rbPmNqRaFWFzUruEh3ustqb0D5DHZ0xj8XCdvUmaetK0Gv6xYeAK7v4KVUlQJllc/wgA== +"@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== dependencies: "@nestjs/schematics" "^8.0.0" - "@nrwl/devkit" "13.4.1" - "@nrwl/jest" "13.4.1" - "@nrwl/linter" "13.4.1" - "@nrwl/node" "13.4.1" - -"@nrwl/node@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-13.4.1.tgz#7984de32d0ce22f29758fcbcd7665072aa8f0bd9" - integrity sha512-W5zEQ22aZ/wTgkaloHP2aAX17FokxoUcSltZpldQ0Bw6tzr0NWZFCtWSjn78AFJisvBGnML6jBh9elda2qbC9w== - dependencies: - "@nrwl/devkit" "13.4.1" - "@nrwl/jest" "13.4.1" - "@nrwl/linter" "13.4.1" - "@nrwl/workspace" "13.4.1" + "@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" chalk "4.1.0" copy-webpack-plugin "^9.0.1" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" fs-extra "^9.1.0" glob "7.1.4" - license-webpack-plugin "2.3.15" + license-webpack-plugin "4.0.0" rxjs "^6.5.4" rxjs-for-await "0.0.2" source-map-support "0.5.19" + terser-webpack-plugin "^5.3.0" tree-kill "1.2.2" ts-loader "^9.2.6" ts-node "~9.1.1" tsconfig-paths "^3.9.0" - tsconfig-paths-webpack-plugin "3.4.1" + tsconfig-paths-webpack-plugin "3.5.2" tslib "^2.3.0" webpack "^5.58.1" webpack-merge "^5.8.0" webpack-node-externals "^3.0.0" -"@nrwl/storybook@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.4.1.tgz#09fc4fc2c1d33dafa9e761e84fa521921529d582" - integrity sha512-EhsIZnWJIhZikI17+AL2YHdsXZzXNs+GVmbPCig6gV3EjfQyc9lfx455ydiy/VYJ8QbAX6+XEixPtXigX30wUg== +"@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== dependencies: - "@nrwl/cypress" "13.4.1" - "@nrwl/devkit" "13.4.1" - "@nrwl/linter" "13.4.1" - "@nrwl/workspace" "13.4.1" + "@nrwl/cypress" "13.8.1" + "@nrwl/devkit" "13.8.1" + "@nrwl/linter" "13.8.1" + "@nrwl/workspace" "13.8.1" core-js "^3.6.5" semver "7.3.4" ts-loader "^9.2.6" - tsconfig-paths-webpack-plugin "3.4.1" + tsconfig-paths-webpack-plugin "3.5.2" -"@nrwl/tao@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.4.1.tgz#914214bda8d64c61369218e7c53b4dfdcfe756e7" - integrity sha512-bgVzHlKfY7TK0ZLlM9+yQxfw8+NGsfl1+612advLYI46NRYAOpY3yOPBWX+69M+W3SwdjKQDyTi57hHbgx1XBA== +"@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== dependencies: chalk "4.1.0" enquirer "~2.3.6" @@ -3251,7 +3372,7 @@ fs-extra "^9.1.0" ignore "^5.0.4" jsonc-parser "3.0.0" - nx "13.4.1" + nx "13.8.1" rxjs "^6.5.4" rxjs-for-await "0.0.2" semver "7.3.4" @@ -3259,32 +3380,32 @@ tslib "^2.3.0" yargs-parser "20.0.0" -"@nrwl/workspace@13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-13.4.1.tgz#7c178702e4908e621df7b8f405f9ed78da8946f3" - integrity sha512-u+/1jQL7fsISJePZlTkBxp27N+aL9BWIbITAaH+Jxazyjrpwy11ImcXbJIpMeYY8P1cpXM7XMw0sn8vz7LIuZQ== +"@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== dependencies: - "@nrwl/cli" "13.4.1" - "@nrwl/devkit" "13.4.1" - "@nrwl/jest" "13.4.1" - "@nrwl/linter" "13.4.1" + "@nrwl/cli" "13.8.1" + "@nrwl/devkit" "13.8.1" + "@nrwl/jest" "13.8.1" + "@nrwl/linter" "13.8.1" "@parcel/watcher" "2.0.4" chalk "4.1.0" chokidar "^3.5.1" - cosmiconfig "^4.0.0" + cli-cursor "3.1.0" + cli-spinners "2.6.1" dotenv "~10.0.0" enquirer "~2.3.6" + figures "3.2.0" flat "^5.0.2" fs-extra "^9.1.0" glob "7.1.4" ignore "^5.0.4" minimatch "3.0.4" - npm-run-all "^4.1.5" npm-run-path "^4.0.1" - open "^7.4.2" + open "^8.4.0" rxjs "^6.5.4" semver "7.3.4" - strip-ansi "6.0.0" tmp "~0.2.1" tslib "^2.3.0" yargs "15.4.1" @@ -3373,13 +3494,13 @@ dependencies: any-observable "^0.3.0" -"@schematics/angular@13.1.2", "@schematics/angular@~13.1.0": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-13.1.2.tgz#bd3fd2fd1bb225bffb24fedad1409b64b1d08323" - integrity sha512-OMbuOsnzUFjIGeo99NYwIPwjX6udJAiT5Sj5K7QZZYj66HuAqNBMV57J8GPA56edx5mOHZZApWMjXLlOxRXbJA== +"@schematics/angular@13.2.3", "@schematics/angular@~13.2.0": + version "13.2.3" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-13.2.3.tgz#600bbe21bff5b090aaee17ba726410ee1328b40b" + integrity sha512-jloooGC7eco9AKxlIMMqFRptJYzZ0jNRBStWOp2dCISg6rmOKqpxbsHLtYFQIT1PnlomSxtKDAgYGQMDi9zhXw== dependencies: - "@angular-devkit/core" "13.1.2" - "@angular-devkit/schematics" "13.1.2" + "@angular-devkit/core" "13.2.3" + "@angular-devkit/schematics" "13.2.3" jsonc-parser "3.0.0" "@simplewebauthn/browser@4.1.0": @@ -3435,17 +3556,17 @@ resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.8.0.tgz#87d950fbbf050dce1566f32ca48c98007239472b" integrity sha512-WazFGUMnbumg8ePNvej8cIOEcxvuZ0ugKQkkE1xFbDYcl7DgJd62MiG+bIqCcQlIdLEfhjAdoixxlFdJgrgjyA== -"@storybook/addon-actions@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.9.tgz#1d4e8c00ad304efe6722043dac759f4716b515ee" - integrity sha512-L1N66p/vr+wPUBfrH3qffjNAcWSS/wvuL370T7cWxALA9LLA8yY9U2EpITc5btuCC5QOxApCeyHkFnrBhNa94g== +"@storybook/addon-actions@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.18.tgz#e997060e1b0af62f9f831301a56a3addfc1f1365" + integrity sha512-qPw5qfbWPmyOdaXxAVAbdVLVVE31gRrkH0ESUps+FXVNypRz1/0lJ6M2VrtOHMrFbGBl94SALdqsHOx6OYZKwg== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.9" + "@storybook/theming" "6.4.18" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -3459,18 +3580,18 @@ util-deprecate "^1.0.2" uuid-browser "^3.1.0" -"@storybook/addon-backgrounds@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.9.tgz#89033aed6f01d6a2dc134cbdb1ce0c46afd130ec" - integrity sha512-/jqUZvk+x8TpDedyFnJamSYC91w/e8prj42xtgLG4+yBlb0UmewX7BAq9i/lhowhUjuLKaOX9E8E0AHftg8L6A== +"@storybook/addon-backgrounds@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.18.tgz#178531ece3848de33b1aaea44af8cdd88da70314" + integrity sha512-LAonQO0s77CkbGx7l8qyeEevOBWDuYZKl9iJ0BSPogU48+4JVEYVSBiystIXPJXcGeEy+M0qFmwg5nHWjf9/cA== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.9" + "@storybook/theming" "6.4.18" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" @@ -3478,28 +3599,28 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-controls@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.9.tgz#286a184336a80981fdd805f44a68f60fb6e38e73" - integrity sha512-2eqtiYugCAOw8MCv0HOfjaZRQ4lHydMYoKIFy/QOv6/mjcJeG9dF01dA30n3miErQ18BaVyAB5+7rrmuqMwXVA== +"@storybook/addon-controls@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.18.tgz#0b65658a141428e9625ddbe7dee0a761911cd04f" + integrity sha512-nP7JCiAES4S5mn8PYfmPZZG9VpsPV7eeQQRPuiPgdidhH93cmsW/FYj8V739lrm5QJc0JSI6uY/y9qHa9rh43w== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-common" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-common" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.9" - "@storybook/store" "6.4.9" - "@storybook/theming" "6.4.9" + "@storybook/node-logger" "6.4.18" + "@storybook/store" "6.4.18" + "@storybook/theming" "6.4.18" core-js "^3.8.2" lodash "^4.17.21" ts-dedent "^2.0.0" -"@storybook/addon-docs@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.9.tgz#dc34c6152085043a771623b2de344bc9d91f0563" - integrity sha512-sJvnbp6Z+e7B1+vDE8gZVhCg1eNotIa7bx9LYd1Y2QwJ4PEv9hE2YxnzmWt3NZJGtrn4gdGaMCk7pmksugHi7g== +"@storybook/addon-docs@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.18.tgz#e1f969a63f286649e46a4846b56cc309bccd07c3" + integrity sha512-NcGcrW+2hrzoyWHEaDmw6wxqyV/FDsdLaOS0XZrIQuBaj1rve0IfA1jqggfNo8owqmXXGp8cQBnFbhRES1a7nQ== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -3510,21 +3631,21 @@ "@mdx-js/loader" "^1.6.22" "@mdx-js/mdx" "^1.6.22" "@mdx-js/react" "^1.6.22" - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/builder-webpack4" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/builder-webpack4" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.9" - "@storybook/node-logger" "6.4.9" - "@storybook/postinstall" "6.4.9" - "@storybook/preview-web" "6.4.9" - "@storybook/source-loader" "6.4.9" - "@storybook/store" "6.4.9" - "@storybook/theming" "6.4.9" + "@storybook/csf-tools" "6.4.18" + "@storybook/node-logger" "6.4.18" + "@storybook/postinstall" "6.4.18" + "@storybook/preview-web" "6.4.18" + "@storybook/source-loader" "6.4.18" + "@storybook/store" "6.4.18" + "@storybook/theming" "6.4.18" acorn "^7.4.1" acorn-jsx "^5.3.1" acorn-walk "^7.2.0" @@ -3539,7 +3660,7 @@ lodash "^4.17.21" nanoid "^3.1.23" p-limit "^3.1.0" - prettier "^2.2.1" + prettier ">=2.2.1 <=2.3.0" prop-types "^15.7.2" react-element-to-jsx-string "^14.3.4" regenerator-runtime "^0.13.7" @@ -3548,116 +3669,116 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-essentials@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.9.tgz#e761a61a9ac9809b8a5d8b6f7c5a1b50f0e2cd91" - integrity sha512-3YOtGJsmS7A4aIaclnEqTgO+fUEX63pHq2CvqIKPGLVPgLmn6MnEhkkV2j30MfAkoe3oynLqFBvkCdYwzwJxNQ== - dependencies: - "@storybook/addon-actions" "6.4.9" - "@storybook/addon-backgrounds" "6.4.9" - "@storybook/addon-controls" "6.4.9" - "@storybook/addon-docs" "6.4.9" - "@storybook/addon-measure" "6.4.9" - "@storybook/addon-outline" "6.4.9" - "@storybook/addon-toolbars" "6.4.9" - "@storybook/addon-viewport" "6.4.9" - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/node-logger" "6.4.9" +"@storybook/addon-essentials@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.18.tgz#0f8a90a53887bfd5dfc0d147d634bf0ae19a9a5d" + integrity sha512-AWKF0Gn7HagzB4ZbZdSXauJ8rgjbIB0Y1jgNCYtReZ//9QDSmF9yrFE0fLJi8O0WBHiQOTeV8Vj+yooGGWRRWQ== + dependencies: + "@storybook/addon-actions" "6.4.18" + "@storybook/addon-backgrounds" "6.4.18" + "@storybook/addon-controls" "6.4.18" + "@storybook/addon-docs" "6.4.18" + "@storybook/addon-measure" "6.4.18" + "@storybook/addon-outline" "6.4.18" + "@storybook/addon-toolbars" "6.4.18" + "@storybook/addon-viewport" "6.4.18" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/node-logger" "6.4.18" core-js "^3.8.2" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-measure@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.9.tgz#d4446e0b0686f4f25bbd7eee8c4cf296d8bea216" - integrity sha512-c7r98kZM0i7ZrNf0BZe/12BwTYGDLUnmyNcLhugquvezkm32R1SaqXF8K1bGkWkSuzBvt49lAXXPPGUh+ByWEQ== +"@storybook/addon-measure@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.18.tgz#feaadddf5905c87f08230d4acec4e8f5f15bcd2e" + integrity sha512-a9bFiQ/QK/5guxWscwOJN+sszhClPTTn2pTX77SKs+bzZUmckCfneto4NUavsHpj/XTxjwAwidowewwqFV1jTQ== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" -"@storybook/addon-outline@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.9.tgz#0f6b20eb41580686cca4b9f12937932dd5f51c64" - integrity sha512-pXXfqisYfdoxyJuSogNBOUiqIugv0sZGYDJXuwEgEDZ27bZD6fCQmsK3mqSmRzAfXwDqTKvWuu2SRbEk/cRRGA== +"@storybook/addon-outline@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.18.tgz#64ded86bd0ed2dbfc083fcfc050967acfb8222b7" + integrity sha512-FyADdeD7x/25OkjCR7oIXDgrlwM5RB0tbslC0qrRMxKXSjZFTJjr3Qwge0bg8I9QbxDRz+blVzBv3xIhOiDNzQ== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-toolbars@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.9.tgz#147534d0b185a1782f3381a47c627b4a4193297d" - integrity sha512-fep1lLDcyaQJdR8rC/lJTamiiJ8Ilio580d9aXDM651b7uHqhxM0dJvM9hObBU8dOj/R3hIAszgTvdTzYlL2cQ== +"@storybook/addon-toolbars@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.18.tgz#2de4a4e8bc17301055f9e2cae75887bdb6f4efc0" + integrity sha512-Vvj8mvorZhoXvYDuUUKqFpcsNNkVJZmhojdLZ4ULPcvjN3z5MWGwtlJfV+9vkVmJWuR1WG93dVK1+PnitYDZAQ== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/theming" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/theming" "6.4.18" core-js "^3.8.2" regenerator-runtime "^0.13.7" -"@storybook/addon-viewport@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.9.tgz#73753ff62043d3d6e6d845590ed70caf775af960" - integrity sha512-iqDcfbOG3TClybDEIi+hOKq8PDKNldyAiqBeak4AfGp+lIZ4NvhHgS5RCNylMVKpOUMbGIeWiSFxQ/oglEN1zA== - dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" - "@storybook/theming" "6.4.9" +"@storybook/addon-viewport@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.18.tgz#7a2f61fdad488fac9fe399dba4f3b947fe70f6db" + integrity sha512-gWSJAdtUaVrpsbdBveFTkz4V3moGnKxS3iwR8djcIWhTqdRVJxGu0gFtxNpKvdOEMIqF4yNmXYj79oLuNZNkcQ== + dependencies: + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" + "@storybook/theming" "6.4.18" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.9.tgz#43b5dabf6781d863fcec0a0b293c236b4d5d4433" - integrity sha512-y+oiN2zd+pbRWwkf6aQj4tPDFn+rQkrv7fiVoMxsYub+kKyZ3CNOuTSJH+A1A+eBL6DmzocChUyO6jvZFuh6Dg== +"@storybook/addons@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.18.tgz#fc92a4a608680f2e182a5e896ed382792f6b774e" + integrity sha512-fd3S79P4jJCYZNA2JxA1Xnkj0UlHGQ4Vg72aroWy4OQFlgGQor1LgPfM6RaJ9rh/4k4BXYPXsS7wzI0UWKG3Lw== dependencies: - "@storybook/api" "6.4.9" - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/api" "6.4.18" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.9" - "@storybook/theming" "6.4.9" + "@storybook/router" "6.4.18" + "@storybook/theming" "6.4.18" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/angular@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.9.tgz#966089d911630166838008e6fcae5f4a7fc5855e" - integrity sha512-lwHPHizr5m2wCqSUi8qbz79cOjudGqNWHFzjWIbhucAS28XBDSj8A2yV7b88JUuX2KH6KJP/TpTd9avTgPHeKw== +"@storybook/angular@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.18.tgz#05dc219600fa01718f179f867dd360b7dd559063" + integrity sha512-FfbtID1/2E6c9aEJwOpzZFCVeZcIfC6hI+VTYbR/AlUWukNJPCHwKSPwezpjds6TobjYghes93Lbsdgb4ZQL9g== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/core" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/core" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.9" + "@storybook/node-logger" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.9" + "@storybook/store" "6.4.18" "@types/webpack-env" "^1.16.0" autoprefixer "^9.8.6" core-js "^3.8.2" @@ -3680,18 +3801,18 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/api@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.9.tgz#6187d08658629580f0a583f2069d55b34964b34a" - integrity sha512-U+YKcDQg8xal9sE5eSMXB9vcqk8fD1pSyewyAjjbsW5hV0B3L3i4u7z/EAD9Ujbnor+Cvxq+XGvp+Qnc5Gd40A== +"@storybook/api@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.18.tgz#92da2b69aeec712419bec9bab5c8434ff1776e97" + integrity sha512-tSbsHKklBysuSmw4T+cKzMj6mQh/42m9F8+2iJns2XG/IUKpMAzFg/9dlgCTW+ay6dJwsR79JGIc9ccIe4SMgQ== dependencies: - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.9" + "@storybook/router" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.9" + "@storybook/theming" "6.4.18" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -3703,10 +3824,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.9.tgz#86cd691c856eeb7a6a7bcafa57e9a66c1e0b9906" - integrity sha512-nDbXDd3A8dvalCiuBZuUT6/GQP14+fuxTj5g+AppCgV1gLO45lXWtX75Hc0IbZrIQte6tDg5xeFQamZSLPMcGg== +"@storybook/builder-webpack4@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.18.tgz#8bae72b9e982d35a5a9f2b7f9af9d85a9c2dc966" + integrity sha512-N/OGjTnc7CpVoDnfoI49uMjAIpGqh2lWHFYNIWaUoG1DNnTt1nCc49hw9awjFc5KgaYOwJmVg1SYYE8Afssu+Q== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3729,22 +3850,22 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/channel-postmessage" "6.4.9" - "@storybook/channels" "6.4.9" - "@storybook/client-api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/core-events" "6.4.9" - "@storybook/node-logger" "6.4.9" - "@storybook/preview-web" "6.4.9" - "@storybook/router" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/channel-postmessage" "6.4.18" + "@storybook/channels" "6.4.18" + "@storybook/client-api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/core-events" "6.4.18" + "@storybook/node-logger" "6.4.18" + "@storybook/preview-web" "6.4.18" + "@storybook/router" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.9" - "@storybook/theming" "6.4.9" - "@storybook/ui" "6.4.9" + "@storybook/store" "6.4.18" + "@storybook/theming" "6.4.18" + "@storybook/ui" "6.4.18" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -3766,7 +3887,6 @@ postcss-flexbugs-fixes "^4.2.1" postcss-loader "^4.2.0" raw-loader "^4.0.2" - react-dev-utils "^11.0.4" stable "^0.1.8" style-loader "^1.3.0" terser-webpack-plugin "^4.2.3" @@ -3779,10 +3899,10 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.2.2" -"@storybook/builder-webpack5@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.9.tgz#86dca9679b611d1e513bf3122e4eb05c785dfd4d" - integrity sha512-OB/PJHgsHwvX03smEsIM45oyYhLD8RK2wBIRS0PuGj1XdisOsiMal2a9hUHcqhZIiFstOEXUltMwBzJnixzprg== +"@storybook/builder-webpack5@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.18.tgz#64e3325efc84831d0efded88d4b6b8e51acff1dd" + integrity sha512-VbLqGVROK9wJsFt2wcBojgY/VMVgoFVVdEYTs0BsZqmuYVBVnKxpkhpzJFnhulFGpIMx1NL5GKwbSNK3Pz7FWQ== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3804,21 +3924,21 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/channel-postmessage" "6.4.9" - "@storybook/channels" "6.4.9" - "@storybook/client-api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/core-events" "6.4.9" - "@storybook/node-logger" "6.4.9" - "@storybook/preview-web" "6.4.9" - "@storybook/router" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/channel-postmessage" "6.4.18" + "@storybook/channels" "6.4.18" + "@storybook/client-api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/core-events" "6.4.18" + "@storybook/node-logger" "6.4.18" + "@storybook/preview-web" "6.4.18" + "@storybook/router" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.9" - "@storybook/theming" "6.4.9" + "@storybook/store" "6.4.18" + "@storybook/theming" "6.4.18" "@types/node" "^14.0.10" babel-loader "^8.0.0" babel-plugin-macros "^3.0.1" @@ -3831,7 +3951,7 @@ glob-promise "^3.4.0" html-webpack-plugin "^5.0.0" path-browserify "^1.0.1" - react-dev-utils "^11.0.4" + process "^0.11.10" stable "^0.1.8" style-loader "^2.0.0" terser-webpack-plugin "^5.0.3" @@ -3842,51 +3962,51 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.4.1" -"@storybook/channel-postmessage@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.9.tgz#b20b7d66f0f2a8ba39fe9002f3a3dc16d9e1f681" - integrity sha512-0Oif4e6/oORv4oc2tHhIRts9faE/ID9BETn4uqIUWSl2CX1wYpKYDm04rEg3M6WvSzsi+6fzoSFvkr9xC5Ns2w== +"@storybook/channel-postmessage@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.18.tgz#24547fe7cee599969fd62df22142ba7046099a8e" + integrity sha512-SKapUREPkqzKoBMpOJrZddE9PCR8CJkPTcDpjDqcRsTvToRWsux3pvzmuW4iGYnHNh+GQml7Rz9x85WfMIpfyQ== dependencies: - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-websocket@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.9.tgz#f012840894f73bac289ddcdc57efb385c4a0b7ef" - integrity sha512-R1O5yrNtN+dIAghqMXUqoaH7XWBcrKi5miVRn7QelKG3qZwPL8HQa7gIPc/b6S2D6hD3kQdSuv/zTIjjMg7wyw== +"@storybook/channel-websocket@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.18.tgz#cf3a03e88b983c2953cb76a40a964806790567c4" + integrity sha512-ROqNZAFB1gP9u8dmlM4KxykXHsd1ifunBgFY3ncQKeRi2Oh30OMVB2ZhNdoIF8i8X5ZBwSpId1o6nQhL2e/EJA== dependencies: - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" core-js "^3.8.2" global "^4.4.0" telejson "^5.3.2" -"@storybook/channels@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.9.tgz#132c574d3fb2e6aaa9c52312c592794699b9d8ec" - integrity sha512-DNW1qDg+1WFS2aMdGh658WJXh8xBXliO5KAn0786DKcWCsKjfsPPQg/QCHczHK0+s5SZyzQT5aOBb4kTRHELQA== +"@storybook/channels@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.18.tgz#2907aca0039b5eb9ae305112f14c488c2621c2f6" + integrity sha512-Bh4l7VKKR2ImLbZ9XgL/DzT3lFv9+SLiCu1ozfpBZGHUCOLyHRnkG/h8wYvRkF9s3tpNwOtaCaqD1vkkZfr3uw== dependencies: core-js "^3.8.2" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.9.tgz#e3d90c66356d6f53f8ceb4f31753f670f704fde0" - integrity sha512-1IljlTr+ea2pIr6oiPhygORtccOdEb7SqaVzWDfLCHOhUnJ2Ka5UY9ADqDa35jvSSdRdynfk9Yl5/msY0yY1yg== +"@storybook/client-api@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.18.tgz#61c7c90f3f099e4d3bcc36576d2adbe2e5ef6eee" + integrity sha512-ua2Q692Fz2b3q5M/Qzjixg2LArwrcHGBmht06bNw/jrRfyFeTUHOhh5BT7LxSEetUgHATH/Y1GW40xza9rXFNw== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/channel-postmessage" "6.4.9" - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/channel-postmessage" "6.4.18" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.9" + "@storybook/store" "6.4.18" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" @@ -3901,23 +4021,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.9.tgz#ef6af30fac861fea69c8917120ed06b4c2f0b54e" - integrity sha512-BVagmmHcuKDZ/XROADfN3tiolaDW2qG0iLmDhyV1gONnbGE6X5Qm19Jt2VYu3LvjKF1zMPSWm4mz7HtgdwKbuQ== +"@storybook/client-logger@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.18.tgz#4ad8ea7d67b17e5db8f15cffcc2f984df3479462" + integrity sha512-ciBaASMaB2ZPksbuyDbp3++5SZxbhcihEpl+RQcAVV8g+TUyBZKIcHt8HNHicTczz5my1EydZovMh1IkSBMICA== dependencies: core-js "^3.8.2" global "^4.4.0" -"@storybook/components@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.9.tgz#caed59eb3f09d1646da748186f718a0e54fb8fd7" - integrity sha512-uOUR97S6kjptkMCh15pYNM1vAqFXtpyneuonmBco5vADJb3ds0n2a8NeVd+myIbhIXn55x0OHKiSwBH/u7swCQ== +"@storybook/components@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.18.tgz#1f3eba9ab69a09b9468af0126d6e7ab040655ca4" + integrity sha512-LAPKYWgB6S10Vzt0IWa1Ihf9EAuQOGxlqehTuxYLOwMOKbto8iEbGRse/XaQfxdZf/RbmOL4u+7nVRROWgOEjg== dependencies: "@popperjs/core" "^2.6.0" - "@storybook/client-logger" "6.4.9" + "@storybook/client-logger" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.9" + "@storybook/theming" "6.4.18" "@types/color-convert" "^2.0.0" "@types/overlayscrollbars" "^1.12.0" "@types/react-syntax-highlighter" "11.0.5" @@ -3939,21 +4059,21 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.9.tgz#324119a67609f758e244a7d58bac00e62020a21f" - integrity sha512-LZSpTtvBlpcn+Ifh0jQXlm/8wva2zZ2v13yxYIxX6tAwQvmB54U0N4VdGVmtkiszEp7TQUAzA8Pcyp4GWE+UMA== - dependencies: - "@storybook/addons" "6.4.9" - "@storybook/channel-postmessage" "6.4.9" - "@storybook/channel-websocket" "6.4.9" - "@storybook/client-api" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" +"@storybook/core-client@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.18.tgz#7f2feb961864dcf6de501a94a41900fd36b43657" + integrity sha512-F9CqW31Mr9Qde90uqPorpkiS+P7UteKYmdHlV0o0czeWaL+MEhpY+3pRJuRIIjX5C7Vc89TvljMqs37Khakmdg== + dependencies: + "@storybook/addons" "6.4.18" + "@storybook/channel-postmessage" "6.4.18" + "@storybook/channel-websocket" "6.4.18" + "@storybook/client-api" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/preview-web" "6.4.9" - "@storybook/store" "6.4.9" - "@storybook/ui" "6.4.9" + "@storybook/preview-web" "6.4.18" + "@storybook/store" "6.4.18" + "@storybook/ui" "6.4.18" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" @@ -3965,10 +4085,10 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.9.tgz#1a892903061f927b8f7b9fa8d25273a2f5c9e227" - integrity sha512-wVHRfUGnj/Tv8nGjv128NDQ5Zp6c63rSXd1lYLzfZPTJmGOz4rpPPez2IZSnnDwbAWeqUSMekFVZPj4v6yuujQ== +"@storybook/core-common@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.18.tgz#0688a0a4a80cdbc161966c5a7ff49e755d64bbab" + integrity sha512-y4e43trNyQ3/v0Wpi240on7yVooUQvJBhJxOGEfcxAMRtcDa0ZCxHO4vAFX3k3voQOSmiXItXfJ1eGo/K+u0Fw== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3991,7 +4111,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.4.9" + "@storybook/node-logger" "6.4.18" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/pretty-hrtime" "^1.0.0" @@ -4020,29 +4140,29 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core-events@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.9.tgz#7febedb8d263fbd6e4a69badbfcdce0101e6f782" - integrity sha512-YhU2zJr6wzvh5naYYuy/0UKNJ/SaXu73sIr0Tx60ur3bL08XkRg7eZ9vBhNBTlAa35oZqI0iiGCh0ljiX7yEVQ== +"@storybook/core-events@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.18.tgz#630a19425eb387c6134f29b967c30458c65f7ea8" + integrity sha512-lCT3l0rFs6CuVpD8+mwmj1lUTomGErySTxi0KmVd2AWQj8kJL90EfS0jHSU5JIXScDvuwXDXLLmvMfqNU+zHdg== dependencies: core-js "^3.8.2" -"@storybook/core-server@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.9.tgz#593fd4cc21a05c908e0eed20767eb6c9cddad428" - integrity sha512-Ht/e17/SNW9BgdvBsnKmqNrlZ6CpHeVsClEUnauMov8I5rxjvKBVmI/UsbJJIy6H6VLiL/RwrA3RvLoAoZE8dA== +"@storybook/core-server@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.18.tgz#520935f7f330a734488e733ad4cf15a9556679b5" + integrity sha512-7e2QUtD8/TE14P9X/xsBDMBbNVi/etTtMKKhsG2TG25daRz+6qadbM9tNP0YwvIDk452cNYJkjflV48mf5+ZEA== dependencies: "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-webpack4" "6.4.9" - "@storybook/core-client" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/builder-webpack4" "6.4.18" + "@storybook/core-client" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.9" - "@storybook/manager-webpack4" "6.4.9" - "@storybook/node-logger" "6.4.9" + "@storybook/csf-tools" "6.4.18" + "@storybook/manager-webpack4" "6.4.18" + "@storybook/node-logger" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.9" + "@storybook/store" "6.4.18" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" "@types/pretty-hrtime" "^1.0.0" @@ -4050,7 +4170,7 @@ better-opn "^2.1.1" boxen "^5.1.2" chalk "^4.1.0" - cli-table3 "0.6.0" + cli-table3 "^0.6.1" commander "^6.2.1" compression "^1.7.4" core-js "^3.8.2" @@ -4075,18 +4195,18 @@ webpack "4" ws "^8.2.3" -"@storybook/core@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.9.tgz#4bf910d7322b524f8166c97c28875e1e3775f391" - integrity sha512-Mzhiy13loMSd3PCygK3t7HIT3X3L35iZmbe6+2xVbVmc/3ypCmq4PQALCUoDOGk37Ifrhop6bo6sl4s9YQ6UFw== +"@storybook/core@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.18.tgz#56f7bb0f20dbcfa205d860022b7bf30bf42fd472" + integrity sha512-7DTMAEXiBIwd1jgalbsZiXCiS2Be9MKKsr6GQdf3WaBm0WYV067oN9jcUY5IgNtJX06arT4Ykp+CGG/TR+sLlw== dependencies: - "@storybook/core-client" "6.4.9" - "@storybook/core-server" "6.4.9" + "@storybook/core-client" "6.4.18" + "@storybook/core-server" "6.4.18" -"@storybook/csf-tools@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.9.tgz#7cccb905875ba5962dda83825f763a111932464b" - integrity sha512-zbgsx9vY5XOA9bBmyw+KyuRspFXAjEJ6I3d/6Z3G1kNBeOEj9i3DT7O99Rd/THfL/3mWl8DJ/7CJVPk1ZYxunA== +"@storybook/csf-tools@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.18.tgz#cdd40b543f9bea79c1481c236868b8ea04af6bd7" + integrity sha512-KtwxKrkndEyvyAiBliI6m4yMFMvnyI4fOjU8t1qCit/0gjutOF5JxmmZ+H8FSI5dIyibEOzQmzHe0MyStAjCEQ== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -4102,7 +4222,7 @@ global "^4.4.0" js-string-escape "^1.0.1" lodash "^4.17.21" - prettier "^2.2.1" + prettier ">=2.2.1 <=2.3.0" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" @@ -4113,20 +4233,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.9.tgz#76edd6f2c627dc64d3362a265c2fe6ae7ee22507" - integrity sha512-828x3rqMuzBNSb13MSDo2nchY7fuywh+8+Vk+fn00MvBYJjogd5RQFx5ocwhHzmwXbnESIerlGwe81AzMck8ng== +"@storybook/manager-webpack4@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.18.tgz#5317c917dbdaf4cf8721647551a785eb13c04146" + integrity sha512-6oX1KrIJBoY4vdZiMftJNusv+Bm8pegVjdJ+aZcbr/41x7ufP3tu5UKebEXDH0UURXtLw0ffl+OgojewGdpC1Q== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.9" - "@storybook/core-client" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/node-logger" "6.4.9" - "@storybook/theming" "6.4.9" - "@storybook/ui" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/core-client" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/node-logger" "6.4.18" + "@storybook/theming" "6.4.18" + "@storybook/ui" "6.4.18" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.0.0" @@ -4155,20 +4275,20 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/manager-webpack5@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.9.tgz#dc553eeeade3de5da8832fed0da94ff2803dc089" - integrity sha512-WJfHs9nPAWx6NONzwoo4s72fqWW/HIBnw+StpRVMNJfi9YojTTFNGMHU0waSd22qT0zOV8XXrXi7XDuHissIwg== +"@storybook/manager-webpack5@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.18.tgz#b8ec804e4a7b765ee40e0c728e6eebd76954611b" + integrity sha512-F3usxo5GKDbs+zMtiJsPFLvcJKteB6bp8sy6lK+++tFJWhlGaiebAE8pOghv7/LuEFzo1HS2NXcinb+9fG8hYA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.9" - "@storybook/core-client" "6.4.9" - "@storybook/core-common" "6.4.9" - "@storybook/node-logger" "6.4.9" - "@storybook/theming" "6.4.9" - "@storybook/ui" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/core-client" "6.4.18" + "@storybook/core-common" "6.4.18" + "@storybook/node-logger" "6.4.18" + "@storybook/theming" "6.4.18" + "@storybook/ui" "6.4.18" "@types/node" "^14.0.10" babel-loader "^8.0.0" case-sensitive-paths-webpack-plugin "^2.3.0" @@ -4181,6 +4301,7 @@ fs-extra "^9.0.1" html-webpack-plugin "^5.0.0" node-fetch "^2.6.1" + process "^0.11.10" read-pkg-up "^7.0.1" regenerator-runtime "^0.13.7" resolve-from "^5.0.0" @@ -4193,10 +4314,10 @@ webpack-dev-middleware "^4.1.0" webpack-virtual-modules "^0.4.1" -"@storybook/node-logger@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.9.tgz#7c28f16f5c61feda8f45fa2c06000ebb87b57df7" - integrity sha512-giil8dA85poH+nslKHIS9tSxp4MP4ensOec7el6GwKiqzAQXITrm3b7gw61ETj39jAQeLIcQYGHLq1oqQo4/YQ== +"@storybook/node-logger@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.18.tgz#8759761ba7526b2fa03a1a08fe82d6d892d7a072" + integrity sha512-wY1qt4XOXtJJdQ+DrO3RijtiwVFqWuWetvCY4RV4lge5yk0FP5Q+MTpmjazYodAvGPUIP0LK9bvEDLwXa0JUfw== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.1.0" @@ -4204,24 +4325,24 @@ npmlog "^5.0.1" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.9.tgz#7b011a2e188bcc54180b16d06f21c9d52a5324ac" - integrity sha512-LNI5ku+Q4DI7DD3Y8liYVgGPasp8r/5gzNLSJZ1ad03OW/mASjhSsOKp2eD8Jxud2T5JDe3/yKH9u/LP6SepBQ== +"@storybook/postinstall@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.18.tgz#e94350471fa3df98215ad3c8f3d0574a3a0a8e04" + integrity sha512-eS91pFvnuC1rFXMhDj3smXJ1OTwt2K5HS1+QtWi3NuE4XRvtdwDA/wZ4KQJWZszWuY/k2HgFfJYbQEumJxVrCQ== dependencies: core-js "^3.8.2" -"@storybook/preview-web@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.9.tgz#21f7d251af0de64ae796834ead08ae4ed67e6456" - integrity sha512-fMB/akK14oc+4FBkeVJBtZQdxikOraXQSVn6zoVR93WVDR7JVeV+oz8rxjuK3n6ZEWN87iKH946k4jLoZ95tdw== +"@storybook/preview-web@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.18.tgz#47c908bf27d2089ccf3296c376a6f5b1e8674b5a" + integrity sha512-0x64uLdGhIOk9hIuRKTHFdP7+iEHyjAOi5U4jtwqFfDtG4n4zxEGSsUWij7pTR2rAYf7g2NWIbAM7qb1AqqcLQ== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/channel-postmessage" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/channel-postmessage" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.9" + "@storybook/store" "6.4.18" ansi-to-html "^0.6.11" core-js "^3.8.2" global "^4.4.0" @@ -4233,12 +4354,12 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/router@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.9.tgz#7cc3f85494f4e14d38925e2802145df69a071201" - integrity sha512-GT2KtVHo/mBjxDBFB5ZtVJVf8vC+3p5kRlQC4jao68caVp7H24ikPOkcY54VnQwwe4A1aXpGbJXUyTisEPFlhQ== +"@storybook/router@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.18.tgz#8803dd78277f8602d6c11dae56f6229474dfa54c" + integrity sha512-itvSWHhG1X/NV1sMlwP1qKtF0HfiIaAHImr0LwQ2K2F6/CI11W68dJAs4WBUdwzA0+H0Joyu/2a/6mCQHcee1A== dependencies: - "@storybook/client-logger" "6.4.9" + "@storybook/client-logger" "6.4.18" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -4258,30 +4379,30 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.9.tgz#918fe93e4bd52622a664398db79d5f71b384ce0b" - integrity sha512-J/Jpcc15hnWa2DB/EZ4gVJvdsY3b3CDIGW/NahuNXk36neS+g4lF3qqVNAEqQ1pPZ0O8gMgazyZPGm0MHwUWlw== +"@storybook/source-loader@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.18.tgz#205423e56f7da752d64a0695f2b22ed94378e5d0" + integrity sha512-sjKvngCCYDbBwjjFTjAXO6VsAzKkjy+UctseeULXxEN3cKIsz/R3y7MrrN9yBrwyYcn0k3pqa9d9e3gE+Jv2Tw== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/client-logger" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/client-logger" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" estraverse "^5.2.0" global "^4.4.0" loader-utils "^2.0.0" lodash "^4.17.21" - prettier "^2.2.1" + prettier ">=2.2.1 <=2.3.0" regenerator-runtime "^0.13.7" -"@storybook/store@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.9.tgz#613c6f13271276837c6a603a16199d2abf90153e" - integrity sha512-H30KfiM2XyGMJcLaOepCEUsU7S3C/f7t46s6Nhw0lc5w/6HTQc2jGV3GgG3lUAUAzEQoxmmu61w3N2a6eyRzmg== +"@storybook/store@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.18.tgz#3b693c9d5555d5cfc04e2318e104746d9d55ad66" + integrity sha512-Vl5oCs/9fP1gUgfgMHTBsnYbwAAoaR93/bzDBeOHI3eo5x9uzzJtA4zcRmEiKahR/wgwGacpWy90JrIX469PDQ== dependencies: - "@storybook/addons" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/core-events" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/core-events" "6.4.18" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" fast-deep-equal "^3.1.3" @@ -4295,15 +4416,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/theming@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.9.tgz#8ece44007500b9a592e71eca693fbeac90803b0d" - integrity sha512-Do6GH6nKjxfnBg6djcIYAjss5FW9SRKASKxLYxX2RyWJBpz0m/8GfcGcRyORy0yFTk6jByA3Hs+WFH3GnEbWkw== +"@storybook/theming@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.18.tgz#05365cc1d3dab5d71b80a82928fc5188106a0ed6" + integrity sha512-1o0w2eP+8sXUesdtXpZR4Yvayp1h3xvK7l9+wuHh+1uCy+EvD5UI9d1HvU5kt5fw7XAJJcInaVAmyAbpwct0TQ== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.27" - "@storybook/client-logger" "6.4.9" + "@storybook/client-logger" "6.4.18" core-js "^3.8.2" deep-object-diff "^1.1.0" emotion-theming "^10.0.27" @@ -4313,21 +4434,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.4.9": - version "6.4.9" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.9.tgz#c01413ca919ede20f84d19e556bf93dd2e7c5110" - integrity sha512-lJbsaMTH4SyhqUTmt+msSYI6fKSSfOnrzZVu6bQ73+MfRyGKh1ki2Nyhh+w8BiGEIOz02WlEpZC0y11FfgEgXw== +"@storybook/ui@6.4.18": + version "6.4.18" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.18.tgz#3ceaf6b317f8f2c1d7d1cdc49daaac7eaf10af6b" + integrity sha512-f2ckcLvEyA9CRcu6W2I2CyEbUnU4j3h5Nz0N40YZ2uRMVNQY2xPywAFZVySZIJAaum/5phDfnOD0Feap/Q6zVQ== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.4.9" - "@storybook/api" "6.4.9" - "@storybook/channels" "6.4.9" - "@storybook/client-logger" "6.4.9" - "@storybook/components" "6.4.9" - "@storybook/core-events" "6.4.9" - "@storybook/router" "6.4.9" + "@storybook/addons" "6.4.18" + "@storybook/api" "6.4.18" + "@storybook/channels" "6.4.18" + "@storybook/client-logger" "6.4.18" + "@storybook/components" "6.4.18" + "@storybook/core-events" "6.4.18" + "@storybook/router" "6.4.18" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.9" + "@storybook/theming" "6.4.18" copy-to-clipboard "^3.3.1" core-js "^3.8.2" core-js-pure "^3.8.2" @@ -4352,11 +4473,127 @@ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.22.0.tgz#9d3d2f0a1ce81f185ec477fd7cc67544b2b2a00c" integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg== +"@swc-node/core@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.8.2.tgz#950ad394a8e8385658e6a951ec554bbf61a1693e" + integrity sha512-IoJ7tGHQ6JOMSmFe4VhP64uLmFKMNasS0QEgUrLFQ0h/dTvpQMynnoGBEJoPL6LfsebZ/q4uKqbpWrth6/yrAA== + dependencies: + "@swc/core" "^1.2.119" + +"@swc-node/register@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@swc-node/register/-/register-1.4.2.tgz#98801cc5ad8792519511bd6ae31c01f40aa487a3" + integrity sha512-wLZz0J7BTO//1Eq7e4eBQjKF380Hr2eVemz849msQSKcVM1D7UJUt/dP2TinEVGx++/BXJ/0q37i6n9Iw0EM0w== + dependencies: + "@swc-node/core" "^1.8.2" + "@swc-node/sourcemap-support" "^0.1.11" + chalk "4" + debug "^4.3.3" + pirates "^4.0.4" + tslib "^2.3.1" + typescript "^4.5.3" + +"@swc-node/sourcemap-support@^0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@swc-node/sourcemap-support/-/sourcemap-support-0.1.11.tgz#50cda396baade0636e8f53596b7a66386490c06d" + integrity sha512-b+Mn3oQl+7nUSt7hPzIbY9B30YhcFo1PT4kd9P4QmD6raycmIealOAhAdZID/JevphzsOXHQB4OqJm7Yi5tMcA== + dependencies: + source-map-support "^0.5.21" + +"@swc/core-android-arm-eabi@1.2.138": + version "1.2.138" + 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-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-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-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-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-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": + 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": + 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": + 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": + 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-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-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-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@^1.2.119": + version "1.2.138" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.138.tgz#e54d8488094f7f90cb00455cb0380693c0935865" + integrity sha512-XMbpq6y2BiTju5KCtveM3h32Ma3chGm/fQEjErZmWNOcPIpupGLPosSU1bH35Udee4GHNJH3NfkZIDR0cjHWIg== + optionalDependencies: + "@swc/core-android-arm-eabi" "1.2.138" + "@swc/core-android-arm64" "1.2.138" + "@swc/core-darwin-arm64" "1.2.138" + "@swc/core-darwin-x64" "1.2.138" + "@swc/core-freebsd-x64" "1.2.138" + "@swc/core-linux-arm-gnueabihf" "1.2.138" + "@swc/core-linux-arm64-gnu" "1.2.138" + "@swc/core-linux-arm64-musl" "1.2.138" + "@swc/core-linux-x64-gnu" "1.2.138" + "@swc/core-linux-x64-musl" "1.2.138" + "@swc/core-win32-arm64-msvc" "1.2.138" + "@swc/core-win32-ia32-msvc" "1.2.138" + "@swc/core-win32-x64-msvc" "1.2.138" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/asn1js@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.2.tgz#bb1992291381b5f06e22a829f2ae009267cdf8c5" @@ -4408,6 +4645,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + "@types/cache-manager@3.4.2": version "3.4.2" resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.2.tgz#d57e7e5e6374d1037bdce753a05c9703e4483401" @@ -4432,6 +4676,14 @@ dependencies: "@types/color-convert" "*" +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -4460,6 +4712,15 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/express-serve-static-core@*": + version "4.17.28" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/express-serve-static-core@^4.17.18": version "4.17.24" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" @@ -4732,6 +4993,13 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + "@types/serve-static@*": version "1.13.10" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" @@ -4750,6 +5018,13 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -4796,15 +5071,6 @@ "@types/source-list-map" "*" source-map "^0.7.3" -"@types/webpack-sources@^0.1.5": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.9.tgz#da69b06eb34f6432e6658acb5a6893c55d983920" - integrity sha512-bvzMnzqoK16PQIC8AYHNdW45eREJQMd6WG/msQWX5V2+vZmODCOPb4TJcbgRljTZZTwTM4wUMcsI8FftNA7new== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.6.1" - "@types/webpack@^4.41.26", "@types/webpack@^4.41.8": version "4.41.30" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.30.tgz#fd3db6d0d41e145a8eeeafcd3c4a7ccde9068ddc" @@ -4817,6 +5083,13 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/ws@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -4874,17 +5147,12 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/experimental-utils@~5.3.0": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.1.tgz#bbd8f9b67b4d5fdcb9d2f90297d8fcda22561e05" - integrity sha512-RgFn5asjZ5daUhbK5Sp0peq0SSMytqcrkNfU4pnDma2D8P3ElZ6JbYjY8IMSFfZAJ0f3x3tnO3vXHweYg0g59w== +"@typescript-eslint/experimental-utils@~5.10.0": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.10.2.tgz#dbb541e2070c7bd6e63d3e3a55b58be73a8fbb34" + integrity sha512-stRnIlxDduzxtaVLtEohESoXI1k7J6jvJHGyIkOT2pvXbg5whPM6f9tzJ51bJJxaJTdmvwgVFDNCopFRb2F5Gw== dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.3.1" - "@typescript-eslint/types" "5.3.1" - "@typescript-eslint/typescript-estree" "5.3.1" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + "@typescript-eslint/utils" "5.10.2" "@typescript-eslint/parser@5.4.0": version "5.4.0" @@ -4896,6 +5164,14 @@ "@typescript-eslint/typescript-estree" "5.4.0" debug "^4.3.2" +"@typescript-eslint/scope-manager@5.10.2": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz#92c0bc935ec00f3d8638cdffb3d0e70c9b879639" + integrity sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw== + dependencies: + "@typescript-eslint/types" "5.10.2" + "@typescript-eslint/visitor-keys" "5.10.2" + "@typescript-eslint/scope-manager@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.0.tgz#97d0ccc7c9158e89e202d5e24ce6ba49052d432e" @@ -4904,14 +5180,6 @@ "@typescript-eslint/types" "5.3.0" "@typescript-eslint/visitor-keys" "5.3.0" -"@typescript-eslint/scope-manager@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.1.tgz#3cfbfbcf5488fb2a9a6fbbe97963ee1e8d419269" - integrity sha512-XksFVBgAq0Y9H40BDbuPOTUIp7dn4u8oOuhcgGq7EoDP50eqcafkMVGrypyVGvDYHzjhdUCUwuwVUK4JhkMAMg== - dependencies: - "@typescript-eslint/types" "5.3.1" - "@typescript-eslint/visitor-keys" "5.3.1" - "@typescript-eslint/scope-manager@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz#aaab08415f4a9cf32b870c7750ae8ba4607126a1" @@ -4920,41 +5188,41 @@ "@typescript-eslint/types" "5.4.0" "@typescript-eslint/visitor-keys" "5.4.0" +"@typescript-eslint/types@5.10.2": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.2.tgz#604d15d795c4601fffba6ecb4587ff9fdec68ce8" + integrity sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w== + "@typescript-eslint/types@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416" integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg== -"@typescript-eslint/types@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.1.tgz#afaa715b69ebfcfde3af8b0403bf27527912f9b7" - integrity sha512-bG7HeBLolxKHtdHG54Uac750eXuQQPpdJfCYuw4ZI3bZ7+GgKClMWM8jExBtp7NSP4m8PmLRM8+lhzkYnSmSxQ== - "@typescript-eslint/types@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.4.0.tgz#b1c130f4b381b77bec19696c6e3366f9781ce8f2" integrity sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA== -"@typescript-eslint/typescript-estree@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf" - integrity sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug== +"@typescript-eslint/typescript-estree@5.10.2": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz#810906056cd3ddcb35aa333fdbbef3713b0fe4a7" + integrity sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ== dependencies: - "@typescript-eslint/types" "5.3.0" - "@typescript-eslint/visitor-keys" "5.3.0" + "@typescript-eslint/types" "5.10.2" + "@typescript-eslint/visitor-keys" "5.10.2" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.1.tgz#50cc4bfb93dc31bc75e08ae52e29fcb786d606ec" - integrity sha512-PwFbh/PKDVo/Wct6N3w+E4rLZxUDgsoII/GrWM2A62ETOzJd4M6s0Mu7w4CWsZraTbaC5UQI+dLeyOIFF1PquQ== +"@typescript-eslint/typescript-estree@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf" + integrity sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug== dependencies: - "@typescript-eslint/types" "5.3.1" - "@typescript-eslint/visitor-keys" "5.3.1" + "@typescript-eslint/types" "5.3.0" + "@typescript-eslint/visitor-keys" "5.3.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" @@ -4974,6 +5242,26 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/utils@5.10.2": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.2.tgz#1fcd37547c32c648ab11aea7173ec30060ee87a8" + integrity sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.10.2" + "@typescript-eslint/types" "5.10.2" + "@typescript-eslint/typescript-estree" "5.10.2" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.10.2": + version "5.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz#fdbf272d8e61c045d865bd6c8b41bea73d222f3d" + integrity sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q== + dependencies: + "@typescript-eslint/types" "5.10.2" + eslint-visitor-keys "^3.0.0" + "@typescript-eslint/visitor-keys@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz#a6258790f3b7b2547f70ed8d4a1e0c3499994523" @@ -4982,14 +5270,6 @@ "@typescript-eslint/types" "5.3.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.1.tgz#c2860ff22939352db4f3806f34b21d8ad00588ba" - integrity sha512-3cHUzUuVTuNHx0Gjjt5pEHa87+lzyqOiHXy/Gz+SJOCW1mpw9xQHIIEwnKn+Thph1mgWyZ90nboOcSuZr/jTTQ== - dependencies: - "@typescript-eslint/types" "5.3.1" - eslint-visitor-keys "^3.0.0" - "@typescript-eslint/visitor-keys@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz#09bc28efd3621f292fe88c86eef3bf4893364c8c" @@ -5347,7 +5627,7 @@ acorn@^8.6.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== -address@1.1.2, address@^1.0.1: +address@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== @@ -5376,6 +5656,15 @@ agentkeepalive@^4.1.3: depd "^1.1.2" humanize-ms "^1.2.1" +agentkeepalive@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.0.tgz#616ce94ccb41d1a39a45d203d8076fe98713062d" + integrity sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -5441,10 +5730,10 @@ ajv@8.6.3: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@8.8.2, ajv@^8.8.0: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb" - integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw== +ajv@8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" + integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -5471,6 +5760,16 @@ ajv@^8.0.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb" + integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + alphavantage@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/alphavantage/-/alphavantage-2.2.0.tgz#a07829c91bdc089cfe84a521aff389673a652ac7" @@ -5885,7 +6184,19 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.6.1, autoprefixer@^9.8.6: +autoprefixer@^10.4.2: + version "10.4.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" + integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== + dependencies: + browserslist "^4.19.1" + caniuse-lite "^1.0.30001297" + fraction.js "^4.1.2" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== @@ -6080,13 +6391,13 @@ babel-plugin-polyfill-corejs3@^0.2.2: "@babel/helper-define-polyfill-provider" "^0.2.2" core-js-compat "^3.14.0" -babel-plugin-polyfill-corejs3@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz#0b571f4cf3d67f911512f5c04842a7b8e8263087" - integrity sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw== +babel-plugin-polyfill-corejs3@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.0" - core-js-compat "^3.18.0" + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.21.0" babel-plugin-polyfill-regenerator@^0.2.2: version "0.2.2" @@ -6419,17 +6730,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.14.2: - version "4.14.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.2.tgz#1b3cec458a1ba87588cc5e9be62f19b6d48813ce" - integrity sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw== - dependencies: - caniuse-lite "^1.0.30001125" - electron-to-chromium "^1.3.564" - escalade "^3.0.2" - node-releases "^1.1.61" - -browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.16.8, browserslist@^4.6.4, browserslist@^4.9.1: +browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.16.8, browserslist@^4.9.1: version "4.17.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c" integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g== @@ -6556,7 +6857,7 @@ bytesish@^0.4.1: resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== -cacache@15.3.0, cacache@^15.0.5, cacache@^15.2.0: +cacache@15.3.0, cacache@^15.0.5, cacache@^15.2.0, cacache@^15.3.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== @@ -6697,7 +6998,7 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001254: +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001254: version "1.0.30001254" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001254.tgz#974d45e8b7f6e3b63d4b1435e97752717612d4b9" integrity sha512-GxeHOvR0LFMYPmFGA+NiTOt9uwYDxB3h154tW2yBYwfz2EMX3i1IBgr6gmJGfU0K8KQsqPa5XqLD8zVdP5lUzA== @@ -6712,10 +7013,10 @@ caniuse-lite@^1.0.30001286: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz#4849f27b101fd59ddee3751598c663801032533d" integrity sha512-LiMlrs1nSKZ8qkNhpUf5KD0Al1KCBE3zaT7OLOwEkagXMEDij98SiOovn9wxVGQpklk9vVC/pUSqgYmkmKOS8g== -canonical-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" - integrity sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg== +caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001299: + version "1.0.30001311" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz#682ef3f4e617f1a177ad943de59775ed3032e511" + integrity sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A== capture-exit@^2.0.0: version "2.0.0" @@ -6747,14 +7048,13 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chalk@2.4.2, chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== +chalk@4, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + ansi-styles "^4.1.0" + supports-color "^7.1.0" chalk@4.1.0: version "4.1.0" @@ -6775,13 +7075,14 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" char-regex@^1.0.2: version "1.0.2" @@ -6974,6 +7275,13 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cli-cursor@3.1.0, cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" @@ -6988,19 +7296,26 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" +cli-spinners@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== -cli-table3@0.6.0, cli-table3@~0.6.0: +cli-table3@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== + dependencies: + string-width "^4.2.0" + optionalDependencies: + colors "1.4.0" + +cli-table3@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== @@ -7177,7 +7492,7 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== -colors@^1.1.2: +colors@1.4.0, colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -7364,10 +7679,10 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" -copy-webpack-plugin@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.0.0.tgz#f25a29ca2398a6ca31183b62e76adacb53b981d1" - integrity sha512-tuCVuFMBbRsb7IH0q1CUb50/Skv+7a6c7DJ+xi4fAbOzNLTYVMUTPnf8uGvKPtmqTvzYBrfEFo7YgP4TsUWmtg== +copy-webpack-plugin@10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.1.tgz#115a41f913070ac236a1b576066204cbf35341a1" + integrity sha512-nr81NhCAIpAWXGCK5thrKmfCQ6GDY0L5RN0U+BnIn/7Us55+UCex5ANNsNKmIVtDRnk0Ecf+/kzp9SUVrrBMLg== dependencies: fast-glob "^3.2.7" glob-parent "^6.0.1" @@ -7396,10 +7711,10 @@ core-js-compat@^3.14.0, core-js-compat@^3.16.0, core-js-compat@^3.8.1: browserslist "^4.16.8" semver "7.0.0" -core-js-compat@^3.18.0, core-js-compat@^3.19.1: - version "3.20.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.20.1.tgz#96917b4db634fbbbc7b36575b2e8fcbf7e4f9691" - integrity sha512-AVhKZNpqMV3Jz8hU0YEXXE06qoxtQGsAqU0u1neUngz5IusDJRX/ZJ6t3i7mS7QxNyEONbCo14GprkBrxPlTZA== +core-js-compat@^3.20.2, core-js-compat@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.0.tgz#bcc86aa5a589cee358e7a7fa0a4979d5a76c3885" + integrity sha512-OSXseNPSK2OPJa6GdtkMz/XxeXx8/CJvfhQWTqd6neuUraujcL4jVsjkLQz1OWnax8xVQJnRPe0V2jqNWORA+A== dependencies: browserslist "^4.19.1" semver "7.0.0" @@ -7409,10 +7724,10 @@ core-js-pure@^3.16.0, core-js-pure@^3.8.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.17.2.tgz#ba6311b6aa1e2f2adeba4ac6ec51a9ff40bdc1af" integrity sha512-2VV7DlIbooyTI7Bh+yzOOWL9tGwLnQKHno7qATE+fqZzDKYr6llVjVQOzpD/QLZFgXDPb8T71pJokHEZHEYJhQ== -core-js@3.19.3: - version "3.19.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.3.tgz#6df8142a996337503019ff3235a7022d7cdf4559" - integrity sha512-LeLBMgEGSsG7giquSzvgBrTS7V5UL6ks3eQlUSbN8dJStlLFiRzUm5iqsRyzUB8carhfKjkJ2vzKqE6z1Vga9g== +core-js@3.20.3: + version "3.20.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" + integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: version "3.17.2" @@ -7437,16 +7752,6 @@ cors@2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc" - integrity sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ== - dependencies: - is-directory "^0.3.1" - js-yaml "^3.9.0" - parse-json "^4.0.0" - require-from-string "^2.0.1" - cosmiconfig@^5.0.5: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -7550,13 +7855,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -critters@0.0.15: - version "0.0.15" - resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.15.tgz#b1c8d18fd18e614471733d7d749deac0f386b738" - integrity sha512-AE7hkXb3eZUbEvS1SKZa+OU4o2kUOXtzVeE/2E/mjU/0mV1wpBT1HfUCWVRS4zwvkBNJ0AQYsVjAoFm+kIhfdw== +critters@0.0.16: + version "0.0.16" + resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" + integrity sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A== dependencies: chalk "^4.1.0" - css-select "^4.1.3" + css-select "^4.2.0" parse5 "^6.0.1" parse5-htmlparser2-tree-adapter "^6.0.1" postcss "^8.3.7" @@ -7576,15 +7881,6 @@ cross-fetch@^3.0.5: dependencies: node-fetch "2.6.1" -cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -7596,6 +7892,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -7618,20 +7923,19 @@ cryptocurrencies@7.0.0: resolved "https://registry.yarnpkg.com/cryptocurrencies/-/cryptocurrencies-7.0.0.tgz#4f8fc826a63c14a1294e0b401bb69503506f98a5" integrity sha512-jCA+ykHJg0BAH4eR3T5uhZJ31kp9JrZZqn7Cga4tUgONSPdg4MGLv0SuL272llVXfUgvZZX2lYM4ZjJa/+hjYw== -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== +css-blank-pseudo@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== dependencies: - postcss "^7.0.5" + postcss-selector-parser "^6.0.9" -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== +css-has-pseudo@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" + postcss-selector-parser "^6.0.9" css-loader@6.5.1: version "6.5.1" @@ -7682,12 +7986,10 @@ css-loader@^5.0.1: schema-utils "^3.0.0" semver "^7.3.5" -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" +css-prefers-color-scheme@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== css-select@^4.1.3: version "4.1.3" @@ -7700,6 +8002,17 @@ css-select@^4.1.3: domutils "^2.6.0" nth-check "^2.0.0" +css-select@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" + integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== + dependencies: + boolbase "^1.0.0" + css-what "^5.1.0" + domhandler "^4.3.0" + domutils "^2.8.0" + nth-check "^2.0.1" + css-selector-tokenizer@^0.7.1: version "0.7.3" resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" @@ -7713,6 +8026,11 @@ css-what@^5.0.0, css-what@^5.0.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +css-what@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== + css@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" @@ -7729,15 +8047,10 @@ cssauron@^1.4.0: dependencies: through X.X.X -cssdb@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== +cssdb@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-5.1.0.tgz#ec728d5f5c0811debd0820cbebda505d43003400" + integrity sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw== cssesc@^3.0.0: version "3.0.0" @@ -7865,7 +8178,7 @@ debug@4, debug@4.3.2, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, de dependencies: ms "2.1.2" -debug@4.3.3: +debug@4.3.3, debug@^4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -7879,13 +8192,6 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -8055,14 +8361,6 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -detect-port-alt@1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - detect-port@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" @@ -8191,7 +8489,14 @@ domhandler@^4.0.0, domhandler@^4.1.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: +domhandler@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" + integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -8234,11 +8539,6 @@ downshift@^6.0.15: react-is "^17.0.2" tslib "^2.3.0" -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -8276,7 +8576,7 @@ ejs@^3.1.5: dependencies: jake "^10.6.1" -electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.830: +electron-to-chromium@^1.3.830: version "1.3.830" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.830.tgz#40e3144204f8ca11b2cebec83cf14c20d3499236" integrity sha512-gBN7wNAxV5vl1430dG+XRcQhD4pIeYeak6p6rjdCtlz5wWNwDad8jwvphe5oi1chL5MV6RNRikfffBBiFuj+rQ== @@ -8539,175 +8839,180 @@ esbuild-android-arm64@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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.2.tgz#256b7cf2f9d382a2a92a4ff4e13187587c9b7c6a" - integrity sha512-hEixaKMN3XXCkoe+0WcexO4CcBVU5DCSUT+7P8JZiWZCbAjSkc9b6Yz2X5DSfQmRCtI/cQRU6TfMYrMQ5NBfdw== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.2.tgz#891a59ce6bc3aded0265f982469b3eb9571b92f8" - integrity sha512-Uq8t0cbJQkxkQdbUfOl2wZqZ/AtLZjvJulR1HHnc96UgyzG9YlCLSDMiqjM+NANEy7/zzvwKJsy3iNC9wwqLJA== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.2.tgz#ab834fffa9c612b2901ca1e77e4695d4d8aa63a2" - integrity sha512-619MSa17sr7YCIrUj88KzQu2ESA4jKYtIYfLU/smX6qNgxQt3Y/gzM4s6sgJ4fPQzirvmXgcHv1ZNQAs/Xh48A== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.2.tgz#f7fc87a83f02de27d5a48472571efa1a432ae86d" - integrity sha512-aP6FE/ZsChZpUV6F3HE3x1Pz0paoYXycJ7oLt06g0G9dhJKknPawXCqQg/WMyD+ldCEZfo7F1kavenPdIT/SGQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.2.tgz#bc8758420431106751f3180293cac0b5bc4ce2ee" - integrity sha512-LSm98WTb1QIhyS83+Po0KTpZNdd2XpVpI9ua5rLWqKWbKeNRFwOsjeiuwBaRNc+O32s9oC2ZMefETxHBV6VNkQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.2.tgz#0cc2dcd816d6d66e255bc7aeac139b1d04246812" - integrity sha512-8VxnNEyeUbiGflTKcuVc5JEPTqXfsx2O6ABwUbfS1Hp26lYPRPC7pKQK5Dxa0MBejGc50jy7YZae3EGQUQ8EkQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.2.tgz#c790f739aa75b15c153609ea3457153fbe4db93d" - integrity sha512-4bzMS2dNxOJoFIiHId4w+tqQzdnsch71JJV1qZnbnErSFWcR9lRgpSqWnTTFtv6XM+MvltRzSXC5wQ7AEBY6Hg== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.2.tgz#96858a1f89ad30274dec780d0e3dd8b5691c6b0c" - integrity sha512-RlIVp0RwJrdtasDF1vTFueLYZ8WuFzxoQ1OoRFZOTyJHCGCNgh7xJIC34gd7B7+RT0CzLBB4LcM5n0LS+hIoww== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.2.tgz#03e193225afa9b1215d2ec6efe8edf0c03eeed6f" - integrity sha512-PaylahvMHhH8YMfJPMKEqi64qA0Su+d4FNfHKvlKes/2dUe4QxgbwXT9oLVgy8iJdcFMrO7By4R8fS8S0p8aVQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.2.tgz#972f218d2cb5125237376d40ad60a6e5356a782c" - integrity sha512-Fdwrq2roFnO5oetIiUQQueZ3+5soCxBSJswg3MvYaXDomj47BN6oAWMZgLrFh1oVrtWrxSDLCJBenYdbm2s+qQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.2.tgz#20b71622ac09142b0e523f633af0829def7fed6b" - integrity sha512-vxptskw8JfCDD9QqpRO0XnsM1osuWeRjPaXX1TwdveLogYsbdFtcuiuK/4FxGiNMUr1ojtnCS2rMPbY8puc5NA== +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.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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.2.tgz#dbd6a25117902ef67aa11d8779dd9c6bca7fbe82" - integrity sha512-I8+LzYK5iSNpspS9eCV9sW67Rj8FgMHimGri4mKiGAmN0pNfx+hFX146rYtzGtewuxKtTsPywWteHx+hPRLDsw== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.2.tgz#3c5f199eed459b2f88865548394c0b77383d9ca4" - integrity sha512-120HgMe9elidWUvM2E6mMf0csrGwx8sYDqUIJugyMy1oHm+/nT08bTAVXuwYG/rkMIqsEO9AlMxuYnwR6En/3Q== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.2.tgz#900a681db6b76c6a7f60fc28d2bfe5b11698641c" - integrity sha512-Q3xcf9Uyfra9UuCFxoLixVvdigo0daZaKJ97TL2KNA4bxRUPK18wwGUk3AxvgDQZpRmg82w9PnkaNYo7a+24ow== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.14.2.tgz#49c59c610a0be48becec87a7d9019d143468f2f9" - integrity sha512-Rs8NjWoo1UdsVjhxT2o6kLCX9Sh65pyd3/h4XeJ3jjQNM6NgL+/CSowuJgvOIjDAXMLXpc6fdGnyZQDil9IUJA== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.2.tgz#61e0ba5bd95b277a55d2b997ac4c04dfe2559220" - integrity sha512-TW7O49tPsrq+N1sW8mb3m24j/iDGa4xzAZH4wHWwoIzgtZAYPKC0hpIhufRRG/LA30bdMChO9pjJZ5mtcybtBQ== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.2.tgz#6ab59ef721ff75c682a1c8ae0570dabb637abddb" - integrity sha512-Rym6ViMNmi1E2QuQMWy0AFAfdY0wGwZD73BnzlsQBX5hZBuy/L+Speh7ucUZ16gwsrMM9v86icZUDrSN/lNBKg== +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.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.2.tgz#aca2a4f83d2f0d1592ad4be832ed0045fc888cda" - integrity sha512-ZrLbhr0vX5Em/P1faMnHucjVVWPS+m3tktAtz93WkMZLmbRJevhiW1y4CbulBd2z0MEdXZ6emDa1zFHq5O5bSA== +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" @@ -8732,30 +9037,31 @@ esbuild@0.13.13: esbuild-windows-64 "0.13.13" esbuild-windows-arm64 "0.13.13" -esbuild@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.2.tgz#9c1e1a652549cc33e44885eea42ea2cc6267edc2" - integrity sha512-l076A6o/PIgcyM24s0dWmDI/b8RQf41uWoJu9I0M71CtW/YSw5T5NUeXxs5lo2tFQD+O4CW4nBHJXx3OY5NpXg== +esbuild@0.14.14: + version "0.14.14" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.14.tgz#3b99f20d628013c3e2ae90e67687e03f1d6eb071" + integrity sha512-aiK4ddv+uui0k52OqSHu4xxu+SzOim7Rlz4i25pMEiC8rlnGU0HJ9r+ZMfdWL5bzifg+nhnn7x4NSWTeehYblg== optionalDependencies: - esbuild-android-arm64 "0.14.2" - esbuild-darwin-64 "0.14.2" - esbuild-darwin-arm64 "0.14.2" - esbuild-freebsd-64 "0.14.2" - esbuild-freebsd-arm64 "0.14.2" - esbuild-linux-32 "0.14.2" - esbuild-linux-64 "0.14.2" - esbuild-linux-arm "0.14.2" - esbuild-linux-arm64 "0.14.2" - esbuild-linux-mips64le "0.14.2" - esbuild-linux-ppc64le "0.14.2" - esbuild-netbsd-64 "0.14.2" - esbuild-openbsd-64 "0.14.2" - esbuild-sunos-64 "0.14.2" - esbuild-windows-32 "0.14.2" - esbuild-windows-64 "0.14.2" - esbuild-windows-arm64 "0.14.2" - -escalade@^3.0.2, escalade@^3.1.1: + esbuild-android-arm64 "0.14.14" + esbuild-darwin-64 "0.14.14" + esbuild-darwin-arm64 "0.14.14" + esbuild-freebsd-64 "0.14.14" + esbuild-freebsd-arm64 "0.14.14" + esbuild-linux-32 "0.14.14" + esbuild-linux-64 "0.14.14" + esbuild-linux-arm "0.14.14" + esbuild-linux-arm64 "0.14.14" + esbuild-linux-mips64le "0.14.14" + esbuild-linux-ppc64le "0.14.14" + esbuild-linux-s390x "0.14.14" + esbuild-netbsd-64 "0.14.14" + esbuild-openbsd-64 "0.14.14" + esbuild-sunos-64 "0.14.14" + esbuild-windows-32 "0.14.14" + esbuild-windows-64 "0.14.14" + esbuild-windows-arm64 "0.14.14" + +escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== @@ -8765,16 +9071,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -8856,14 +9162,6 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978" - integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-scope@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" @@ -8901,50 +9199,6 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== -eslint@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815" - integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw== - dependencies: - "@eslint/eslintrc" "^1.0.4" - "@humanwhocodes/config-array" "^0.6.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^6.0.0" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.0.0" - espree "^9.0.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.2.0" - semver "^7.2.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - eslint@8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.3.0.tgz#a3c2409507403c1c7f6c42926111d6cbefbc3e85" @@ -9403,6 +9657,13 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== +figures@3.2.0, figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -9418,13 +9679,6 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" @@ -9473,11 +9727,6 @@ filelist@^1.0.1: dependencies: minimatch "^3.0.4" -filesize@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" - integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== - fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -9531,24 +9780,11 @@ find-line-column@^0.5.2: resolved "https://registry.yarnpkg.com/find-line-column/-/find-line-column-0.5.2.tgz#db00238ff868551a182e74a103416d295a98c8ca" integrity sha1-2wAjj/hoVRoYLnShA0FtKVqYyMo= -find-parent-dir@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.1.tgz#c5c385b96858c3351f95d446cab866cbf9f11125" - integrity sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A== - find-root@^1.0.0, find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@4.1.0, find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -9563,6 +9799,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -9603,11 +9847,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -9641,19 +9880,6 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -fork-ts-checker-webpack-plugin@4.1.6, fork-ts-checker-webpack-plugin@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" - integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== - dependencies: - "@babel/code-frame" "^7.5.5" - chalk "^2.4.1" - micromatch "^3.1.10" - minimatch "^3.0.4" - semver "^5.6.0" - tapable "^1.0.0" - worker-rpc "^0.1.0" - fork-ts-checker-webpack-plugin@6.2.10: version "6.2.10" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.2.10.tgz#800ab1fa523c76011a3413bc4e7815e45b63e826" @@ -9673,6 +9899,19 @@ fork-ts-checker-webpack-plugin@6.2.10: semver "^7.3.2" tapable "^1.0.0" +fork-ts-checker-webpack-plugin@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" + integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== + dependencies: + "@babel/code-frame" "^7.5.5" + chalk "^2.4.1" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + worker-rpc "^0.1.0" + fork-ts-checker-webpack-plugin@^6.0.4: version "6.3.3" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.3.3.tgz#73a9d8e1dc5821fa19a3daedc8be7568b095c8ab" @@ -9720,6 +9959,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +fraction.js@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.3.tgz#be65b0f20762ef27e1e793860bc2dfb716e99e65" + integrity sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -10051,22 +10295,6 @@ global-dirs@^2.0.1: dependencies: ini "1.3.7" -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - global@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -10094,18 +10322,6 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" @@ -10180,6 +10396,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.2.9: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + gtoken@^5.0.4: version "5.3.1" resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.1.tgz#c1c2598a826f2b5df7c6bb53d7be6cf6d50c3c78" @@ -10189,14 +10410,6 @@ gtoken@^5.0.4: google-p12-pem "^3.0.3" jws "^4.0.0" -gzip-size@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" - integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== - dependencies: - duplexer "^0.1.1" - pify "^4.0.1" - handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -10609,6 +10822,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-proxy-middleware@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz#7ef3417a479fb7666a571e09966c66a39bd2c15f" @@ -10680,7 +10902,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -10743,11 +10965,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= -immer@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" - integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== - immutable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" @@ -10871,11 +11088,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - infer-owner@^1.0.3, infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -10914,11 +11126,6 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -11140,6 +11347,13 @@ is-core-module@^2.8.0: dependencies: has "^1.0.3" +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -11381,11 +11595,6 @@ is-regex@^1.0.4, is-regex@^1.1.2, is-regex@^1.1.3, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-root@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - is-set@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" @@ -12191,6 +12400,15 @@ jest-worker@^27.2.2, jest-worker@^27.3.1: merge-stream "^2.0.0" supports-color "^8.0.0" +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jest@27.2.3: version "27.2.3" resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.3.tgz#9c2af9ce874a3eb202f83d92fbc1cc61ccc73248" @@ -12210,7 +12428,7 @@ js-string-escape@^1.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.9.0: +js-yaml@^3.13.0, js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -12567,14 +12785,6 @@ libphonenumber-js@^1.9.7: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.26.tgz#888aca88a4019d0b108e45db8e0509d55785ae45" integrity sha512-j0brmDsZLSKJCrcli1HjzHYP29VOf5P/Yz+xxe7WmKTe+0XVnKa0J+UEAaT96D21w1mfLPhlm0ZXBQ13EVF+XQ== -license-webpack-plugin@2.3.15: - version "2.3.15" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.15.tgz#9b33e5c287b6f2367d77aecd4d5916e02898eee8" - integrity sha512-reA0yvwvkkFMRsyqVikTcLGFXmgWKPVXrFaR3tRvAnFoZozM4zvwlNNQxuB5Il6fgTtS7nGkrIPm9xS2KZtu7g== - dependencies: - "@types/webpack-sources" "^0.1.5" - webpack-sources "^1.2.0" - license-webpack-plugin@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.0.tgz#11cf6ac96559f4a987cf55d3d2a33f295ae8f13b" @@ -12582,6 +12792,13 @@ license-webpack-plugin@4.0.0: dependencies: webpack-sources "^3.0.0" +license-webpack-plugin@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.1.tgz#957930fa595f5b65aa0b21bfd2c19644486f3d9f" + integrity sha512-SQum9mg3BgnY5BK+2KYl4W7pk9b26Q8tW2lTsO6tidD0/Ds9ksdXvp3ip2s9LqDjj5gtBMyWRfOPZptWj4PfCg== + dependencies: + webpack-sources "^3.0.0" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -12631,16 +12848,6 @@ listr@^0.14.3: p-map "^2.0.0" rxjs "^6.3.3" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -12834,6 +13041,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.3.1.tgz#7702e80694ec2bf19865567a469f2b081fcf53f5" + integrity sha512-nX1x4qUrKqwbIAhv4s9et4FIUVzNOpeY07bsjGUy8gwJrXH/wScImSQqXErmo/b2jZY2r0mohbLA9zVj7u1cNw== + magic-string@0.25.7, magic-string@^0.25.0: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -12861,7 +13073,29 @@ make-error@1.x, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^9.0.1, make-fetch-happen@^9.1.0: +make-fetch-happen@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.0.2.tgz#0afb38d2f951b17ebc482b0b16c8d77f39dfe389" + integrity sha512-JSFLK53NJP22FL/eAGOyKsWbc2G3v+toPMD7Dq9PJKQCvK0i3t8hGkKxe+3YZzwYa+c0kxRHu7uxH3fvO+rsaA== + dependencies: + agentkeepalive "^4.2.0" + cacache "^15.3.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.3.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^1.4.1" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^6.1.1" + ssri "^8.0.1" + +make-fetch-happen@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== @@ -13005,6 +13239,13 @@ memfs@^3.1.2, memfs@^3.2.2: dependencies: fs-monkey "1.0.3" +memfs@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" + integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== + dependencies: + fs-monkey "1.0.3" + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -13028,11 +13269,6 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" - integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -13137,10 +13373,10 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" -mini-css-extract-plugin@2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef" - integrity sha512-oEIhRucyn1JbT/1tU2BhnwO6ft1jjH1iCX9Gc59WFMg0n5773rQU0oyQ0zzeYFFuBfONaRbQJyGoPtuNseMxjA== +mini-css-extract-plugin@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz#c5c79f9b22ce9b4f164e9492267358dbe35376d9" + integrity sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw== dependencies: schema-utils "^4.0.0" @@ -13173,7 +13409,7 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" -minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: +minipass-fetch@^1.3.2, minipass-fetch@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== @@ -13220,7 +13456,14 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: dependencies: yallist "^4.0.0" -minizlib@^2.0.0, minizlib@^2.1.1: +minipass@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -13259,7 +13502,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5: dependencies: minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -13360,6 +13603,11 @@ nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== +nanoid@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -13396,6 +13644,11 @@ negotiator@0.6.2, negotiator@^0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -13476,6 +13729,11 @@ node-forge@^0.10.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-forge@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" + integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== + node-gyp-build@^4.2.2: version "4.2.3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" @@ -13541,7 +13799,7 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-releases@^1.1.61, node-releases@^1.1.75: +node-releases@^1.1.75: version "1.1.75" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== @@ -13570,7 +13828,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: +normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -13616,7 +13874,7 @@ npm-normalize-package-bin@^1.0.1: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -npm-package-arg@8.1.5, npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2: +npm-package-arg@8.1.5, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: version "8.1.5" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== @@ -13645,32 +13903,17 @@ npm-pick-manifest@6.1.1, npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: npm-package-arg "^8.1.2" semver "^7.3.4" -npm-registry-fetch@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" - integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== +npm-registry-fetch@^12.0.0: + version "12.0.2" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-12.0.2.tgz#ae583bb3c902a60dae43675b5e33b5b1f6159f1e" + integrity sha512-Df5QT3RaJnXYuOwtXBXS9BWs+tHH2olvkCLh6jcR/b/u3DvPMlp3J0TvvYwplPKxHMOwfg287PYih9QqaVFoKA== dependencies: - make-fetch-happen "^9.0.1" - minipass "^3.1.3" - minipass-fetch "^1.3.0" + make-fetch-happen "^10.0.1" + minipass "^3.1.6" + minipass-fetch "^1.4.1" minipass-json-stream "^1.0.1" - minizlib "^2.0.0" - npm-package-arg "^8.0.0" - -npm-run-all@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" - integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== - dependencies: - ansi-styles "^3.2.1" - chalk "^2.4.1" - cross-spawn "^6.0.5" - memorystream "^0.3.1" - minimatch "^3.0.4" - pidtree "^0.3.0" - read-pkg "^3.0.0" - shell-quote "^1.6.1" - string.prototype.padend "^3.0.0" + minizlib "^2.1.2" + npm-package-arg "^8.1.5" npm-run-path@^2.0.0: version "2.0.2" @@ -13713,6 +13956,13 @@ nth-check@^2.0.0: dependencies: boolbase "^1.0.0" +nth-check@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -13728,12 +13978,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.4.1: - version "13.4.1" - resolved "https://registry.yarnpkg.com/nx/-/nx-13.4.1.tgz#e96df3f954fc6c8c67c109c1d773ac3c8195efdf" - integrity sha512-x/fbcY/mg6rHhVho2oWDKPk+ZTHCuNvaBSe959BY7OkPiGaP5yydfXfP+tuRdxrEGIL0ZD+UzsjH/1ITdnZP3w== +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== dependencies: - "@nrwl/cli" "13.4.1" + "@nrwl/cli" "13.8.1" oauth-sign@~0.9.0: version "0.9.0" @@ -13895,7 +14145,7 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@8.4.0, open@^8.0.9: +open@8.4.0, open@^8.0.9, open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== @@ -13904,7 +14154,7 @@ open@8.4.0, open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -open@^7.0.2, open@^7.0.3, open@^7.4.2: +open@^7.0.3: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== @@ -14114,10 +14364,10 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pacote@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-12.0.2.tgz#14ae30a81fe62ec4fc18c071150e6763e932527c" - integrity sha512-Ar3mhjcxhMzk+OVZ8pbnXdb0l8+pimvlsqBGRNkble2NVgyqOGE3yrCGi/lAYq7E7NRDMz89R1Wx5HIMCGgeYg== +pacote@12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-12.0.3.tgz#b6f25868deb810e7e0ddf001be88da2bcaca57c7" + integrity sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow== dependencies: "@npmcli/git" "^2.1.0" "@npmcli/installed-package-contents" "^1.0.6" @@ -14132,7 +14382,7 @@ pacote@12.0.2: npm-package-arg "^8.0.1" npm-packlist "^3.0.0" npm-pick-manifest "^6.0.0" - npm-registry-fetch "^11.0.0" + npm-registry-fetch "^12.0.0" promise-retry "^2.0.1" read-package-json-fast "^2.0.1" rimraf "^3.0.2" @@ -14353,7 +14603,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -14423,11 +14673,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pidtree@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" - integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== - pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -14450,10 +14695,15 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -piscina@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/piscina/-/piscina-3.1.0.tgz#2333636865b6cb69c5a370bbc499a98cabcf3e04" - integrity sha512-KTW4sjsCD34MHrUbx9eAAbuUSpVj407hQSgk/6Epkg0pbRBmv4a3UX7Sr8wxm9xYqQLnsN4mFOjqGDzHAdgKQg== +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +piscina@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/piscina/-/piscina-3.2.0.tgz#f5a1dde0c05567775690cccefe59d9223924d154" + integrity sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA== dependencies: eventemitter-asyncresource "^1.0.0" hdr-histogram-js "^2.0.1" @@ -14489,13 +14739,6 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkg-up@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - pluralize@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" @@ -14529,102 +14772,73 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" - integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^6.0.2" - -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-gray@^5.0.0: +postcss-attribute-case-insensitive@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz#39cbf6babf3ded1e4abf37d09d6eda21c644105c" + integrity sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ== dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" + postcss-selector-parser "^6.0.2" -postcss-color-hex-alpha@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" - integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== +postcss-color-functional-notation@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz#f59ccaeb4ee78f1b32987d43df146109cc743073" + integrity sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ== dependencies: - postcss "^7.0.14" - postcss-values-parser "^2.0.1" + postcss-value-parser "^4.2.0" -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== +postcss-color-hex-alpha@^8.0.2: + version "8.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz#61a0fd151d28b128aa6a8a21a2dad24eebb34d52" + integrity sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw== dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== +postcss-color-rebeccapurple@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz#5d397039424a58a9ca628762eb0b88a61a66e079" + integrity sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" +postcss-custom-media@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz#1be6aff8be7dc9bf1fe014bde3b71b92bb4552f1" + integrity sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g== -postcss-custom-properties@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" - integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== +postcss-custom-properties@^12.1.2: + version "12.1.4" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.4.tgz#e3d8a8000f28094453b836dff5132385f2862285" + integrity sha512-i6AytuTCoDLJkWN/MtAIGriJz3j7UX6bV7Z5t+KgFz+dwZS15/mlTJY1S0kRizlk6ba0V8u8hN50Fz5Nm7tdZw== dependencies: - postcss "^7.0.17" - postcss-values-parser "^2.0.1" + postcss-value-parser "^4.2.0" -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== +postcss-custom-selectors@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz#022839e41fbf71c47ae6e316cb0e6213012df5ef" + integrity sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" + postcss-selector-parser "^6.0.4" -postcss-dir-pseudo-class@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== +postcss-dir-pseudo-class@^6.0.3: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz#9afe49ea631f0cb36fa0076e7c2feb4e7e3f049c" + integrity sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" + postcss-selector-parser "^6.0.9" -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== +postcss-double-position-gradients@^3.0.4: + version "3.0.5" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.0.5.tgz#f6b755e9850bb9816dfbf8fa346d9ce2e8a03848" + integrity sha512-XiZzvdxLOWZwtt/1GgHJYGoD9scog/DD/yI5dcvPrXNdNDEv7T53/6tL7ikl+EM3jcerII5/XIQzd1UHOdTi2w== dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== +postcss-env-function@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.5.tgz#b9614d50abd91e4c88a114644a9766880dabe393" + integrity sha512-gPUJc71ji9XKyl0WSzAalBeEA/89kU+XpffpPxSaaaZ1c48OL36r1Ep5R6+9XAPkIiDlSvVAwP4io12q/vTcvA== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" postcss-flexbugs-fixes@^4.2.1: version "4.2.1" @@ -14633,41 +14847,36 @@ postcss-flexbugs-fixes@^4.2.1: dependencies: postcss "^7.0.26" -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== +postcss-focus-visible@^6.0.3: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== dependencies: - postcss "^7.0.2" + postcss-selector-parser "^6.0.9" -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== +postcss-focus-within@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== dependencies: - postcss "^7.0.2" + postcss-selector-parser "^6.0.9" -postcss-font-variant@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" - integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== - dependencies: - postcss "^7.0.2" +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" +postcss-gap-properties@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz#6401bb2f67d9cf255d677042928a70a915e6ba60" + integrity sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ== -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== +postcss-image-set-function@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz#bcff2794efae778c09441498f40e0c77374870a9" + integrity sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" postcss-import@14.0.2: version "14.0.2" @@ -14678,21 +14887,17 @@ postcss-import@14.0.2: read-cache "^1.0.0" resolve "^1.1.7" -postcss-initial@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" - integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== - dependencies: - postcss "^7.0.2" +postcss-initial@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== +postcss-lab-function@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.0.4.tgz#504747ab2754e046fb01e72779bbb434a05357df" + integrity sha512-TAEW8X/ahMYV33mvLFQARtBPAy1VVJsiR9VVx3Pcbu+zlqQj0EIyJ/Ie1/EwxwIt530CWtEDzzTXBDzfdb+qIQ== dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" postcss-loader@6.2.1: version "6.2.1" @@ -14714,19 +14919,15 @@ postcss-loader@^4.2.0: schema-utils "^3.0.0" semver "^7.3.4" -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== - dependencies: - postcss "^7.0.2" +postcss-logical@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" +postcss-media-minmax@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== postcss-modules-extract-imports@^2.0.0: version "2.0.0" @@ -14789,117 +14990,87 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nesting@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" - integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== - dependencies: - postcss "^7.0.2" - -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== +postcss-nesting@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.2.tgz#2e5f811b3d75602ea18a95dd445bde5297145141" + integrity sha512-dJGmgmsvpzKoVMtDMQQG/T6FSqs6kDtUDirIfl4KnjMCiY9/ETX8jdKyCd20swSRAbUYkaBKV20pxkzxoOXLqQ== dependencies: - postcss "^7.0.2" + postcss-selector-parser "^6.0.8" -postcss-page-break@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" +postcss-overflow-shorthand@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz#ebcfc0483a15bbf1b27fdd9b3c10125372f4cbc2" + integrity sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg== -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== -postcss-preset-env@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" - integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== - dependencies: - autoprefixer "^9.6.1" - browserslist "^4.6.4" - caniuse-lite "^1.0.30000981" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.4.0" - postcss "^7.0.17" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.3" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.8" - postcss-custom-properties "^8.0.11" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== +postcss-place@^7.0.3: + version "7.0.4" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.4.tgz#eb026650b7f769ae57ca4f938c1addd6be2f62c9" + integrity sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" + postcss-value-parser "^4.2.0" -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== +postcss-preset-env@7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz#01b9b6eea0ff16c27a3d514f10105d56363428a6" + integrity sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA== dependencies: - postcss "^7.0.2" - -postcss-selector-matches@^4.0.0: + autoprefixer "^10.4.2" + browserslist "^4.19.1" + caniuse-lite "^1.0.30001299" + css-blank-pseudo "^3.0.2" + css-has-pseudo "^3.0.3" + css-prefers-color-scheme "^6.0.2" + cssdb "^5.0.0" + postcss-attribute-case-insensitive "^5.0.0" + postcss-color-functional-notation "^4.2.1" + postcss-color-hex-alpha "^8.0.2" + postcss-color-rebeccapurple "^7.0.2" + postcss-custom-media "^8.0.0" + postcss-custom-properties "^12.1.2" + postcss-custom-selectors "^6.0.0" + postcss-dir-pseudo-class "^6.0.3" + postcss-double-position-gradients "^3.0.4" + postcss-env-function "^4.0.4" + postcss-focus-visible "^6.0.3" + postcss-focus-within "^5.0.3" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^3.0.2" + postcss-image-set-function "^4.0.4" + postcss-initial "^4.0.1" + postcss-lab-function "^4.0.3" + postcss-logical "^5.0.3" + postcss-media-minmax "^5.0.0" + postcss-nesting "^10.1.2" + postcss-overflow-shorthand "^3.0.2" + postcss-page-break "^3.0.4" + postcss-place "^7.0.3" + postcss-pseudo-class-any-link "^7.0.2" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^5.0.0" + +postcss-pseudo-class-any-link@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz#534eb1dadd9945eb07830dbcc06fb4d5d865b8e0" + integrity sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-replace-overflow-wrap@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-not@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" - integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== -postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: +postcss-selector-not@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz#ac5fc506f7565dd872f82f5314c0f81a05630dc7" + integrity sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ== dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" + balanced-match "^1.0.0" postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.6" @@ -14909,30 +15080,34 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.8, postcss-selector-parser@^6.0.9: + version "6.0.9" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" + integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.4: - version "8.4.4" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.4.tgz#d53d4ec6a75fd62557a66bb41978bf47ff0c2869" - integrity sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q== +postcss@8.4.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== dependencies: nanoid "^3.1.30" picocolors "^1.0.0" source-map-js "^1.0.1" -postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6: version "7.0.36" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== @@ -14941,6 +15116,15 @@ postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0. source-map "^0.6.1" supports-color "^6.1.0" +postcss@^8.2.14: + version "8.4.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" + integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + dependencies: + nanoid "^3.2.0" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postcss@^8.2.15: version "8.3.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" @@ -14974,10 +15158,10 @@ prettier@2.5.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== -prettier@^2.2.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" - integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== +"prettier@>=2.2.1 <=2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" + integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -15091,14 +15275,6 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.17.0-next.0" function-bind "^1.1.1" -prompts@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" - integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - prompts@^2.0.1, prompts@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" @@ -15295,36 +15471,6 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.4.0.tgz#e05e602469f9768234f29c1bad9ec1f4e86145a2" integrity sha512-k7QJXuQGWevr/V8hoMJ1wBW9i2CVhBdDXpBf3jy/AAtzVyYtsFqEAT+y+NOGiSG1cmnGTreqm5EFLXlVaKbPLQ== -react-dev-utils@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" - integrity sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A== - dependencies: - "@babel/code-frame" "7.10.4" - address "1.1.2" - browserslist "4.14.2" - chalk "2.4.2" - cross-spawn "7.0.3" - detect-port-alt "1.1.6" - escape-string-regexp "2.0.0" - filesize "6.1.0" - find-up "4.1.0" - fork-ts-checker-webpack-plugin "4.1.6" - global-modules "2.0.0" - globby "11.0.1" - gzip-size "5.1.1" - immer "8.0.1" - is-root "2.1.0" - loader-utils "2.0.0" - open "^7.0.2" - pkg-up "3.1.0" - prompts "2.4.0" - react-error-overlay "^6.0.9" - recursive-readdir "2.2.2" - shell-quote "1.7.2" - strip-ansi "6.0.0" - text-table "0.2.0" - react-dom@16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" @@ -15352,11 +15498,6 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" -react-error-overlay@^6.0.9: - version "6.0.9" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" - integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== - react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -15487,15 +15628,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" @@ -15554,13 +15686,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -recursive-readdir@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== - dependencies: - minimatch "3.0.4" - redis-commands@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -15602,6 +15727,13 @@ refractor@^3.1.0: parse-entities "^2.0.0" prismjs "~1.24.0" +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -15609,7 +15741,7 @@ regenerate-unicode-properties@^8.2.0: dependencies: regenerate "^1.4.0" -regenerate@^1.4.0: +regenerate@^1.4.0, regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== @@ -15669,11 +15801,28 @@ regexpu-core@^4.7.1: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" +regexpu-core@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + regjsgen@^0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== + regjsparser@^0.6.4: version "0.6.9" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6" @@ -15681,6 +15830,13 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== + dependencies: + jsesc "~0.5.0" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -15844,7 +16000,7 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-from-string@^2.0.1, require-from-string@^2.0.2: +require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== @@ -15886,15 +16042,15 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url-loader@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" - integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== +resolve-url-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== dependencies: adjust-sourcemap-loader "^4.0.0" convert-source-map "^1.7.0" loader-utils "^2.0.0" - postcss "^7.0.35" + postcss "^8.2.14" source-map "0.6.1" resolve-url@^0.2.1: @@ -15907,7 +16063,16 @@ resolve.exports@1.1.0, resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@1.20.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.8.1: +resolve@1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.8.1: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -16104,13 +16269,14 @@ sass-loader@^10.1.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb" - integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw== +sass@1.49.0: + version "1.49.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078" + integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" sax@^1.2.4, sax@~1.2.4: version "1.2.4" @@ -16183,12 +16349,12 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selfsigned@^1.10.11: - version "1.10.11" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9" - integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA== +selfsigned@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" + integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ== dependencies: - node-forge "^0.10.0" + node-forge "^1.2.0" semver-dsl@^1.0.1: version "1.0.1" @@ -16374,11 +16540,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@1.7.2, shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -16487,6 +16648,15 @@ socks-proxy-agent@^6.0.0: debug "^4.3.1" socks "^2.6.1" +socks-proxy-agent@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + socks@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" @@ -16500,6 +16670,11 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-js@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" @@ -16510,14 +16685,14 @@ source-map-js@^1.0.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== -source-map-loader@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049" - integrity sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw== +source-map-loader@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" + integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== dependencies: abab "^2.0.5" - iconv-lite "^0.6.2" - source-map-js "^0.6.2" + iconv-lite "^0.6.3" + source-map-js "^1.0.1" source-map-resolve@^0.5.0: version "0.5.3" @@ -16546,7 +16721,7 @@ source-map-support@0.5.19, source-map-support@^0.5.16, source-map-support@^0.5.1 buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@0.5.21, source-map-support@~0.5.20: +source-map-support@0.5.21, source-map-support@^0.5.21, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -16880,13 +17055,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@6.0.0, strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -16908,6 +17076,13 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -16992,18 +17167,16 @@ stylus-loader@6.2.0: klona "^2.0.4" normalize-path "^3.0.0" -stylus@0.55.0: - version "0.55.0" - resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.55.0.tgz#bd404a36dd93fa87744a9dd2d2b1b8450345e5fc" - integrity sha512-MuzIIVRSbc8XxHH7FjkvWqkIcr1BvoMZoR/oFuAJDlh7VSaNJzrB4uJ38GRQa+mWjLXODAMzeDe0xi9GYbGwnw== +stylus@0.56.0: + version "0.56.0" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.56.0.tgz#13fc85c48082db483c90d2530942fe8b0be988eb" + integrity sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA== dependencies: css "^3.0.0" - debug "~3.1.0" + debug "^4.3.2" glob "^7.1.6" - mkdirp "~1.0.4" safer-buffer "^2.1.2" sax "~1.2.4" - semver "^6.3.0" source-map "^0.7.3" supports-color@^2.0.0: @@ -17047,6 +17220,11 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + svg-pan-zoom@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/svg-pan-zoom/-/svg-pan-zoom-3.6.1.tgz#f880a1bb32d18e9c625d7715350bebc269b450cf" @@ -17185,6 +17363,17 @@ terser-webpack-plugin@^5.0.3, terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" +terser-webpack-plugin@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54" + integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g== + dependencies: + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.2" + terser@5.10.0: version "5.10.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" @@ -17426,7 +17615,7 @@ ts-loader@^9.2.6: micromatch "^4.0.0" semver "^7.3.4" -ts-node@9.1.1, ts-node@^9.1.1, ts-node@~9.1.1: +ts-node@9.1.1, ts-node@~9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== @@ -17443,10 +17632,10 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tsconfig-paths-webpack-plugin@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.4.1.tgz#4f0d7aa7c8258e7f99e0aa9b27c5687693b55eb1" - integrity sha512-HN1aWCPOXLF3dDke1w4z3RfCgmm9yTppg51FMCqZ02p6leKD4JZvvnPZtqhvnQVmoWWaQjbpO93h2WFjRJjQcA== +tsconfig-paths-webpack-plugin@3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz#01aafff59130c04a8c4ebc96a3045c43c376449a" + integrity sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw== dependencies: chalk "^4.1.0" enhanced-resolve "^5.7.0" @@ -17486,7 +17675,7 @@ tslib@2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== -tslib@2.3.1, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0: +tslib@2.3.1, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -17589,10 +17778,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +typescript@4.5.5, typescript@^4.5.3: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== typescript@^3.2.4: version "3.9.10" @@ -17637,6 +17826,11 @@ unicode-canonical-property-names-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + unicode-match-property-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" @@ -17645,16 +17839,34 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + unicode-property-aliases-ecmascript@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + unified@9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8" @@ -17677,11 +17889,6 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -18068,10 +18275,10 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-dev-middleware@5.2.2, webpack-dev-middleware@^5.2.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.2.2.tgz#eb5193faa5479ca1086b9f7bed68b89c731bff62" - integrity sha512-DjZyYrsHhkikAFNvSNKrpnziXukU1EChFAh9j4LAm6ndPLPW8cN0KhM7T+RAiOqsQ6ABfQ8hoKIs9IWMTjov+w== +webpack-dev-middleware@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.0.tgz#8fc02dba6e72e1d373eca361623d84610f27be7c" + integrity sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg== dependencies: colorette "^2.0.10" memfs "^3.2.2" @@ -18102,11 +18309,27 @@ webpack-dev-middleware@^4.1.0: range-parser "^1.2.1" schema-utils "^3.0.0" -webpack-dev-server@4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.6.0.tgz#e8648601c440172d9b6f248d28db98bed335315a" - integrity sha512-oojcBIKvx3Ya7qs1/AVWHDgmP1Xml8rGsEBnSobxU/UJSX1xP1GPM3MwsAnDzvqcVmVki8tV7lbcsjEjk0PtYg== +webpack-dev-middleware@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz#aa079a8dedd7e58bfeab358a9af7dab304cee57f" + integrity sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg== + dependencies: + colorette "^2.0.10" + memfs "^3.4.1" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz#4e995b141ff51fa499906eebc7906f6925d0beaa" + integrity sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q== dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/serve-index" "^1.9.1" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.2.2" ansi-html-community "^0.0.8" bonjour "^3.5.0" chokidar "^3.5.2" @@ -18124,13 +18347,12 @@ webpack-dev-server@4.6.0: p-retry "^4.5.0" portfinder "^1.0.28" schema-utils "^4.0.0" - selfsigned "^1.10.11" + selfsigned "^2.0.0" serve-index "^1.9.1" sockjs "^0.3.21" spdy "^4.0.2" strip-ansi "^7.0.0" - url "^0.11.0" - webpack-dev-middleware "^5.2.1" + webpack-dev-middleware "^5.3.0" ws "^8.1.0" webpack-filter-warnings-plugin@^1.2.1: @@ -18177,7 +18399,7 @@ webpack-node-externals@^3.0.0: resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== -webpack-sources@^1.2.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -18195,10 +18417,15 @@ webpack-sources@^3.2.0: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d" integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw== -webpack-subresource-integrity@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-5.0.0.tgz#8268b9cc1a229a8f8129ca9eeb59cde52185b6b1" - integrity sha512-x9514FpLRydO+UAQ8DY4aLtCjxmdLkuQVcDFN1kGzuusREYJ1B0rzk/iIlWiL6dnvrhEGFj2+UsdxDkP8Z4UKg== +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack-subresource-integrity@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz#8b7606b033c6ccac14e684267cb7fb1f5c2a132a" + integrity sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q== dependencies: typed-assert "^1.0.8" @@ -18243,10 +18470,10 @@ webpack@4: watchpack "^1.7.4" webpack-sources "^1.4.1" -webpack@5.65.0: - version "5.65.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.65.0.tgz#ed2891d9145ba1f0d318e4ea4f89c3fa18e6f9be" - integrity sha512-Q5or2o6EKs7+oKmJo7LaqZaMOlDWQse9Tm5l1WAfU/ujLGN5Pb0SqGeVkN/4bpPmEqEP5RnVhiqsOtWtUVwGRw== +webpack@5.67.0: + version "5.67.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.67.0.tgz#cb43ca2aad5f7cc81c4cd36b626e6b819805dbfd" + integrity sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50" @@ -18262,7 +18489,7 @@ webpack@5.65.0: eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" json-parse-better-errors "^1.0.2" loader-runner "^4.2.0" mime-types "^2.1.27" @@ -18271,7 +18498,7 @@ webpack@5.65.0: tapable "^2.1.1" terser-webpack-plugin "^5.1.3" watchpack "^2.3.1" - webpack-sources "^3.2.2" + webpack-sources "^3.2.3" webpack@^5.58.1: version "5.64.3" @@ -18384,7 +18611,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.2.9, which@^1.3.1: +which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== From fa66cd5bce09079783fc7143ba478cc5ff66f71f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:22:03 +0100 Subject: [PATCH 150/337] Feature/add countries and sectors to position detail dialog (#692) * Add asset and asset sub class * Add countries and sectors to position detail dialog * Update changelog --- CHANGELOG.md | 5 ++ .../portfolio-position-detail.interface.ts | 8 +- .../src/app/portfolio/portfolio.controller.ts | 1 + .../app/portfolio/portfolio.service-new.ts | 22 ++---- .../src/app/portfolio/portfolio.service.ts | 22 ++---- ...orm-data-source-in-response.interceptor.ts | 13 ++++ .../position-detail-dialog.component.ts | 47 ++++++++---- .../position-detail-dialog.html | 76 +++++++++++++++++-- .../position-detail-dialog.module.ts | 2 + libs/ui/src/lib/value/value.component.html | 6 +- libs/ui/src/lib/value/value.component.ts | 21 ++--- 11 files changed, 152 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97df251e5..11d366e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the asset and asset sub class to the position detail dialog +- Added the countries and sectors to the position detail dialog + ### Changed - Upgraded `angular` from version `13.1.2` to `13.2.3` diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index ddc5fbf37..99c7c6911 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,11 +1,8 @@ +import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { OrderWithAccount } from '@ghostfolio/common/types'; -import { AssetClass, AssetSubClass } from '@prisma/client'; export interface PortfolioPositionDetail { - assetClass?: AssetClass; - assetSubClass?: AssetSubClass; averagePrice: number; - currency: string; firstBuyDate: string; grossPerformance: number; grossPerformancePercent: number; @@ -14,12 +11,11 @@ export interface PortfolioPositionDetail { marketPrice: number; maxPrice: number; minPrice: number; - name: string; netPerformance: number; netPerformancePercent: number; orders: OrderWithAccount[]; quantity: number; - symbol: string; + SymbolProfile: EnhancedSymbolProfile; transactionCount: number; value: number; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index db4786527..38a083c75 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -344,6 +344,7 @@ export class PortfolioController { @Get('position/:dataSource/:symbol') @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) @UseGuards(AuthGuard('jwt')) public async getPosition( @Headers('impersonation-id') impersonationId: string, diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 04803d14b..adeea5c91 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -417,7 +417,6 @@ export class PortfolioServiceNew { if (orders.length <= 0) { return { averagePrice: undefined, - currency: undefined, firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, @@ -426,21 +425,20 @@ export class PortfolioServiceNew { marketPrice: undefined, maxPrice: undefined, minPrice: undefined, - name: undefined, netPerformance: undefined, netPerformancePercent: undefined, orders: [], quantity: undefined, - symbol: aSymbol, + SymbolProfile: undefined, transactionCount: undefined, value: undefined }; } - const assetClass = orders[0].SymbolProfile?.assetClass; - const assetSubClass = orders[0].SymbolProfile?.assetSubClass; const positionCurrency = orders[0].currency; - const name = orders[0].SymbolProfile?.name ?? ''; + const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ + aSymbol + ]); const portfolioOrders: PortfolioOrder[] = orders .filter((order) => { @@ -557,18 +555,15 @@ export class PortfolioServiceNew { } return { - assetClass, - assetSubClass, - currency, firstBuyDate, grossPerformance, investment, marketPrice, maxPrice, minPrice, - name, netPerformance, orders, + SymbolProfile, transactionCount, averagePrice: averagePrice.toNumber(), grossPerformancePercent: @@ -576,7 +571,6 @@ export class PortfolioServiceNew { historicalData: historicalDataArray, netPerformancePercent: position.netPerformancePercentage?.toNumber(), quantity: quantity.toNumber(), - symbol: aSymbol, value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice).toNumber(), currency, @@ -621,15 +615,12 @@ export class PortfolioServiceNew { } return { - assetClass, - assetSubClass, marketPrice, maxPrice, minPrice, - name, orders, + SymbolProfile, averagePrice: 0, - currency: currentData[aSymbol]?.currency, firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, @@ -638,7 +629,6 @@ export class PortfolioServiceNew { netPerformance: undefined, netPerformancePercent: undefined, quantity: 0, - symbol: aSymbol, transactionCount: undefined, value: 0 }; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 4b02e4d0a..0a164708c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -405,7 +405,6 @@ export class PortfolioService { if (orders.length <= 0) { return { averagePrice: undefined, - currency: undefined, firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, @@ -414,21 +413,20 @@ export class PortfolioService { marketPrice: undefined, maxPrice: undefined, minPrice: undefined, - name: undefined, netPerformance: undefined, netPerformancePercent: undefined, orders: [], quantity: undefined, - symbol: aSymbol, + SymbolProfile: undefined, transactionCount: undefined, value: undefined }; } - const assetClass = orders[0].SymbolProfile?.assetClass; - const assetSubClass = orders[0].SymbolProfile?.assetSubClass; const positionCurrency = orders[0].currency; - const name = orders[0].SymbolProfile?.name ?? ''; + const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ + aSymbol + ]); const portfolioOrders: PortfolioOrder[] = orders .filter((order) => { @@ -543,25 +541,21 @@ export class PortfolioService { } return { - assetClass, - assetSubClass, - currency, firstBuyDate, grossPerformance, investment, marketPrice, maxPrice, minPrice, - name, netPerformance, orders, + SymbolProfile, transactionCount, averagePrice: averagePrice.toNumber(), grossPerformancePercent: position.grossPerformancePercentage.toNumber(), historicalData: historicalDataArray, netPerformancePercent: position.netPerformancePercentage.toNumber(), quantity: quantity.toNumber(), - symbol: aSymbol, value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice).toNumber(), currency, @@ -606,15 +600,12 @@ export class PortfolioService { } return { - assetClass, - assetSubClass, marketPrice, maxPrice, minPrice, - name, orders, + SymbolProfile, averagePrice: 0, - currency: currentData[aSymbol]?.currency, firstBuyDate: undefined, grossPerformance: undefined, grossPerformancePercent: undefined, @@ -623,7 +614,6 @@ export class PortfolioService { netPerformance: undefined, netPerformancePercent: undefined, quantity: 0, - symbol: aSymbol, transactionCount: undefined, value: 0 }; 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 59e6c0e20..720f02b67 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 @@ -58,12 +58,25 @@ 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); return position; }); } + + if (data.SymbolProfile) { + data.SymbolProfile.dataSource = encodeDataSource( + data.SymbolProfile.dataSource + ); + } } return data; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 02563afba..db4cbf471 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -11,7 +11,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; -import { AssetSubClass } from '@prisma/client'; +import { SymbolProfile } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -26,10 +26,11 @@ import { PositionDetailDialogParams } from './interfaces/interfaces'; styleUrls: ['./position-detail-dialog.component.scss'] }) export class PositionDetailDialog implements OnDestroy, OnInit { - public assetSubClass: AssetSubClass; public averagePrice: number; public benchmarkDataItems: LineChartItem[]; - public currency: string; + public countries: { + [code: string]: { name: string; value: number }; + }; public firstBuyDate: string; public grossPerformance: number; public grossPerformancePercent: number; @@ -38,13 +39,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public marketPrice: number; public maxPrice: number; public minPrice: number; - public name: string; public netPerformance: number; public netPerformancePercent: number; public orders: OrderWithAccount[]; public quantity: number; public quantityPrecision = 2; - public symbol: string; + public sectors: { + [name: string]: { name: string; value: number }; + }; + public SymbolProfile: SymbolProfile; public transactionCount: number; public value: number; @@ -66,9 +69,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ - assetSubClass, averagePrice, - currency, firstBuyDate, grossPerformance, grossPerformancePercent, @@ -77,19 +78,17 @@ export class PositionDetailDialog implements OnDestroy, OnInit { marketPrice, maxPrice, minPrice, - name, netPerformance, netPerformancePercent, orders, quantity, - symbol, + SymbolProfile, transactionCount, value }) => { - this.assetSubClass = assetSubClass; this.averagePrice = averagePrice; this.benchmarkDataItems = []; - this.currency = currency; + this.countries = {}; this.firstBuyDate = firstBuyDate; this.grossPerformance = grossPerformance; this.grossPerformancePercent = grossPerformancePercent; @@ -110,15 +109,33 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.marketPrice = marketPrice; this.maxPrice = maxPrice; this.minPrice = minPrice; - this.name = name; this.netPerformance = netPerformance; this.netPerformancePercent = netPerformancePercent; this.orders = orders; this.quantity = quantity; - this.symbol = symbol; + this.sectors = {}; + this.SymbolProfile = SymbolProfile; this.transactionCount = transactionCount; this.value = value; + if (SymbolProfile?.countries?.length > 0) { + for (const country of SymbolProfile.countries) { + this.countries[country.code] = { + name: country.name, + value: country.weight + }; + } + } + + if (SymbolProfile?.sectors?.length > 0) { + for (const sector of SymbolProfile.sectors) { + this.sectors[sector.name] = { + name: sector.name, + value: sector.weight + }; + } + } + if (isToday(parseISO(this.firstBuyDate))) { // Add average price this.historicalDataItems.push({ @@ -166,7 +183,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { if (Number.isInteger(this.quantity)) { this.quantityPrecision = 0; - } else if (assetSubClass === 'CRYPTOCURRENCY') { + } else if (this.SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') { if (this.quantity < 1) { this.quantityPrecision = 7; } else if (this.quantity < 1000) { @@ -196,7 +213,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { .subscribe((data) => { downloadAsFile( data, - `ghostfolio-export-${this.symbol}-${format( + `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index db8f78bc3..3a4026a14 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2,7 +2,7 @@ mat-dialog-title position="center" [deviceType]="data.deviceType" - [title]="name ?? symbol" + [title]="SymbolProfile?.name ?? SymbolProfile?.symbol" (closeButtonClicked)="onClose()" > @@ -55,7 +55,7 @@ @@ -64,7 +64,7 @@ @@ -73,7 +73,7 @@
+
+ +
+
+ +
+ + +
+ +
+
+ +
+
+ +
+
Countries
+ +
+
+
Sectors
+ +
+
+
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts index 4cd013fd4..72f80f065 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts @@ -6,6 +6,7 @@ import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-foote import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; +import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -20,6 +21,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component'; GfDialogFooterModule, GfDialogHeaderModule, GfLineChartModule, + GfPortfolioProportionChartModule, GfValueModule, MatButtonModule, MatDialogModule, diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index 282a99ca7..e4f8b0f45 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -34,12 +34,12 @@ {{ currency }}
- +
- {{ formattedDate }} + {{ formattedValue | titlecase }}
diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 07fc2b136..29863e9bd 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -5,7 +5,7 @@ import { OnChanges } from '@angular/core'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; -import { format, isDate } from 'date-fns'; +import { format, isDate, parseISO } from 'date-fns'; import { isNumber } from 'lodash'; @Component({ @@ -28,10 +28,9 @@ export class ValueComponent implements OnChanges { @Input() value: number | string = ''; public absoluteValue = 0; - public formattedDate = ''; public formattedValue = ''; - public isDate = false; public isNumber = false; + public isString = false; public useAbsoluteValue = false; public constructor() {} @@ -39,8 +38,8 @@ export class ValueComponent implements OnChanges { public ngOnChanges() { if (this.value || this.value === 0) { if (isNumber(this.value)) { - this.isDate = false; this.isNumber = true; + this.isString = false; this.absoluteValue = Math.abs(this.value); if (this.colorizeSign) { @@ -98,17 +97,19 @@ export class ValueComponent implements OnChanges { this.formattedValue = this.formattedValue.replace(/^-/, ''); } } else { - try { - if (isDate(new Date(this.value))) { - this.isDate = true; - this.isNumber = false; + this.isNumber = false; + this.isString = true; - this.formattedDate = format( + try { + if (isDate(parseISO(this.value))) { + this.formattedValue = format( new Date(this.value), DEFAULT_DATE_FORMAT ); } - } catch {} + } catch { + this.formattedValue = this.value; + } } } From 23da1bd29382c69a00235928d0b7b533ec9ba375 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 13 Feb 2022 18:12:59 +0100 Subject: [PATCH 151/337] Feature/add feature page (#694) * Add feature page * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/app-routing.module.ts | 7 + .../components/header/header.component.html | 11 + .../home-holdings/home-holdings.html | 2 +- .../positions-table.component.html | 2 +- apps/client/src/app/core/auth.guard.ts | 1 + .../src/app/pages/about/about-page.html | 3 +- .../features/features-page-routing.module.ts | 15 ++ .../pages/features/features-page.component.ts | 44 ++++ .../src/app/pages/features/features-page.html | 220 ++++++++++++++++++ .../pages/features/features-page.module.ts | 19 ++ .../src/app/pages/features/features-page.scss | 17 ++ apps/client/src/assets/sitemap.xml | 24 +- 13 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 apps/client/src/app/pages/features/features-page-routing.module.ts create mode 100644 apps/client/src/app/pages/features/features-page.component.ts create mode 100644 apps/client/src/app/pages/features/features-page.html create mode 100644 apps/client/src/app/pages/features/features-page.module.ts create mode 100644 apps/client/src/app/pages/features/features-page.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d366e19..b3f89dfcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a feature overview page - Added the asset and asset sub class to the position detail dialog - Added the countries and sectors to the position detail dialog diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 8fde79cd7..0c6e7c2ab 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -66,6 +66,13 @@ const routes: Routes = [ './pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module' ).then((m) => m.FirstMonthsInOpenSourcePageModule) }, + { + path: 'features', + loadChildren: () => + import('./pages/features/features-page.module').then( + (m) => m.FeaturesPageModule + ) + }, { path: 'home', loadChildren: () => diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index 4aeab5574..493e719f3 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -238,6 +238,17 @@ > + Features Manage Activities...Manage Activities
diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index f6529f55c..38f5110b6 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -139,7 +139,7 @@ class="my-3 text-center" >
diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index b8c629a7c..b6b95e8a6 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -20,6 +20,7 @@ export class AuthGuard implements CanActivate { '/blog', '/de/blog', '/en/blog', + '/features', '/p', '/pricing', '/register', diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index ca3375652..90c1632a6 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -32,7 +32,8 @@

If you encounter a bug or would like to suggest an improvement or a - new feature, please join the Ghostfolio + new feature, please join the + Ghostfolio (); + + /** + * @constructor + */ + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private userService: UserService + ) {} + + /** + * Initializes the controller + */ + public ngOnInit() { + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.changeDetectorRef.markForCheck(); + } + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html new file mode 100644 index 000000000..baa2aa844 --- /dev/null +++ b/apps/client/src/app/pages/features/features-page.html @@ -0,0 +1,220 @@ +

+
+
+

+ Features +

+ + +

+ Check out the numerous features of Ghostfolio to + manage your wealth. +

+
+
+
+
+ +
+

Stocks

+

Keep track of your stock purchases and sales.

+
+
+
+
+ +
+

ETFs

+

+ Are you into ETFs (Exchange Traded Funds)? Track your ETF + investments. +

+
+
+
+
+ +
+

Cryptocurrencies

+

+ Keep track of your Bitcoin and Altcoin holdings. +

+
+
+
+
+ +
+

Dividend

+

+ Are you building a dividend portfolio? Track your dividend in + Ghostfolio. +

+
+
+
+
+ +
+

Wealth Items

+

+ Track all your treasuries, be it your luxury watch or rare + trading cards. +

+
+
+
+
+ +
+

Import and Export

+

Import and export your investment activities.

+
+
+
+
+ +
+

Multi-Accounts

+

+ Keep an eye on all your accounts across multiple platforms + (multi-banking). +

+
+
+
+
+ +
+

+ Portfolio Calculations + +

+

+ Check the rate of return of your portfolio for + Today, YTD, 1Y, + 5Y, and Max. +

+
+
+
+
+ +
+

+ Portfolio Allocations + +

+

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

+
+
+
+
+ +
+

Dark Mode

+

+ Ghostfolio automatically switches to a dark color theme based on + your operating system's preferences. +

+
+
+
+
+ +
+

Zen Mode

+

+ Keep calm and activate Zen Mode if the markets are going crazy. +

+
+
+
+
+
+ +
+

+ Static Analysis + +

+

+ Identify potential risks in your portfolio with Ghostfolio + X-ray, the static portfolio analysis. +

+
+
+
+
+ +
+

Community

+

+ Join the Ghostfolio + Slack channel + full of enthusiastic investors and discuss the latest market + trends. +

+
+
+
+
+ +
+

Open Source Software

+

+ The source code is fully available as + open source software + (OSS) and licensed under the AGPLv3 License. +

+
+
+
+
+
+
+ +
diff --git a/apps/client/src/app/pages/features/features-page.module.ts b/apps/client/src/app/pages/features/features-page.module.ts new file mode 100644 index 000000000..2a4204304 --- /dev/null +++ b/apps/client/src/app/pages/features/features-page.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; + +import { FeaturesPageRoutingModule } from './features-page-routing.module'; +import { FeaturesPageComponent } from './features-page.component'; + +@NgModule({ + declarations: [FeaturesPageComponent], + imports: [ + FeaturesPageRoutingModule, + CommonModule, + MatButtonModule, + MatCardModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class FeaturesPageModule {} diff --git a/apps/client/src/app/pages/features/features-page.scss b/apps/client/src/app/pages/features/features-page.scss new file mode 100644 index 000000000..4a8680714 --- /dev/null +++ b/apps/client/src/app/pages/features/features-page.scss @@ -0,0 +1,17 @@ +:host { + color: rgb(var(--dark-primary-text)); + display: block; + + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; + + &:hover { + color: rgba(var(--palette-primary-300), 1); + } + } +} + +:host-context(.is-dark-theme) { + color: rgb(var(--light-primary-text)); +} diff --git a/apps/client/src/assets/sitemap.xml b/apps/client/src/assets/sitemap.xml index 441fac85a..2801d9167 100644 --- a/apps/client/src/assets/sitemap.xml +++ b/apps/client/src/assets/sitemap.xml @@ -6,42 +6,46 @@ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> https://ghostfol.io - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/about - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/about/changelog - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/blog - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/en/blog/2021/07/hello-ghostfolio - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source - 2022-01-05T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 + + + https://ghostfol.io/features + 2022-02-13T00:00:00+00:00 https://ghostfol.io/pricing - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/register - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 https://ghostfol.io/resources - 2022-01-01T00:00:00+00:00 + 2022-02-13T00:00:00+00:00 From 893ca83d3a415f98e46b0f90579ebb2174bb3931 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 13 Feb 2022 18:14:55 +0100 Subject: [PATCH 152/337] Release 1.115.0 (#695) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f89dfcd..51e4c9e3e 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.115.0 - 13.02.2022 ### Added diff --git a/package.json b/package.json index 10d767ebc..0044211f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.114.1", + "version": "1.115.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 809ee97f6f31546cbdaf7eea04dff1f0a570007a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 14 Feb 2022 18:50:47 +0100 Subject: [PATCH 153/337] Add tests (#693) --- .../portfolio/current-rate.service.mock.ts | 60 + ...o-calculator-new-baln-buy-and-sell.spec.ts | 95 ++ .../portfolio-calculator-new-baln-buy.spec.ts | 84 ++ ...portfolio-calculator-new-no-orders.spec.ts | 56 + .../app/portfolio/portfolio-calculator-new.ts | 1028 ++++++++--------- package.json | 1 + 6 files changed, 810 insertions(+), 514 deletions(-) create mode 100644 apps/api/src/app/portfolio/current-rate.service.mock.ts create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts new file mode 100644 index 000000000..124a27f45 --- /dev/null +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -0,0 +1,60 @@ +import { parseDate, resetHours } from '@ghostfolio/common/helper'; +import { addDays, endOfDay, isBefore, isSameDay } from 'date-fns'; + +import { GetValuesParams } from './interfaces/get-values-params.interface'; + +function mockGetValue(symbol: string, date: Date) { + switch (symbol) { + case 'BALN.SW': + if (isSameDay(parseDate('2021-11-12'), date)) { + return { marketPrice: 146 }; + } else if (isSameDay(parseDate('2021-11-22'), date)) { + return { marketPrice: 142.9 }; + } else if (isSameDay(parseDate('2021-11-26'), date)) { + return { marketPrice: 139.9 }; + } else if (isSameDay(parseDate('2021-11-30'), date)) { + return { marketPrice: 136.6 }; + } else if (isSameDay(parseDate('2021-12-18'), date)) { + return { marketPrice: 148.9 }; + } + + return { marketPrice: 0 }; + + default: + return { marketPrice: 0 }; + } +} + +export const CurrentRateServiceMock = { + getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => { + const result = []; + if (dateQuery.lt) { + for ( + let date = resetHours(dateQuery.gte); + isBefore(date, endOfDay(dateQuery.lt)); + date = addDays(date, 1) + ) { + for (const dataGatheringItem of dataGatheringItems) { + result.push({ + date, + marketPrice: mockGetValue(dataGatheringItem.symbol, date) + .marketPrice, + symbol: dataGatheringItem.symbol + }); + } + } + } else { + for (const date of dateQuery.in) { + for (const dataGatheringItem of dataGatheringItems) { + result.push({ + date, + marketPrice: mockGetValue(dataGatheringItem.symbol, date) + .marketPrice, + symbol: dataGatheringItem.symbol + }); + } + } + } + return Promise.resolve(result); + } +}; 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 new file mode 100644 index 000000000..8906431fb --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts @@ -0,0 +1,95 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculatorNew } from './portfolio-calculator-new'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculatorNew', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy and sell', async () => { + const portfolioCalculatorNew = new PortfolioCalculatorNew({ + currentRateService, + currency: 'CHF', + orders: [ + { + currency: 'CHF', + date: '2021-11-22', + dataSource: 'YAHOO', + fee: new Big(1.55), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'BUY', + unitPrice: new Big(142.9) + }, + { + currency: 'CHF', + date: '2021-11-30', + dataSource: 'YAHOO', + fee: new Big(1.65), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'SELL', + unitPrice: new Big(136.6) + } + ] + }); + + portfolioCalculatorNew.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + + const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + parseDate('2021-11-22') + ); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValue: new Big('0'), + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.0440867739678096571'), + hasErrors: false, + netPerformance: new Big('-15.8'), + netPerformancePercentage: new Big('-0.0552834149755073478'), + positions: [ + { + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + firstBuyDate: '2021-11-22', + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.0440867739678096571'), + investment: new Big('0'), + netPerformance: new Big('-15.8'), + netPerformancePercentage: new Big('-0.0552834149755073478'), + marketPrice: 148.9, + quantity: new Big('0'), + symbol: 'BALN.SW', + transactionCount: 2 + } + ], + totalInvestment: new Big('0') + }); + }); + }); +}); 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 new file mode 100644 index 000000000..230fb04ab --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts @@ -0,0 +1,84 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculatorNew } from './portfolio-calculator-new'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculatorNew', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy', async () => { + const portfolioCalculatorNew = new PortfolioCalculatorNew({ + currentRateService, + currency: 'CHF', + orders: [ + { + currency: 'CHF', + date: '2021-11-30', + dataSource: 'YAHOO', + fee: new Big(1.55), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'BUY', + unitPrice: new Big(136.6) + } + ] + }); + + portfolioCalculatorNew.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + + const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + parseDate('2021-11-30') + ); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValue: new Big('297.8'), + grossPerformance: new Big('24.6'), + grossPerformancePercentage: new Big('0.09004392386530014641'), + hasErrors: false, + netPerformance: new Big('23.05'), + netPerformancePercentage: new Big('0.08437042459736456808'), + positions: [ + { + averagePrice: new Big('136.6'), + currency: 'CHF', + dataSource: 'YAHOO', + firstBuyDate: '2021-11-30', + grossPerformance: new Big('24.6'), + grossPerformancePercentage: new Big('0.09004392386530014641'), + investment: new Big('273.2'), + netPerformance: new Big('23.05'), + netPerformancePercentage: new Big('0.08437042459736456808'), + marketPrice: 148.9, + quantity: new Big('2'), + symbol: 'BALN.SW', + transactionCount: 1 + } + ], + totalInvestment: new Big('273.2') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts new file mode 100644 index 000000000..41e2ca381 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts @@ -0,0 +1,56 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculatorNew } from './portfolio-calculator-new'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculatorNew', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('get current positions', () => { + it('with no orders', async () => { + const portfolioCalculatorNew = new PortfolioCalculatorNew({ + currentRateService, + currency: 'CHF', + orders: [] + }); + + portfolioCalculatorNew.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + + const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + new Date() + ); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValue: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0), + hasErrors: false, + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + positions: [], + totalInvestment: new Big(0) + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index ebf181885..83bbc663f 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -281,608 +281,608 @@ export class PortfolioCalculatorNew { }; } - public getSymbolMetrics({ - marketSymbolMap, - start, - symbol - }: { - marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - }; - start: Date; - symbol: string; - }) { - let orders: PortfolioOrderItem[] = this.orders.filter((order) => { - return order.symbol === symbol; - }); + public getInvestments(): { date: string; investment: Big }[] { + if (this.transactionPoints.length === 0) { + return []; + } - if (orders.length <= 0) { + return this.transactionPoints.map((transactionPoint) => { return { - hasErrors: false, - initialValue: new Big(0), - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0) + date: transactionPoint.date, + investment: transactionPoint.items.reduce( + (investment, transactionPointSymbol) => + investment.plus(transactionPointSymbol.investment), + new Big(0) + ) }; - } - - const dateOfFirstTransaction = new Date(first(orders).date); - const endDate = new Date(Date.now()); - - const unitPriceAtStartDate = - marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; - - const unitPriceAtEndDate = - marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; + }); + } - if ( - !unitPriceAtEndDate || - (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) - ) { + public async calculateTimeline( + timelineSpecification: TimelineSpecification[], + endDate: string + ): Promise { + if (timelineSpecification.length === 0) { return { - hasErrors: true, - initialValue: new Big(0), - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0) + maxNetPerformance: new Big(0), + minNetPerformance: new Big(0), + timelinePeriods: [] }; } - let feesAtStartDate = new Big(0); - let fees = new Big(0); - let grossPerformance = new Big(0); - let grossPerformanceAtStartDate = new Big(0); - let grossPerformanceFromSells = new Big(0); - let initialValue: Big; - let lastAveragePrice = new Big(0); - let lastTransactionInvestment = new Big(0); - let lastValueOfInvestmentBeforeTransaction = new Big(0); - let timeWeightedGrossPerformancePercentage = new Big(1); - let timeWeightedNetPerformancePercentage = new Big(1); - let totalInvestment = 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, - currency: null, - date: format(start, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'start', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtStartDate ?? new Big(0) - }); - - orders.push({ - symbol, - currency: null, - date: format(endDate, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'end', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtEndDate ?? new Big(0) - }); - - // Sort orders so that the start and end placeholder order are at the right - // position - orders = sortBy(orders, (order) => { - let sortIndex = new Date(order.date); - - if (order.itemType === 'start') { - sortIndex = addMilliseconds(sortIndex, -1); - } + const startDate = timelineSpecification[0].start; + const start = parseDate(startDate); + const end = parseDate(endDate); - if (order.itemType === 'end') { - sortIndex = addMilliseconds(sortIndex, 1); + const timelinePeriodPromises: Promise[] = []; + let i = 0; + let j = -1; + for ( + let currentDate = start; + !isAfter(currentDate, end); + currentDate = this.addToDate( + currentDate, + timelineSpecification[i].accuracy + ) + ) { + if (this.isNextItemActive(timelineSpecification, currentDate, i)) { + i++; } - - return sortIndex.getTime(); - }); - - const indexOfStartOrder = orders.findIndex((order) => { - return order.itemType === 'start'; - }); - - for (let i = 0; i < orders.length; i += 1) { - const order = orders[i]; - - const valueOfInvestmentBeforeTransaction = totalUnits.mul( - order.unitPrice - ); - - const transactionInvestment = order.quantity - .mul(order.unitPrice) - .mul(this.getFactor(order.type)); - - if ( - !initialValue && - order.itemType !== 'start' && - order.itemType !== 'end' + while ( + j + 1 < this.transactionPoints.length && + !isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate) ) { - initialValue = transactionInvestment; + j++; } - fees = fees.plus(order.fee); - - totalUnits = totalUnits.plus( - order.quantity.mul(this.getFactor(order.type)) + let periodEndDate = currentDate; + if (timelineSpecification[i].accuracy === 'day') { + let nextEndDate = end; + if (j + 1 < this.transactionPoints.length) { + nextEndDate = parseDate(this.transactionPoints[j + 1].date); + } + periodEndDate = min([ + addMonths(currentDate, 3), + max([currentDate, nextEndDate]) + ]); + } + const timePeriodForDates = this.getTimePeriodForDate( + j, + currentDate, + endOfDay(periodEndDate) ); + currentDate = periodEndDate; + if (timePeriodForDates != null) { + timelinePeriodPromises.push(timePeriodForDates); + } + } - const valueOfInvestment = totalUnits.mul(order.unitPrice); + const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( + timelinePeriodPromises + ); + const minNetPerformance = timelineInfoInterfaces + .map((timelineInfo) => timelineInfo.minNetPerformance) + .filter((performance) => performance !== null) + .reduce((minPerformance, current) => { + if (minPerformance.lt(current)) { + return minPerformance; + } else { + return current; + } + }); - const grossPerformanceFromSell = - order.type === TypeOfOrder.SELL - ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) - : new Big(0); + const maxNetPerformance = timelineInfoInterfaces + .map((timelineInfo) => timelineInfo.maxNetPerformance) + .filter((performance) => performance !== null) + .reduce((maxPerformance, current) => { + if (maxPerformance.gt(current)) { + return maxPerformance; + } else { + return current; + } + }); - grossPerformanceFromSells = grossPerformanceFromSells.plus( - grossPerformanceFromSell - ); + const timelinePeriods = timelineInfoInterfaces.map( + (timelineInfo) => timelineInfo.timelinePeriods + ); - totalInvestment = totalInvestment - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); + return { + maxNetPerformance, + minNetPerformance, + timelinePeriods: flatten(timelinePeriods) + }; + } - lastAveragePrice = totalUnits.eq(0) - ? new Big(0) - : totalInvestment.div(totalUnits); + private calculateOverallPerformance( + positions: TimelinePosition[], + initialValues: { [p: 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 netPerformance = new Big(0); + let netPerformancePercentage = new Big(0); + let completeInitialValue = new Big(0); - const newGrossPerformance = valueOfInvestment - .minus(totalInvestment) - .plus(grossPerformanceFromSells); + for (const currentPosition of positions) { + if (currentPosition.marketPrice) { + currentValue = currentValue.plus( + new Big(currentPosition.marketPrice).mul(currentPosition.quantity) + ); + } 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 ( - i > indexOfStartOrder && - !lastValueOfInvestmentBeforeTransaction - .plus(lastTransactionInvestment) - .eq(0) + currentPosition.grossPerformancePercentage && + initialValues[currentPosition.symbol] ) { - const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction - .minus( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ) - .div( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ); + const currentInitialValue = initialValues[currentPosition.symbol]; + completeInitialValue = completeInitialValue.plus(currentInitialValue); + grossPerformancePercentage = grossPerformancePercentage.plus( + currentPosition.grossPerformancePercentage.mul(currentInitialValue) + ); + netPerformancePercentage = netPerformancePercentage.plus( + currentPosition.netPerformancePercentage.mul(currentInitialValue) + ); + } else if (!currentPosition.quantity.eq(0)) { + Logger.warn( + `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` + ); + hasErrors = true; + } + } - timeWeightedGrossPerformancePercentage = - timeWeightedGrossPerformancePercentage.mul( - new Big(1).plus(grossHoldingPeriodReturn) - ); + if (!completeInitialValue.eq(0)) { + grossPerformancePercentage = + grossPerformancePercentage.div(completeInitialValue); + netPerformancePercentage = + netPerformancePercentage.div(completeInitialValue); + } - const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction - .minus(fees.minus(feesAtStartDate)) - .minus( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ) - .div( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ); + return { + currentValue, + grossPerformance, + grossPerformancePercentage, + hasErrors, + netPerformance, + netPerformancePercentage, + totalInvestment + }; + } - timeWeightedNetPerformancePercentage = - timeWeightedNetPerformancePercentage.mul( - new Big(1).plus(netHoldingPeriodReturn) - ); + private async getTimePeriodForDate( + j: number, + startDate: Date, + endDate: Date + ): Promise { + let investment: Big = new Big(0); + let fees: Big = new Big(0); - holdingPeriodPerformances.push({ - grossReturn: grossHoldingPeriodReturn, - netReturn: netHoldingPeriodReturn, - valueOfInvestment: lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) + const marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + } = {}; + if (j >= 0) { + const currencies: { [name: string]: string } = {}; + const dataGatheringItems: IDataGatheringItem[] = []; + + for (const item of this.transactionPoints[j].items) { + currencies[item.symbol] = item.currency; + dataGatheringItems.push({ + dataSource: item.dataSource, + symbol: item.symbol }); + investment = investment.plus(item.investment); + fees = fees.plus(item.fee); } - grossPerformance = newGrossPerformance; - - lastTransactionInvestment = transactionInvestment; - - lastValueOfInvestmentBeforeTransaction = - valueOfInvestmentBeforeTransaction; + let marketSymbols: GetValueObject[] = []; + if (dataGatheringItems.length > 0) { + try { + marketSymbols = await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + gte: startDate, + lt: endOfDay(endDate) + }, + userCurrency: this.currency + }); + } catch (error) { + Logger.error( + `Failed to fetch info for date ${startDate} with exception`, + error + ); + return null; + } + } - if (order.itemType === 'start') { - feesAtStartDate = fees; - grossPerformanceAtStartDate = grossPerformance; + for (const marketSymbol of marketSymbols) { + const date = format(marketSymbol.date, DATE_FORMAT); + if (!marketSymbolMap[date]) { + marketSymbolMap[date] = {}; + } + if (marketSymbol.marketPrice) { + marketSymbolMap[date][marketSymbol.symbol] = new Big( + marketSymbol.marketPrice + ); + } } } - timeWeightedGrossPerformancePercentage = - timeWeightedGrossPerformancePercentage.minus(1); - - timeWeightedNetPerformancePercentage = - timeWeightedNetPerformancePercentage.minus(1); - - const totalGrossPerformance = grossPerformance.minus( - grossPerformanceAtStartDate - ); - - const totalNetPerformance = grossPerformance - .minus(grossPerformanceAtStartDate) - .minus(fees.minus(feesAtStartDate)); - - let valueOfInvestmentSum = new Big(0); + const results: TimelinePeriod[] = []; + let maxNetPerformance: Big = null; + let minNetPerformance: Big = null; + for ( + let currentDate = startDate; + isBefore(currentDate, endDate); + currentDate = addDays(currentDate, 1) + ) { + let value = new Big(0); + const currentDateAsString = format(currentDate, DATE_FORMAT); + let invalid = false; + if (j >= 0) { + for (const item of this.transactionPoints[j].items) { + if ( + !marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol) + ) { + invalid = true; + break; + } + value = value.plus( + item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol]) + ); + } + } + if (!invalid) { + const grossPerformance = value.minus(investment); + const netPerformance = grossPerformance.minus(fees); + if ( + minNetPerformance === null || + minNetPerformance.gt(netPerformance) + ) { + minNetPerformance = netPerformance; + } + if ( + maxNetPerformance === null || + maxNetPerformance.lt(netPerformance) + ) { + maxNetPerformance = netPerformance; + } - for (const holdingPeriodPerformance of holdingPeriodPerformances) { - valueOfInvestmentSum = valueOfInvestmentSum.plus( - holdingPeriodPerformance.valueOfInvestment - ); + const result = { + grossPerformance, + investment, + netPerformance, + value, + date: currentDateAsString + }; + results.push(result); + } } - let totalWeightedGrossPerformance = new Big(0); - let totalWeightedNetPerformance = new Big(0); + return { + maxNetPerformance, + minNetPerformance, + timelinePeriods: results + }; + } - // 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) - ); + private getFactor(type: TypeOfOrder) { + let factor: number; - totalWeightedNetPerformance = totalWeightedNetPerformance.plus( - holdingPeriodPerformance.netReturn - .mul(holdingPeriodPerformance.valueOfInvestment) - .div(valueOfInvestmentSum) - ); + switch (type) { + case 'BUY': + factor = 1; + break; + case 'SELL': + factor = -1; + break; + default: + factor = 0; + break; } - return { - initialValue, - hasErrors: !initialValue || !unitPriceAtEndDate, - netPerformance: totalNetPerformance, - netPerformancePercentage: totalWeightedNetPerformance, - grossPerformance: totalGrossPerformance, - grossPerformancePercentage: totalWeightedGrossPerformance - }; + return factor; } - public getInvestments(): { date: string; investment: Big }[] { - if (this.transactionPoints.length === 0) { - return []; + private addToDate(date: Date, accuracy: Accuracy): Date { + switch (accuracy) { + case 'day': + return addDays(date, 1); + case 'month': + return addMonths(date, 1); + case 'year': + return addYears(date, 1); } + } - return this.transactionPoints.map((transactionPoint) => { - return { - date: transactionPoint.date, - investment: transactionPoint.items.reduce( - (investment, transactionPointSymbol) => - investment.plus(transactionPointSymbol.investment), - new Big(0) - ) - }; + private getSymbolMetrics({ + marketSymbolMap, + start, + symbol + }: { + marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + }; + start: Date; + symbol: string; + }) { + let orders: PortfolioOrderItem[] = this.orders.filter((order) => { + return order.symbol === symbol; }); - } - public async calculateTimeline( - timelineSpecification: TimelineSpecification[], - endDate: string - ): Promise { - if (timelineSpecification.length === 0) { + if (orders.length <= 0) { return { - maxNetPerformance: new Big(0), - minNetPerformance: new Big(0), - timelinePeriods: [] + hasErrors: false, + initialValue: new Big(0), + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0) }; } - const startDate = timelineSpecification[0].start; - const start = parseDate(startDate); - const end = parseDate(endDate); + const dateOfFirstTransaction = new Date(first(orders).date); + const endDate = new Date(Date.now()); - const timelinePeriodPromises: Promise[] = []; - let i = 0; - let j = -1; - for ( - let currentDate = start; - !isAfter(currentDate, end); - currentDate = this.addToDate( - currentDate, - timelineSpecification[i].accuracy - ) - ) { - if (this.isNextItemActive(timelineSpecification, currentDate, i)) { - i++; - } - while ( - j + 1 < this.transactionPoints.length && - !isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate) - ) { - j++; - } + const unitPriceAtStartDate = + marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; - let periodEndDate = currentDate; - if (timelineSpecification[i].accuracy === 'day') { - let nextEndDate = end; - if (j + 1 < this.transactionPoints.length) { - nextEndDate = parseDate(this.transactionPoints[j + 1].date); - } - periodEndDate = min([ - addMonths(currentDate, 3), - max([currentDate, nextEndDate]) - ]); - } - const timePeriodForDates = this.getTimePeriodForDate( - j, - currentDate, - endOfDay(periodEndDate) - ); - currentDate = periodEndDate; - if (timePeriodForDates != null) { - timelinePeriodPromises.push(timePeriodForDates); - } + const unitPriceAtEndDate = + marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; + + if ( + !unitPriceAtEndDate || + (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) + ) { + return { + hasErrors: true, + initialValue: new Big(0), + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0) + }; } - const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( - timelinePeriodPromises - ); - const minNetPerformance = timelineInfoInterfaces - .map((timelineInfo) => timelineInfo.minNetPerformance) - .filter((performance) => performance !== null) - .reduce((minPerformance, current) => { - if (minPerformance.lt(current)) { - return minPerformance; - } else { - return current; - } - }); + let feesAtStartDate = new Big(0); + let fees = new Big(0); + let grossPerformance = new Big(0); + let grossPerformanceAtStartDate = new Big(0); + let grossPerformanceFromSells = new Big(0); + let initialValue: Big; + let lastAveragePrice = new Big(0); + let lastTransactionInvestment = new Big(0); + let lastValueOfInvestmentBeforeTransaction = new Big(0); + let timeWeightedGrossPerformancePercentage = new Big(1); + let timeWeightedNetPerformancePercentage = new Big(1); + let totalInvestment = new Big(0); + let totalUnits = new Big(0); - const maxNetPerformance = timelineInfoInterfaces - .map((timelineInfo) => timelineInfo.maxNetPerformance) - .filter((performance) => performance !== null) - .reduce((maxPerformance, current) => { - if (maxPerformance.gt(current)) { - return maxPerformance; - } else { - return current; - } - }); + const holdingPeriodPerformances: { + grossReturn: Big; + netReturn: Big; + valueOfInvestment: Big; + }[] = []; - const timelinePeriods = timelineInfoInterfaces.map( - (timelineInfo) => timelineInfo.timelinePeriods - ); + // Add a synthetic order at the start and the end date + orders.push({ + symbol, + currency: null, + date: format(start, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'start', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtStartDate ?? new Big(0) + }); - return { - maxNetPerformance, - minNetPerformance, - timelinePeriods: flatten(timelinePeriods) - }; - } + orders.push({ + symbol, + currency: null, + date: format(endDate, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'end', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtEndDate ?? new Big(0) + }); - private calculateOverallPerformance( - positions: TimelinePosition[], - initialValues: { [p: 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 netPerformance = new Big(0); - let netPerformancePercentage = new Big(0); - let completeInitialValue = new Big(0); + // Sort orders so that the start and end placeholder order are at the right + // position + orders = sortBy(orders, (order) => { + let sortIndex = new Date(order.date); - for (const currentPosition of positions) { - if (currentPosition.marketPrice) { - currentValue = currentValue.plus( - new Big(currentPosition.marketPrice).mul(currentPosition.quantity) - ); - } else { - hasErrors = true; + if (order.itemType === 'start') { + sortIndex = addMilliseconds(sortIndex, -1); } - 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 (order.itemType === 'end') { + sortIndex = addMilliseconds(sortIndex, 1); } + return sortIndex.getTime(); + }); + + const indexOfStartOrder = orders.findIndex((order) => { + return order.itemType === 'start'; + }); + + for (let i = 0; i < orders.length; i += 1) { + const order = orders[i]; + + const valueOfInvestmentBeforeTransaction = totalUnits.mul( + order.unitPrice + ); + + const transactionInvestment = order.quantity + .mul(order.unitPrice) + .mul(this.getFactor(order.type)); + if ( - currentPosition.grossPerformancePercentage && - initialValues[currentPosition.symbol] + !initialValue && + order.itemType !== 'start' && + order.itemType !== 'end' ) { - const currentInitialValue = initialValues[currentPosition.symbol]; - completeInitialValue = completeInitialValue.plus(currentInitialValue); - grossPerformancePercentage = grossPerformancePercentage.plus( - currentPosition.grossPerformancePercentage.mul(currentInitialValue) - ); - netPerformancePercentage = netPerformancePercentage.plus( - currentPosition.netPerformancePercentage.mul(currentInitialValue) - ); - } else if (!currentPosition.quantity.eq(0)) { - Logger.warn( - `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}` - ); - hasErrors = true; + initialValue = transactionInvestment; } - } - if (!completeInitialValue.eq(0)) { - grossPerformancePercentage = - grossPerformancePercentage.div(completeInitialValue); - netPerformancePercentage = - netPerformancePercentage.div(completeInitialValue); - } + fees = fees.plus(order.fee); + + totalUnits = totalUnits.plus( + order.quantity.mul(this.getFactor(order.type)) + ); + + const valueOfInvestment = totalUnits.mul(order.unitPrice); - return { - currentValue, - grossPerformance, - grossPerformancePercentage, - hasErrors, - netPerformance, - netPerformancePercentage, - totalInvestment - }; - } + const grossPerformanceFromSell = + order.type === TypeOfOrder.SELL + ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) + : new Big(0); - private async getTimePeriodForDate( - j: number, - startDate: Date, - endDate: Date - ): Promise { - let investment: Big = new Big(0); - let fees: Big = new Big(0); + grossPerformanceFromSells = grossPerformanceFromSells.plus( + grossPerformanceFromSell + ); - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - if (j >= 0) { - const currencies: { [name: string]: string } = {}; - const dataGatheringItems: IDataGatheringItem[] = []; + totalInvestment = totalInvestment + .plus(transactionInvestment) + .plus(grossPerformanceFromSell); - for (const item of this.transactionPoints[j].items) { - currencies[item.symbol] = item.currency; - dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol - }); - investment = investment.plus(item.investment); - fees = fees.plus(item.fee); - } + lastAveragePrice = totalUnits.eq(0) + ? new Big(0) + : totalInvestment.div(totalUnits); - let marketSymbols: GetValueObject[] = []; - if (dataGatheringItems.length > 0) { - try { - marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - gte: startDate, - lt: endOfDay(endDate) - }, - userCurrency: this.currency - }); - } catch (error) { - Logger.error( - `Failed to fetch info for date ${startDate} with exception`, - error + const newGrossPerformance = valueOfInvestment + .minus(totalInvestment) + .plus(grossPerformanceFromSells); + + if ( + i > indexOfStartOrder && + !lastValueOfInvestmentBeforeTransaction + .plus(lastTransactionInvestment) + .eq(0) + ) { + const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction + .minus( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ) + .div( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) ); - return null; - } - } - for (const marketSymbol of marketSymbols) { - const date = format(marketSymbol.date, DATE_FORMAT); - if (!marketSymbolMap[date]) { - marketSymbolMap[date] = {}; - } - if (marketSymbol.marketPrice) { - marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice + timeWeightedGrossPerformancePercentage = + timeWeightedGrossPerformancePercentage.mul( + new Big(1).plus(grossHoldingPeriodReturn) ); - } - } - } - const results: TimelinePeriod[] = []; - let maxNetPerformance: Big = null; - let minNetPerformance: Big = null; - for ( - let currentDate = startDate; - isBefore(currentDate, endDate); - currentDate = addDays(currentDate, 1) - ) { - let value = new Big(0); - const currentDateAsString = format(currentDate, DATE_FORMAT); - let invalid = false; - if (j >= 0) { - for (const item of this.transactionPoints[j].items) { - if ( - !marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol) - ) { - invalid = true; - break; - } - value = value.plus( - item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol]) + const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction + .minus(fees.minus(feesAtStartDate)) + .minus( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ) + .div( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) ); - } + + timeWeightedNetPerformancePercentage = + timeWeightedNetPerformancePercentage.mul( + new Big(1).plus(netHoldingPeriodReturn) + ); + + holdingPeriodPerformances.push({ + grossReturn: grossHoldingPeriodReturn, + netReturn: netHoldingPeriodReturn, + valueOfInvestment: lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + }); } - if (!invalid) { - const grossPerformance = value.minus(investment); - const netPerformance = grossPerformance.minus(fees); - if ( - minNetPerformance === null || - minNetPerformance.gt(netPerformance) - ) { - minNetPerformance = netPerformance; - } - if ( - maxNetPerformance === null || - maxNetPerformance.lt(netPerformance) - ) { - maxNetPerformance = netPerformance; - } - const result = { - grossPerformance, - investment, - netPerformance, - value, - date: currentDateAsString - }; - results.push(result); + grossPerformance = newGrossPerformance; + + lastTransactionInvestment = transactionInvestment; + + lastValueOfInvestmentBeforeTransaction = + valueOfInvestmentBeforeTransaction; + + if (order.itemType === 'start') { + feesAtStartDate = fees; + grossPerformanceAtStartDate = grossPerformance; } } - return { - maxNetPerformance, - minNetPerformance, - timelinePeriods: results - }; - } + timeWeightedGrossPerformancePercentage = + timeWeightedGrossPerformancePercentage.minus(1); - private getFactor(type: TypeOfOrder) { - let factor: number; + timeWeightedNetPerformancePercentage = + timeWeightedNetPerformancePercentage.minus(1); - switch (type) { - case 'BUY': - factor = 1; - break; - case 'SELL': - factor = -1; - break; - default: - factor = 0; - break; + const totalGrossPerformance = grossPerformance.minus( + grossPerformanceAtStartDate + ); + + const totalNetPerformance = grossPerformance + .minus(grossPerformanceAtStartDate) + .minus(fees.minus(feesAtStartDate)); + + let valueOfInvestmentSum = new Big(0); + + for (const holdingPeriodPerformance of holdingPeriodPerformances) { + valueOfInvestmentSum = valueOfInvestmentSum.plus( + holdingPeriodPerformance.valueOfInvestment + ); } - return factor; - } + let totalWeightedGrossPerformance = new Big(0); + let totalWeightedNetPerformance = new Big(0); - private addToDate(date: Date, accuracy: Accuracy): Date { - switch (accuracy) { - case 'day': - return addDays(date, 1); - case 'month': - return addMonths(date, 1); - case 'year': - return addYears(date, 1); + // 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) + ); } + + return { + initialValue, + hasErrors: !initialValue || !unitPriceAtEndDate, + netPerformance: totalNetPerformance, + netPerformancePercentage: totalWeightedNetPerformance, + grossPerformance: totalGrossPerformance, + grossPerformancePercentage: totalWeightedGrossPerformance + }; } private isNextItemActive( diff --git a/package.json b/package.json index 0044211f0..cd8c0ec80 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "start:server": "nx serve api --watch", "start:storybook": "nx run ui:storybook", "test": "nx test", + "test:single": "nx test --test-file portfolio-calculator-new.spec.ts", "ts-node": "ts-node", "update": "nx migrate latest", "watch:server": "nx build api --watch", From ec806be45f156959a8410ba53c7ff62060be6f76 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 14 Feb 2022 18:51:16 +0100 Subject: [PATCH 154/337] Add detached mode (#696) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7927faa6a..5280d46fe 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio): ```bash -docker-compose -f docker/docker-compose.yml up +docker-compose -f docker/docker-compose.yml up -d ``` #### Setup Database @@ -102,7 +102,7 @@ Run the following commands to build and start the Docker images: ```bash docker-compose -f docker/docker-compose.build.yml build -docker-compose -f docker/docker-compose.build.yml up +docker-compose -f docker/docker-compose.build.yml up -d ``` #### Setup Database From e0bb2b1c787d3e9b3d6f5c5f56bfa0af43aba57a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 14 Feb 2022 21:20:07 +0100 Subject: [PATCH 155/337] Feature/improve position detail dialog (#697) * Improve mobile layout * Update changelog --- CHANGELOG.md | 6 ++++++ .../position-detail-dialog.html | 13 +++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e4c9e3e..887afd333 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 + +- Improved the mobile layout of the position detail dialog (countries and sectors charts) + ## 1.115.0 - 13.02.2022 ### Added diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 3a4026a14..e641a8f1f 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -126,14 +126,15 @@
@@ -164,8 +165,8 @@
-
-
Countries
+
+
Countries
-
-
Sectors
+
+
Sectors
Date: Tue, 15 Feb 2022 09:36:55 +0100 Subject: [PATCH 156/337] Bugfix/fix max items attribute (#698) * Fix maxItems * Update changelog --- CHANGELOG.md | 4 ++++ .../portfolio-proportion-chart.component.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887afd333..a5bd00d3f 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 - Improved the mobile layout of the position detail dialog (countries and sectors charts) +### Fixed + +- Fixed the `maxItems` attribute of the portfolio proportion chart component + ## 1.115.0 - 13.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 470d135c2..ca0475d66 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 @@ -150,11 +150,11 @@ export class PortfolioProportionChartComponent }); if (!unknownItem) { - const index = chartDataSorted.push([ + chartDataSorted.push([ UNKNOWN_KEY, { name: UNKNOWN_KEY, subCategory: {}, value: 0 } ]); - unknownItem = chartDataSorted[index]; + unknownItem = chartDataSorted[chartDataSorted.length - 1]; } rest.forEach((restItem) => { From 52e4504de98fefe6208064c32c356a171eb4d087 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 15 Feb 2022 09:47:56 +0100 Subject: [PATCH 157/337] Fix time in market (#700) * Fix time in market * Update changelog --- CHANGELOG.md | 1 + .../portfolio-summary/portfolio-summary.component.html | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5bd00d3f..a92543cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed the `maxItems` attribute of the portfolio proportion chart component +- Fixed the time in market display of the portfolio summary tab on the home page ## 1.115.0 - 13.02.2022 diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index dd2847c37..1fc5e1f85 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -2,7 +2,6 @@
Time in Market
- {{ timeInMarket }}
From 280030ae7fa20799a01b8c62458e3d19974f068a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 16 Feb 2022 21:17:11 +0100 Subject: [PATCH 158/337] Feature/add twitter bot for fear and greed index (#702) * Add twitter bot for fear and greed index * Update changelog --- CHANGELOG.md | 4 ++ apps/api/src/app/app.module.ts | 2 + apps/api/src/app/info/info.service.ts | 8 ++- apps/api/src/app/symbol/symbol.module.ts | 3 +- .../api/src/services/configuration.service.ts | 4 ++ apps/api/src/services/cron.service.ts | 9 ++- .../interfaces/environment.interface.ts | 4 ++ .../twitter-bot/twitter-bot.module.ts | 11 ++++ .../twitter-bot/twitter-bot.service.ts | 60 +++++++++++++++++++ .../fear-and-greed-index.component.ts | 9 +-- libs/common/src/lib/config.ts | 3 + package.json | 1 + yarn.lock | 5 ++ 13 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 apps/api/src/services/twitter-bot/twitter-bot.module.ts create mode 100644 apps/api/src/services/twitter-bot/twitter-bot.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a92543cb9..17708a810 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 a service to tweet the current _Fear & Greed Index_ (market mood) + ### Changed - Improved the mobile layout of the position detail dialog (countries and sectors charts) diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 46b2c9060..e3d8a3bea 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -8,6 +8,7 @@ import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.mod import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; +import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; @@ -65,6 +66,7 @@ import { UserModule } from './user/user.module'; }), SubscriptionModule, SymbolModule, + TwitterBotModule, UserModule ], controllers: [AppController], diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 203dc6dc4..f13679efc 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -9,7 +9,8 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_STRIPE_CONFIG, - PROPERTY_SYSTEM_MESSAGE + PROPERTY_SYSTEM_MESSAGE, + ghostfolioFearAndGreedIndexDataSource } from '@ghostfolio/common/config'; import { encodeDataSource } from '@ghostfolio/common/helper'; import { InfoItem } from '@ghostfolio/common/interfaces'; @@ -18,7 +19,6 @@ import { Subscription } from '@ghostfolio/common/interfaces/subscription.interfa import { permissions } from '@ghostfolio/common/permissions'; import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { DataSource } from '@prisma/client'; import * as bent from 'bent'; import { subDays } from 'date-fns'; @@ -52,7 +52,9 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - info.fearAndGreedDataSource = encodeDataSource(DataSource.RAKUTEN); + info.fearAndGreedDataSource = encodeDataSource( + ghostfolioFearAndGreedIndexDataSource + ); } if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { diff --git a/apps/api/src/app/symbol/symbol.module.ts b/apps/api/src/app/symbol/symbol.module.ts index c5143632b..2b47334a6 100644 --- a/apps/api/src/app/symbol/symbol.module.ts +++ b/apps/api/src/app/symbol/symbol.module.ts @@ -8,13 +8,14 @@ import { SymbolController } from './symbol.controller'; import { SymbolService } from './symbol.service'; @Module({ + controllers: [SymbolController], + exports: [SymbolService], imports: [ ConfigurationModule, DataProviderModule, MarketDataModule, PrismaModule ], - controllers: [SymbolController], providers: [SymbolService] }) export class SymbolModule {} diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 0c358f099..abbfa6641 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -39,6 +39,10 @@ export class ConfigurationService { ROOT_URL: str({ default: 'http://localhost:4200' }), STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }), + TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), + TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }), + TWITTER_API_KEY: str({ default: 'dummyApiKey' }), + TWITTER_API_SECRET: str({ default: 'dummyApiSecret' }), WEB_AUTH_RP_ID: host({ default: 'localhost' }) }); } diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index be29d5a7b..fdda206ef 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -3,12 +3,14 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { DataGatheringService } from './data-gathering.service'; import { ExchangeRateDataService } from './exchange-rate-data.service'; +import { TwitterBotService } from './twitter-bot/twitter-bot.service'; @Injectable() export class CronService { public constructor( private readonly dataGatheringService: DataGatheringService, - private readonly exchangeRateDataService: ExchangeRateDataService + private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly twitterBotService: TwitterBotService ) {} @Cron(CronExpression.EVERY_MINUTE) @@ -21,6 +23,11 @@ export class CronService { await this.exchangeRateDataService.loadCurrencies(); } + @Cron(CronExpression.EVERY_DAY_AT_6PM) + public async runEveryDayAtSixPM() { + this.twitterBotService.tweetFearAndGreedIndex(); + } + @Cron(CronExpression.EVERY_WEEKEND) public async runEveryWeekend() { await this.dataGatheringService.gatherProfileData(); diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 86b049546..82cf08cbe 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -30,5 +30,9 @@ export interface Environment extends CleanedEnvAccessors { ROOT_URL: string; STRIPE_PUBLIC_KEY: string; STRIPE_SECRET_KEY: string; + TWITTER_ACCESS_TOKEN: string; + TWITTER_ACCESS_TOKEN_SECRET: string; + TWITTER_API_KEY: string; + TWITTER_API_SECRET: string; WEB_AUTH_RP_ID: string; } diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts new file mode 100644 index 000000000..d74d6f10f --- /dev/null +++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts @@ -0,0 +1,11 @@ +import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; +import { Module } from '@nestjs/common'; + +@Module({ + exports: [TwitterBotService], + imports: [ConfigurationModule, SymbolModule], + providers: [TwitterBotService] +}) +export class TwitterBotModule {} diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts new file mode 100644 index 000000000..750e4fe30 --- /dev/null +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -0,0 +1,60 @@ +import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + ghostfolioFearAndGreedIndexDataSource, + ghostfolioFearAndGreedIndexSymbol +} from '@ghostfolio/common/config'; +import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; +import { Injectable, Logger } from '@nestjs/common'; +import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2'; + +@Injectable() +export class TwitterBotService { + private twitterClient: TwitterApiReadWrite; + + public constructor( + private readonly configurationService: ConfigurationService, + private readonly symbolService: SymbolService + ) { + this.twitterClient = new TwitterApi({ + accessSecret: this.configurationService.get( + 'TWITTER_ACCESS_TOKEN_SECRET' + ), + accessToken: this.configurationService.get('TWITTER_ACCESS_TOKEN'), + appKey: this.configurationService.get('TWITTER_API_KEY'), + appSecret: this.configurationService.get('TWITTER_API_SECRET') + }).readWrite; + } + + public async tweetFearAndGreedIndex() { + if (!this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + return; + } + + try { + const symbolItem = await this.symbolService.get({ + dataGatheringItem: { + dataSource: ghostfolioFearAndGreedIndexDataSource, + symbol: ghostfolioFearAndGreedIndexSymbol + } + }); + + if (symbolItem?.marketPrice) { + const { emoji, text } = resolveFearAndGreedIndex( + symbolItem.marketPrice + ); + + const status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)\n\n#FearAndGreed #Markets #ServiceTweet`; + const { data: createdTweet } = await this.twitterClient.v2.tweet( + status + ); + + Logger.log( + `Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}` + ); + } + } catch (error) { + Logger.error(error); + } + } +} diff --git a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts index a024b0f51..a42c80a37 100644 --- a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts +++ b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts @@ -24,12 +24,9 @@ export class FearAndGreedIndexComponent implements OnChanges, OnInit { public ngOnInit() {} public ngOnChanges() { - this.fearAndGreedIndexEmoji = resolveFearAndGreedIndex( - this.fearAndGreedIndex - ).emoji; + const { emoji, text } = resolveFearAndGreedIndex(this.fearAndGreedIndex); - this.fearAndGreedIndexText = resolveFearAndGreedIndex( - this.fearAndGreedIndex - ).text; + this.fearAndGreedIndexEmoji = emoji; + this.fearAndGreedIndexText = text; } } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 8f2c9d8fa..a04736620 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -1,3 +1,5 @@ +import { DataSource } from '@prisma/client'; + import { ToggleOption } from './types'; export const baseCurrency = 'USD'; @@ -14,6 +16,7 @@ export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; export const ghostfolioScraperApiSymbolPrefix = '_GF_'; export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`; +export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN; export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`; export const locale = 'de-CH'; diff --git a/package.json b/package.json index cd8c0ec80..8e2894752 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "stripe": "8.199.0", "svgmap": "2.6.0", "tslib": "2.0.0", + "twitter-api-v2": "1.10.3", "uuid": "8.3.2", "yahoo-finance": "0.3.6", "zone.js": "0.11.4" diff --git a/yarn.lock b/yarn.lock index 8671fd039..756eca7bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17714,6 +17714,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +twitter-api-v2@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.10.3.tgz#07441bd9c4d27433aa0284d900cf60f6328b8239" + integrity sha512-AbCboiTOWv4DUPbAlF43Uyk4iK/QRk354pNdKgtOmv45+BWGB5Kdv6ls+C99pww/DyLBiXgQEnuyGv4d1HdRhw== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" From 5a869a90daf7602be64b2549c62b642d652e4ff3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 16 Feb 2022 21:23:13 +0100 Subject: [PATCH 159/337] Release 1.116.0 (#703) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17708a810..44bc6ec12 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.116.0 - 16.02.2022 ### Added diff --git a/package.json b/package.json index 8e2894752..85fcb7747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.115.0", + "version": "1.116.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1eb4041837fda3fb01d9f499f31c497161180d23 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 17 Feb 2022 20:40:28 +0100 Subject: [PATCH 160/337] Feature/move countries and sectors chart (#704) * Move countries and sectors charts * Update changelog --- CHANGELOG.md | 6 +++++ .../position-detail-dialog.html | 24 +++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bc6ec12..b266833e8 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 + +- Moved the countries and sectors charts in the position detail dialog + ## 1.116.0 - 16.02.2022 ### Added diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index e641a8f1f..9ea26573f 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -144,6 +144,14 @@ +
+ +
-
- -
-
Countries
+
Sectors
-
Sectors
+
Countries
From d3382f08096fa782849f521a0a08d907c20e2690 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 17 Feb 2022 21:31:08 +0100 Subject: [PATCH 161/337] Update time (#707) --- apps/api/src/services/cron.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index fdda206ef..b1ca2a513 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -23,8 +23,8 @@ export class CronService { await this.exchangeRateDataService.loadCurrencies(); } - @Cron(CronExpression.EVERY_DAY_AT_6PM) - public async runEveryDayAtSixPM() { + @Cron(CronExpression.EVERY_DAY_AT_5PM) + public async runEveryDayAtFivePM() { this.twitterBotService.tweetFearAndGreedIndex(); } From baa6a3d0f0c2e576319db4fb63be24fbe295b4b2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:32:25 +0100 Subject: [PATCH 162/337] Feature/restructure api modules (#706) * Restructure modules * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/access/access.module.ts | 6 +++--- .../src/app/auth-device/auth-device.module.ts | 10 ++++++---- apps/api/src/app/auth/auth.module.ts | 8 ++++---- apps/api/src/app/cache/cache.module.ts | 17 +++++++---------- apps/api/src/app/import/import.module.ts | 7 ++++--- apps/api/src/app/info/info.module.ts | 16 ++++++---------- apps/api/src/app/order/order.module.ts | 9 +++++---- apps/api/src/app/portfolio/portfolio.module.ts | 2 +- .../src/app/redis-cache/redis-cache.module.ts | 6 ++++-- .../src/app/subscription/subscription.module.ts | 10 +++++----- apps/api/src/app/user/user.module.ts | 12 +++++++----- 12 files changed, 53 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b266833e8..5efe84cd0 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 +- Restructured the server modules ## 1.116.0 - 16.02.2022 diff --git a/apps/api/src/app/access/access.module.ts b/apps/api/src/app/access/access.module.ts index 303989b21..d70388038 100644 --- a/apps/api/src/app/access/access.module.ts +++ b/apps/api/src/app/access/access.module.ts @@ -1,4 +1,4 @@ -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { Module } from '@nestjs/common'; import { AccessController } from './access.controller'; @@ -7,7 +7,7 @@ import { AccessService } from './access.service'; @Module({ controllers: [AccessController], exports: [AccessService], - imports: [], - providers: [AccessService, PrismaService] + imports: [PrismaModule], + providers: [AccessService] }) export class AccessModule {} diff --git a/apps/api/src/app/auth-device/auth-device.module.ts b/apps/api/src/app/auth-device/auth-device.module.ts index 360930cf2..de6cd1cbc 100644 --- a/apps/api/src/app/auth-device/auth-device.module.ts +++ b/apps/api/src/app/auth-device/auth-device.module.ts @@ -1,18 +1,20 @@ import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @Module({ controllers: [AuthDeviceController], imports: [ + ConfigurationModule, JwtModule.register({ secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '180 days' } - }) + }), + PrismaModule ], - providers: [AuthDeviceService, ConfigurationService, PrismaService] + providers: [AuthDeviceService] }) export class AuthDeviceModule {} diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 8a59ff82a..b25e4c18b 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -2,8 +2,8 @@ import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.s import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -15,20 +15,20 @@ import { JwtStrategy } from './jwt.strategy'; @Module({ controllers: [AuthController], imports: [ + ConfigurationModule, JwtModule.register({ secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '180 days' } }), + PrismaModule, SubscriptionModule, UserModule ], providers: [ AuthDeviceService, AuthService, - ConfigurationService, GoogleStrategy, JwtStrategy, - PrismaService, WebAuthService ] }) diff --git a/apps/api/src/app/cache/cache.module.ts b/apps/api/src/app/cache/cache.module.ts index a823c2d1e..7b427b7a0 100644 --- a/apps/api/src/app/cache/cache.module.ts +++ b/apps/api/src/app/cache/cache.module.ts @@ -1,30 +1,27 @@ import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; -import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { Module } from '@nestjs/common'; import { CacheController } from './cache.controller'; @Module({ + exports: [CacheService], + controllers: [CacheController], imports: [ + ConfigurationModule, DataGatheringModule, DataProviderModule, ExchangeRateDataModule, + PrismaModule, RedisCacheModule, SymbolProfileModule ], - controllers: [CacheController], - providers: [ - CacheService, - ConfigurationService, - DataGatheringService, - PrismaService - ] + providers: [CacheService] }) export class CacheModule {} diff --git a/apps/api/src/app/import/import.module.ts b/apps/api/src/app/import/import.module.ts index 35781e499..03ff2d3f8 100644 --- a/apps/api/src/app/import/import.module.ts +++ b/apps/api/src/app/import/import.module.ts @@ -1,4 +1,4 @@ -import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; +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'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; @@ -11,7 +11,9 @@ import { ImportController } from './import.controller'; import { ImportService } from './import.service'; @Module({ + controllers: [ImportController], imports: [ + CacheModule, ConfigurationModule, DataGatheringModule, DataProviderModule, @@ -19,7 +21,6 @@ import { ImportService } from './import.service'; PrismaModule, RedisCacheModule ], - controllers: [ImportController], - providers: [CacheService, ImportService] + providers: [ImportService] }) export class ImportModule {} diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 50edd0eed..5828ac963 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -1,10 +1,9 @@ import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; -import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { Module } from '@nestjs/common'; @@ -14,7 +13,9 @@ import { InfoController } from './info.controller'; import { InfoService } from './info.service'; @Module({ + controllers: [InfoController], imports: [ + ConfigurationModule, DataGatheringModule, DataProviderModule, ExchangeRateDataModule, @@ -22,16 +23,11 @@ import { InfoService } from './info.service'; secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } }), + PrismaModule, PropertyModule, RedisCacheModule, SymbolProfileModule ], - controllers: [InfoController], - providers: [ - ConfigurationService, - DataGatheringService, - InfoService, - PrismaService - ] + providers: [InfoService] }) export class InfoModule {} diff --git a/apps/api/src/app/order/order.module.ts b/apps/api/src/app/order/order.module.ts index f2c790ce8..52ffc0266 100644 --- a/apps/api/src/app/order/order.module.ts +++ b/apps/api/src/app/order/order.module.ts @@ -1,5 +1,5 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; -import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; +import { CacheModule } from '@ghostfolio/api/app/cache/cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; @@ -15,7 +15,10 @@ import { OrderController } from './order.controller'; import { OrderService } from './order.service'; @Module({ + controllers: [OrderController], + exports: [OrderService], imports: [ + CacheModule, ConfigurationModule, DataGatheringModule, DataProviderModule, @@ -26,8 +29,6 @@ import { OrderService } from './order.service'; SymbolProfileModule, UserModule ], - controllers: [OrderController], - providers: [AccountService, CacheService, OrderService], - exports: [OrderService] + providers: [AccountService, OrderService] }) export class OrderModule {} diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 85bc98e7a..5204f1795 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -20,6 +20,7 @@ import { PortfolioServiceNew } from './portfolio.service-new'; import { RulesService } from './rules.service'; @Module({ + controllers: [PortfolioController], exports: [PortfolioServiceStrategy], imports: [ AccessModule, @@ -34,7 +35,6 @@ import { RulesService } from './rules.service'; SymbolProfileModule, UserModule ], - controllers: [PortfolioController], providers: [ AccountService, CurrentRateService, diff --git a/apps/api/src/app/redis-cache/redis-cache.module.ts b/apps/api/src/app/redis-cache/redis-cache.module.ts index e3275276b..dcda94041 100644 --- a/apps/api/src/app/redis-cache/redis-cache.module.ts +++ b/apps/api/src/app/redis-cache/redis-cache.module.ts @@ -1,3 +1,4 @@ +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { CacheModule, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; @@ -17,9 +18,10 @@ import { RedisCacheService } from './redis-cache.service'; store: redisStore, ttl: configurationService.get('CACHE_TTL') }) - }) + }), + ConfigurationModule ], - providers: [ConfigurationService, RedisCacheService], + providers: [RedisCacheService], exports: [RedisCacheService] }) export class RedisCacheModule {} diff --git a/apps/api/src/app/subscription/subscription.module.ts b/apps/api/src/app/subscription/subscription.module.ts index 95d16fb4d..df0861657 100644 --- a/apps/api/src/app/subscription/subscription.module.ts +++ b/apps/api/src/app/subscription/subscription.module.ts @@ -1,5 +1,5 @@ -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { Module } from '@nestjs/common'; @@ -7,9 +7,9 @@ import { SubscriptionController } from './subscription.controller'; import { SubscriptionService } from './subscription.service'; @Module({ - imports: [PropertyModule], controllers: [SubscriptionController], - providers: [ConfigurationService, PrismaService, SubscriptionService], - exports: [SubscriptionService] + exports: [SubscriptionService], + imports: [ConfigurationModule, PrismaModule, PropertyModule], + providers: [SubscriptionService] }) export class SubscriptionModule {} diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index ffbdc80db..976f5b6c3 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -1,6 +1,6 @@ import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; -import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -9,16 +9,18 @@ import { UserController } from './user.controller'; import { UserService } from './user.service'; @Module({ + controllers: [UserController], + exports: [UserService], imports: [ + ConfigurationModule, JwtModule.register({ secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } }), + PrismaModule, PropertyModule, SubscriptionModule ], - controllers: [UserController], - providers: [ConfigurationService, PrismaService, UserService], - exports: [UserService] + providers: [UserService] }) export class UserModule {} 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 163/337] 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 164/337] 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 165/337] 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 166/337] 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 167/337] 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 168/337] 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 169/337] 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 170/337] 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 171/337] 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 172/337] 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 173/337] 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 174/337] 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 175/337] 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 176/337] 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 177/337] 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 178/337] 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 179/337] 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 180/337] 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 181/337] 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 182/337] 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 183/337] 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 184/337] 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 185/337] 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 186/337] 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 187/337] 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 188/337] 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 189/337] 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 190/337] 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 191/337] 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 192/337] 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 193/337] 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 194/337] 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 195/337] 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 196/337] 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 From 7b6893b5edb56e8eaedcb22e834587540daec47f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Mar 2022 13:44:47 +0100 Subject: [PATCH 197/337] Feature/add support for emergency fund (#749) * Add support for emergency fund * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 1 + .../app/portfolio/portfolio.service-new.ts | 51 +++++++++++++++++-- .../src/app/portfolio/portfolio.service.ts | 51 +++++++++++++++++-- .../interfaces/user-settings.interface.ts | 2 + .../src/app/user/update-user-setting.dto.ts | 6 ++- apps/api/src/app/user/user.controller.ts | 1 - apps/api/src/app/user/user.service.ts | 2 +- .../home-summary/home-summary.component.ts | 26 ++++++++++ .../components/home-summary/home-summary.html | 2 + .../portfolio-summary.component.html | 20 ++++++++ .../portfolio-summary.component.ts | 19 ++++++- .../portfolio-summary.module.ts | 4 +- .../positions-table.component.ts | 6 ++- libs/common/src/lib/config.ts | 2 + .../portfolio-position.interface.ts | 2 +- .../interfaces/portfolio-summary.interface.ts | 1 + .../portfolio-proportion-chart.component.ts | 2 +- 18 files changed, 182 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b9a9251..13f3cf93b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for an emergency fund - Added the contexts to the logger commands ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index fd11334d9..2d7993859 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -334,6 +334,7 @@ export class PortfolioController { 'currentNetPerformance', 'currentValue', 'dividend', + 'emergencyFund', 'fees', 'items', 'netWorth', diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index b34a9206f..f11909b85 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -5,6 +5,8 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; +import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -19,7 +21,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; +import { + ASSET_SUB_CLASS_EMERGENCY_FUND, + UNKNOWN_KEY, + baseCurrency +} from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, @@ -76,7 +82,8 @@ export class PortfolioServiceNew { private readonly orderService: OrderService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly rulesService: RulesService, - private readonly symbolProfileService: SymbolProfileService + private readonly symbolProfileService: SymbolProfileService, + private readonly userService: UserService ) {} public async getAccounts(aUserId: string): Promise { @@ -295,7 +302,11 @@ export class PortfolioServiceNew { aDateRange: DateRange = 'max' ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); + const user = await this.userService.user({ id: userId }); + const emergencyFund = new Big( + (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 + ); const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const { orders, portfolioOrders, transactionPoints } = @@ -393,6 +404,7 @@ export class PortfolioServiceNew { const cashPositions = await this.getCashPositions({ cashDetails, + emergencyFund, userCurrency, investment: totalInvestment, value: totalValue @@ -883,6 +895,7 @@ export class PortfolioServiceNew { public async getSummary(aImpersonationId: string): Promise { const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); + const user = await this.userService.user({ id: userId }); const performanceInformation = await this.getPerformance(aImpersonationId); @@ -895,6 +908,9 @@ export class PortfolioServiceNew { userId }); const dividend = this.getDividend(orders).toNumber(); + const emergencyFund = new Big( + (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 + ); const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; const items = this.getItems(orders).toNumber(); @@ -902,6 +918,7 @@ export class PortfolioServiceNew { const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); const netWorth = new Big(balanceInBaseCurrency) @@ -927,6 +944,7 @@ export class PortfolioServiceNew { return { ...performanceInformation.performance, annualizedPerformancePercent, + cash, dividend, fees, firstOrderDate, @@ -934,8 +952,8 @@ export class PortfolioServiceNew { netWorth, totalBuy, totalSell, - cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), + emergencyFund: emergencyFund.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; }).length @@ -944,16 +962,18 @@ export class PortfolioServiceNew { private async getCashPositions({ cashDetails, + emergencyFund, investment, userCurrency, value }: { cashDetails: CashDetails; + emergencyFund: Big; investment: Big; value: Big; userCurrency: string; }) { - const cashPositions = {}; + const cashPositions: PortfolioDetails['holdings'] = {}; for (const account of cashDetails.accounts) { const convertedBalance = this.exchangeRateDataService.toCurrency( @@ -977,6 +997,7 @@ export class PortfolioServiceNew { assetSubClass: AssetClass.CASH, countries: [], currency: account.currency, + dataSource: undefined, grossPerformance: 0, grossPerformancePercent: 0, investment: convertedBalance, @@ -994,6 +1015,28 @@ export class PortfolioServiceNew { } } + if (emergencyFund.gt(0)) { + cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = { + ...cashPositions[userCurrency], + assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND, + investment: emergencyFund.toNumber(), + name: ASSET_SUB_CLASS_EMERGENCY_FUND, + symbol: ASSET_SUB_CLASS_EMERGENCY_FUND, + value: emergencyFund.toNumber() + }; + + cashPositions[userCurrency].investment = new Big( + cashPositions[userCurrency].investment + ) + .minus(emergencyFund) + .toNumber(); + cashPositions[userCurrency].value = new Big( + cashPositions[userCurrency].value + ) + .minus(emergencyFund) + .toNumber(); + } + for (const symbol of Object.keys(cashPositions)) { // Calculate allocations for each currency cashPositions[symbol].allocationCurrent = new Big( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f3df18c30..f7902a2d7 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -6,6 +6,8 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator'; +import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -20,7 +22,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; +import { + ASSET_SUB_CLASS_EMERGENCY_FUND, + UNKNOWN_KEY, + baseCurrency +} from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, @@ -75,7 +81,8 @@ export class PortfolioService { private readonly orderService: OrderService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly rulesService: RulesService, - private readonly symbolProfileService: SymbolProfileService + private readonly symbolProfileService: SymbolProfileService, + private readonly userService: UserService ) {} public async getAccounts(aUserId: string): Promise { @@ -286,7 +293,11 @@ export class PortfolioService { aDateRange: DateRange = 'max' ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); + const user = await this.userService.user({ id: userId }); + const emergencyFund = new Big( + (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 + ); const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioCalculator = new PortfolioCalculator( this.currentRateService, @@ -381,6 +392,7 @@ export class PortfolioService { const cashPositions = await this.getCashPositions({ cashDetails, + emergencyFund, userCurrency, investment: totalInvestment, value: totalValue @@ -861,6 +873,7 @@ export class PortfolioService { public async getSummary(aImpersonationId: string): Promise { const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); + const user = await this.userService.user({ id: userId }); const performanceInformation = await this.getPerformance(aImpersonationId); @@ -873,6 +886,9 @@ export class PortfolioService { userId }); const dividend = this.getDividend(orders).toNumber(); + const emergencyFund = new Big( + (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 + ); const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; const items = this.getItems(orders).toNumber(); @@ -880,6 +896,7 @@ export class PortfolioService { const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); const netWorth = new Big(balanceInBaseCurrency) @@ -889,6 +906,7 @@ export class PortfolioService { return { ...performanceInformation.performance, + cash, dividend, fees, firstOrderDate, @@ -898,8 +916,8 @@ export class PortfolioService { totalSell, annualizedPerformancePercent: performanceInformation.performance.annualizedPerformancePercent, - cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), + emergencyFund: emergencyFund.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; }).length @@ -908,16 +926,18 @@ export class PortfolioService { private async getCashPositions({ cashDetails, + emergencyFund, investment, userCurrency, value }: { cashDetails: CashDetails; + emergencyFund: Big; investment: Big; userCurrency: string; value: Big; }) { - const cashPositions = {}; + const cashPositions: PortfolioDetails['holdings'] = {}; for (const account of cashDetails.accounts) { const convertedBalance = this.exchangeRateDataService.toCurrency( @@ -941,6 +961,7 @@ export class PortfolioService { assetSubClass: AssetClass.CASH, countries: [], currency: account.currency, + dataSource: undefined, grossPerformance: 0, grossPerformancePercent: 0, investment: convertedBalance, @@ -958,6 +979,28 @@ export class PortfolioService { } } + if (emergencyFund.gt(0)) { + cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = { + ...cashPositions[userCurrency], + assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND, + investment: emergencyFund.toNumber(), + name: ASSET_SUB_CLASS_EMERGENCY_FUND, + symbol: ASSET_SUB_CLASS_EMERGENCY_FUND, + value: emergencyFund.toNumber() + }; + + cashPositions[userCurrency].investment = new Big( + cashPositions[userCurrency].investment + ) + .minus(emergencyFund) + .toNumber(); + cashPositions[userCurrency].value = new Big( + cashPositions[userCurrency].value + ) + .minus(emergencyFund) + .toNumber(); + } + for (const symbol of Object.keys(cashPositions)) { // Calculate allocations for each currency cashPositions[symbol].allocationCurrent = new Big( diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts index 7870abee9..fb4b2af35 100644 --- a/apps/api/src/app/user/interfaces/user-settings.interface.ts +++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts @@ -1,3 +1,5 @@ export interface UserSettings { + emergencyFund?: number; + isNewCalculationEngine?: boolean; isRestrictedView?: boolean; } diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index 38dc9cafb..5af2f5f8d 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,6 +1,10 @@ -import { IsBoolean, IsOptional } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional } from 'class-validator'; export class UpdateUserSettingDto { + @IsNumber() + @IsOptional() + emergencyFund?: number; + @IsBoolean() @IsOptional() isNewCalculationEngine?: boolean; diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index a615d2f2b..97cf25b6e 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -23,7 +23,6 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; -import { Provider, Role } from '@prisma/client'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 5a21e9303..43b3b52c7 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -15,7 +15,7 @@ import { } from '@ghostfolio/common/permissions'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable } from '@nestjs/common'; -import { Prisma, Provider, Role, User, ViewMode } from '@prisma/client'; +import { Prisma, Role, User, ViewMode } from '@prisma/client'; import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettings } from './interfaces/user-settings.interface'; diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index b2d8d91f4..09bcb0454 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -1,7 +1,9 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -11,6 +13,8 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './home-summary.html' }) export class HomeSummaryComponent implements OnDestroy, OnInit { + public hasImpersonationId: boolean; + public hasPermissionToUpdateUserSettings: boolean; public isLoading = true; public summary: PortfolioSummary; public user: User; @@ -23,6 +27,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private impersonationStorageService: ImpersonationStorageService, private userService: UserService ) { this.userService.stateChanged @@ -31,6 +36,11 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.hasPermissionToUpdateUserSettings = hasPermission( + this.user.permissions, + permissions.updateUserSettings + ); + this.changeDetectorRef.markForCheck(); } }); @@ -40,9 +50,25 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { * Initializes the controller */ public ngOnInit() { + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + this.update(); } + public onChangeEmergencyFund(emergencyFund: number) { + this.dataService + .putUserSetting({ emergencyFund }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.update(); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/home-summary/home-summary.html b/apps/client/src/app/components/home-summary/home-summary.html index fe0b99a2b..7905130bd 100644 --- a/apps/client/src/app/components/home-summary/home-summary.html +++ b/apps/client/src/app/components/home-summary/home-summary.html @@ -8,9 +8,11 @@ diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 1fc5e1f85..5335b77a7 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -130,6 +130,26 @@ >
+
+
Emergency Fund
+
+ + +
+
Cash (Buying Power)
diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts index 77debbb0f..d8cda52f4 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -1,9 +1,11 @@ import { ChangeDetectionStrategy, Component, + EventEmitter, Input, OnChanges, - OnInit + OnInit, + Output } from '@angular/core'; import { PortfolioSummary } from '@ghostfolio/common/interfaces'; import { formatDistanceToNow } from 'date-fns'; @@ -16,10 +18,13 @@ import { formatDistanceToNow } from 'date-fns'; }) export class PortfolioSummaryComponent implements OnChanges, OnInit { @Input() baseCurrency: string; + @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() locale: string; @Input() summary: PortfolioSummary; + @Output() emergencyFundChanged = new EventEmitter(); + public timeInMarket: string; public constructor() {} @@ -37,4 +42,16 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit { this.timeInMarket = undefined; } } + + public onEditEmergencyFund() { + const emergencyFundInput = prompt( + 'Please enter the amount of your emergency fund:', + this.summary.emergencyFund.toString() + ); + const emergencyFund = parseFloat(emergencyFundInput?.trim()); + + if (emergencyFund >= 0) { + this.emergencyFundChanged.emit(emergencyFund); + } + } } diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts index 306de7f06..1724aa8c5 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { GfValueModule } from '@ghostfolio/ui/value'; import { PortfolioSummaryComponent } from './portfolio-summary.component'; @@ -8,6 +8,6 @@ import { PortfolioSummaryComponent } from './portfolio-summary.component'; declarations: [PortfolioSummaryComponent], exports: [PortfolioSummaryComponent], imports: [CommonModule, GfValueModule], - providers: [] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfPortfolioSummaryModule {} 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 9dadb27df..ec40d9bf3 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,6 +13,7 @@ import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; +import { ASSET_SUB_CLASS_EMERGENCY_FUND } from '@ghostfolio/common/config'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; @@ -39,7 +40,10 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { public dataSource: MatTableDataSource = new MatTableDataSource(); public displayedColumns = []; - public ignoreAssetSubClasses = [AssetClass.CASH.toString()]; + public ignoreAssetSubClasses = [ + AssetClass.CASH.toString(), + ASSET_SUB_CLASS_EMERGENCY_FUND + ]; public isLoading = true; public pageSize = 7; public routeQueryParams: Subscription; diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index a04736620..535fa2ef3 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -42,6 +42,8 @@ export const warnColorRgb = { b: 69 }; +export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND'; + export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 1145d98ff..87ee917d4 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -8,7 +8,7 @@ export interface PortfolioPosition { allocationCurrent: number; allocationInvestment: number; assetClass?: AssetClass; - assetSubClass?: AssetSubClass | 'CASH'; + assetSubClass?: AssetSubClass | 'CASH' | 'EMERGENCY_FUND'; countries: Country[]; currency: string; dataSource: DataSource; diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 7ffec5d4d..b692e9ba9 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -5,6 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance { cash: number; dividend: number; committedFunds: number; + emergencyFund: number; fees: number; firstOrderDate: Date; items: number; 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 4fe51497d..9fee13d0e 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 @@ -122,7 +122,7 @@ export class PortfolioProportionChartComponent chartData[this.positions[symbol][this.keys[0]]] = { name: this.positions[symbol].name, subCategory: {}, - value: new Big(this.positions[symbol].value) + value: new Big(this.positions[symbol].value ?? 0) }; if (this.positions[symbol][this.keys[1]]) { From f781eb207ca24c1e8d0dec80b81541fbba0158aa Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Mar 2022 18:40:09 +0100 Subject: [PATCH 198/337] Release 1.125.0 (#752) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13f3cf93b..b2080c09a 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.125.0 - 12.03.2022 ### Added diff --git a/package.json b/package.json index f2d990a6e..f64fd1a18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.124.0", + "version": "1.125.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 122ba9046faa883cf9e7062cd2ac486af0c0fafc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 13 Mar 2022 21:00:40 +0100 Subject: [PATCH 199/337] Add type (#751) * Add type * Refactor import to import type --- .../data-provider/yahoo-finance/yahoo-finance.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 95a14c1ab..ef4328fc8 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 @@ -21,6 +21,7 @@ import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; import yahooFinance from 'yahoo-finance2'; +import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -303,7 +304,7 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private parseAssetClass(aPrice: any): { + private parseAssetClass(aPrice: Price): { assetClass: AssetClass; assetSubClass: AssetSubClass; } { From 5a369f29d421d7e8dcf579da45039565f09687e6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 13 Mar 2022 21:01:15 +0100 Subject: [PATCH 200/337] Feature/restructure portfolio summary tab (#754) * Restructure portfolio summary * Update changelog --- CHANGELOG.md | 6 +++++ .../portfolio-summary.component.html | 26 +++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2080c09a..ea930a8ca 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 + +- Restructured the portfolio summary tab on the home page + ## 1.125.0 - 12.03.2022 ### Added diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 5335b77a7..5206aaedc 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -119,7 +119,7 @@

-
Value
+
Total
+
+
Valuables
+
+ +
+
Emergency Fund
-
Cash (Buying Power)
+
Buying Power
-
-
Items
-
- -
-

From e769fabbaeac67edf173e4aadb0b4da3d3ef1a39 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 13 Mar 2022 21:39:06 +0100 Subject: [PATCH 201/337] Feature/add multilines to tooltips in proportion chart (#753) * Introduce multilines for tooltips * Update changelog --- CHANGELOG.md | 1 + .../portfolio-proportion-chart.component.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea930a8ca..3502aef38 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 - Restructured the portfolio summary tab on the home page +- Improved the tooltips in the portfolio proportion chart component by introducing multilines ## 1.125.0 - 12.03.2022 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 9fee13d0e..803e52fea 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 @@ -324,16 +324,16 @@ export class PortfolioProportionChartComponent const percentage = (context.parsed * 100) / sum; if (this.isInPercent) { - return `${name ?? symbol} (${percentage.toFixed(2)}%)`; + return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } else { const value = context.raw; - return `${name ?? symbol}: ${value.toLocaleString( - this.locale, - { + return [ + `${name ?? symbol}`, + `${value.toLocaleString(this.locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 - } - )} ${this.baseCurrency} (${percentage.toFixed(2)}%)`; + })} ${this.baseCurrency} (${percentage.toFixed(2)}%)` + ]; } } } From d8da574ae416f33371669d21da3933880003faed Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:39:09 +0100 Subject: [PATCH 202/337] Feature/add support for bonds (#755) * Add support for bonds * Update changelog --- CHANGELOG.md | 8 ++++++++ .../migration.sql | 2 ++ .../migration.sql | 2 ++ prisma/schema.prisma | 2 ++ 4 files changed, 14 insertions(+) create mode 100644 prisma/migrations/20220313200604_added_fixed_income_to_asset_class/migration.sql create mode 100644 prisma/migrations/20220313200721_added_bond_to_asset_sub_class/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3502aef38..146f17e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for bonds + ### Changed - Restructured the portfolio summary tab on the home page - Improved the tooltips in the portfolio proportion chart component by introducing multilines +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.125.0 - 12.03.2022 ### Added diff --git a/prisma/migrations/20220313200604_added_fixed_income_to_asset_class/migration.sql b/prisma/migrations/20220313200604_added_fixed_income_to_asset_class/migration.sql new file mode 100644 index 000000000..c9e812f64 --- /dev/null +++ b/prisma/migrations/20220313200604_added_fixed_income_to_asset_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetClass" ADD VALUE 'FIXED_INCOME'; diff --git a/prisma/migrations/20220313200721_added_bond_to_asset_sub_class/migration.sql b/prisma/migrations/20220313200721_added_bond_to_asset_sub_class/migration.sql new file mode 100644 index 000000000..4dd560edb --- /dev/null +++ b/prisma/migrations/20220313200721_added_bond_to_asset_sub_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetSubClass" ADD VALUE 'BOND'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 738797ed6..6dea69159 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -171,9 +171,11 @@ enum AssetClass { CASH COMMODITY EQUITY + FIXED_INCOME } enum AssetSubClass { + BOND CRYPTOCURRENCY ETF MUTUALFUND From 5356bf568e9db71f3e2d66244b768a0797106b12 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:41:23 +0100 Subject: [PATCH 203/337] Release 1.126.0 (#756) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 146f17e7f..f2c8aff60 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.126.0 - 14.03.2022 ### Added diff --git a/package.json b/package.json index f64fd1a18..74c29aee8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.125.0", + "version": "1.126.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 4826a51199a8ad0e0e9de72fb7a24be7eeb6a324 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:13:42 +0100 Subject: [PATCH 204/337] Feature/improve handling of scraper configuration (#757) * Improve handling of missing scraper configuration * Update changelog --- CHANGELOG.md | 6 ++++++ .../ghostfolio-scraper-api.service.ts | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2c8aff60..ce8e7faca 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 + +- Improved the error handling in the scraper configuration + ## 1.126.0 - 14.03.2022 ### Added 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 35c53bc7a..bd126e235 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 @@ -50,16 +50,18 @@ export class GhostfolioScraperApiService implements DataProviderInterface { const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [symbol] ); - const scraperConfiguration = symbolProfile?.scraperConfiguration; + const { selector, url } = symbolProfile.scraperConfiguration; - const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {}); + if (selector === undefined || url === undefined) { + return {}; + } + + const get = bent(url, 'GET', 'string', 200, {}); const html = await get(); const $ = cheerio.load(html); - const value = this.extractNumberFromString( - $(scraperConfiguration?.selector).text() - ); + const value = this.extractNumberFromString($(selector).text()); return { [symbol]: { From 402d73a12c9e7ab011c86d9dfa36c80b04c7c3bd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 16 Mar 2022 22:07:18 +0100 Subject: [PATCH 205/337] Bugfix/fix get quotes for multiple ghostfolio symbols (#758) * Support multiple symbols in getQuotes() * Update changelog --- CHANGELOG.md | 4 +++ .../ghostfolio-scraper-api.service.ts | 33 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8e7faca..691f1b259 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 - Improved the error handling in the scraper configuration +### Fixed + +- Fixed the support for multiple symbols of the data source `GHOSTFOLIO` + ## 1.126.0 - 14.03.2022 ### Added 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 bd126e235..fb4df11e8 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 @@ -84,33 +84,42 @@ export class GhostfolioScraperApiService implements DataProviderInterface { public async getQuotes( aSymbols: string[] ): Promise<{ [symbol: string]: IDataProviderResponse }> { + const response: { [symbol: string]: IDataProviderResponse } = {}; + if (aSymbols.length <= 0) { - return {}; + return response; } try { - const [symbol] = aSymbols; - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( - [symbol] + const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( + aSymbols ); - const { marketPrice } = await this.prismaService.marketData.findFirst({ + const marketData = await this.prismaService.marketData.findMany({ + distinct: ['symbol'], orderBy: { date: 'desc' }, + take: aSymbols.length, where: { - symbol + symbol: { + in: aSymbols + } } }); - return { - [symbol]: { - marketPrice, - currency: symbolProfile?.currency, + for (const symbolProfile of symbolProfiles) { + response[symbolProfile.symbol] = { + currency: symbolProfile.currency, dataSource: this.getName(), + marketPrice: marketData.find((marketDataItem) => { + return marketDataItem.symbol === symbolProfile.symbol; + }).marketPrice, marketState: MarketState.delayed - } - }; + }; + } + + return response; } catch (error) { Logger.error(error, 'GhostfolioScraperApiService'); } From 6e582fe50517fd7b7673b301c00ae057c49b4213 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 16 Mar 2022 22:08:48 +0100 Subject: [PATCH 206/337] Release 1.127.0 (#759) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691f1b259..ebe008da7 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.127.0 - 16.03.2022 ### Changed diff --git a/package.json b/package.json index 74c29aee8..ceb78f57a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.126.0", + "version": "1.127.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From eb0444603b6d8bc99d1cc3d942244548de394207 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Mar 2022 09:25:20 +0100 Subject: [PATCH 207/337] Bugfix/fix user currency of public page (#761) * Fix user currency * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/portfolio/portfolio.service-new.ts | 5 ++++- apps/api/src/app/portfolio/portfolio.service.ts | 5 ++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe008da7..46f78f8ec 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 + +- Fixed an issue with the user currency of the public page + ## 1.127.0 - 16.03.2022 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index f11909b85..2ea49de8f 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -307,7 +307,10 @@ export class PortfolioServiceNew { const emergencyFund = new Big( (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); - const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; + const userCurrency = + this.request.user?.Settings?.currency ?? + user.Settings?.currency ?? + baseCurrency; const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f7902a2d7..a00b0cabd 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -298,7 +298,10 @@ export class PortfolioService { const emergencyFund = new Big( (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); - const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; + const userCurrency = + this.request.user?.Settings?.currency ?? + user.Settings?.currency ?? + baseCurrency; const portfolioCalculator = new PortfolioCalculator( this.currentRateService, userCurrency From 30411b1502665a3648169c1f4efb3cabc28f2140 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Mar 2022 09:56:50 +0100 Subject: [PATCH 208/337] Feature/add hover to table (#760) * Add hover * Update changelog --- CHANGELOG.md | 4 ++++ apps/client/src/styles/table.scss | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f78f8ec..803f39c44 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 a hover effect to the table style + ### Fixed - Fixed an issue with the user currency of the public page diff --git a/apps/client/src/styles/table.scss b/apps/client/src/styles/table.scss index 49fcd6df0..8131c3dad 100644 --- a/apps/client/src/styles/table.scss +++ b/apps/client/src/styles/table.scss @@ -11,20 +11,22 @@ .mat-row { &:nth-child(even) { - background-color: rgba( - var(--palette-foreground-base), - var(--palette-background-hover-alpha) - ); + background-color: rgba(var(--palette-foreground-base), 0.02); + } + + &:hover { + background-color: rgba(var(--palette-foreground-base), 0.05); } } @if $darkTheme { .mat-row { &:nth-child(even) { - background-color: rgba( - var(--palette-foreground-base-dark), - var(--palette-background-hover-alpha) - ); + background-color: rgba(var(--palette-foreground-base-dark), 0.02); + } + + &:hover { + background-color: rgba(var(--palette-foreground-base-dark), 0.05); } } } From 32fb3551dccf6fd736d6fe620c31c60fa2f4184f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Mar 2022 12:17:28 +0100 Subject: [PATCH 209/337] Feature/add default market price to scraper configuration (#762) * Add default market price to scraper configuration * Update changelog --- CHANGELOG.md | 1 + .../ghostfolio-scraper-api.service.ts | 24 ++++++++++++++++--- .../scraper-configuration.interface.ts | 1 + .../src/services/symbol-profile.service.ts | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803f39c44..6c53c1503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added the attribute `defaultMarketPrice` to the scraper configuration to improve the support for bonds - Added a hover effect to the table style ### Fixed 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 fb4df11e8..58ac5de3c 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 @@ -13,7 +13,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import * as cheerio from 'cheerio'; -import { format } from 'date-fns'; +import { addDays, format, isBefore } from 'date-fns'; @Injectable() export class GhostfolioScraperApiService implements DataProviderInterface { @@ -50,9 +50,27 @@ export class GhostfolioScraperApiService implements DataProviderInterface { const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [symbol] ); - const { selector, url } = symbolProfile.scraperConfiguration; + const { defaultMarketPrice, selector, url } = + symbolProfile.scraperConfiguration; + + if (defaultMarketPrice) { + const historical: { + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + } = { + [symbol]: {} + }; + let date = from; + + while (isBefore(date, to)) { + historical[symbol][format(date, DATE_FORMAT)] = { + marketPrice: defaultMarketPrice + }; + + date = addDays(date, 1); + } - if (selector === undefined || url === undefined) { + return historical; + } else if (selector === undefined || url === undefined) { return {}; } diff --git a/apps/api/src/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface.ts index 21380253e..9bae19418 100644 --- a/apps/api/src/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface.ts +++ b/apps/api/src/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface.ts @@ -1,4 +1,5 @@ export interface ScraperConfiguration { + defaultMarketPrice?: number; selector: string; url: string; } diff --git a/apps/api/src/services/symbol-profile.service.ts b/apps/api/src/services/symbol-profile.service.ts index b3c6a4c21..b45c44af9 100644 --- a/apps/api/src/services/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile.service.ts @@ -79,6 +79,7 @@ export class SymbolProfileService { if (scraperConfiguration) { return { + defaultMarketPrice: scraperConfiguration.defaultMarketPrice as number, selector: scraperConfiguration.selector as string, url: scraperConfiguration.url as string }; From 9f67993c0352de460032b2d35395d1a7abb659eb Mon Sep 17 00:00:00 2001 From: gizmodus Date: Sat, 19 Mar 2022 14:33:43 +0100 Subject: [PATCH 210/337] Feature/fix issue with recent transactions (#750) * Fix percentage performance issue with recent transactions Co-authored-by: Reto Kaul --- .../app/portfolio/portfolio-calculator-new.ts | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index d1ed8fa91..6a5659d85 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -37,6 +37,9 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in import { TransactionPoint } from './interfaces/transaction-point.interface'; export class PortfolioCalculatorNew { + private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = + true; + private static readonly ENABLE_LOGGING = false; private currency: string; @@ -688,6 +691,7 @@ export class PortfolioCalculatorNew { let grossPerformanceAtStartDate = new Big(0); let grossPerformanceFromSells = new Big(0); let initialValue: Big; + let investmentAtStartDate: Big; let lastAveragePrice = new Big(0); let lastTransactionInvestment = new Big(0); let lastValueOfInvestmentBeforeTransaction = new Big(0); @@ -697,6 +701,7 @@ export class PortfolioCalculatorNew { let totalInvestment = new Big(0); let totalInvestmentWithGrossPerformanceFromSell = new Big(0); let totalUnits = new Big(0); + let valueAtStartDate: Big; // Add a synthetic order at the start and the end date orders.push({ @@ -774,13 +779,18 @@ export class PortfolioCalculatorNew { order.unitPrice ); + if (!investmentAtStartDate && i >= indexOfStartOrder) { + investmentAtStartDate = totalInvestment ?? new Big(0); + valueAtStartDate = valueOfInvestmentBeforeTransaction; + } + const transactionInvestment = order.quantity .mul(order.unitPrice) .mul(this.getFactor(order.type)); totalInvestment = totalInvestment.plus(transactionInvestment); - if (totalInvestment.gt(maxTotalInvestment)) { + if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) { maxTotalInvestment = totalInvestment; } @@ -898,12 +908,22 @@ export class PortfolioCalculatorNew { .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); + const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( + maxTotalInvestment.minus(investmentAtStartDate) + ); + const grossPerformancePercentage = + PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || averagePriceAtStartDate.eq(0) || averagePriceAtEndDate.eq(0) || orders[indexOfStartOrder].unitPrice.eq(0) - ? totalGrossPerformance.div(maxTotalInvestment) - : unitPriceAtEndDate + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + // lower than the average buying price results in a positive + // performance even if the market pricse stays constant + unitPriceAtEndDate .div(averagePriceAtEndDate) .div( orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) @@ -915,11 +935,17 @@ export class PortfolioCalculatorNew { : new Big(0); const netPerformancePercentage = + PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || averagePriceAtStartDate.eq(0) || averagePriceAtEndDate.eq(0) || orders[indexOfStartOrder].unitPrice.eq(0) - ? totalNetPerformance.div(maxTotalInvestment) - : unitPriceAtEndDate + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + // lower than the average buying price results in a positive + // performance even if the market pricse stays constant + unitPriceAtEndDate .minus(feesPerUnit) .div(averagePriceAtEndDate) .div( From 86a1589834c95c520765fb3c39cfef8119bed9d7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Mar 2022 14:39:25 +0100 Subject: [PATCH 211/337] Release/1.128.0 (#763) --- CHANGELOG.md | 3 ++- apps/api/src/app/portfolio/portfolio-calculator-new.ts | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c53c1503..1ca220626 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.128.0 - 19.03.2022 ### Added @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with the user currency of the public page +- Fixed an issue of the performance calculation with recent activities in the new calculation engine ## 1.127.0 - 16.03.2022 diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 6a5659d85..3b8d30cf8 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -922,7 +922,7 @@ export class PortfolioCalculatorNew { : new Big(0) : // This formula has the issue that buying more units with a price // lower than the average buying price results in a positive - // performance even if the market pricse stays constant + // performance even if the market price stays constant unitPriceAtEndDate .div(averagePriceAtEndDate) .div( @@ -944,7 +944,7 @@ export class PortfolioCalculatorNew { : new Big(0) : // This formula has the issue that buying more units with a price // lower than the average buying price results in a positive - // performance even if the market pricse stays constant + // performance even if the market price stays constant unitPriceAtEndDate .minus(feesPerUnit) .div(averagePriceAtEndDate) diff --git a/package.json b/package.json index ceb78f57a..3f4b66808 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.127.0", + "version": "1.128.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From e2faaf6faafd8e8d30392520dcc253771f62ac54 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 21 Mar 2022 18:59:00 +0100 Subject: [PATCH 212/337] Feature/add hover effect to page tabs (#764) * Add hover effect * Update changelog --- CHANGELOG.md | 6 ++++++ apps/client/src/app/pages/admin/admin-page.scss | 6 ++++++ apps/client/src/app/pages/home/home-page.scss | 6 ++++++ apps/client/src/app/pages/zen/zen-page.scss | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca220626..a4e5f976a 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 a hover effect to the page tabs + ## 1.128.0 - 19.03.2022 ### Added diff --git a/apps/client/src/app/pages/admin/admin-page.scss b/apps/client/src/app/pages/admin/admin-page.scss index d9958edc3..31ff834c7 100644 --- a/apps/client/src/app/pages/admin/admin-page.scss +++ b/apps/client/src/app/pages/admin/admin-page.scss @@ -29,6 +29,12 @@ color: rgba(var(--palette-primary-500), 1); opacity: 1; } + + .mat-tab-link { + &:hover { + opacity: 0.75; + } + } } } } diff --git a/apps/client/src/app/pages/home/home-page.scss b/apps/client/src/app/pages/home/home-page.scss index 344e79d3c..ecb03cf29 100644 --- a/apps/client/src/app/pages/home/home-page.scss +++ b/apps/client/src/app/pages/home/home-page.scss @@ -30,6 +30,12 @@ color: rgba(var(--palette-primary-500), 1); opacity: 1; } + + .mat-tab-link { + &:hover { + opacity: 0.75; + } + } } } } diff --git a/apps/client/src/app/pages/zen/zen-page.scss b/apps/client/src/app/pages/zen/zen-page.scss index 5f26d0dfc..73e482c91 100644 --- a/apps/client/src/app/pages/zen/zen-page.scss +++ b/apps/client/src/app/pages/zen/zen-page.scss @@ -28,6 +28,12 @@ color: rgba(var(--palette-primary-500), 1); opacity: 1; } + + .mat-tab-link { + &:hover { + opacity: 0.75; + } + } } } } From 2b4319454d8b669ead21185bf14af876f9fc0146 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 25 Mar 2022 23:37:06 +0100 Subject: [PATCH 213/337] Feature/add bonds and emergency fund to feature overview (#768) * Add bonds and emergency fund * Update changelog --- CHANGELOG.md | 1 + .../src/app/pages/features/features-page.html | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e5f976a..e57661406 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 ### Added - Added a hover effect to the page tabs +- Extended the feature overview page by _Bonds_ and _Emergency Fund_ ## 1.128.0 - 19.03.2022 diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 42c460542..3e1a4a9e0 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -32,6 +32,17 @@
+
+ +
+

Bonds

+

+ Manage your investment in bonds and other assets with fixed + income. +

+
+
+
@@ -64,6 +75,17 @@
+
+ +
+

Emergency Fund

+

+ Define your emergency fund you are comfortable with for + difficult times. +

+
+
+
From b4848be91438db213f4d051d1a4e9a601b77f9df Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:11:30 +0100 Subject: [PATCH 214/337] Feature/add developed vs emerging markets calculation (#767) * Add allocations by market * Update changelog --- CHANGELOG.md | 1 + .../portfolio/portfolio-service.strategy.ts | 3 +- .../src/app/portfolio/portfolio.controller.ts | 5 ++- .../app/portfolio/portfolio.service-new.ts | 28 +++++++++++++ .../assets/countries/developed-markets.json | 26 ++++++++++++ .../assets/countries/emerging-markets.json | 28 +++++++++++++ .../allocations/allocations-page.component.ts | 41 ++++++++++++++++++- .../allocations/allocations-page.html | 26 ++++++++++++ .../allocations/allocations-page.module.ts | 2 + .../app/pages/public/public-page.component.ts | 37 +++++++++++++++++ .../src/app/pages/public/public-page.html | 28 ++++++++++++- .../app/pages/public/public-page.module.ts | 2 + .../portfolio-position.interface.ts | 2 + .../portfolio-public-details.interface.ts | 1 + libs/common/src/lib/types/index.ts | 2 + libs/common/src/lib/types/market.type.ts | 1 + libs/ui/src/lib/value/value.component.html | 11 +++-- libs/ui/src/lib/value/value.component.ts | 4 +- 18 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/assets/countries/developed-markets.json create mode 100644 apps/api/src/assets/countries/emerging-markets.json create mode 100644 libs/common/src/lib/types/market.type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e57661406..2034a8f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added the calculation for developed vs. emerging markets to the allocations page - Added a hover effect to the page tabs - Extended the feature overview page by _Bonds_ and _Emergency Fund_ diff --git a/apps/api/src/app/portfolio/portfolio-service.strategy.ts b/apps/api/src/app/portfolio/portfolio-service.strategy.ts index 49ec0422f..a85b28852 100644 --- a/apps/api/src/app/portfolio/portfolio-service.strategy.ts +++ b/apps/api/src/app/portfolio/portfolio-service.strategy.ts @@ -13,8 +13,9 @@ export class PortfolioServiceStrategy { @Inject(REQUEST) private readonly request: RequestWithUser ) {} - public get() { + public get(newCalculationEngine?: boolean) { if ( + newCalculationEngine || this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true ) { return this.portfolioServiceNew; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 2d7993859..1bb42a0ed 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -120,7 +120,7 @@ export class PortfolioController { const { accounts, holdings, hasErrors } = await this.portfolioServiceStrategy - .get() + .get(true) .getDetails(impersonationId, this.request.user.id, range); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -277,7 +277,7 @@ export class PortfolioController { } const { holdings } = await this.portfolioServiceStrategy - .get() + .get(true) .getDetails(access.userId, access.userId); const portfolioPublicDetails: PortfolioPublicDetails = { @@ -304,6 +304,7 @@ export class PortfolioController { allocationCurrent: portfolioPosition.allocationCurrent, countries: hasDetails ? portfolioPosition.countries : [], currency: portfolioPosition.currency, + markets: portfolioPosition.markets, name: portfolioPosition.name, sectors: hasDetails ? portfolioPosition.sectors : [], value: portfolioPosition.value / totalValue diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 2ea49de8f..63b8544a8 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -40,6 +40,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in import type { AccountWithValue, DateRange, + Market, OrderWithAccount, RequestWithUser } from '@ghostfolio/common/types'; @@ -71,6 +72,9 @@ import { import { PortfolioCalculatorNew } from './portfolio-calculator-new'; import { RulesService } from './rules.service'; +const developedMarkets = require('../../assets/countries/developed-markets.json'); +const emergingMarkets = require('../../assets/countries/emerging-markets.json'); + @Injectable() export class PortfolioServiceNew { public constructor( @@ -380,7 +384,31 @@ export class PortfolioServiceNew { const value = item.quantity.mul(item.marketPrice); const symbolProfile = symbolProfileMap[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol]; + + const markets: { [key in Market]: number } = { + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + + for (const country of symbolProfile.countries) { + if (developedMarkets.includes(country.code)) { + markets.developedMarkets = new Big(markets.developedMarkets) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + markets.emergingMarkets = new Big(markets.emergingMarkets) + .plus(country.weight) + .toNumber(); + } else { + markets.otherMarkets = new Big(markets.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + holdings[item.symbol] = { + markets, allocationCurrent: value.div(totalValue).toNumber(), allocationInvestment: item.investment.div(totalInvestment).toNumber(), assetClass: symbolProfile.assetClass, diff --git a/apps/api/src/assets/countries/developed-markets.json b/apps/api/src/assets/countries/developed-markets.json new file mode 100644 index 000000000..5e281d475 --- /dev/null +++ b/apps/api/src/assets/countries/developed-markets.json @@ -0,0 +1,26 @@ +[ + "AT", + "AU", + "BE", + "CA", + "CH", + "DE", + "DK", + "ES", + "FI", + "FR", + "GB", + "HK", + "IE", + "IL", + "IT", + "JP", + "LU", + "NL", + "NO", + "NZ", + "PT", + "SE", + "SG", + "US" +] diff --git a/apps/api/src/assets/countries/emerging-markets.json b/apps/api/src/assets/countries/emerging-markets.json new file mode 100644 index 000000000..328187964 --- /dev/null +++ b/apps/api/src/assets/countries/emerging-markets.json @@ -0,0 +1,28 @@ +[ + "AE", + "BR", + "CL", + "CN", + "CO", + "CY", + "CZ", + "EG", + "GR", + "HK", + "HU", + "ID", + "IN", + "KR", + "KW", + "MX", + "MY", + "PE", + "PH", + "PL", + "QA", + "SA", + "TH", + "TR", + "TW", + "ZA" +] 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 2ca5f6930..5d7be99c4 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 { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { ToggleOption } from '@ghostfolio/common/types'; +import { Market, ToggleOption } from '@ghostfolio/common/types'; import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; @@ -42,6 +42,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; + public markets: { + [key in Market]: { name: string; value: number }; + }; public period = 'current'; public periodOptions: ToggleOption[] = [ { label: 'Initial', value: 'original' }, @@ -160,6 +163,20 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: 0 } }; + this.markets = { + developedMarkets: { + name: 'developedMarkets', + value: 0 + }, + emergingMarkets: { + name: 'emergingMarkets', + value: 0 + }, + otherMarkets: { + name: 'otherMarkets', + value: 0 + } + }; this.positions = {}; this.positionsArray = []; this.sectors = { @@ -219,6 +236,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { // Prepare analysis data by continents, countries and sectors except for cash if (position.countries.length > 0) { + this.markets.developedMarkets.value += + position.markets.developedMarkets * + (aPeriod === 'original' ? position.investment : position.value); + this.markets.emergingMarkets.value += + position.markets.emergingMarkets * + (aPeriod === 'original' ? position.investment : position.value); + this.markets.otherMarkets.value += + position.markets.otherMarkets * + (aPeriod === 'original' ? position.investment : position.value); + for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -294,6 +321,18 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; } } + + const marketsTotal = + this.markets.developedMarkets.value + + this.markets.emergingMarkets.value + + this.markets.otherMarkets.value; + + this.markets.developedMarkets.value = + this.markets.developedMarkets.value / marketsTotal; + this.markets.emergingMarkets.value = + this.markets.emergingMarkets.value / marketsTotal; + this.markets.otherMarkets.value = + this.markets.otherMarkets.value / marketsTotal; } public onChangePeriod(aValue: string) { 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 276de32ce..679570998 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -190,6 +190,32 @@ [countries]="countries" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" > +
+
+ +
+
+ +
+
+ +
+
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts index 809b29bd7..62dd05e4e 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts @@ -5,6 +5,7 @@ import { GfPositionsTableModule } from '@ghostfolio/client/components/positions- import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { AllocationsPageRoutingModule } from './allocations-page-routing.module'; import { AllocationsPageComponent } from './allocations-page.component'; @@ -19,6 +20,7 @@ import { AllocationsPageComponent } from './allocations-page.component'; GfPositionsTableModule, GfToggleModule, GfWorldMapChartModule, + GfValueModule, MatCardModule ], providers: [], diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index a3ac43721..aea98e471 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -7,6 +7,7 @@ import { PortfolioPosition, PortfolioPublicDetails } from '@ghostfolio/common/interfaces'; +import { Market } from '@ghostfolio/common/types'; import { StatusCodes } from 'http-status-codes'; import { DeviceDetectorService } from 'ngx-device-detector'; import { EMPTY, Subject } from 'rxjs'; @@ -26,6 +27,9 @@ export class PublicPageComponent implements OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; + public markets: { + [key in Market]: { name: string; value: number }; + }; public portfolioPublicDetails: PortfolioPublicDetails; public positions: { [symbol: string]: Pick; @@ -96,6 +100,20 @@ export class PublicPageComponent implements OnInit { value: 0 } }; + this.markets = { + developedMarkets: { + name: 'developedMarkets', + value: 0 + }, + emergingMarkets: { + name: 'emergingMarkets', + value: 0 + }, + otherMarkets: { + name: 'otherMarkets', + value: 0 + } + }; this.positions = {}; this.sectors = { [UNKNOWN_KEY]: { @@ -123,6 +141,13 @@ export class PublicPageComponent implements OnInit { }; if (position.countries.length > 0) { + this.markets.developedMarkets.value += + position.markets.developedMarkets * position.value; + this.markets.emergingMarkets.value += + position.markets.emergingMarkets * position.value; + this.markets.otherMarkets.value += + position.markets.otherMarkets * position.value; + for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -176,6 +201,18 @@ export class PublicPageComponent implements OnInit { value: position.value }; } + + const marketsTotal = + this.markets.developedMarkets.value + + this.markets.emergingMarkets.value + + this.markets.otherMarkets.value; + + this.markets.developedMarkets.value = + this.markets.developedMarkets.value / marketsTotal; + this.markets.emergingMarkets.value = + this.markets.emergingMarkets.value / marketsTotal; + this.markets.otherMarkets.value = + this.markets.otherMarkets.value / marketsTotal; } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 49edef614..5af657d67 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -79,12 +79,38 @@ [countries]="countries" [isInPercent]="true" > +
+
+ +
+
+ +
+
+ +
+
-
+

Would you like to refine your personal investment strategy? diff --git a/apps/client/src/app/pages/public/public-page.module.ts b/apps/client/src/app/pages/public/public-page.module.ts index e7818ede2..21a3b4f7b 100644 --- a/apps/client/src/app/pages/public/public-page.module.ts +++ b/apps/client/src/app/pages/public/public-page.module.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { PublicPageRoutingModule } from './public-page-routing.module'; import { PublicPageComponent } from './public-page.component'; @@ -14,6 +15,7 @@ import { PublicPageComponent } from './public-page.component'; imports: [ CommonModule, GfPortfolioProportionChartModule, + GfValueModule, GfWorldMapChartModule, MatButtonModule, MatCardModule, diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 87ee917d4..1b5714a9e 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,6 +1,7 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { Market } from '../types'; import { Country } from './country.interface'; import { Sector } from './sector.interface'; @@ -19,6 +20,7 @@ export interface PortfolioPosition { marketChange?: number; marketChangePercent?: number; marketPrice: number; + markets?: { [key in Market]: number }; marketState: MarketState; name: string; netPerformance: number; diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts index b24df034e..10fb4edfd 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts @@ -8,6 +8,7 @@ export interface PortfolioPublicDetails { | 'allocationCurrent' | 'countries' | 'currency' + | 'markets' | 'name' | 'sectors' | 'value' diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 6953b5863..40ba51272 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -2,6 +2,7 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import { AccountWithValue } from './account-with-value.type'; import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; +import { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; import type { RequestWithUser } from './request-with-user.type'; import { ToggleOption } from './toggle-option.type'; @@ -11,6 +12,7 @@ export type { AccountWithValue, DateRange, Granularity, + Market, OrderWithAccount, RequestWithUser, ToggleOption diff --git a/libs/common/src/lib/types/market.type.ts b/libs/common/src/lib/types/market.type.ts new file mode 100644 index 000000000..d6981d256 --- /dev/null +++ b/libs/common/src/lib/types/market.type.ts @@ -0,0 +1 @@ +export type Market = 'developedMarkets' | 'emergingMarkets' | 'otherMarkets'; diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index e4f8b0f45..026eb2794 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -43,9 +43,14 @@

- - {{ label }} - + +
+ {{ label }} +
+ + {{ label }} + +
Date: Sat, 26 Mar 2022 13:13:07 +0100 Subject: [PATCH 215/337] Release 1.129.0 (#770) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2034a8f9d..28831ec4f 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.129.0 - 26.03.2022 ### Added diff --git a/package.json b/package.json index 3f4b66808..33affd876 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.128.0", + "version": "1.129.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 14773bf1aab99b40780b03241842eaf55405a177 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 29 Mar 2022 17:47:08 +0200 Subject: [PATCH 216/337] Bugfix/fix duplicate currency conversion in account calculations (#771) * Fix currency conversion (duplicate) * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/portfolio/portfolio.service-new.ts | 12 ++++++------ apps/api/src/app/portfolio/portfolio.service.ts | 12 ++++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28831ec4f..3bac1d543 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 + +- Fixed an issue with the currency conversion (duplicate) in the account calculations + ## 1.129.0 - 26.03.2022 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 63b8544a8..c01f30242 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -111,21 +111,21 @@ export class PortfolioServiceNew { } } - const value = details.accounts[account.id]?.current ?? 0; + const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0; const result = { ...account, transactionCount, - value, + valueInBaseCurrency, balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( account.balance, account.currency, userCurrency ), - valueInBaseCurrency: this.exchangeRateDataService.toCurrency( - value, - account.currency, - userCurrency + value: this.exchangeRateDataService.toCurrency( + valueInBaseCurrency, + userCurrency, + account.currency ) }; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a00b0cabd..f8e617432 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -106,21 +106,21 @@ export class PortfolioService { } } - const value = details.accounts[account.id]?.current ?? 0; + const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0; const result = { ...account, transactionCount, - value, + valueInBaseCurrency, balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( account.balance, account.currency, userCurrency ), - valueInBaseCurrency: this.exchangeRateDataService.toCurrency( - value, - account.currency, - userCurrency + value: this.exchangeRateDataService.toCurrency( + valueInBaseCurrency, + userCurrency, + account.currency ) }; From 57e4163848166da8d841b0633c6356541c92759c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 29 Mar 2022 20:49:44 +0200 Subject: [PATCH 217/337] Feature/add 14 days in coupon system (#772) * Add 14 days * Update changelog --- CHANGELOG.md | 4 ++++ .../src/app/components/admin-overview/admin-overview.html | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bac1d543..06fd92002 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 more durations in the coupon system + ### Fixed - Fixed an issue with the currency conversion (duplicate) in the account calculations 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 2457a12b6..b174ad130 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -180,6 +180,7 @@ [value]="couponDuration" (selectionChange)="onChangeCouponDuration($event.value)" > + 14 Days 30 Days 1 Year From 7f0c98cae68f6656f55fdbe086f85b88e117fef9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 30 Mar 2022 21:14:49 +0200 Subject: [PATCH 218/337] Feature/setup fire page with 4 percent rule (#773) * Setup page for FIRE * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/app-routing.module.ts | 7 ++ .../fire/fire-page-routing.module.ts | 15 ++++ .../portfolio/fire/fire-page.component.ts | 86 +++++++++++++++++++ .../app/pages/portfolio/fire/fire-page.html | 53 ++++++++++++ .../pages/portfolio/fire/fire-page.module.ts | 19 ++++ .../app/pages/portfolio/fire/fire-page.scss | 3 + .../app/pages/portfolio/portfolio-page.html | 27 ++++++ 8 files changed, 211 insertions(+) create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.component.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.html create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.module.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fd92002..dc2de273e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a _FIRE_ (Financial Independence, Retire Early) section including the 4% rule - Added more durations in the coupon system ### Fixed diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 0c6e7c2ab..28a9d1b49 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -113,6 +113,13 @@ const routes: Routes = [ (m) => m.AnalysisPageModule ) }, + { + path: 'portfolio/fire', + loadChildren: () => + import('./pages/portfolio/fire/fire-page.module').then( + (m) => m.FirePageModule + ) + }, { path: 'portfolio/report', loadChildren: () => diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts new file mode 100644 index 000000000..445457e01 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { FirePageComponent } from './fire-page.component'; + +const routes: Routes = [ + { path: '', component: FirePageComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class FirePageRoutingModule {} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts new file mode 100644 index 000000000..1eb132dfd --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -0,0 +1,86 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { User } from '@ghostfolio/common/interfaces'; +import Big from 'big.js'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + host: { class: 'page' }, + selector: 'gf-fire-page', + styleUrls: ['./fire-page.scss'], + templateUrl: './fire-page.html' +}) +export class FirePageComponent implements OnDestroy, OnInit { + public fireWealth: number; + public hasImpersonationId: boolean; + public isLoading = false; + public user: User; + public withdrawalRatePerMonth: number; + public withdrawalRatePerYear: number; + + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, + private impersonationStorageService: ImpersonationStorageService, + private userService: UserService + ) {} + + /** + * Initializes the controller + */ + public ngOnInit() { + this.isLoading = true; + + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + + this.dataService + .fetchPortfolioSummary() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ cash, currentValue }) => { + if (cash === null || currentValue === null) { + return; + } + + this.fireWealth = new Big(currentValue).plus(cash).toNumber(); + this.withdrawalRatePerYear = new Big(this.fireWealth) + .mul(4) + .div(100) + .toNumber(); + this.withdrawalRatePerMonth = new Big(this.withdrawalRatePerYear) + .div(12) + .toNumber(); + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.changeDetectorRef.markForCheck(); + } + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html new file mode 100644 index 000000000..69735c196 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -0,0 +1,53 @@ +
+
+
+

FIRE

+
+

4% Rule

+
+ + +
+
+ If you retire today, you would be able to withdraw + + per year + or + + per month, based on your net worth of + + (excluding emergency fund) and a withdrawal rate of 4%. +
+
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts new file mode 100644 index 000000000..86fb0a953 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { GfValueModule } from '@ghostfolio/ui/value'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { FirePageRoutingModule } from './fire-page-routing.module'; +import { FirePageComponent } from './fire-page.component'; + +@NgModule({ + declarations: [FirePageComponent], + imports: [ + CommonModule, + FirePageRoutingModule, + GfValueModule, + NgxSkeletonLoaderModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class FirePageModule {} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.html b/apps/client/src/app/pages/portfolio/portfolio-page.html index 7c221d9d5..d0717805d 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.html +++ b/apps/client/src/app/pages/portfolio/portfolio-page.html @@ -101,5 +101,32 @@
+
+ +

+ FIRE + +

+
+ Ghostfolio FIRE calculates metrics for the + Financial Independence, Retire Early lifestyle. +
+ +
+
From 16b79a7e605e16f4232d1aa36ced59cfa29a7da0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 30 Mar 2022 21:16:43 +0200 Subject: [PATCH 219/337] Release 1.130.0 (#774) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2de273e..08da70cba 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.130.0 - 30.03.2022 ### Added diff --git a/package.json b/package.json index 33affd876..9d4e00129 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.129.0", + "version": "1.130.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From d530cb38fa96728b6e65095189c15e57389bd39b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 1 Apr 2022 19:49:05 +0200 Subject: [PATCH 220/337] Feature/add 7 days in coupon system (#777) * Add 7 days * Update changelog --- CHANGELOG.md | 6 ++++++ .../src/app/components/admin-overview/admin-overview.html | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08da70cba..667f5b3e0 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 more durations in the coupon system + ## 1.130.0 - 30.03.2022 ### Added 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 b174ad130..6118fccaa 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -180,6 +180,7 @@ [value]="couponDuration" (selectionChange)="onChangeCouponDuration($event.value)" > + 7 Days 14 Days 30 Days 1 Year From dd71f2be458eb4ba06352cf08d76f085ebe1a9fa Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 1 Apr 2022 19:58:33 +0200 Subject: [PATCH 221/337] Feature/improve pricing page (#778) * Improve pricing page * Update changelog --- CHANGELOG.md | 4 ++++ .../src/app/pages/pricing/pricing-page.html | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667f5b3e0..8c969c176 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 more durations in the coupon system +### Changed + +- Improved the pricing page + ## 1.130.0 - 30.03.2022 ### Added diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index 60e44f700..4e6c5c705 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -178,16 +178,19 @@

Fully managed Ghostfolio cloud offering.

- {{ baseCurrency }} + {{ price - coupon | number : '1.2-2' }} - {{ price }} + >{{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ price - coupon }} {{ price }} - per year{{ baseCurrency }} {{ price }} per year

From a7b59f4ec672e1551ecd3c4b146b972286def3f5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 06:47:58 +0200 Subject: [PATCH 222/337] Extend prerequisites (#781) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5280d46fe..955ae07bc 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater ### Prerequisites - [Docker](https://www.docker.com/products/docker-desktop) +- A local copy of this Git repository (clone) ### a. Run environment @@ -136,6 +137,7 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat - [Docker](https://www.docker.com/products/docker-desktop) - [Node.js](https://nodejs.org/en/download) (version 14+) - [Yarn](https://yarnpkg.com/en/docs/install) +- A local copy of this Git repository (clone) ### Setup From eb77652d6a73ec12fb985840f51abf0423b88957 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:03:17 +0200 Subject: [PATCH 223/337] Clean up import (#784) --- libs/ui/src/lib/activities-table/activities-table.component.ts | 1 - 1 file changed, 1 deletion(-) 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 9fce3f703..73182bced 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -23,7 +23,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf 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'; import { isUUID } from 'class-validator'; import { endOfToday, format, isAfter } from 'date-fns'; From 676257265886e7ad765e62beaad83ead30fc9073 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:46:24 +0200 Subject: [PATCH 224/337] Feature/setup api versioning (#783) * Setup API versioning * Update changelog --- CHANGELOG.md | 1 + apps/api/src/main.ts | 9 ++- .../login-with-access-token-dialog.html | 2 +- .../src/app/pages/register/register-page.html | 2 +- apps/client/src/app/services/admin.service.ts | 16 ++--- apps/client/src/app/services/cache.service.ts | 2 +- apps/client/src/app/services/data.service.ts | 68 +++++++++---------- .../services/import-transactions.service.ts | 2 +- .../src/app/services/user/user.service.ts | 2 +- .../src/app/services/web-authn.service.ts | 30 ++++---- apps/client/src/main.ts | 2 +- 11 files changed, 70 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c969c176..be4fb1f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added API versioning - Added more durations in the coupon system ### Changed diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 5a077910e..cf565a36d 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,4 +1,4 @@ -import { Logger, ValidationPipe } from '@nestjs/common'; +import { Logger, ValidationPipe, VersioningType } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; @@ -7,8 +7,11 @@ import { environment } from './environments/environment'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); - const globalPrefix = 'api'; - app.setGlobalPrefix(globalPrefix); + app.enableVersioning({ + defaultVersion: '1', + type: VersioningType.URI + }); + app.setGlobalPrefix('api'); app.useGlobalPipes( new ValidationPipe({ forbidNonWhitelisted: true, diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html index 614c28bfe..2186368ca 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -8,7 +8,7 @@
- Sign in with Google diff --git a/apps/client/src/app/pages/register/register-page.html b/apps/client/src/app/pages/register/register-page.html index 96d49251b..fd8c07f8d 100644 --- a/apps/client/src/app/pages/register/register-page.html +++ b/apps/client/src/app/pages/register/register-page.html @@ -35,7 +35,7 @@ > or
- Continue with Google diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index cf9b36b1f..ecb33c0df 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -19,7 +19,7 @@ export class AdminService { public deleteProfileData({ dataSource, symbol }: UniqueAsset) { return this.http.delete( - `/api/admin/profile-data/${dataSource}/${symbol}` + `/api/v1/admin/profile-data/${dataSource}/${symbol}` ); } @@ -31,7 +31,7 @@ export class AdminService { symbol: string; }): Observable { return this.http - .get(`/api/admin/market-data/${dataSource}/${symbol}`) + .get(`/api/v1/admin/market-data/${dataSource}/${symbol}`) .pipe( map((data) => { for (const item of data.marketData) { @@ -43,16 +43,16 @@ export class AdminService { } public gatherMax() { - return this.http.post(`/api/admin/gather/max`, {}); + return this.http.post(`/api/v1/admin/gather/max`, {}); } public gatherProfileData() { - return this.http.post(`/api/admin/gather/profile-data`, {}); + return this.http.post(`/api/v1/admin/gather/profile-data`, {}); } public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { return this.http.post( - `/api/admin/gather/profile-data/${dataSource}/${symbol}`, + `/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`, {} ); } @@ -64,7 +64,7 @@ export class AdminService { }: UniqueAsset & { date?: Date; }) { - let url = `/api/admin/gather/${dataSource}/${symbol}`; + let url = `/api/v1/admin/gather/${dataSource}/${symbol}`; if (date) { url = `${url}/${format(date, DATE_FORMAT)}`; @@ -82,7 +82,7 @@ export class AdminService { date: Date; symbol: string; }) { - const url = `/api/symbol/${dataSource}/${symbol}/${format( + const url = `/api/v1/symbol/${dataSource}/${symbol}/${format( date, DATE_FORMAT )}`; @@ -101,7 +101,7 @@ export class AdminService { marketData: UpdateMarketDataDto; symbol: string; }) { - const url = `/api/admin/market-data/${dataSource}/${symbol}/${format( + const url = `/api/v1/admin/market-data/${dataSource}/${symbol}/${format( date, DATE_FORMAT )}`; diff --git a/apps/client/src/app/services/cache.service.ts b/apps/client/src/app/services/cache.service.ts index bcf17f574..69a85f926 100644 --- a/apps/client/src/app/services/cache.service.ts +++ b/apps/client/src/app/services/cache.service.ts @@ -8,6 +8,6 @@ export class CacheService { public constructor(private http: HttpClient) {} public flush() { - return this.http.post(`/api/cache/flush`, {}); + return this.http.post(`/api/v1/cache/flush`, {}); } } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index c61730d3c..b51a00ce8 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -23,12 +23,10 @@ import { PortfolioChart, PortfolioDetails, PortfolioInvestments, - PortfolioPerformance, PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, PortfolioSummary, - UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -52,46 +50,46 @@ export class DataService { couponId?: string; priceId: string; }) { - return this.http.post('/api/subscription/stripe/checkout-session', { + return this.http.post('/api/v1/subscription/stripe/checkout-session', { couponId, priceId }); } public fetchAccounts() { - return this.http.get('/api/account'); + return this.http.get('/api/v1/account'); } public fetchAdminData() { - return this.http.get('/api/admin'); + return this.http.get('/api/v1/admin'); } public fetchAdminMarketData() { - return this.http.get('/api/admin/market-data'); + return this.http.get('/api/v1/admin/market-data'); } public deleteAccess(aId: string) { - return this.http.delete(`/api/access/${aId}`); + return this.http.delete(`/api/v1/access/${aId}`); } public deleteAccount(aId: string) { - return this.http.delete(`/api/account/${aId}`); + return this.http.delete(`/api/v1/account/${aId}`); } public deleteOrder(aId: string) { - return this.http.delete(`/api/order/${aId}`); + return this.http.delete(`/api/v1/order/${aId}`); } public deleteUser(aId: string) { - return this.http.delete(`/api/user/${aId}`); + return this.http.delete(`/api/v1/user/${aId}`); } public fetchAccesses() { - return this.http.get('/api/access'); + return this.http.get('/api/v1/access'); } public fetchChart({ range }: { range: DateRange }) { - return this.http.get('/api/portfolio/chart', { + return this.http.get('/api/v1/portfolio/chart', { params: { range } }); } @@ -103,7 +101,7 @@ export class DataService { params = params.append('activityIds', activityIds.join(',')); } - return this.http.get('/api/export', { + return this.http.get('/api/v1/export', { params }); } @@ -121,7 +119,7 @@ export class DataService { } public fetchInvestments(): Observable { - return this.http.get('/api/portfolio/investments').pipe( + return this.http.get('/api/v1/portfolio/investments').pipe( map((response) => { if (response.firstOrderDate) { response.firstOrderDate = parseISO(response.firstOrderDate); @@ -147,7 +145,7 @@ export class DataService { params = params.append('includeHistoricalData', includeHistoricalData); } - return this.http.get(`/api/symbol/${dataSource}/${symbol}`, { + return this.http.get(`/api/v1/symbol/${dataSource}/${symbol}`, { params }); } @@ -157,14 +155,14 @@ export class DataService { }: { range: DateRange; }): Observable { - return this.http.get('/api/portfolio/positions', { + return this.http.get('/api/v1/portfolio/positions', { params: { range } }); } public fetchSymbols(aQuery: string) { return this.http - .get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`) + .get<{ items: LookupItem[] }>(`/api/v1/symbol/lookup?query=${aQuery}`) .pipe( map((respose) => { return respose.items; @@ -173,7 +171,7 @@ export class DataService { } public fetchOrders(): Observable { - return this.http.get('/api/order').pipe( + return this.http.get('/api/v1/order').pipe( map(({ activities }) => { for (const activity of activities) { activity.createdAt = parseISO(activity.createdAt); @@ -185,14 +183,14 @@ export class DataService { } public fetchPortfolioDetails(aParams: { [param: string]: any }) { - return this.http.get('/api/portfolio/details', { + return this.http.get('/api/v1/portfolio/details', { params: aParams }); } public fetchPortfolioPerformance(params: { [param: string]: any }) { return this.http.get( - '/api/portfolio/performance', + '/api/v1/portfolio/performance', { params } @@ -201,16 +199,16 @@ export class DataService { public fetchPortfolioPublic(aId: string) { return this.http.get( - `/api/portfolio/public/${aId}` + `/api/v1/portfolio/public/${aId}` ); } public fetchPortfolioReport() { - return this.http.get('/api/portfolio/report'); + return this.http.get('/api/v1/portfolio/report'); } public fetchPortfolioSummary(): Observable { - return this.http.get('/api/portfolio/summary').pipe( + return this.http.get('/api/v1/portfolio/summary').pipe( map((summary) => { if (summary.firstOrderDate) { summary.firstOrderDate = parseISO(summary.firstOrderDate); @@ -229,7 +227,7 @@ export class DataService { symbol: string; }) { return this.http - .get(`/api/portfolio/position/${dataSource}/${symbol}`) + .get(`/api/v1/portfolio/position/${dataSource}/${symbol}`) .pipe( map((data) => { if (data.orders) { @@ -245,47 +243,47 @@ export class DataService { } public loginAnonymous(accessToken: string) { - return this.http.get(`/api/auth/anonymous/${accessToken}`); + return this.http.get(`/api/v1/auth/anonymous/${accessToken}`); } public postAccess(aAccess: CreateAccessDto) { - return this.http.post(`/api/access`, aAccess); + return this.http.post(`/api/v1/access`, aAccess); } public postAccount(aAccount: CreateAccountDto) { - return this.http.post(`/api/account`, aAccount); + return this.http.post(`/api/v1/account`, aAccount); } public postOrder(aOrder: CreateOrderDto) { - return this.http.post(`/api/order`, aOrder); + return this.http.post(`/api/v1/order`, aOrder); } public postUser() { - return this.http.post(`/api/user`, {}); + return this.http.post(`/api/v1/user`, {}); } public putAccount(aAccount: UpdateAccountDto) { - return this.http.put(`/api/account/${aAccount.id}`, aAccount); + return this.http.put(`/api/v1/account/${aAccount.id}`, aAccount); } public putAdminSetting(key: string, aData: PropertyDto) { - return this.http.put(`/api/admin/settings/${key}`, aData); + return this.http.put(`/api/v1/admin/settings/${key}`, aData); } public putOrder(aOrder: UpdateOrderDto) { - return this.http.put(`/api/order/${aOrder.id}`, aOrder); + return this.http.put(`/api/v1/order/${aOrder.id}`, aOrder); } public putUserSetting(aData: UpdateUserSettingDto) { - return this.http.put(`/api/user/setting`, aData); + return this.http.put(`/api/v1/user/setting`, aData); } public putUserSettings(aData: UpdateUserSettingsDto) { - return this.http.put(`/api/user/settings`, aData); + return this.http.put(`/api/v1/user/settings`, aData); } public redeemCoupon(couponCode: string) { - return this.http.post('/api/subscription/redeem-coupon', { + return this.http.post('/api/v1/subscription/redeem-coupon', { couponCode }); } diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index 8fe829d55..6b01dad47 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -282,6 +282,6 @@ export class ImportTransactionsService { } private postImport(aImportData: { orders: CreateOrderDto[] }) { - return this.http.post('/api/import', aImportData); + return this.http.post('/api/v1/import', aImportData); } } diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index 0d2df2b69..2e6652591 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -36,7 +36,7 @@ export class UserService extends ObservableStore { } private fetchUser() { - return this.http.get('/api/user').pipe( + return this.http.get('/api/v1/user').pipe( map((user) => { this.setState({ user }, UserStoreActions.GetUser); return user; diff --git a/apps/client/src/app/services/web-authn.service.ts b/apps/client/src/app/services/web-authn.service.ts index 154efd9d9..eb033884c 100644 --- a/apps/client/src/app/services/web-authn.service.ts +++ b/apps/client/src/app/services/web-authn.service.ts @@ -35,7 +35,7 @@ export class WebAuthnService { public register() { return this.http .get( - `/api/auth/webauthn/generate-registration-options`, + `/api/v1/auth/webauthn/generate-registration-options`, {} ) .pipe( @@ -48,7 +48,7 @@ export class WebAuthnService { }), switchMap((attResp) => { return this.http.post( - `/api/auth/webauthn/verify-attestation`, + `/api/v1/auth/webauthn/verify-attestation`, { credential: attResp } @@ -65,31 +65,33 @@ export class WebAuthnService { public deregister() { const deviceId = this.getDeviceId(); - return this.http.delete(`/api/auth-device/${deviceId}`).pipe( - catchError((error) => { - console.warn(`Could not deregister device ${deviceId}`, error); - return of(null); - }), - tap(() => - this.settingsStorageService.removeSetting( - WebAuthnService.WEB_AUTH_N_DEVICE_ID + return this.http + .delete(`/api/v1/auth-device/${deviceId}`) + .pipe( + catchError((error) => { + console.warn(`Could not deregister device ${deviceId}`, error); + return of(null); + }), + tap(() => + this.settingsStorageService.removeSetting( + WebAuthnService.WEB_AUTH_N_DEVICE_ID + ) ) - ) - ); + ); } public login() { const deviceId = this.getDeviceId(); return this.http .post( - `/api/auth/webauthn/generate-assertion-options`, + `/api/v1/auth/webauthn/generate-assertion-options`, { deviceId } ) .pipe( switchMap(startAuthentication), switchMap((assertionResponse) => { return this.http.post<{ authToken: string }>( - `/api/auth/webauthn/verify-assertion`, + `/api/v1/auth/webauthn/verify-assertion`, { credential: assertionResponse, deviceId diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts index 33556621d..54e4b175a 100644 --- a/apps/client/src/main.ts +++ b/apps/client/src/main.ts @@ -8,7 +8,7 @@ import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; (async () => { - const response = await fetch('/api/info'); + const response = await fetch('/api/v1/info'); const info: InfoItem = await response.json(); if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') { From e986310302d323b694350ae135ee79b04e842526 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 09:52:59 +0200 Subject: [PATCH 225/337] Improve price (#785) --- .../client/src/app/pages/account/account-page.html | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index ff518f082..c11f020bd 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -39,13 +39,15 @@ Upgrade
- {{ baseCurrency }} {{ price - coupon | number : '1.2-2' }} - {{ price }} - - {{ price }} - per year + >{{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ price - coupon + }} + {{ baseCurrency }} {{ price }} per year
Date: Sat, 2 Apr 2022 10:26:17 +0200 Subject: [PATCH 226/337] Feature/rename orders to activities in import and export (#786) * Rename orders to activities * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/export/export.controller.ts | 9 +-- apps/api/src/app/export/export.service.ts | 8 +-- apps/api/src/app/import/import-data.dto.ts | 2 +- apps/api/src/app/import/import.controller.ts | 2 +- apps/api/src/app/import/import.service.ts | 58 +++++++++---------- .../import-transaction-dialog.component.ts | 6 +- .../interfaces/interfaces.ts | 2 +- .../transactions-page.component.ts | 38 ++++++++---- .../services/import-transactions.service.ts | 44 +++++++++----- .../src/lib/interfaces/export.interface.ts | 2 +- test/import/invalid-date.json | 2 +- test/import/invalid-symbol.json | 2 +- test/import/ok.json | 39 +++++++++++++ 14 files changed, 142 insertions(+), 73 deletions(-) create mode 100644 test/import/ok.json diff --git a/CHANGELOG.md b/CHANGELOG.md index be4fb1f68..f29845007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Renamed `orders` to `activities` in import and export functionality - Improved the pricing page ## 1.130.0 - 30.03.2022 diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index 3617ebe24..ce02d9835 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -1,13 +1,6 @@ import { Export } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; -import { - Controller, - Get, - Headers, - Inject, - Query, - UseGuards -} from '@nestjs/common'; +import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 124fe6325..007429a38 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -14,7 +14,7 @@ export class ExportService { activityIds?: string[]; userId: string; }): Promise { - let orders = await this.prismaService.order.findMany({ + let activities = await this.prismaService.order.findMany({ orderBy: { date: 'desc' }, select: { accountId: true, @@ -30,14 +30,14 @@ export class ExportService { }); if (activityIds) { - orders = orders.filter((order) => { - return activityIds.includes(order.id); + activities = activities.filter((activity) => { + return activityIds.includes(activity.id); }); } return { meta: { date: new Date().toISOString(), version: environment.version }, - orders: orders.map( + activities: activities.map( ({ accountId, date, diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts index 488ac786f..f3a0ba8fe 100644 --- a/apps/api/src/app/import/import-data.dto.ts +++ b/apps/api/src/app/import/import-data.dto.ts @@ -6,5 +6,5 @@ export class ImportDataDto { @IsArray() @Type(() => CreateOrderDto) @ValidateNested({ each: true }) - orders: CreateOrderDto[]; + activities: CreateOrderDto[]; } diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index d14bd69af..00350f819 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -36,7 +36,7 @@ export class ImportController { try { return await this.importService.import({ - orders: importData.orders, + activities: importData.activities, userId: this.request.user.id }); } catch (error) { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 3ddd29040..40d677d9b 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -16,23 +16,23 @@ export class ImportService { ) {} public async import({ - orders, + activities, userId }: { - orders: Partial[]; + activities: Partial[]; userId: string; }): Promise { - for (const order of orders) { - if (!order.dataSource) { - if (order.type === 'ITEM') { - order.dataSource = 'MANUAL'; + for (const activity of activities) { + if (!activity.dataSource) { + if (activity.type === 'ITEM') { + activity.dataSource = 'MANUAL'; } else { - order.dataSource = this.dataProviderService.getPrimaryDataSource(); + activity.dataSource = this.dataProviderService.getPrimaryDataSource(); } } } - await this.validateOrders({ orders, userId }); + await this.validateActivities({ activities, userId }); const accountIds = (await this.accountService.getAccounts(userId)).map( (account) => { @@ -50,7 +50,7 @@ export class ImportService { symbol, type, unitPrice - } of orders) { + } of activities) { await this.orderService.createOrder({ fee, quantity, @@ -79,24 +79,24 @@ export class ImportService { } } - private async validateOrders({ - orders, + private async validateActivities({ + activities, userId }: { - orders: Partial[]; + activities: Partial[]; userId: string; }) { if ( - orders?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT') + activities?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT') ) { throw new Error( - `Too many transactions (${this.configurationService.get( + `Too many activities (${this.configurationService.get( 'MAX_ORDERS_TO_IMPORT' )} at most)` ); } - const existingOrders = await this.orderService.orders({ + const existingActivities = await this.orderService.orders({ include: { SymbolProfile: true }, orderBy: { date: 'desc' }, where: { userId } @@ -105,22 +105,22 @@ export class ImportService { for (const [ index, { currency, dataSource, date, fee, quantity, symbol, type, unitPrice } - ] of orders.entries()) { - const duplicateOrder = existingOrders.find((order) => { + ] of activities.entries()) { + const duplicateActivity = existingActivities.find((activity) => { return ( - order.SymbolProfile.currency === currency && - order.SymbolProfile.dataSource === dataSource && - isSameDay(order.date, parseISO((date))) && - order.fee === fee && - order.quantity === quantity && - order.SymbolProfile.symbol === symbol && - order.type === type && - order.unitPrice === unitPrice + activity.SymbolProfile.currency === currency && + activity.SymbolProfile.dataSource === dataSource && + isSameDay(activity.date, parseISO((date))) && + activity.fee === fee && + activity.quantity === quantity && + activity.SymbolProfile.symbol === symbol && + activity.type === type && + activity.unitPrice === unitPrice ); }); - if (duplicateOrder) { - throw new Error(`orders.${index} is a duplicate transaction`); + if (duplicateActivity) { + throw new Error(`activities.${index} is a duplicate activity`); } if (dataSource !== 'MANUAL') { @@ -130,13 +130,13 @@ export class ImportService { if (quotes[symbol] === undefined) { throw new Error( - `orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` + `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } if (quotes[symbol].currency !== currency) { throw new Error( - `orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"` + `activities.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"` ); } } diff --git a/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts b/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts index ac7832ff5..8a28b4ea4 100644 --- a/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/import-transaction-dialog.component.ts @@ -27,12 +27,12 @@ export class ImportTransactionDialog implements OnDestroy { public ngOnInit() { for (const message of this.data.messages) { - if (message.includes('orders.')) { + if (message.includes('activities.')) { let [index] = message.split(' '); - index = index.replace('orders.', ''); + index = index.replace('activities.', ''); [index] = index.split('.'); - this.details.push(this.data.orders[index]); + this.details.push(this.data.activities[index]); } else { this.details.push(''); } diff --git a/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts b/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts index 3800514a6..123238544 100644 --- a/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/pages/portfolio/transactions/import-transaction-dialog/interfaces/interfaces.ts @@ -1,5 +1,5 @@ export interface ImportTransactionDialogParams { + activities: any[]; deviceType: string; messages: string[]; - orders: any[]; } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index a40d44a4f..ba73739b7 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -185,19 +185,31 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { if (file.name.endsWith('.json')) { const content = JSON.parse(fileContent); - if (!isArray(content.orders)) { - throw new Error(); + if (!isArray(content.activities)) { + if (isArray(content.orders)) { + this.handleImportError({ + activities: [], + error: { + error: { + message: [`orders needs to be renamed to activities`] + } + } + }); + return; + } else { + throw new Error(); + } } try { await this.importTransactionsService.importJson({ - content: content.orders + content: content.activities }); this.handleImportSuccess(); } catch (error) { console.error(error); - this.handleImportError({ error, orders: content.orders }); + this.handleImportError({ error, activities: content.activities }); } return; @@ -212,10 +224,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } catch (error) { console.error(error); this.handleImportError({ + activities: error?.activities ?? [], error: { error: { message: error?.error?.message ?? [error?.message] } - }, - orders: error?.orders ?? [] + } }); } @@ -226,8 +238,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } catch (error) { console.error(error); this.handleImportError({ - error: { error: { message: ['Unexpected format'] } }, - orders: [] + activities: [], + error: { error: { message: ['Unexpected format'] } } }); } }; @@ -281,12 +293,18 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private handleImportError({ error, orders }: { error: any; orders: any[] }) { + private handleImportError({ + activities, + error + }: { + activities: any[]; + error: any; + }) { this.snackBar.dismiss(); this.dialog.open(ImportTransactionDialog, { data: { - orders, + activities, deviceType: this.deviceType, messages: error?.error?.message }, diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index 6b01dad47..8557fe0af 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -37,9 +37,9 @@ export class ImportTransactionsService { skipEmptyLines: true }).data; - const orders: CreateOrderDto[] = []; + const activities: CreateOrderDto[] = []; for (const [index, item] of content.entries()) { - orders.push({ + activities.push({ accountId: this.parseAccount({ item, userAccounts }), currency: this.parseCurrency({ content, index, item }), dataSource: this.parseDataSource({ item }), @@ -52,13 +52,13 @@ export class ImportTransactionsService { }); } - await this.importJson({ content: orders }); + await this.importJson({ content: activities }); } public importJson({ content }: { content: CreateOrderDto[] }): Promise { return new Promise((resolve, reject) => { this.postImport({ - orders: content + activities: content }) .pipe( catchError((error) => { @@ -121,7 +121,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.currency is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.currency is not valid` + }; } private parseDataSource({ item }: { item: any }) { @@ -164,7 +167,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.date is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.date is not valid` + }; } private parseFee({ @@ -184,7 +190,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.fee is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.fee is not valid` + }; } private parseQuantity({ @@ -204,7 +213,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.quantity is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.quantity is not valid` + }; } private parseSymbol({ @@ -224,7 +236,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.symbol is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.symbol is not valid` + }; } private parseType({ @@ -255,7 +270,10 @@ export class ImportTransactionsService { } } - throw { message: `orders.${index}.type is not valid`, orders: content }; + throw { + activities: content, + message: `activities.${index}.type is not valid` + }; } private parseUnitPrice({ @@ -276,12 +294,12 @@ export class ImportTransactionsService { } throw { - message: `orders.${index}.unitPrice is not valid`, - orders: content + activities: content, + message: `activities.${index}.unitPrice is not valid` }; } - private postImport(aImportData: { orders: CreateOrderDto[] }) { + private postImport(aImportData: { activities: CreateOrderDto[] }) { return this.http.post('/api/v1/import', aImportData); } } diff --git a/libs/common/src/lib/interfaces/export.interface.ts b/libs/common/src/lib/interfaces/export.interface.ts index ff83b619a..48ddd2c98 100644 --- a/libs/common/src/lib/interfaces/export.interface.ts +++ b/libs/common/src/lib/interfaces/export.interface.ts @@ -5,5 +5,5 @@ export interface Export { date: string; version: string; }; - orders: Partial[]; + activities: Partial[]; } diff --git a/test/import/invalid-date.json b/test/import/invalid-date.json index 2ddaf372e..99cd6d156 100644 --- a/test/import/invalid-date.json +++ b/test/import/invalid-date.json @@ -3,7 +3,7 @@ "date": "2021-01-01T00:00:00.000Z", "version": "dev" }, - "orders": [ + "activities": [ { "currency": "USD", "dataSource": "YAHOO", diff --git a/test/import/invalid-symbol.json b/test/import/invalid-symbol.json index c03f86e5d..14f0051ec 100644 --- a/test/import/invalid-symbol.json +++ b/test/import/invalid-symbol.json @@ -3,7 +3,7 @@ "date": "2021-01-01T00:00:00.000Z", "version": "dev" }, - "orders": [ + "activities": [ { "currency": "USD", "dataSource": "YAHOO", diff --git a/test/import/ok.json b/test/import/ok.json new file mode 100644 index 000000000..ae08aee73 --- /dev/null +++ b/test/import/ok.json @@ -0,0 +1,39 @@ +{ + "meta": { + "date": "2022-04-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "accountId": null, + "date": "2021-12-31T23:00:00.000Z", + "fee": 0, + "quantity": 1, + "type": "ITEM", + "unitPrice": 500000, + "currency": "USD", + "dataSource": "MANUAL", + "symbol": "Penthouse Apartment" + }, + { + "date": "2021-11-16T23:00:00.000Z", + "fee": 0, + "quantity": 5, + "type": "DIVIDEND", + "unitPrice": 0.62, + "currency": "USD", + "dataSource": "YAHOO", + "symbol": "MSFT" + }, + { + "date": "2021-09-15T22:00:00.000Z", + "fee": 19, + "quantity": 5, + "type": "BUY", + "unitPrice": 298.58, + "currency": "USD", + "dataSource": "YAHOO", + "symbol": "MSFT" + } + ] +} From b8c43ecf894248349b5ba561da91faa42c21dfb3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 10:29:41 +0200 Subject: [PATCH 227/337] Add documentation for import API (#787) --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index 955ae07bc..7bdef258f 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,72 @@ Run `yarn start:storybook` Run `yarn test` +## Public API (experimental) + +### Import Activities + +#### Request + +`POST http://localhost:4200/api/v1/import` + +#### Authorization: Bearer Token + +Set the header as follows: + +``` +"Authorization": "Bearer eyJh..." +``` + +#### Body + +``` +{ + "activities": [ + { + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-09-15T00:00:00.000Z", + "fee": 19, + "quantity": 5, + "symbol": "MSFT" + "type": "BUY", + "unitPrice": 298.58 + } + ] +} +``` + +| Field | Type | Description | +| ---------- | ------------------- | -------------------------------------------------- | +| accountId | string (`optional`) | Id of the account | +| currency | string | `CHF` \| `EUR` \| `USD` etc. | +| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` | +| date | string | Date in the format `ISO-8601` | +| fee | number | Fee of the activity | +| quantity | number | Quantity of the activity | +| symbol | string | Symbol of the activity (suitable for `dataSource`) | +| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` | +| unitPrice | number | Price per unit of the activity | + +#### Response + +##### Success + +`201 Created` + +##### Error + +`400 Bad Request` + +``` +{ + "error": "Bad Request", + "message": [ + "activities.1 is a duplicate activity" + ] +} +``` + ## Contributing Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you. From 65f6bcb166f3fcee64edd772486e67f20c0de762 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 10:44:19 +0200 Subject: [PATCH 228/337] Feature/harmonize algebraic sign of gross and net performance percent (#776) * Harmonize algebraic sign * Update changelog --- CHANGELOG.md | 1 + .../app/portfolio/portfolio.service-new.ts | 34 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f29845007..7acac3be6 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 ### Changed - Renamed `orders` to `activities` in import and export functionality +- Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance` - Improved the pricing page ## 1.130.0 - 30.03.2022 diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index c01f30242..e3b9e8536 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -810,23 +810,33 @@ export class PortfolioServiceNew { const hasErrors = currentPositions.hasErrors; const currentValue = currentPositions.currentValue.toNumber(); - const currentGrossPerformance = - currentPositions.grossPerformance.toNumber(); - const currentGrossPerformancePercent = - currentPositions.grossPerformancePercentage.toNumber(); - const currentNetPerformance = currentPositions.netPerformance.toNumber(); - const currentNetPerformancePercent = - currentPositions.netPerformancePercentage.toNumber(); + const currentGrossPerformance = currentPositions.grossPerformance; + let currentGrossPerformancePercent = + currentPositions.grossPerformancePercentage; + const currentNetPerformance = currentPositions.netPerformance; + let currentNetPerformancePercent = + currentPositions.netPerformancePercentage; + + if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) { + // If algebraic sign is different, harmonize it + currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1); + } + + if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) { + // If algebraic sign is different, harmonize it + currentNetPerformancePercent = currentNetPerformancePercent.mul(-1); + } return { errors: currentPositions.errors, hasErrors: currentPositions.hasErrors || hasErrors, performance: { - currentGrossPerformance, - currentGrossPerformancePercent, - currentNetPerformance, - currentNetPerformancePercent, - currentValue + currentValue, + currentGrossPerformance: currentGrossPerformance.toNumber(), + currentGrossPerformancePercent: + currentGrossPerformancePercent.toNumber(), + currentNetPerformance: currentNetPerformance.toNumber(), + currentNetPerformancePercent: currentNetPerformancePercent.toNumber() } }; } From bc117fe601b17b28d88e0421f5391ee126071434 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 10:47:26 +0200 Subject: [PATCH 229/337] Fix port (#788) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bdef258f..17cb0738a 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Run `yarn test` #### Request -`POST http://localhost:4200/api/v1/import` +`POST http://localhost:3333/api/v1/import` #### Authorization: Bearer Token From 3d21e2eac69833ad7d635994960ed9fbdb9e79a0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 12:06:06 +0200 Subject: [PATCH 230/337] Improve upgrade guide (#780) --- README.md | 18 ++++++++++++------ docker/docker-compose.yml | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 17cb0738a..dbb047861 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,11 @@ Open http://localhost:3333 in your browser and accomplish these steps: 1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data 1. Click _Sign out_ and check out the _Live Demo_ -### Migrate Database +### Upgrade Version -With the following command you can keep your database schema in sync after a Ghostfolio version update: - -```bash -docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn database:migrate -``` +1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml` +1. Run the following command to start the new Docker image: `docker-compose -f docker/docker-compose.yml up -d` +1. Then, run the following command to keep your database schema in sync: `docker-compose -f docker/docker-compose.yml exec ghostfolio yarn database:migrate` ## Development @@ -164,6 +162,14 @@ Run `yarn start:client` Run `yarn start:storybook` +### Migrate Database + +With the following command you can keep your database schema in sync: + +```bash +yarn database:push +``` + ## Testing Run `yarn test` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7c260f3cd..7afd19a19 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' services: ghostfolio: - image: ghostfolio/ghostfolio + image: ghostfolio/ghostfolio:latest env_file: - ../.env environment: From ebbdd47fa2e193f7ff7321cbd6af82d0f90dd219 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:36:15 +0200 Subject: [PATCH 231/337] Exclude google login callback endpoint from versioning (#793) --- apps/api/src/app/auth/auth.controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 5244307dd..513e85b44 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -9,7 +9,9 @@ import { Post, Req, Res, - UseGuards + UseGuards, + Version, + VERSION_NEUTRAL } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -51,6 +53,7 @@ export class AuthController { @Get('google/callback') @UseGuards(AuthGuard('google')) + @Version(VERSION_NEUTRAL) public googleLoginCallback(@Req() req, @Res() res) { // Handles the Google OAuth2 callback const jwt: string = req.user.jwt; From a256b783bc004c4b5d713a6267cb20263ea76f1b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:36:49 +0200 Subject: [PATCH 232/337] Feature/upgrade yahoo finance to 2.3.0 (#792) * Upgrade yahoo-finance2 * 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 7acac3be6..08b5b3621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `orders` to `activities` in import and export functionality - Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance` - Improved the pricing page +- Upgraded `yahoo-finance2` from version `2.2.0` to `2.3.0` ## 1.130.0 - 30.03.2022 diff --git a/package.json b/package.json index 9d4e00129..cada1aa9a 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", - "yahoo-finance2": "2.2.0", + "yahoo-finance2": "2.3.0", "zone.js": "0.11.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index ce1504790..343d56909 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18836,10 +18836,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.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.2.0.tgz#8694b04e69f4a79996812b6d082e5b738c51cee6" - integrity sha512-ZxLCcoh+J51F7Tol1jpVBmy50IBQSoxsECWYDToBxjZwPloFNHtEVOXNqJlyzTysnzVbPA5TeCNT6G0DoaJnNQ== +yahoo-finance2@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.0.tgz#81bd76732dfd38aa5d7019a97caf0f938c0127c2" + integrity sha512-7oj8n/WJH9MtX+q99WbHdjEVPdobTX8IyYjg7v4sDOh4f9ByT2Frxmp+Uj+rctrO0EiiD9QWTuwV4h8AemGuCg== dependencies: ajv "8.10.0" ajv-formats "2.1.1" From 69a1316cfed4dc302935810f9f168fac7be8fa37 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:38:47 +0200 Subject: [PATCH 233/337] Feature/upgrade prisma to 3.11.1 (#794) * Upgrade prisma dependencies to version 3.11.1 * 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 08b5b3621..0110e088f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `orders` to `activities` in import and export functionality - Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance` - Improved the pricing page +- Upgraded `prisma` from version `3.10.0` to `3.11.1` - Upgraded `yahoo-finance2` from version `2.2.0` to `2.3.0` ## 1.130.0 - 30.03.2022 diff --git a/package.json b/package.json index cada1aa9a..9b97d71f3 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.5", - "@prisma/client": "3.10.0", + "@prisma/client": "3.11.1", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -109,7 +109,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.10.0", + "prisma": "3.11.1", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 343d56909..cb313769c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3487,22 +3487,22 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@prisma/client@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.10.0.tgz#4782fe6f1b0e43c2a11a75ad4bb1098599d1dfb1" - integrity sha512-6P4sV7WFuODSfSoSEzCH1qfmWMrCUBk1LIIuTbQf6m1LI/IOpLN4lnqGDmgiBGprEzuWobnGLfe9YsXLn0inrg== +"@prisma/client@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7" + integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng== dependencies: - "@prisma/engines-version" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" + "@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" -"@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-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": + version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9" + integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ== -"@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== +"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": + version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c" + integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -15334,12 +15334,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prisma@3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.10.0.tgz#872d87afbeb1cbcaa77c3d6a63c125e0d704b04d" - integrity sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA== +prisma@3.11.1: + version "3.11.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028" + integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg== dependencies: - "@prisma/engines" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86" + "@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1" From d1230ca3ad4779c708aa8c4768cea2d673a26fe2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Apr 2022 18:47:53 +0200 Subject: [PATCH 234/337] Support coupon codes on Google Play (#795) --- .../src/app/pages/account/account-page.html | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index c11f020bd..47f8a3714 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -27,28 +27,28 @@ Valid until {{ user?.subscription?.expiresAt | date: defaultDateFormat }}
-
- -
- {{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ price - coupon - }} + +
+ Upgrade + +
+ {{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ price - coupon + }} + {{ baseCurrency }} {{ price }} per year +
+
Date: Sat, 2 Apr 2022 19:28:32 +0200 Subject: [PATCH 235/337] Feature/display values in base currency on mobile (#796) * Display values in base currency on mobile * Update changelog --- CHANGELOG.md | 2 + .../accounts-table.component.html | 73 +++++++++++++++++-- .../accounts-table.component.ts | 3 +- .../activities-table.component.html | 41 ++++++++++- .../activities-table.component.ts | 1 + 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0110e088f..3cd272a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Display the value in base currency in the accounts table on mobile +- Display the value in base currency in the activities table on mobile - Renamed `orders` to `activities` in import and export functionality - Harmonized the algebraic sign of `currentGrossPerformancePercent` and `currentNetPerformancePercent` with `currentGrossPerformance` and `currentNetPerformance` - Improved the pricing page 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 51ad3e58d..5e8fcdba9 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 @@ -78,10 +78,19 @@ - + Cash Balance - + - + - + Value - + - + + + + + + + + Value + + + + + Value - +
- + +
+ +
+ +
+ + + + Value + + +
+ +
+ +
Date: Sat, 2 Apr 2022 19:32:47 +0200 Subject: [PATCH 236/337] Release 1.131.0 (#797) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd272a95..ce97a8743 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.131.0 - 02.04.2022 ### Added diff --git a/package.json b/package.json index 9b97d71f3..e98c07da8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.130.0", + "version": "1.131.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From fa41e25c8f654c027863b6564d0fc55d9bb252d3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 4 Apr 2022 07:30:17 +0200 Subject: [PATCH 237/337] Release/1.131.1 (#804) * Add API version * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/auth/auth.controller.ts | 4 ++-- apps/api/src/app/subscription/subscription.service.ts | 2 +- .../models/rules/account-cluster-risk/initial-investment.ts | 2 +- .../src/models/rules/account-cluster-risk/single-account.ts | 2 +- .../base-currency-initial-investment.ts | 2 +- .../rules/currency-cluster-risk/current-investment.ts | 2 +- .../rules/currency-cluster-risk/initial-investment.ts | 2 +- .../src/models/rules/fees/fee-ratio-initial-investment.ts | 2 +- package.json | 2 +- 10 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce97a8743..1e584bb04 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.131.1 - 04.04.2022 + +### Fixed + +- Fixed the missing API version in the _Stripe_ success callback url + ## 1.131.0 - 02.04.2022 ### Added diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 513e85b44..738f81ab2 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -10,8 +10,8 @@ import { Req, Res, UseGuards, - Version, - VERSION_NEUTRAL + VERSION_NEUTRAL, + Version } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index f7db04728..5a4f75c20 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -45,7 +45,7 @@ export class SubscriptionService { payment_method_types: ['card'], success_url: `${this.configurationService.get( 'ROOT_URL' - )}/api/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}` + )}/api/v1/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}` }; if (couponId) { diff --git a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts index 0eed098cc..7aa363c73 100644 --- a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts @@ -1,10 +1,10 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PortfolioDetails, PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index 90d93c1e3..41988ee68 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -1,7 +1,7 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PortfolioDetails } from '@ghostfolio/common/interfaces'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts index e98f4f652..ed7242d09 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts @@ -1,7 +1,7 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts index 6d001ec09..c8e3c30eb 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts @@ -1,7 +1,7 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts index 541728d03..95e3b4b76 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts @@ -1,7 +1,7 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index 105a9f199..f0ba72932 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -1,6 +1,6 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; -import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { Rule } from '../../rule'; diff --git a/package.json b/package.json index e98c07da8..23e58a66b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.131.0", + "version": "1.131.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 204c7360c3e345ad2084d827f6bf095fc3aeb27f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 5 Apr 2022 21:02:07 +0200 Subject: [PATCH 238/337] Feature/prepare for localized date format (#803) * Support localized date and number format * Update changelog --- CHANGELOG.md | 6 +++ .../interfaces/user-settings.interface.ts | 1 + .../src/app/user/update-user-setting.dto.ts | 6 ++- apps/api/src/app/user/user.controller.ts | 18 ++++---- apps/api/src/app/user/user.service.ts | 21 +++++---- apps/client/src/app/adapter/date-formats.ts | 12 +++-- .../admin-market-data-detail.component.ts | 12 +++-- .../admin-market-data.component.ts | 25 ++++++++--- .../admin-market-data/admin-market-data.html | 1 + .../admin-overview.component.ts | 2 - .../portfolio-performance.component.ts | 7 ++- .../pages/account/account-page.component.ts | 32 ++++++++++++- .../src/app/pages/account/account-page.html | 25 +++++++++++ .../app/pages/account/account-page.module.ts | 2 + apps/client/src/main.ts | 3 +- libs/common/src/lib/config.ts | 3 +- libs/common/src/lib/helper.ts | 45 ++++++++++++++++++- .../activities-table.component.ts | 6 ++- libs/ui/src/lib/value/value.component.ts | 17 ++++--- 19 files changed, 193 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e584bb04..1c918a903 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 localization (date and number format) in user settings + ## 1.131.1 - 04.04.2022 ### Fixed diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts index fb4b2af35..ef3b03f1b 100644 --- a/apps/api/src/app/user/interfaces/user-settings.interface.ts +++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts @@ -1,5 +1,6 @@ export interface UserSettings { emergencyFund?: number; + locale?: string; isNewCalculationEngine?: boolean; isRestrictedView?: boolean; } diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index 5af2f5f8d..eaa41464a 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsNumber, IsOptional } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator'; export class UpdateUserSettingDto { @IsNumber() @@ -12,4 +12,8 @@ export class UpdateUserSettingDto { @IsBoolean() @IsOptional() isRestrictedView?: boolean; + + @IsString() + @IsOptional() + locale?: string; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 97cf25b6e..5bd14cfaa 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -2,17 +2,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; -import { - hasPermission, - hasRole, - permissions -} from '@ghostfolio/common/permissions'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, Delete, Get, + Headers, HttpException, Inject, Param, @@ -63,8 +60,13 @@ export class UserController { @Get() @UseGuards(AuthGuard('jwt')) - public async getUser(@Param('id') id: string): Promise { - return this.userService.getUser(this.request.user); + public async getUser( + @Headers('accept-language') acceptLanguage: string + ): Promise { + return this.userService.getUser( + this.request.user, + acceptLanguage?.split(',')?.[0] + ); } @Post() @@ -118,7 +120,7 @@ export class UserController { }; for (const key in userSettings) { - if (userSettings[key] === false) { + if (userSettings[key] === false || userSettings[key] === null) { delete userSettings[key]; } } diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 43b3b52c7..a4e9267ef 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -33,14 +33,17 @@ export class UserService { private readonly subscriptionService: SubscriptionService ) {} - public async getUser({ - Account, - alias, - id, - permissions, - Settings, - subscription - }: UserWithSettings): Promise { + public async getUser( + { + Account, + alias, + id, + permissions, + Settings, + subscription + }: UserWithSettings, + aLocale = locale + ): Promise { const access = await this.prismaService.access.findMany({ include: { User: true @@ -63,8 +66,8 @@ export class UserService { accounts: Account, settings: { ...(Settings.settings), - locale, baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, + locale: (Settings.settings).locale ?? aLocale, viewMode: Settings?.viewMode ?? ViewMode.DEFAULT } }; diff --git a/apps/client/src/app/adapter/date-formats.ts b/apps/client/src/app/adapter/date-formats.ts index 554f7c76e..fdf32bef8 100644 --- a/apps/client/src/app/adapter/date-formats.ts +++ b/apps/client/src/app/adapter/date-formats.ts @@ -1,16 +1,14 @@ -import { - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_MONTH_YEAR -} from '@ghostfolio/common/config'; +import { DEFAULT_DATE_FORMAT_MONTH_YEAR } from '@ghostfolio/common/config'; +import { getDateFormatString } from '@ghostfolio/common/helper'; export const DateFormats = { display: { - dateInput: DEFAULT_DATE_FORMAT, + dateInput: getDateFormatString(), monthYearLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR, - dateA11yLabel: DEFAULT_DATE_FORMAT, + dateA11yLabel: getDateFormatString(), monthYearA11yLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR }, parse: { - dateInput: DEFAULT_DATE_FORMAT + dateInput: getDateFormatString() } }; 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 9f935cb91..f28253b90 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 @@ -8,8 +8,11 @@ import { Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getDateFormatString, + getLocale +} from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { DataSource, MarketData } from '@prisma/client'; import { @@ -35,13 +38,14 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data- export class AdminMarketDataDetailComponent implements OnChanges, OnInit { @Input() dataSource: DataSource; @Input() dateOfFirstActivity: string; + @Input() locale = getLocale(); @Input() marketData: MarketData[]; @Input() symbol: string; @Output() marketDataChanged = new EventEmitter(); public days = Array(31); - public defaultDateFormat = DEFAULT_DATE_FORMAT; + public defaultDateFormat: string; public deviceType: string; public historicalDataItems: LineChartItem[]; public marketDataByMonth: { @@ -62,6 +66,8 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { public ngOnInit() {} public ngOnChanges() { + this.defaultDateFormat = getDateFormatString(this.locale); + this.historicalDataItems = this.marketData.map((marketDataItem) => { return { date: format(marketDataItem.date, DATE_FORMAT), 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 a2900ae6b..2229a3609 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 @@ -7,8 +7,9 @@ import { } from '@angular/core'; 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 { UserService } from '@ghostfolio/client/services/user/user.service'; +import { getDateFormatString } from '@ghostfolio/common/helper'; +import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { DataSource, MarketData } from '@prisma/client'; import { Subject } from 'rxjs'; @@ -23,9 +24,10 @@ import { takeUntil } from 'rxjs/operators'; export class AdminMarketDataComponent implements OnDestroy, OnInit { public currentDataSource: DataSource; public currentSymbol: string; - public defaultDateFormat = DEFAULT_DATE_FORMAT; + public defaultDateFormat: string; public marketData: AdminMarketDataItem[] = []; public marketDataDetails: MarketData[] = []; + public user: User; private unsubscribeSubject = new Subject(); @@ -35,8 +37,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService - ) {} + private dataService: DataService, + private userService: UserService + ) { + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.defaultDateFormat = getDateFormatString( + this.user.settings.locale + ); + } + }); + } /** * Initializes the controller 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 7638d6110..725c75e22 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 @@ -65,6 +65,7 @@ = 100000 ? 0 : 2, duration: 1, - separator: `'` + separator: getNumberFormatGroup(this.locale) }).start(); } else if (this.performance?.currentValue === null) { this.unit = '%'; 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 8352dc35e..072d91482 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -20,9 +20,11 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; -import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config'; +import { baseCurrency } from '@ghostfolio/common/config'; +import { getDateFormatString } from '@ghostfolio/common/helper'; import { Access, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { uniq } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { StripeService } from 'ngx-stripe'; import { EMPTY, Subject } from 'rxjs'; @@ -45,13 +47,14 @@ export class AccountPageComponent implements OnDestroy, OnInit { public coupon: number; public couponId: string; public currencies: string[] = []; - public defaultDateFormat = DEFAULT_DATE_FORMAT; + public defaultDateFormat: string; public deviceType: string; public hasPermissionForSubscription: boolean; public hasPermissionToCreateAccess: boolean; public hasPermissionToDeleteAccess: boolean; public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateUserSettings: boolean; + public locales = ['de', 'de-CH', 'en-GB', 'en-US']; public price: number; public priceId: string; public snackBarRef: MatSnackBarRef; @@ -101,6 +104,10 @@ export class AccountPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.defaultDateFormat = getDateFormatString( + this.user.settings.locale + ); + this.hasPermissionToCreateAccess = hasPermission( this.user.permissions, permissions.createAccess @@ -121,6 +128,9 @@ export class AccountPageComponent implements OnDestroy, OnInit { permissions.updateViewMode ); + this.locales.push(this.user.settings.locale); + this.locales = uniq(this.locales.sort()); + this.changeDetectorRef.markForCheck(); } }); @@ -143,6 +153,24 @@ export class AccountPageComponent implements OnDestroy, OnInit { this.update(); } + public onChangeUserSetting(aKey: string, aValue: string) { + this.dataService + .putUserSetting({ [aKey]: aValue }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService.remove(); + + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + }); + } + public onChangeUserSettings(aKey: string, aValue: string) { const settings = { ...this.user.settings, [aKey]: aValue }; diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 47f8a3714..96cc04d16 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -111,6 +111,31 @@
+
+
+
Locale
+
+ Date and number format +
+
+
+ + + + {{ locale }} + + +
+
View Mode diff --git a/apps/client/src/app/pages/account/account-page.module.ts b/apps/client/src/app/pages/account/account-page.module.ts index cf0f52a03..d583e47cd 100644 --- a/apps/client/src/app/pages/account/account-page.module.ts +++ b/apps/client/src/app/pages/account/account-page.module.ts @@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { RouterModule } from '@angular/router'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { AccountPageRoutingModule } from './account-page-routing.module'; import { AccountPageComponent } from './account-page.component'; @@ -24,6 +25,7 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di FormsModule, GfCreateOrUpdateAccessDialogModule, GfPortfolioAccessTableModule, + GfValueModule, MatButtonModule, MatCardModule, MatDialogModule, diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts index 54e4b175a..915236117 100644 --- a/apps/client/src/main.ts +++ b/apps/client/src/main.ts @@ -1,6 +1,7 @@ import { enableProdMode } from '@angular/core'; import { LOCALE_ID } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { locale } from '@ghostfolio/common/config'; import { InfoItem } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -27,7 +28,7 @@ import { environment } from './environments/environment'; platformBrowserDynamic() .bootstrapModule(AppModule, { - providers: [{ provide: LOCALE_ID, useValue: 'de-CH' }] + providers: [{ provide: LOCALE_ID, useValue: locale }] }) .catch((error) => console.error(error)); })(); diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 535fa2ef3..133dbeef6 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -19,7 +19,7 @@ export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`; export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN; export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`; -export const locale = 'de-CH'; +export const locale = 'en-US'; export const primaryColorHex = '#36cfcc'; export const primaryColorRgb = { @@ -44,7 +44,6 @@ export const warnColorRgb = { export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND'; -export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const PROPERTY_COUPONS = 'COUPONS'; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index e337493c7..2e45d40cd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -2,7 +2,7 @@ import * as currencies from '@dinero.js/currencies'; import { DataSource } from '@prisma/client'; import { getDate, getMonth, getYear, parse, subDays } from 'date-fns'; -import { ghostfolioScraperApiSymbolPrefix } from './config'; +import { ghostfolioScraperApiSymbolPrefix, locale } from './config'; export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); @@ -44,6 +44,49 @@ export function getCssVariable(aCssVariable: string) { ); } +export function getDateFormatString(aLocale?: string) { + const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts( + new Date() + ); + + return formatObject + .map((object) => { + switch (object.type) { + case 'day': + return 'dd'; + case 'month': + return 'MM'; + case 'year': + return 'yyyy'; + default: + return object.value; + } + }) + .join(''); +} + +export function getLocale() { + return navigator.languages?.length + ? navigator.languages[0] + : navigator.language ?? locale; +} + +export function getNumberFormatDecimal(aLocale?: string) { + const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99); + + return formatObject.find((object) => { + return object.type === 'decimal'; + }).value; +} + +export function getNumberFormatGroup(aLocale?: string) { + const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99); + + return formatObject.find((object) => { + return object.type === 'group'; + }).value; +} + export function getTextColor() { const cssVariable = getCssVariable( window.matchMedia('(prefers-color-scheme: dark)').matches 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 b8d8b0597..52bc841ff 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -20,7 +20,7 @@ import { MatSort } from '@angular/material/sort'; 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 { getDateFormatString } from '@ghostfolio/common/helper'; import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import Big from 'big.js'; @@ -63,7 +63,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { @ViewChild(MatSort) sort: MatSort; public dataSource: MatTableDataSource = new MatTableDataSource(); - public defaultDateFormat = DEFAULT_DATE_FORMAT; + public defaultDateFormat: string; public displayedColumns = []; public endOfToday = endOfToday(); public filters$: Subject = new BehaviorSubject([]); @@ -153,6 +153,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.isLoading = true; + this.defaultDateFormat = getDateFormatString(this.locale); + if (this.activities) { this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index daa8d72bc..08fdb413e 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -4,8 +4,8 @@ import { Input, OnChanges } from '@angular/core'; -import { DEFAULT_DATE_FORMAT, locale } from '@ghostfolio/common/config'; -import { format, isDate, parseISO } from 'date-fns'; +import { getLocale } from '@ghostfolio/common/helper'; +import { isDate, parseISO } from 'date-fns'; import { isNumber } from 'lodash'; @Component({ @@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges { @Input() isCurrency = false; @Input() isPercent = false; @Input() label = ''; - @Input() locale = locale; + @Input() locale = getLocale(); @Input() position = ''; @Input() precision: number | undefined; @Input() size: 'large' | 'medium' | 'small' = 'small'; @@ -102,10 +102,13 @@ export class ValueComponent implements OnChanges { try { if (isDate(parseISO(this.value))) { - this.formattedValue = format( - new Date(this.value), - DEFAULT_DATE_FORMAT - ); + this.formattedValue = new Date( + this.value + ).toLocaleDateString(this.locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); } } catch { this.formattedValue = this.value; From 52d113e71ff85417dfcaa24f785feee65287a29f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Apr 2022 18:02:21 +0200 Subject: [PATCH 239/337] Feature/improve label of average price (#805) * Improve label * Update changelog --- CHANGELOG.md | 4 ++++ .../position-detail-dialog/position-detail-dialog.html | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c918a903..74ab9de13 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 localization (date and number format) in user settings +### Changed + +- Improved the label of the average price from _Ø Buy Price_ to _Average Unit Price_ + ## 1.131.1 - 04.04.2022 ### Fixed diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 9ea26573f..b5b10f3c9 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -21,7 +21,7 @@
Date: Wed, 6 Apr 2022 21:21:53 +0200 Subject: [PATCH 240/337] Various improvements (#807) --- .../src/app/portfolio/portfolio.controller.ts | 10 ++++++++ apps/api/src/app/user/user.service.ts | 2 +- .../portfolio-performance.component.ts | 5 ++-- .../src/app/core/http-response.interceptor.ts | 9 ++++++- .../app/pages/portfolio/portfolio-page.html | 24 +++++-------------- libs/ui/src/lib/value/value.component.ts | 4 ++++ 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 1bb42a0ed..272721e76 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -320,6 +320,16 @@ export class PortfolioController { public async getSummary( @Headers('impersonation-id') impersonationId ): Promise { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic' + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + let summary = await this.portfolioServiceStrategy .get() .getSummary(impersonationId); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index a4e9267ef..c94c5a458 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -67,7 +67,7 @@ export class UserService { settings: { ...(Settings.settings), baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, - locale: (Settings.settings).locale ?? aLocale, + locale: (Settings.settings)?.locale ?? aLocale, viewMode: Settings?.viewMode ?? ViewMode.DEFAULT } }; diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index f1daa7e72..aae5e5f5e 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -70,9 +70,10 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit { 'value', this.performance?.currentNetPerformancePercent * 100, { + decimal: getNumberFormatDecimal(this.locale), decimalPlaces: 2, - duration: 0.75, - separator: `'` + duration: 1, + separator: getNumberFormatGroup(this.locale) } ).start(); } diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 9ac221e62..b76884ffa 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -17,12 +17,14 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { InfoItem } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { StatusCodes } from 'http-status-codes'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; @Injectable() export class HttpResponseInterceptor implements HttpInterceptor { + public hasPermissionForSubscription: boolean; public info: InfoItem; public snackBarRef: MatSnackBarRef; @@ -34,6 +36,11 @@ export class HttpResponseInterceptor implements HttpInterceptor { private webAuthnService: WebAuthnService ) { this.info = this.dataService.fetchInfo(); + + this.hasPermissionForSubscription = hasPermission( + this.info?.globalPermissions, + permissions.enableSubscription + ); } public intercept( @@ -56,7 +63,7 @@ export class HttpResponseInterceptor implements HttpInterceptor { } else { this.snackBarRef = this.snackBar.open( 'This feature requires a subscription.', - 'Upgrade Plan', + this.hasPermissionForSubscription ? 'Upgrade Plan' : undefined, { duration: 6000 } ); } diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.html b/apps/client/src/app/pages/portfolio/portfolio-page.html index d0717805d..f7bc75f91 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.html +++ b/apps/client/src/app/pages/portfolio/portfolio-page.html @@ -25,7 +25,7 @@

Allocations @@ -38,7 +38,6 @@ Open Allocations @@ -52,7 +51,7 @@

Analysis @@ -65,7 +64,6 @@ Open Analysis @@ -79,7 +77,7 @@

X-ray @@ -89,12 +87,7 @@ risks in your portfolio.

- + Open X-ray @@ -106,7 +99,7 @@

FIRE @@ -116,12 +109,7 @@ Financial Independence, Retire Early lifestyle.

- + Open FIRE diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 08fdb413e..7bd15076f 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -119,5 +119,9 @@ export class ValueComponent implements OnChanges { if (this.formattedValue === '0.00') { this.useAbsoluteValue = true; } + + if (this.isPercent) { + this.formattedValue = '– '; + } } } From 795a6a6799eaa1061227c3f7e16ef6b94da60d96 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Apr 2022 21:23:20 +0200 Subject: [PATCH 241/337] Release 1.132.0 (#808) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ab9de13..bd6b1b6d6 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.132.0 - 06.04.2022 ### Added diff --git a/package.json b/package.json index 23e58a66b..dcfe07ef2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.131.1", + "version": "1.132.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 255af6a6e950744a58dfb26a03f8d4c472d1fe4f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Apr 2022 22:02:40 +0200 Subject: [PATCH 242/337] Release 1.132.1 (#809) --- CHANGELOG.md | 6 ++++++ libs/ui/src/lib/value/value.component.ts | 4 ---- package.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6b1b6d6..a948fc24a 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.132.1 - 06.04.2022 + +### Fixed + +- Fixed an issue with percentages in the value component + ## 1.132.0 - 06.04.2022 ### Added diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 7bd15076f..08fdb413e 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -119,9 +119,5 @@ export class ValueComponent implements OnChanges { if (this.formattedValue === '0.00') { this.useAbsoluteValue = true; } - - if (this.isPercent) { - this.formattedValue = '– '; - } } } diff --git a/package.json b/package.json index dcfe07ef2..fd81b87f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.132.0", + "version": "1.132.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 65754408773eb90710adacd238c96542eff59e79 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 7 Apr 2022 17:20:12 +0200 Subject: [PATCH 243/337] Bugfix/fix dates in value component (#810) * Fix dates * Update changelog --- CHANGELOG.md | 6 ++++++ .../position-detail-dialog.html | 2 ++ libs/ui/src/lib/value/value.component.ts | 17 ++++++++--------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a948fc24a..5d03715a7 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 + +- Fixed an issue with dates in the value component + ## 1.132.1 - 06.04.2022 ### Fixed diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index b5b10f3c9..c832c07b1 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -111,6 +111,8 @@
diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 08fdb413e..2004065db 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -5,7 +5,6 @@ import { OnChanges } from '@angular/core'; import { getLocale } from '@ghostfolio/common/helper'; -import { isDate, parseISO } from 'date-fns'; import { isNumber } from 'lodash'; @Component({ @@ -19,6 +18,7 @@ export class ValueComponent implements OnChanges { @Input() currency = ''; @Input() isAbsolute = false; @Input() isCurrency = false; + @Input() isDate = false; @Input() isPercent = false; @Input() label = ''; @Input() locale = getLocale(); @@ -100,17 +100,16 @@ export class ValueComponent implements OnChanges { this.isNumber = false; this.isString = true; - try { - if (isDate(parseISO(this.value))) { - this.formattedValue = new Date( - this.value - ).toLocaleDateString(this.locale, { + if (this.isDate) { + this.formattedValue = new Date(this.value).toLocaleDateString( + this.locale, + { day: '2-digit', month: '2-digit', year: 'numeric' - }); - } - } catch { + } + ); + } else { this.formattedValue = this.value; } } From 957200854c0b495ac10fd588a2aec3075e5dccb8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 7 Apr 2022 22:13:41 +0200 Subject: [PATCH 244/337] Feature/improve empty state of proportion chart (#811) * Improve empty state * Update changelog --- CHANGELOG.md | 4 + .../src/app/portfolio/portfolio.controller.ts | 16 ++-- apps/api/src/app/user/user.service.ts | 7 -- .../positions-table.component.html | 11 --- .../positions-table.component.ts | 1 - .../src/app/pages/account/account-page.html | 5 -- .../allocations/allocations-page.component.ts | 7 -- .../allocations/allocations-page.html | 74 +++++++++++++++---- .../portfolio-proportion-chart.component.ts | 10 ++- 9 files changed, 76 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d03715a7..d6b316c1e 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 +### Changed + +- Improved the empty state of the portfolio proportion chart component + ### Fixed - Fixed an issue with dates in the value component diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 272721e76..a47f43035 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -106,16 +106,6 @@ export class PortfolioController { @Headers('impersonation-id') impersonationId: string, @Query('range') range ): Promise { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && - this.request.user.subscription.type === 'Basic' - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - let hasError = false; const { accounts, holdings, hasErrors } = @@ -162,7 +152,11 @@ export class PortfolioController { } } - return { accounts, hasError, holdings }; + const isBasicUser = + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic'; + + return { accounts, hasError, holdings: isBasicUser ? {} : holdings }; } @Get('investments') diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index c94c5a458..feed46434 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -147,13 +147,6 @@ export class UserService { user.subscription = this.subscriptionService.getSubscription( userFromDatabase?.Subscription ); - - if (user.subscription.type === SubscriptionType.Basic) { - user.permissions = user.permissions.filter((permission) => { - return permission !== permissions.updateViewMode; - }); - user.Settings.viewMode = ViewMode.ZEN; - } } return user; diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index 38f5110b6..1c8f1539f 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -123,17 +123,6 @@ }" > -
- -
-
View Mode -
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 5d7be99c4..da5e07025 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 @@ -13,7 +13,6 @@ import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Market, ToggleOption } from '@ghostfolio/common/types'; import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -41,7 +40,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; public deviceType: string; public hasImpersonationId: boolean; - public hasPermissionToCreateOrder: boolean; public markets: { [key in Market]: { name: string; value: number }; }; @@ -139,11 +137,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - this.hasPermissionToCreateOrder = hasPermission( - this.user.permissions, - permissions.createOrder - ); - this.changeDetectorRef.markForCheck(); } }); 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 679570998..dac241b1d 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -30,9 +30,14 @@
- By Asset Class + By Currency @@ -54,9 +59,14 @@
- By Currency + By Asset Class @@ -78,7 +88,14 @@
- By Symbol + By Symbol - By Sector + By Sector - By Continent + By Continent - By Country + By Country - Regions + Regions 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 803e52fea..207c9b3ca 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 @@ -246,6 +246,12 @@ export class PortfolioProportionChartComponent labels = labelSubCategory.concat(labels); } + if (datasets[0]?.data?.length === 0 || datasets[0]?.data?.[0] === 0) { + labels = ['']; + datasets[0].backgroundColor = [this.colorMap[UNKNOWN_KEY]]; + datasets[0].data[0] = Number.MAX_SAFE_INTEGER; + } + const data = { datasets, labels @@ -323,7 +329,9 @@ export class PortfolioProportionChartComponent const percentage = (context.parsed * 100) / sum; - if (this.isInPercent) { + if (context.raw === Number.MAX_SAFE_INTEGER) { + return 'No data available'; + } else if (this.isInPercent) { return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } else { const value = context.raw; From bfc8f87d88bd540af70b5f4c1ef7dde2d283ff10 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 7 Apr 2022 22:15:09 +0200 Subject: [PATCH 245/337] Release 1.133.0 (#812) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b316c1e..0a2791e31 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.133.0 - 07.04.2022 ### Changed diff --git a/package.json b/package.json index fd81b87f9..105a9b843 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.132.1", + "version": "1.133.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 3d3a6c120442a076bd8c9dbc3c9b4f4c452dd2ff Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Apr 2022 09:03:39 +0200 Subject: [PATCH 246/337] Feature/improve fire section (#813) * Improve FIRE section * Update changelog --- CHANGELOG.md | 6 +++++ .../portfolio/fire/fire-page.component.ts | 25 +++++-------------- .../app/pages/portfolio/fire/fire-page.html | 13 ++++++---- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2791e31..a59abcf33 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 + +- Improved the 4% rule in the _FIRE_ section + ## 1.133.0 - 07.04.2022 ### Changed diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 1eb132dfd..1ca20dd11 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -14,12 +14,11 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './fire-page.html' }) export class FirePageComponent implements OnDestroy, OnInit { - public fireWealth: number; - public hasImpersonationId: boolean; + public fireWealth: Big; public isLoading = false; public user: User; - public withdrawalRatePerMonth: number; - public withdrawalRatePerYear: number; + public withdrawalRatePerMonth: Big; + public withdrawalRatePerYear: Big; private unsubscribeSubject = new Subject(); @@ -39,13 +38,6 @@ export class FirePageComponent implements OnDestroy, OnInit { public ngOnInit() { this.isLoading = true; - this.impersonationStorageService - .onChangeHasImpersonation() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((aId) => { - this.hasImpersonationId = !!aId; - }); - this.dataService .fetchPortfolioSummary() .pipe(takeUntil(this.unsubscribeSubject)) @@ -54,14 +46,9 @@ export class FirePageComponent implements OnDestroy, OnInit { return; } - this.fireWealth = new Big(currentValue).plus(cash).toNumber(); - this.withdrawalRatePerYear = new Big(this.fireWealth) - .mul(4) - .div(100) - .toNumber(); - this.withdrawalRatePerMonth = new Big(this.withdrawalRatePerYear) - .div(12) - .toNumber(); + this.fireWealth = new Big(currentValue); + this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100); + this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); this.isLoading = false; diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index 69735c196..4aa0f31ed 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -27,7 +27,8 @@ > per year @@ -36,16 +37,18 @@ > per month, based on your net worth of + >, based on your investment of - (excluding emergency fund) and a withdrawal rate of 4%. + and a withdrawal rate of 4%.
From 67f2b326f3c6efd3455613da67611e3a757ecc68 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Apr 2022 10:17:31 +0200 Subject: [PATCH 247/337] Switch to new calculation engine (#814) * Switch to new calculation engine * Clean up old portfolio calculation engine (#815) * Rename new portfolio calculation engine (#816) * Update changelog --- CHANGELOG.md | 1 + .../api/src/app/account/account.controller.ts | 11 +- ...olio-calculator-baln-buy-and-sell.spec.ts} | 10 +- ... => portfolio-calculator-baln-buy.spec.ts} | 10 +- .../portfolio-calculator-new.spec.ts | 73 - .../app/portfolio/portfolio-calculator-new.ts | 997 ------ ...=> portfolio-calculator-no-orders.spec.ts} | 10 +- .../portfolio/portfolio-calculator.spec.ts | 2970 +---------------- .../src/app/portfolio/portfolio-calculator.ts | 589 +++- .../portfolio/portfolio-service.strategy.ts | 26 - .../src/app/portfolio/portfolio.controller.ts | 60 +- .../api/src/app/portfolio/portfolio.module.ts | 6 +- .../app/portfolio/portfolio.service-new.ts | 1324 -------- .../src/app/portfolio/portfolio.service.ts | 213 +- .../interfaces/user-settings.interface.ts | 1 - .../src/app/user/update-user-setting.dto.ts | 4 - .../pages/account/account-page.component.ts | 18 - .../src/app/pages/account/account-page.html | 17 - 18 files changed, 644 insertions(+), 5696 deletions(-) rename apps/api/src/app/portfolio/{portfolio-calculator-new-baln-buy-and-sell.spec.ts => portfolio-calculator-baln-buy-and-sell.spec.ts} (89%) rename apps/api/src/app/portfolio/{portfolio-calculator-new-baln-buy.spec.ts => portfolio-calculator-baln-buy.spec.ts} (88%) delete mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts delete mode 100644 apps/api/src/app/portfolio/portfolio-calculator-new.ts rename apps/api/src/app/portfolio/{portfolio-calculator-new-no-orders.spec.ts => portfolio-calculator-no-orders.spec.ts} (81%) delete mode 100644 apps/api/src/app/portfolio/portfolio-service.strategy.ts delete mode 100644 apps/api/src/app/portfolio/portfolio.service-new.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a59abcf33..4a2fb77ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Switched to the new calculation engine - Improved the 4% rule in the _FIRE_ section ## 1.133.0 - 07.04.2022 diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 64530c377..819fc5a0d 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,4 +1,4 @@ -import { PortfolioServiceStrategy } from '@ghostfolio/api/app/portfolio/portfolio-service.strategy'; +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObject, @@ -35,7 +35,7 @@ export class AccountController { public constructor( private readonly accountService: AccountService, private readonly impersonationService: ImpersonationService, - private readonly portfolioServiceStrategy: PortfolioServiceStrategy, + private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @@ -91,9 +91,10 @@ export class AccountController { this.request.user.id ); - let accountsWithAggregations = await this.portfolioServiceStrategy - .get() - .getAccountsWithAggregations(impersonationUserId || this.request.user.id); + let accountsWithAggregations = + await this.portfolioService.getAccountsWithAggregations( + impersonationUserId || this.request.user.id + ); if ( impersonationUserId || 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-baln-buy-and-sell.spec.ts similarity index 89% rename from apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts rename to apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index 5dddc53fd..ea35cdd79 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -3,7 +3,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; -import { PortfolioCalculatorNew } from './portfolio-calculator-new'; +import { PortfolioCalculator } from './portfolio-calculator'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -14,7 +14,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); -describe('PortfolioCalculatorNew', () => { +describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; beforeEach(() => { @@ -23,7 +23,7 @@ describe('PortfolioCalculatorNew', () => { describe('get current positions', () => { it.only('with BALN.SW buy and sell', async () => { - const portfolioCalculatorNew = new PortfolioCalculatorNew({ + const portfolioCalculator = new PortfolioCalculator({ currentRateService, currency: 'CHF', orders: [ @@ -52,13 +52,13 @@ describe('PortfolioCalculatorNew', () => { ] }); - portfolioCalculatorNew.computeTransactionPoints(); + portfolioCalculator.computeTransactionPoints(); const spy = jest .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); - const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2021-11-22') ); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts similarity index 88% rename from apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts rename to apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index de0f1f0bf..a6fe1af40 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -3,7 +3,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; -import { PortfolioCalculatorNew } from './portfolio-calculator-new'; +import { PortfolioCalculator } from './portfolio-calculator'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -14,7 +14,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); -describe('PortfolioCalculatorNew', () => { +describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; beforeEach(() => { @@ -23,7 +23,7 @@ describe('PortfolioCalculatorNew', () => { describe('get current positions', () => { it.only('with BALN.SW buy', async () => { - const portfolioCalculatorNew = new PortfolioCalculatorNew({ + const portfolioCalculator = new PortfolioCalculator({ currentRateService, currency: 'CHF', orders: [ @@ -41,13 +41,13 @@ describe('PortfolioCalculatorNew', () => { ] }); - portfolioCalculatorNew.computeTransactionPoints(); + portfolioCalculator.computeTransactionPoints(); const spy = jest .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); - const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2021-11-30') ); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts deleted file mode 100644 index 72e3091f1..000000000 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Big from 'big.js'; - -import { CurrentRateService } from './current-rate.service'; -import { PortfolioCalculatorNew } from './portfolio-calculator-new'; - -describe('PortfolioCalculatorNew', () => { - let currentRateService: CurrentRateService; - - beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); - }); - - describe('annualized performance percentage', () => { - const portfolioCalculatorNew = new PortfolioCalculatorNew({ - currentRateService, - currency: 'USD', - orders: [] - }); - - it('Get annualized performance', async () => { - expect( - portfolioCalculatorNew - .getAnnualizedPerformancePercent({ - daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day - netPerformancePercent: new Big(0) - }) - .toNumber() - ).toEqual(0); - - expect( - portfolioCalculatorNew - .getAnnualizedPerformancePercent({ - daysInMarket: 0, - netPerformancePercent: new Big(0) - }) - .toNumber() - ).toEqual(0); - - /** - * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html - */ - expect( - portfolioCalculatorNew - .getAnnualizedPerformancePercent({ - daysInMarket: 65, // < 1 year - netPerformancePercent: new Big(0.1025) - }) - .toNumber() - ).toBeCloseTo(0.729705); - - expect( - portfolioCalculatorNew - .getAnnualizedPerformancePercent({ - daysInMarket: 365, // 1 year - netPerformancePercent: new Big(0.05) - }) - .toNumber() - ).toBeCloseTo(0.05); - - /** - * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation - */ - expect( - portfolioCalculatorNew - .getAnnualizedPerformancePercent({ - daysInMarket: 575, // > 1 year - netPerformancePercent: new Big(0.2374) - }) - .toNumber() - ).toBeCloseTo(0.145); - }); - }); -}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts deleted file mode 100644 index 3b8d30cf8..000000000 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ /dev/null @@ -1,997 +0,0 @@ -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 { - ResponseError, - TimelinePosition, - UniqueAsset -} from '@ghostfolio/common/interfaces'; -import { Logger } from '@nestjs/common'; -import { Type as TypeOfOrder } from '@prisma/client'; -import Big from 'big.js'; -import { - addDays, - addMilliseconds, - addMonths, - addYears, - endOfDay, - format, - isAfter, - isBefore, - max, - min -} from 'date-fns'; -import { first, flatten, isNumber, sortBy } from 'lodash'; - -import { CurrentRateService } from './current-rate.service'; -import { CurrentPositions } from './interfaces/current-positions.interface'; -import { GetValueObject } from './interfaces/get-value-object.interface'; -import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; -import { PortfolioOrder } from './interfaces/portfolio-order.interface'; -import { TimelinePeriod } from './interfaces/timeline-period.interface'; -import { - Accuracy, - TimelineSpecification -} from './interfaces/timeline-specification.interface'; -import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; -import { TransactionPoint } from './interfaces/transaction-point.interface'; - -export class PortfolioCalculatorNew { - private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = - true; - - private static readonly ENABLE_LOGGING = false; - - private currency: string; - private currentRateService: CurrentRateService; - private orders: PortfolioOrder[]; - private transactionPoints: TransactionPoint[]; - - public constructor({ - currency, - currentRateService, - orders - }: { - currency: string; - currentRateService: CurrentRateService; - orders: PortfolioOrder[]; - }) { - this.currency = currency; - this.currentRateService = currentRateService; - this.orders = orders; - - this.orders.sort((a, b) => a.date.localeCompare(b.date)); - } - - public computeTransactionPoints() { - this.transactionPoints = []; - const symbols: { [symbol: string]: TransactionPointSymbol } = {}; - - let lastDate: string = null; - let lastTransactionPoint: TransactionPoint = null; - for (const order of this.orders) { - const currentDate = order.date; - - let currentTransactionPointItem: TransactionPointSymbol; - const oldAccumulatedSymbol = symbols[order.symbol]; - - const factor = this.getFactor(order.type); - const unitPrice = new Big(order.unitPrice); - if (oldAccumulatedSymbol) { - const newQuantity = order.quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); - currentTransactionPointItem = { - currency: order.currency, - dataSource: order.dataSource, - fee: order.fee.plus(oldAccumulatedSymbol.fee), - firstBuyDate: oldAccumulatedSymbol.firstBuyDate, - investment: newQuantity.eq(0) - ? new Big(0) - : unitPrice - .mul(order.quantity) - .mul(factor) - .plus(oldAccumulatedSymbol.investment), - quantity: newQuantity, - symbol: order.symbol, - transactionCount: oldAccumulatedSymbol.transactionCount + 1 - }; - } else { - currentTransactionPointItem = { - currency: order.currency, - dataSource: order.dataSource, - fee: order.fee, - firstBuyDate: order.date, - investment: unitPrice.mul(order.quantity).mul(factor), - quantity: order.quantity.mul(factor), - symbol: order.symbol, - transactionCount: 1 - }; - } - - symbols[order.symbol] = currentTransactionPointItem; - - const items = lastTransactionPoint?.items ?? []; - const newItems = items.filter( - (transactionPointItem) => transactionPointItem.symbol !== order.symbol - ); - newItems.push(currentTransactionPointItem); - newItems.sort((a, b) => a.symbol.localeCompare(b.symbol)); - if (lastDate !== currentDate || lastTransactionPoint === null) { - lastTransactionPoint = { - date: currentDate, - items: newItems - }; - this.transactionPoints.push(lastTransactionPoint); - } else { - lastTransactionPoint.items = newItems; - } - lastDate = currentDate; - } - } - - public getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent - }: { - daysInMarket: number; - netPerformancePercent: Big; - }): Big { - if (isNumber(daysInMarket) && daysInMarket > 0) { - const exponent = new Big(365).div(daysInMarket).toNumber(); - return new Big( - Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) - ).minus(1); - } - - return new Big(0); - } - - public getTransactionPoints(): TransactionPoint[] { - return this.transactionPoints; - } - - public setTransactionPoints(transactionPoints: TransactionPoint[]) { - this.transactionPoints = transactionPoints; - } - - public async getCurrentPositions(start: Date): Promise { - if (!this.transactionPoints?.length) { - return { - currentValue: new Big(0), - hasErrors: false, - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0), - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - positions: [], - totalInvestment: new Big(0) - }; - } - - const lastTransactionPoint = - this.transactionPoints[this.transactionPoints.length - 1]; - - // use Date.now() to use the mock for today - const today = new Date(Date.now()); - - let firstTransactionPoint: TransactionPoint = null; - let firstIndex = this.transactionPoints.length; - const dates = []; - const dataGatheringItems: IDataGatheringItem[] = []; - const currencies: { [symbol: string]: string } = {}; - - dates.push(resetHours(start)); - for (const item of this.transactionPoints[firstIndex - 1].items) { - dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol - }); - currencies[item.symbol] = item.currency; - } - for (let i = 0; i < this.transactionPoints.length; i++) { - if ( - !isBefore(parseDate(this.transactionPoints[i].date), start) && - firstTransactionPoint === null - ) { - firstTransactionPoint = this.transactionPoints[i]; - firstIndex = i; - } - if (firstTransactionPoint !== null) { - dates.push(resetHours(parseDate(this.transactionPoints[i].date))); - } - } - - dates.push(resetHours(today)); - - const marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); - - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - - for (const marketSymbol of marketSymbols) { - const date = format(marketSymbol.date, DATE_FORMAT); - if (!marketSymbolMap[date]) { - marketSymbolMap[date] = {}; - } - if (marketSymbol.marketPrice) { - marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice - ); - } - } - - const todayString = format(today, DATE_FORMAT); - - if (firstIndex > 0) { - firstIndex--; - } - const initialValues: { [symbol: string]: Big } = {}; - - const positions: TimelinePosition[] = []; - let hasAnySymbolMetricsErrors = false; - - const errors: ResponseError['errors'] = []; - - for (const item of lastTransactionPoint.items) { - const marketValue = marketSymbolMap[todayString]?.[item.symbol]; - - const { - grossPerformance, - grossPerformancePercentage, - hasErrors, - initialValue, - netPerformance, - netPerformancePercentage - } = this.getSymbolMetrics({ - marketSymbolMap, - start, - symbol: item.symbol - }); - - hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; - initialValues[item.symbol] = initialValue; - - positions.push({ - averagePrice: item.quantity.eq(0) - ? new Big(0) - : item.investment.div(item.quantity), - currency: item.currency, - dataSource: item.dataSource, - firstBuyDate: item.firstBuyDate, - grossPerformance: !hasErrors ? grossPerformance ?? null : null, - grossPerformancePercentage: !hasErrors - ? grossPerformancePercentage ?? null - : null, - investment: item.investment, - marketPrice: marketValue?.toNumber() ?? null, - netPerformance: !hasErrors ? netPerformance ?? null : null, - netPerformancePercentage: !hasErrors - ? netPerformancePercentage ?? null - : null, - quantity: item.quantity, - 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 - }; - } - - public getInvestments(): { date: string; investment: Big }[] { - if (this.transactionPoints.length === 0) { - return []; - } - - return this.transactionPoints.map((transactionPoint) => { - return { - date: transactionPoint.date, - investment: transactionPoint.items.reduce( - (investment, transactionPointSymbol) => - investment.plus(transactionPointSymbol.investment), - new Big(0) - ) - }; - }); - } - - public async calculateTimeline( - timelineSpecification: TimelineSpecification[], - endDate: string - ): Promise { - if (timelineSpecification.length === 0) { - return { - maxNetPerformance: new Big(0), - minNetPerformance: new Big(0), - timelinePeriods: [] - }; - } - - const startDate = timelineSpecification[0].start; - const start = parseDate(startDate); - const end = parseDate(endDate); - - const timelinePeriodPromises: Promise[] = []; - let i = 0; - let j = -1; - for ( - let currentDate = start; - !isAfter(currentDate, end); - currentDate = this.addToDate( - currentDate, - timelineSpecification[i].accuracy - ) - ) { - if (this.isNextItemActive(timelineSpecification, currentDate, i)) { - i++; - } - while ( - j + 1 < this.transactionPoints.length && - !isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate) - ) { - j++; - } - - let periodEndDate = currentDate; - if (timelineSpecification[i].accuracy === 'day') { - let nextEndDate = end; - if (j + 1 < this.transactionPoints.length) { - nextEndDate = parseDate(this.transactionPoints[j + 1].date); - } - periodEndDate = min([ - addMonths(currentDate, 3), - max([currentDate, nextEndDate]) - ]); - } - const timePeriodForDates = this.getTimePeriodForDate( - j, - currentDate, - endOfDay(periodEndDate) - ); - currentDate = periodEndDate; - if (timePeriodForDates != null) { - timelinePeriodPromises.push(timePeriodForDates); - } - } - - const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( - timelinePeriodPromises - ); - const minNetPerformance = timelineInfoInterfaces - .map((timelineInfo) => timelineInfo.minNetPerformance) - .filter((performance) => performance !== null) - .reduce((minPerformance, current) => { - if (minPerformance.lt(current)) { - return minPerformance; - } else { - return current; - } - }); - - const maxNetPerformance = timelineInfoInterfaces - .map((timelineInfo) => timelineInfo.maxNetPerformance) - .filter((performance) => performance !== null) - .reduce((maxPerformance, current) => { - if (maxPerformance.gt(current)) { - return maxPerformance; - } else { - return current; - } - }); - - const timelinePeriods = timelineInfoInterfaces.map( - (timelineInfo) => timelineInfo.timelinePeriods - ); - - return { - maxNetPerformance, - minNetPerformance, - timelinePeriods: flatten(timelinePeriods) - }; - } - - private calculateOverallPerformance( - positions: TimelinePosition[], - initialValues: { [symbol: string]: Big } - ) { - let currentValue = 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 sumOfWeights = new Big(0); - let totalInvestment = new Big(0); - - for (const currentPosition of positions) { - if (currentPosition.marketPrice) { - currentValue = currentValue.plus( - new Big(currentPosition.marketPrice).mul(currentPosition.quantity) - ); - } 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) { - // 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(weight) - ); - - netPerformancePercentage = netPerformancePercentage.plus( - currentPosition.netPerformancePercentage.mul(weight) - ); - } else if (!currentPosition.quantity.eq(0)) { - Logger.warn( - `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`, - 'PortfolioCalculatorNew' - ); - hasErrors = true; - } - } - - if (sumOfWeights.gt(0)) { - grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights); - netPerformancePercentage = netPerformancePercentage.div(sumOfWeights); - } else { - grossPerformancePercentage = new Big(0); - netPerformancePercentage = new Big(0); - } - - return { - currentValue, - grossPerformance, - grossPerformancePercentage, - hasErrors, - netPerformance, - netPerformancePercentage, - totalInvestment - }; - } - - private async getTimePeriodForDate( - j: number, - startDate: Date, - endDate: Date - ): Promise { - let investment: Big = new Big(0); - let fees: Big = new Big(0); - - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - if (j >= 0) { - const currencies: { [name: string]: string } = {}; - const dataGatheringItems: IDataGatheringItem[] = []; - - for (const item of this.transactionPoints[j].items) { - currencies[item.symbol] = item.currency; - dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol - }); - investment = investment.plus(item.investment); - fees = fees.plus(item.fee); - } - - let marketSymbols: GetValueObject[] = []; - if (dataGatheringItems.length > 0) { - try { - marketSymbols = await this.currentRateService.getValues({ - currencies, - dataGatheringItems, - dateQuery: { - gte: startDate, - lt: endOfDay(endDate) - }, - userCurrency: this.currency - }); - } catch (error) { - Logger.error( - `Failed to fetch info for date ${startDate} with exception`, - error, - 'PortfolioCalculatorNew' - ); - return null; - } - } - - for (const marketSymbol of marketSymbols) { - const date = format(marketSymbol.date, DATE_FORMAT); - if (!marketSymbolMap[date]) { - marketSymbolMap[date] = {}; - } - if (marketSymbol.marketPrice) { - marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice - ); - } - } - } - - const results: TimelinePeriod[] = []; - let maxNetPerformance: Big = null; - let minNetPerformance: Big = null; - for ( - let currentDate = startDate; - isBefore(currentDate, endDate); - currentDate = addDays(currentDate, 1) - ) { - let value = new Big(0); - const currentDateAsString = format(currentDate, DATE_FORMAT); - let invalid = false; - if (j >= 0) { - for (const item of this.transactionPoints[j].items) { - if ( - !marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol) - ) { - invalid = true; - break; - } - value = value.plus( - item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol]) - ); - } - } - if (!invalid) { - const grossPerformance = value.minus(investment); - const netPerformance = grossPerformance.minus(fees); - if ( - minNetPerformance === null || - minNetPerformance.gt(netPerformance) - ) { - minNetPerformance = netPerformance; - } - if ( - maxNetPerformance === null || - maxNetPerformance.lt(netPerformance) - ) { - maxNetPerformance = netPerformance; - } - - const result = { - grossPerformance, - investment, - netPerformance, - value, - date: currentDateAsString - }; - results.push(result); - } - } - - return { - maxNetPerformance, - minNetPerformance, - timelinePeriods: results - }; - } - - private getFactor(type: TypeOfOrder) { - let factor: number; - - switch (type) { - case 'BUY': - factor = 1; - break; - case 'SELL': - factor = -1; - break; - default: - factor = 0; - break; - } - - return factor; - } - - private addToDate(date: Date, accuracy: Accuracy): Date { - switch (accuracy) { - case 'day': - return addDays(date, 1); - case 'month': - return addMonths(date, 1); - case 'year': - return addYears(date, 1); - } - } - - private getSymbolMetrics({ - marketSymbolMap, - start, - symbol - }: { - marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - }; - start: Date; - symbol: string; - }) { - let orders: PortfolioOrderItem[] = this.orders.filter((order) => { - return order.symbol === symbol; - }); - - if (orders.length <= 0) { - return { - hasErrors: false, - initialValue: new Big(0), - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0) - }; - } - - const dateOfFirstTransaction = new Date(first(orders).date); - const endDate = new Date(Date.now()); - - const unitPriceAtStartDate = - marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; - - const unitPriceAtEndDate = - marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; - - if ( - !unitPriceAtEndDate || - (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) - ) { - return { - hasErrors: true, - initialValue: new Big(0), - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0) - }; - } - - 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); - let grossPerformanceAtStartDate = new Big(0); - let grossPerformanceFromSells = new Big(0); - let initialValue: Big; - let investmentAtStartDate: Big; - 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); - let valueAtStartDate: Big; - - // Add a synthetic order at the start and the end date - orders.push({ - symbol, - currency: null, - date: format(start, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'start', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtStartDate - }); - - orders.push({ - symbol, - currency: null, - date: format(endDate, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'end', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtEndDate - }); - - // Sort orders so that the start and end placeholder order are at the right - // position - orders = sortBy(orders, (order) => { - let sortIndex = new Date(order.date); - - if (order.itemType === 'start') { - sortIndex = addMilliseconds(sortIndex, -1); - } - - if (order.itemType === 'end') { - sortIndex = addMilliseconds(sortIndex, 1); - } - - return sortIndex.getTime(); - }); - - const indexOfStartOrder = orders.findIndex((order) => { - 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 - ); - - if (!investmentAtStartDate && i >= indexOfStartOrder) { - investmentAtStartDate = totalInvestment ?? new Big(0); - valueAtStartDate = valueOfInvestmentBeforeTransaction; - } - - const transactionInvestment = order.quantity - .mul(order.unitPrice) - .mul(this.getFactor(order.type)); - - totalInvestment = totalInvestment.plus(transactionInvestment); - - if (i >= indexOfStartOrder && 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); - - totalUnits = totalUnits.plus( - order.quantity.mul(this.getFactor(order.type)) - ); - - const valueOfInvestment = totalUnits.mul(order.unitPrice); - - const grossPerformanceFromSell = - order.type === TypeOfOrder.SELL - ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) - : new Big(0); - - grossPerformanceFromSells = grossPerformanceFromSells.plus( - grossPerformanceFromSell - ); - - totalInvestmentWithGrossPerformanceFromSell = - totalInvestmentWithGrossPerformanceFromSell - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); - - lastAveragePrice = totalUnits.eq(0) - ? new Big(0) - : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); - - const newGrossPerformance = valueOfInvestment - .minus(totalInvestmentWithGrossPerformanceFromSell) - .plus(grossPerformanceFromSells); - - if ( - i > indexOfStartOrder && - !lastValueOfInvestmentBeforeTransaction - .plus(lastTransactionInvestment) - .eq(0) - ) { - const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction - .minus( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ) - .div( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ); - - timeWeightedGrossPerformancePercentage = - timeWeightedGrossPerformancePercentage.mul( - new Big(1).plus(grossHoldingPeriodReturn) - ); - - const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction - .minus(fees.minus(feesAtStartDate)) - .minus( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ) - .div( - lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - ); - - timeWeightedNetPerformancePercentage = - timeWeightedNetPerformancePercentage.mul( - new Big(1).plus(netHoldingPeriodReturn) - ); - } - - grossPerformance = newGrossPerformance; - - lastTransactionInvestment = transactionInvestment; - - lastValueOfInvestmentBeforeTransaction = - valueOfInvestmentBeforeTransaction; - - if (order.itemType === 'start') { - feesAtStartDate = fees; - grossPerformanceAtStartDate = grossPerformance; - } - } - - timeWeightedGrossPerformancePercentage = - timeWeightedGrossPerformancePercentage.minus(1); - - timeWeightedNetPerformancePercentage = - timeWeightedNetPerformancePercentage.minus(1); - - const totalGrossPerformance = grossPerformance.minus( - grossPerformanceAtStartDate - ); - - const totalNetPerformance = grossPerformance - .minus(grossPerformanceAtStartDate) - .minus(fees.minus(feesAtStartDate)); - - const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( - maxTotalInvestment.minus(investmentAtStartDate) - ); - - const grossPerformancePercentage = - PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || - averagePriceAtStartDate.eq(0) || - averagePriceAtEndDate.eq(0) || - orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - 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 = - PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || - averagePriceAtStartDate.eq(0) || - averagePriceAtEndDate.eq(0) || - orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - 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, - grossPerformancePercentage, - netPerformancePercentage, - hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), - netPerformance: totalNetPerformance, - grossPerformance: totalGrossPerformance - }; - } - - private isNextItemActive( - timelineSpecification: TimelineSpecification[], - currentDate: Date, - i: number - ) { - return ( - i + 1 < timelineSpecification.length && - !isBefore(currentDate, parseDate(timelineSpecification[i + 1].start)) - ); - } -} diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts similarity index 81% rename from apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts rename to apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts index 41e2ca381..18d6cb34d 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts @@ -3,7 +3,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import Big from 'big.js'; import { CurrentRateServiceMock } from './current-rate.service.mock'; -import { PortfolioCalculatorNew } from './portfolio-calculator-new'; +import { PortfolioCalculator } from './portfolio-calculator'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -14,7 +14,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); -describe('PortfolioCalculatorNew', () => { +describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; beforeEach(() => { @@ -23,19 +23,19 @@ describe('PortfolioCalculatorNew', () => { describe('get current positions', () => { it('with no orders', async () => { - const portfolioCalculatorNew = new PortfolioCalculatorNew({ + const portfolioCalculator = new PortfolioCalculator({ currentRateService, currency: 'CHF', orders: [] }); - portfolioCalculatorNew.computeTransactionPoints(); + portfolioCalculator.computeTransactionPoints(); const spy = jest .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); - const currentPositions = await portfolioCalculatorNew.getCurrentPositions( + const currentPositions = await portfolioCalculator.getCurrentPositions( new Date() ); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 74c45f026..23f0a8a8d 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1,308 +1,8 @@ -import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; -import { DataSource } from '@prisma/client'; import Big from 'big.js'; -import { addDays, endOfDay, format, isBefore, isSameDay } from 'date-fns'; import { CurrentRateService } from './current-rate.service'; -import { GetValuesParams } from './interfaces/get-values-params.interface'; -import { PortfolioOrder } from './interfaces/portfolio-order.interface'; -import { TimelinePeriod } from './interfaces/timeline-period.interface'; -import { TimelineSpecification } from './interfaces/timeline-specification.interface'; -import { TransactionPoint } from './interfaces/transaction-point.interface'; import { PortfolioCalculator } from './portfolio-calculator'; -function mockGetValue(symbol: string, date: Date) { - switch (symbol) { - case 'AMZN': - return { marketPrice: 2021.99 }; - case 'BALN.SW': - if (isSameDay(parseDate('2021-11-12'), date)) { - return { marketPrice: 146 }; - } else if (isSameDay(parseDate('2021-11-22'), date)) { - return { marketPrice: 142.9 }; - } else if (isSameDay(parseDate('2021-11-26'), date)) { - return { marketPrice: 139.9 }; - } else if (isSameDay(parseDate('2021-11-30'), date)) { - return { marketPrice: 136.6 }; - } else if (isSameDay(parseDate('2021-12-18'), date)) { - return { marketPrice: 143.9 }; - } - - return { marketPrice: 0 }; - case 'MFA': - if (isSameDay(parseDate('2010-12-31'), date)) { - return { marketPrice: 1 }; - } else if (isSameDay(parseDate('2011-08-15'), date)) { - return { marketPrice: 1.162484 }; // 1162484 / 1000000 - } else if (isSameDay(parseDate('2011-12-31'), date)) { - return { marketPrice: 1.097884981 }; // 1192328 / 1086022.689344541 - } - - return { marketPrice: 0 }; - case 'SPA': - if (isSameDay(parseDate('2013-12-31'), date)) { - return { marketPrice: 1.025 }; // 205 / 200 - } - - return { marketPrice: 0 }; - case 'SPB': - if (isSameDay(parseDate('2013-12-31'), date)) { - return { marketPrice: 1.04 }; // 312 / 300 - } - - return { marketPrice: 0 }; - case 'TSLA': - if (isSameDay(parseDate('2021-01-02'), date)) { - return { marketPrice: 666.66 }; - } else if (isSameDay(parseDate('2021-07-26'), date)) { - return { marketPrice: 657.62 }; - } - - return { marketPrice: 0 }; - case 'VTI': - switch (format(date, DATE_FORMAT)) { - case '2019-01-01': - return { marketPrice: 144.38 }; - case '2019-02-01': - return { marketPrice: 144.38 }; - case '2019-03-01': - return { marketPrice: 146.62 }; - case '2019-04-01': - return { marketPrice: 149.1 }; - case '2019-05-01': - return { marketPrice: 151.5 }; - case '2019-06-01': - return { marketPrice: 153.98 }; - case '2019-07-01': - return { marketPrice: 156.38 }; - case '2019-08-01': - return { marketPrice: 158.86 }; - case '2019-08-03': - return { marketPrice: 159.02 }; - case '2019-09-01': - return { marketPrice: 161.34 }; - case '2019-10-01': - return { marketPrice: 163.74 }; - case '2019-11-01': - return { marketPrice: 166.22 }; - case '2019-12-01': - return { marketPrice: 168.62 }; - case '2020-01-01': - return { marketPrice: 171.1 }; - case '2020-02-01': - return { marketPrice: 173.58 }; - case '2020-02-02': - return { marketPrice: 173.66 }; - case '2020-03-01': - return { marketPrice: 175.9 }; - case '2020-04-01': - return { marketPrice: 178.38 }; - case '2020-05-01': - return { marketPrice: 180.78 }; - case '2020-06-01': - return { marketPrice: 183.26 }; - case '2020-07-01': - return { marketPrice: 185.66 }; - case '2020-08-01': - return { marketPrice: 188.14 }; - case '2020-08-02': - return { marketPrice: 188.22 }; - case '2020-08-03': - return { marketPrice: 188.3 }; - case '2020-09-01': - return { marketPrice: 190.62 }; - case '2020-10-01': - return { marketPrice: 193.02 }; - case '2020-11-01': - return { marketPrice: 195.5 }; - case '2020-12-01': - return { marketPrice: 197.9 }; - case '2021-01-01': - return { marketPrice: 200.38 }; - case '2021-02-01': - return { marketPrice: 202.86 }; - case '2021-03-01': - return { marketPrice: 205.1 }; - case '2021-04-01': - return { marketPrice: 207.58 }; - case '2021-05-01': - return { marketPrice: 209.98 }; - case '2021-06-01': - return { marketPrice: 212.46 }; - case '2021-06-02': - return { marketPrice: 212.54 }; - case '2021-06-03': - return { marketPrice: 212.62 }; - case '2021-06-04': - return { marketPrice: 212.7 }; - case '2021-06-05': - return { marketPrice: 212.78 }; - case '2021-06-06': - return { marketPrice: 212.86 }; - case '2021-06-07': - return { marketPrice: 212.94 }; - case '2021-06-08': - return { marketPrice: 213.02 }; - case '2021-06-09': - return { marketPrice: 213.1 }; - case '2021-06-10': - return { marketPrice: 213.18 }; - case '2021-06-11': - return { marketPrice: 213.26 }; - case '2021-06-12': - return { marketPrice: 213.34 }; - case '2021-06-13': - return { marketPrice: 213.42 }; - case '2021-06-14': - return { marketPrice: 213.5 }; - case '2021-06-15': - return { marketPrice: 213.58 }; - case '2021-06-16': - return { marketPrice: 213.66 }; - case '2021-06-17': - return { marketPrice: 213.74 }; - case '2021-06-18': - return { marketPrice: 213.82 }; - case '2021-06-19': - return { marketPrice: 213.9 }; - case '2021-06-20': - return { marketPrice: 213.98 }; - case '2021-06-21': - return { marketPrice: 214.06 }; - case '2021-06-22': - return { marketPrice: 214.14 }; - case '2021-06-23': - return { marketPrice: 214.22 }; - case '2021-06-24': - return { marketPrice: 214.3 }; - case '2021-06-25': - return { marketPrice: 214.38 }; - case '2021-06-26': - return { marketPrice: 214.46 }; - case '2021-06-27': - return { marketPrice: 214.54 }; - case '2021-06-28': - return { marketPrice: 214.62 }; - case '2021-06-29': - return { marketPrice: 214.7 }; - case '2021-06-30': - return { marketPrice: 214.78 }; - case '2021-07-01': - return { marketPrice: 214.86 }; - case '2021-07-02': - return { marketPrice: 214.94 }; - case '2021-07-03': - return { marketPrice: 215.02 }; - case '2021-07-04': - return { marketPrice: 215.1 }; - case '2021-07-05': - return { marketPrice: 215.18 }; - case '2021-07-06': - return { marketPrice: 215.26 }; - case '2021-07-07': - return { marketPrice: 215.34 }; - case '2021-07-08': - return { marketPrice: 215.42 }; - case '2021-07-09': - return { marketPrice: 215.5 }; - case '2021-07-10': - return { marketPrice: 215.58 }; - case '2021-07-11': - return { marketPrice: 215.66 }; - case '2021-07-12': - return { marketPrice: 215.74 }; - case '2021-07-13': - return { marketPrice: 215.82 }; - case '2021-07-14': - return { marketPrice: 215.9 }; - case '2021-07-15': - return { marketPrice: 215.98 }; - case '2021-07-16': - return { marketPrice: 216.06 }; - case '2021-07-17': - return { marketPrice: 216.14 }; - case '2021-07-18': - return { marketPrice: 216.22 }; - case '2021-07-19': - return { marketPrice: 216.3 }; - case '2021-07-20': - return { marketPrice: 216.38 }; - case '2021-07-21': - return { marketPrice: 216.46 }; - case '2021-07-22': - return { marketPrice: 216.54 }; - case '2021-07-23': - return { marketPrice: 216.62 }; - case '2021-07-24': - return { marketPrice: 216.7 }; - case '2021-07-25': - return { marketPrice: 216.78 }; - case '2021-07-26': - return { marketPrice: 216.86 }; - case '2021-07-27': - return { marketPrice: 216.94 }; - case '2021-07-28': - return { marketPrice: 217.02 }; - case '2021-07-29': - return { marketPrice: 217.1 }; - case '2021-07-30': - return { marketPrice: 217.18 }; - case '2021-07-31': - return { marketPrice: 217.26 }; - case '2021-08-01': - return { marketPrice: 217.34 }; - case '2020-10-24': - return { marketPrice: 194.86 }; - default: - return { marketPrice: 0 }; - } - - default: - return { marketPrice: 0 }; - } -} - -jest.mock('./current-rate.service', () => { - return { - // eslint-disable-next-line @typescript-eslint/naming-convention - CurrentRateService: jest.fn().mockImplementation(() => { - return { - getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => { - const result = []; - if (dateQuery.lt) { - for ( - let date = resetHours(dateQuery.gte); - isBefore(date, endOfDay(dateQuery.lt)); - date = addDays(date, 1) - ) { - for (const dataGatheringItem of dataGatheringItems) { - result.push({ - date, - marketPrice: mockGetValue(dataGatheringItem.symbol, date) - .marketPrice, - symbol: dataGatheringItem.symbol - }); - } - } - } else { - for (const date of dateQuery.in) { - for (const dataGatheringItem of dataGatheringItems) { - result.push({ - date, - marketPrice: mockGetValue(dataGatheringItem.symbol, date) - .marketPrice, - symbol: dataGatheringItem.symbol - }); - } - } - } - return Promise.resolve(result); - } - }; - }) - }; -}); - describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; @@ -310,2326 +10,12 @@ describe('PortfolioCalculator', () => { currentRateService = new CurrentRateService(null, null, null); }); - describe('calculate transaction points', () => { - it('with orders of only one symbol', () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.computeTransactionPoints(ordersVTI); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual( - ordersVTITransactionPoints - ); - }); - - it('with orders of only one symbol and a fee', () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const orders: PortfolioOrder[] = [ - { - date: '2019-02-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('144.38'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('5') - }, - { - date: '2019-08-03', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('147.99'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('10') - }, - { - date: '2020-02-02', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('15'), - symbol: 'VTI', - type: 'SELL', - unitPrice: new Big('151.41'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('5') - } - ]; - portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual([ - { - date: '2019-02-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 1, - fee: new Big('5') - } - ] - }, - { - date: '2019-08-03', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 2, - fee: new Big('15') - } - ] - }, - { - date: '2020-02-02', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 3, - fee: new Big('20') - } - ] - } - ]); - }); - - it('with orders of two different symbols and a fee', () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const orders: PortfolioOrder[] = [ - { - date: '2019-02-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('144.38'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('5') - }, - { - date: '2019-08-03', - name: 'Something else', - quantity: new Big('10'), - symbol: 'VTX', - type: 'BUY', - unitPrice: new Big('147.99'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('10') - }, - { - date: '2020-02-02', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('5'), - symbol: 'VTI', - type: 'SELL', - unitPrice: new Big('151.41'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big('5') - } - ]; - portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual([ - { - date: '2019-02-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 1, - fee: new Big('5') - } - ] - }, - { - date: '2019-08-03', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 1, - fee: new Big('5') - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTX', - investment: new Big('1479.9'), - currency: 'USD', - firstBuyDate: '2019-08-03', - transactionCount: 1, - fee: new Big('10') - } - ] - }, - { - date: '2020-02-02', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('686.75'), - currency: 'USD', - firstBuyDate: '2019-02-01', - transactionCount: 2, - fee: new Big('10') - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTX', - investment: new Big('1479.9'), - currency: 'USD', - firstBuyDate: '2019-08-03', - transactionCount: 1, - fee: new Big('10') - } - ] - } - ]); - }); - - it('with two orders at the same day of the same type', () => { - const orders: PortfolioOrder[] = [ - ...ordersVTI, - { - currency: 'USD', - dataSource: DataSource.YAHOO, - date: '2021-02-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('20'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('197.15'), - fee: new Big(0) - } - ]; - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual([ - { - date: '2019-02-01', - items: [ - { - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - investment: new Big('1443.8'), - quantity: new Big('10'), - symbol: 'VTI', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2019-08-03', - items: [ - { - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - investment: new Big('2923.7'), - quantity: new Big('20'), - symbol: 'VTI', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2020-02-02', - items: [ - { - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - investment: new Big('652.55'), - quantity: new Big('5'), - symbol: 'VTI', - fee: new Big(0), - transactionCount: 3 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - investment: new Big('6627.05'), - quantity: new Big('35'), - symbol: 'VTI', - fee: new Big(0), - transactionCount: 5 - } - ] - }, - { - date: '2021-08-01', - items: [ - { - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - investment: new Big('8403.95'), - quantity: new Big('45'), - symbol: 'VTI', - fee: new Big(0), - transactionCount: 6 - } - ] - } - ]); - }); - - it('with additional order', () => { - const orders: PortfolioOrder[] = [ - ...ordersVTI, - { - currency: 'USD', - dataSource: DataSource.YAHOO, - date: '2019-09-01', - name: 'Amazon.com, Inc.', - quantity: new Big('5'), - symbol: 'AMZN', - type: 'BUY', - unitPrice: new Big('2021.99'), - fee: new Big(0) - } - ]; - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual([ - { - date: '2019-02-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2019-08-03', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2019-09-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2020-02-02', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 3 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 4 - } - ] - }, - { - date: '2021-08-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: 'USD', - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 5 - } - ] - } - ]); - }); - - it('with additional buy & sell', () => { - const orders: PortfolioOrder[] = [ - ...ordersVTI, - { - date: '2019-09-01', - name: 'Amazon.com, Inc.', - quantity: new Big('5'), - symbol: 'AMZN', - type: 'BUY', - unitPrice: new Big('2021.99'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2020-08-02', - name: 'Amazon.com, Inc.', - quantity: new Big('5'), - symbol: 'AMZN', - type: 'SELL', - unitPrice: new Big('2412.23'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - } - ]; - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual( - transactionPointsBuyAndSell - ); - }); - - it('with mixed symbols', () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.computeTransactionPoints(ordersMixedSymbols); - const portfolioItemsAtTransactionPoints = - portfolioCalculator.getTransactionPoints(); - - expect(portfolioItemsAtTransactionPoints).toEqual([ - { - date: '2017-01-03', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: 'USD', - firstBuyDate: '2017-01-03', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2017-07-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('0.5614682'), - symbol: 'BTCUSD', - investment: new Big('1999.9999999999998659756'), - currency: 'USD', - firstBuyDate: '2017-07-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: 'USD', - firstBuyDate: '2017-01-03', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2018-09-01', - items: [ - { - dataSource: DataSource.YAHOO, - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - firstBuyDate: '2018-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('0.5614682'), - symbol: 'BTCUSD', - investment: new Big('1999.9999999999998659756'), - currency: 'USD', - firstBuyDate: '2017-07-01', - fee: new Big(0), - transactionCount: 1 - }, - { - dataSource: DataSource.YAHOO, - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: 'USD', - firstBuyDate: '2017-01-03', - fee: new Big(0), - transactionCount: 1 - } - ] - } - ]); - }); - }); - - describe('get current positions', () => { - it('with single TSLA and early start', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2020-01-21') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-61.84'), - grossPerformancePercentage: new Big('-0.08595335390431712673'), - totalInvestment: new Big('719.46'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 - grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - }) - ] - }) - ); - }); - - it('with single TSLA and buy day start', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2021-01-01') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-61.84'), - grossPerformancePercentage: new Big('-0.08595335390431712673'), - totalInvestment: new Big('719.46'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-61.84'), // 657.62-719.46=-61.84 - grossPerformancePercentage: new Big('-0.08595335390431712673'), // (657.62-719.46)/719.46=-0.08595335390431712673 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - }) - ] - }) - ); - }); - - it('with single TSLA and late start', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(orderTslaTransactionPoint); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2021-01-02') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('657.62'), - grossPerformance: new Big('-9.04'), - grossPerformancePercentage: new Big('-0.01356013560135601356'), - totalInvestment: new Big('719.46'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('719.46'), - currency: 'USD', - firstBuyDate: '2021-01-01', - grossPerformance: new Big('-9.04'), // 657.62-666.66=-9.04 - grossPerformancePercentage: new Big('-0.01356013560135601356'), // 657.62/666.66-1=-0.013560136 - investment: new Big('719.46'), - marketPrice: 657.62, - quantity: new Big('1'), - symbol: 'TSLA', - transactionCount: 1 - }) - ] - }) - ); - }); - - it('with VTI only', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2019-01-01') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('4871.5'), - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big('0.08839407904876477102'), - totalInvestment: new Big('4460.95'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('178.438'), - currency: 'USD', - firstBuyDate: '2019-02-01', - // see next test for details about how to calculate this - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big( - '0.0883940790487647710162214425767848424215253864940558186258745429269647266073266478435285352186572448' - ), - investment: new Big('4460.95'), - marketPrice: 194.86, - quantity: new Big('25'), - symbol: 'VTI', - transactionCount: 5 - }) - ] - }) - ); - }); - - it('with buy and sell', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2019-01-01') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('4871.5'), - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big('0.01104605615757711361'), - totalInvestment: new Big('4460.95'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('0'), - currency: 'USD', - firstBuyDate: '2019-09-01', - grossPerformance: new Big('0'), - grossPerformancePercentage: new Big('0'), - investment: new Big('0'), - marketPrice: 2021.99, - quantity: new Big('0'), - symbol: 'AMZN', - transactionCount: 2 - }), - expect.objectContaining({ - averagePrice: new Big('178.438'), - currency: 'USD', - firstBuyDate: '2019-02-01', - grossPerformance: new Big('240.4'), - grossPerformancePercentage: new Big( - '0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552' - ), - investment: new Big('4460.95'), - marketPrice: 194.86, - quantity: new Big('25'), - symbol: 'VTI', - transactionCount: 5 - }) - ] - }) - ); - }); - - it('with buy, sell, buy', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints([ - { - date: '2019-09-01', - items: [ - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('805.9'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2020-08-02', - items: [ - { - quantity: new Big('0'), - symbol: 'VTI', - investment: new Big('0'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('1013.9'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 3 - } - ] - } - ]); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2021, 7, 1)).getTime()); // 2021-08-01 - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2019-02-01') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('1086.7'), - grossPerformance: new Big('207.6'), - grossPerformancePercentage: new Big('0.2516103956224511062'), - totalInvestment: new Big('1013.9'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('202.78'), - currency: 'USD', - firstBuyDate: '2019-09-01', - grossPerformance: new Big('207.6'), - grossPerformancePercentage: new Big( - '0.2516103956224511061954915466429950404846' - ), - investment: new Big('1013.9'), - marketPrice: 217.34, - quantity: new Big('5'), - symbol: 'VTI', - transactionCount: 3 - }) - ] - }) - ); - }); - - it('with performance since Jan 1st, 2020', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const transactionPoints: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2020-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - } - ]; - - portfolioCalculator.setTransactionPoints(transactionPoints); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 - - // 2020-01-01 -> days 334 => value: VTI: 144.38+334*0.08=171.1 => 10*171.10=1711 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 10*188.30=1883 => 1883/1711 = 1.100526008 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 20*188.30=3766 - // cash flow: 2923.7-1443.8=1479.9 - // 2020-10-24 [today] -> days 631 => value: VTI: 144.38+631*0.08=194.86 => 20*194.86=3897.2 => 3897.2/(1883+1479.9) = 1.158880728 - // gross performance: 1883-1711 + 3897.2-3766 = 303.2 - // gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 % - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2020-01-01') - ); - - spy.mockRestore(); - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('3897.2'), - grossPerformance: new Big('303.2'), - grossPerformancePercentage: new Big('0.27537838148272398344'), - totalInvestment: new Big('2923.7'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('146.185'), - firstBuyDate: '2019-02-01', - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - marketPrice: 194.86, - transactionCount: 2, - grossPerformance: new Big('303.2'), - grossPerformancePercentage: new Big( - '0.2753783814827239834392742298083677500037' - ), - currency: 'USD' - }) - ] - }) - ); - }); - - it('with net performance since Jan 1st, 2020 - include fees', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const transactionPoints: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(50), - transactionCount: 1 - } - ] - }, - { - date: '2020-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(100), - transactionCount: 2 - } - ] - } - ]; - - portfolioCalculator.setTransactionPoints(transactionPoints); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 - - // 2020-01-01 -> days 334 => value: VTI: 144.38+334*0.08=171.1 => 10*171.10=1711 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 10*188.30=1883 => 1883/1711 = 1.100526008 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 20*188.30=3766 - // cash flow: 2923.7-1443.8=1479.9 - // 2020-10-24 [today] -> days 631 => value: VTI: 144.38+631*0.08=194.86 => 20*194.86=3897.2 => 3897.2/(1883+1479.9) = 1.158880728 - // and net: 3897.2/(1883+1479.9+50) = 1.14190278 - // gross performance: 1883-1711 + 3897.2-3766 = 303.2 - // gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 % - // net performance percentage: 1.100526008 * 1.14190278 = 1.25669371 => 25.669371 % - - // more details: https://github.com/ghostfolio/ghostfolio/issues/324#issuecomment-910530823 - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2020-01-01') - ); - - spy.mockRestore(); - expect(currentPositions).toEqual({ - hasErrors: false, - currentValue: new Big('3897.2'), - grossPerformance: new Big('303.2'), - grossPerformancePercentage: new Big('0.27537838148272398344'), - netAnnualizedPerformance: new Big('0.1412977563032074'), - netPerformance: new Big('253.2'), - netPerformancePercentage: new Big('0.2566937088951485493'), - totalInvestment: new Big('2923.7'), - positions: [ - { - averagePrice: new Big('146.185'), - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - marketPrice: 194.86, - transactionCount: 2, - grossPerformance: new Big('303.2'), - grossPerformancePercentage: new Big( - '0.2753783814827239834392742298083677500037' - ), - netPerformance: new Big('253.2'), // gross - 50 fees - netPerformancePercentage: new Big( - '0.2566937088951485493029975263687800261527' - ), // see details above - currency: 'USD' - } - ] - }); - }); - - it('with net performance since Feb 1st, 2019 - include fees', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const transactionPoints: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(50), - transactionCount: 1 - } - ] - }, - { - date: '2020-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(100), - transactionCount: 2 - } - ] - } - ]; - - portfolioCalculator.setTransactionPoints(transactionPoints); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 - - // 2019-02-01 -> value: VTI: 1443.8 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 10*188.30=1883 => net: 1883/(1443.8+50) = 1.26054358 - // 2020-08-03 -> days 549 => value: VTI: 144.38+549*0.08=188.3 => 20*188.30=3766 - // cash flow: 2923.7-1443.8=1479.9 - // 2020-10-24 [today] -> days 631 => value: VTI: 144.38+631*0.08=194.86 => 20*194.86=3897.2 => net: 3897.2/(1883+1479.9+50) = 1.14190278 - // gross performance: 1883-1443.8 + 3897.2-3766 = 570.4 => net performance: 470.4 - // net performance percentage: 1.26054358 * 1.14190278 = 1.43941822 => 43.941822 % - - // more details: https://github.com/ghostfolio/ghostfolio/issues/324#issuecomment-910530823 - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2019-02-01') - ); - - spy.mockRestore(); - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('3897.2'), - netPerformance: new Big('470.4'), - netPerformancePercentage: new Big('0.4394182192526437059'), - totalInvestment: new Big('2923.7'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('146.185'), - firstBuyDate: '2019-02-01', - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - marketPrice: 194.86, - transactionCount: 2, - netPerformance: new Big('470.4'), - netPerformancePercentage: new Big( - '0.4394182192526437058970248283134805555953' - ), // see details above - currency: 'USD' - }) - ] - }) - ); - }); - - /** - * Source: https://www.investopedia.com/terms/t/time-weightedror.asp - */ - it('with TWR example from Investopedia: Scenario 1', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints([ - { - date: '2010-12-31', - items: [ - { - quantity: new Big('1000000'), // 1 million - symbol: 'MFA', // Mutual Fund A - investment: new Big('1000000'), // 1 million - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2010-12-31', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2011-08-15', - items: [ - { - quantity: new Big('1086022.689344541'), // 1,000,000 + 100,000 / 1.162484 - symbol: 'MFA', // Mutual Fund A - investment: new Big('1100000'), // 1,000,000 + 100,000 - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2010-12-31', - fee: new Big(0), - transactionCount: 2 - } - ] - } - ]); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2011, 11, 31)).getTime()); // 2011-12-31 - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2010-12-31') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - hasErrors: false, - currentValue: new Big('1192327.999656600298238721'), - grossPerformance: new Big('92327.999656600898394721'), - grossPerformancePercentage: new Big('0.09788498099999947809'), - totalInvestment: new Big('1100000'), - positions: [ - expect.objectContaining({ - averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542 - firstBuyDate: '2010-12-31', - quantity: new Big('1086022.689344541'), - symbol: 'MFA', - investment: new Big('1100000'), - marketPrice: 1.097884981, - transactionCount: 2, - grossPerformance: new Big('92327.999656600898394721'), // 1'192'328 - 1'100'000 = 92'328 - grossPerformancePercentage: new Big( - '0.09788498099999947808927632' - ), // 9.79 % - currency: 'USD' - }) - ] - }) - ); - }); - - /** - * Source: https://www.chsoft.ch/en/assets/Dateien/files/PDF/ePoca/en/Practical%20Performance%20Calculation.pdf - */ - it('with example from chsoft.ch: Performance of a Combination of Investments', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'CHF' - ); - portfolioCalculator.setTransactionPoints([ - { - date: '2012-12-31', - items: [ - { - quantity: new Big('200'), - symbol: 'SPA', // Sub Portfolio A - investment: new Big('200'), - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2012-12-31', - fee: new Big(0), - transactionCount: 1 - }, - { - quantity: new Big('300'), - symbol: 'SPB', // Sub Portfolio B - investment: new Big('300'), - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2012-12-31', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2013-12-31', - items: [ - { - quantity: new Big('200'), - symbol: 'SPA', // Sub Portfolio A - investment: new Big('200'), - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2012-12-31', - fee: new Big(0), - transactionCount: 1 - }, - { - quantity: new Big('300'), - symbol: 'SPB', // Sub Portfolio B - investment: new Big('300'), - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2012-12-31', - fee: new Big(0), - transactionCount: 1 - } - ] - } - ]); - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2013, 11, 31)).getTime()); // 2013-12-31 - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2012-12-31') - ); - spy.mockRestore(); - - expect(currentPositions).toEqual( - expect.objectContaining({ - currentValue: new Big('517'), - grossPerformance: new Big('17'), // 517 - 500 - grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4% - totalInvestment: new Big('500'), - hasErrors: false, - positions: [ - expect.objectContaining({ - averagePrice: new Big('1'), - firstBuyDate: '2012-12-31', - quantity: new Big('200'), - symbol: 'SPA', - investment: new Big('200'), - marketPrice: 1.025, // 205 / 200 - transactionCount: 1, - grossPerformance: new Big('5'), // 205 - 200 - grossPerformancePercentage: new Big('0.025'), - currency: 'CHF' - }), - expect.objectContaining({ - averagePrice: new Big('1'), - firstBuyDate: '2012-12-31', - quantity: new Big('300'), - symbol: 'SPB', - investment: new Big('300'), - marketPrice: 1.04, // 312 / 300 - transactionCount: 1, - grossPerformance: new Big('12'), // 312 - 300 - grossPerformancePercentage: new Big('0.04'), - currency: 'CHF' - }) - ] - }) - ); - }); - - it('with BALN.SW', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'CHF' - ); - - // date,type,ticker,currency,units,price,fee - portfolioCalculator.setTransactionPoints([ - // 12.11.2021,BUY,BALN.SW,CHF,2.00,146.00,1.65 - { - date: '2021-11-12', - items: [ - { - quantity: new Big('2'), - symbol: 'BALN.SW', - investment: new Big('292'), - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2021-11-12', - fee: new Big('1.65'), - transactionCount: 1 - } - ] - }, - // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) - // End Value: 142.9 * 2 = 285.8 - // Initial Value: 292 (Investment) - // Cash Flow: 0 - // HWR_n0: (285.8 - 292) / 292 = -0.021232877 - - // 22.11.2021,BUY,BALN.SW,CHF,7.00,142.90,5.75 - { - date: '2021-11-22', - items: [ - { - quantity: new Big('9'), // 7 + 2 - symbol: 'BALN.SW', - investment: new Big('1292.3'), // 142.9 * 7 + 146 * 2 - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2021-11-12', - fee: new Big('7.4'), // 1.65 + 5.75 - transactionCount: 2 - } - ] - }, - // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) - // End Value: 139.9 * 9 = 1259.1 - // Initial Value: 285.8 (End Value n0) - // Cash Flow: 1000.3 - // Initial Value + Cash Flow: 285.8 + 1000.3 = 1286.1 - // HWR_n1: (1259.1 - 1286.1) / 1286.1 = -0.020993702 - - // 26.11.2021,BUY,BALN.SW,CHF,3.00,139.90,2.40 - { - date: '2021-11-26', - items: [ - { - quantity: new Big('12'), // 3 + 7 + 2 - symbol: 'BALN.SW', - investment: new Big('1712'), // 139.9 * 3 + 142.9 * 7 + 146 * 2 - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2021-11-12', - fee: new Big('9.8'), // 2.40 + 1.65 + 5.75 - transactionCount: 3 - } - ] - }, - // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) - // End Value: 136.6 * 12 = 1639.2 - // Initial Value: 1259.1 (End Value n1) - // Cash Flow: 139.9 * 3 = 419.7 - // Initial Value + Cash Flow: 1259.1 + 419.7 = 1678.8 - // HWR_n2: (1639.2 - 1678.8) / 1678.8 = -0.023588277 - - // 30.11.2021,BUY,BALN.SW,CHF,2.00,136.60,1.55 - { - date: '2021-11-30', - items: [ - { - quantity: new Big('14'), // 2 + 3 + 7 + 2 - symbol: 'BALN.SW', - investment: new Big('1985.2'), // 136.6 * 2 + 139.9 * 3 + 142.9 * 7 + 146 * 2 - currency: 'CHF', - dataSource: DataSource.YAHOO, - firstBuyDate: '2021-11-12', - fee: new Big('11.35'), // 1.55 + 2.40 + 1.65 + 5.75 - transactionCount: 4 - } - ] - } - // HWR: (End Value - (Initial Value + Cash Flow)) / (Initial Value + Cash Flow) - // End Value: 143.9 * 14 = 2014.6 - // Initial Value: 1639.2 (End Value n2) - // Cash Flow: 136.6 * 2 = 273.2 - // Initial Value + Cash Flow: 1639.2 + 273.2 = 1912.4 - // HWR_n3: (2014.6 - 1912.4) / 1912.4 = 0.053440703 - ]); - - // HWR_total = 1 - (HWR_n0 + 1) * (HWR_n1 + 1) * (HWR_n2 + 1) * (HWR_n3 + 1) - // HWR_total = 1 - (-0.021232877 + 1) * (-0.020993702 + 1) * (-0.023588277 + 1) * (0.053440703 + 1) = 0.014383561 - - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => new Date(Date.UTC(2021, 11, 18)).getTime()); // 2021-12-18 - - const currentPositions = await portfolioCalculator.getCurrentPositions( - parseDate('2021-11-01') - ); - spy.mockRestore(); - - expect(currentPositions).toBeDefined(); - expect(currentPositions.grossPerformance).toEqual(new Big('29.4')); - expect(currentPositions.netPerformance).toEqual(new Big('18.05')); - expect(currentPositions.grossPerformancePercentage).toEqual( - new Big('-0.01438356164383561644') - ); - }); - }); - - describe('calculate timeline', () => { - it('with yearly', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'year' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2020-01-01', - grossPerformance: new Big('498.3'), - netPerformance: new Big('498.3'), - investment: new Big('2923.7'), - value: new Big('3422') // 20 * 171.1 - }, - { - date: '2021-01-01', - grossPerformance: new Big('349.35'), - netPerformance: new Big('349.35'), - investment: new Big('652.55'), - value: new Big('1001.9') // 5 * 200.38 - } - ]); - }); - - it('with yearly and fees', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - const transactionPoints: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(50), - transactionCount: 1 - } - ] - }, - { - date: '2019-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(100), - transactionCount: 2 - } - ] - }, - { - date: '2020-02-02', - items: [ - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(150), - transactionCount: 3 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(200), - transactionCount: 4 - } - ] - }, - { - date: '2021-08-01', - items: [ - { - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(250), - transactionCount: 5 - } - ] - } - ]; - portfolioCalculator.setTransactionPoints(transactionPoints); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'year' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2020-01-01', - grossPerformance: new Big('498.3'), - netPerformance: new Big('398.3'), // 100 fees - investment: new Big('2923.7'), - value: new Big('3422') // 20 * 171.1 - }, - { - date: '2021-01-01', - grossPerformance: new Big('349.35'), - netPerformance: new Big('199.35'), // 150 fees - investment: new Big('652.55'), - value: new Big('1001.9') // 5 * 200.38 - } - ]); - }); - - it('with monthly', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'month' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2019-02-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('1443.8'), - value: new Big('1443.8') // 10 * 144.38 - }, - { - date: '2019-03-01', - grossPerformance: new Big('22.4'), - netPerformance: new Big('22.4'), - investment: new Big('1443.8'), - value: new Big('1466.2') // 10 * 146.62 - }, - { - date: '2019-04-01', - grossPerformance: new Big('47.2'), - netPerformance: new Big('47.2'), - investment: new Big('1443.8'), - value: new Big('1491') // 10 * 149.1 - }, - { - date: '2019-05-01', - grossPerformance: new Big('71.2'), - netPerformance: new Big('71.2'), - investment: new Big('1443.8'), - value: new Big('1515') // 10 * 151.5 - }, - { - date: '2019-06-01', - grossPerformance: new Big('96'), - netPerformance: new Big('96'), - investment: new Big('1443.8'), - value: new Big('1539.8') // 10 * 153.98 - }, - { - date: '2019-07-01', - grossPerformance: new Big('120'), - netPerformance: new Big('120'), - investment: new Big('1443.8'), - value: new Big('1563.8') // 10 * 156.38 - }, - { - date: '2019-08-01', - grossPerformance: new Big('144.8'), - netPerformance: new Big('144.8'), - investment: new Big('1443.8'), - value: new Big('1588.6') // 10 * 158.86 - }, - { - date: '2019-09-01', - grossPerformance: new Big('303.1'), - netPerformance: new Big('303.1'), - investment: new Big('2923.7'), - value: new Big('3226.8') // 20 * 161.34 - }, - { - date: '2019-10-01', - grossPerformance: new Big('351.1'), - netPerformance: new Big('351.1'), - investment: new Big('2923.7'), - value: new Big('3274.8') // 20 * 163.74 - }, - { - date: '2019-11-01', - grossPerformance: new Big('400.7'), - netPerformance: new Big('400.7'), - investment: new Big('2923.7'), - value: new Big('3324.4') // 20 * 166.22 - }, - { - date: '2019-12-01', - grossPerformance: new Big('448.7'), - netPerformance: new Big('448.7'), - investment: new Big('2923.7'), - value: new Big('3372.4') // 20 * 168.62 - }, - { - date: '2020-01-01', - grossPerformance: new Big('498.3'), - netPerformance: new Big('498.3'), - investment: new Big('2923.7'), - value: new Big('3422') // 20 * 171.1 - }, - { - date: '2020-02-01', - grossPerformance: new Big('547.9'), - netPerformance: new Big('547.9'), - investment: new Big('2923.7'), - value: new Big('3471.6') // 20 * 173.58 - }, - { - date: '2020-03-01', - grossPerformance: new Big('226.95'), - netPerformance: new Big('226.95'), - investment: new Big('652.55'), - value: new Big('879.5') // 5 * 175.9 - }, - { - date: '2020-04-01', - grossPerformance: new Big('239.35'), - netPerformance: new Big('239.35'), - investment: new Big('652.55'), - value: new Big('891.9') // 5 * 178.38 - }, - { - date: '2020-05-01', - grossPerformance: new Big('251.35'), - netPerformance: new Big('251.35'), - investment: new Big('652.55'), - value: new Big('903.9') // 5 * 180.78 - }, - { - date: '2020-06-01', - grossPerformance: new Big('263.75'), - netPerformance: new Big('263.75'), - investment: new Big('652.55'), - value: new Big('916.3') // 5 * 183.26 - }, - { - date: '2020-07-01', - grossPerformance: new Big('275.75'), - netPerformance: new Big('275.75'), - investment: new Big('652.55'), - value: new Big('928.3') // 5 * 185.66 - }, - { - date: '2020-08-01', - grossPerformance: new Big('288.15'), - netPerformance: new Big('288.15'), - investment: new Big('652.55'), - value: new Big('940.7') // 5 * 188.14 - }, - { - date: '2020-09-01', - grossPerformance: new Big('300.55'), - netPerformance: new Big('300.55'), - investment: new Big('652.55'), - value: new Big('953.1') // 5 * 190.62 - }, - { - date: '2020-10-01', - grossPerformance: new Big('312.55'), - netPerformance: new Big('312.55'), - investment: new Big('652.55'), - value: new Big('965.1') // 5 * 193.02 - }, - { - date: '2020-11-01', - grossPerformance: new Big('324.95'), - netPerformance: new Big('324.95'), - investment: new Big('652.55'), - value: new Big('977.5') // 5 * 195.5 - }, - { - date: '2020-12-01', - grossPerformance: new Big('336.95'), - netPerformance: new Big('336.95'), - investment: new Big('652.55'), - value: new Big('989.5') // 5 * 197.9 - }, - { - date: '2021-01-01', - grossPerformance: new Big('349.35'), - netPerformance: new Big('349.35'), - investment: new Big('652.55'), - value: new Big('1001.9') // 5 * 200.38 - }, - { - date: '2021-02-01', - grossPerformance: new Big('358.85'), - netPerformance: new Big('358.85'), - investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * 202.86 - }, - { - date: '2021-03-01', - grossPerformance: new Big('392.45'), - netPerformance: new Big('392.45'), - investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * 205.1 - }, - { - date: '2021-04-01', - grossPerformance: new Big('429.65'), - netPerformance: new Big('429.65'), - investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * 207.58 - }, - { - date: '2021-05-01', - grossPerformance: new Big('465.65'), - netPerformance: new Big('465.65'), - investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * 209.98 - }, - { - date: '2021-06-01', - grossPerformance: new Big('502.85'), - netPerformance: new Big('502.85'), - investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * 212.46 - } - ]); - - expect(timelineInfo.maxNetPerformance).toEqual(new Big('547.9')); - expect(timelineInfo.minNetPerformance).toEqual(new Big('0')); - }); - - it('with yearly and monthly mixed', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'year' - }, - { - start: '2021-01-01', - accuracy: 'month' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2020-01-01', - grossPerformance: new Big('498.3'), - netPerformance: new Big('498.3'), - investment: new Big('2923.7'), - value: new Big('3422') // 20 * 171.1 - }, - { - date: '2021-01-01', - grossPerformance: new Big('349.35'), - netPerformance: new Big('349.35'), - investment: new Big('652.55'), - value: new Big('1001.9') // 5 * 200.38 - }, - { - date: '2021-02-01', - grossPerformance: new Big('358.85'), - netPerformance: new Big('358.85'), - investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * 202.86 - }, - { - date: '2021-03-01', - grossPerformance: new Big('392.45'), - netPerformance: new Big('392.45'), - investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * 205.1 - }, - { - date: '2021-04-01', - grossPerformance: new Big('429.65'), - netPerformance: new Big('429.65'), - investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * 207.58 - }, - { - date: '2021-05-01', - grossPerformance: new Big('465.65'), - netPerformance: new Big('465.65'), - investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * 209.98 - }, - { - date: '2021-06-01', - grossPerformance: new Big('502.85'), - netPerformance: new Big('502.85'), - investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * 212.46 - } - ]); - }); - - it('with all mixed', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'year' - }, - { - start: '2021-01-01', - accuracy: 'month' - }, - { - start: '2021-06-01', - accuracy: 'day' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual( - expect.objectContaining([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2020-01-01', - grossPerformance: new Big('498.3'), - netPerformance: new Big('498.3'), - investment: new Big('2923.7'), - value: new Big('3422') // 20 * 171.1 - }, - { - date: '2021-01-01', - grossPerformance: new Big('349.35'), - netPerformance: new Big('349.35'), - investment: new Big('652.55'), - value: new Big('1001.9') // 5 * 200.38 - }, - { - date: '2021-02-01', - grossPerformance: new Big('358.85'), - netPerformance: new Big('358.85'), - investment: new Big('2684.05'), - value: new Big('3042.9') // 15 * 202.86 - }, - { - date: '2021-03-01', - grossPerformance: new Big('392.45'), - netPerformance: new Big('392.45'), - investment: new Big('2684.05'), - value: new Big('3076.5') // 15 * 205.1 - }, - { - date: '2021-04-01', - grossPerformance: new Big('429.65'), - netPerformance: new Big('429.65'), - investment: new Big('2684.05'), - value: new Big('3113.7') // 15 * 207.58 - }, - { - date: '2021-05-01', - grossPerformance: new Big('465.65'), - netPerformance: new Big('465.65'), - investment: new Big('2684.05'), - value: new Big('3149.7') // 15 * 209.98 - }, - { - date: '2021-06-01', - grossPerformance: new Big('502.85'), - netPerformance: new Big('502.85'), - investment: new Big('2684.05'), - value: new Big('3186.9') // 15 * 212.46 - }, - { - date: '2021-06-02', - grossPerformance: new Big('504.05'), - netPerformance: new Big('504.05'), - investment: new Big('2684.05'), - value: new Big('3188.1') // 15 * 212.54 - }, - { - date: '2021-06-03', - grossPerformance: new Big('505.25'), - netPerformance: new Big('505.25'), - investment: new Big('2684.05'), - value: new Big('3189.3') // 15 * 212.62 - }, - { - date: '2021-06-04', - grossPerformance: new Big('506.45'), - netPerformance: new Big('506.45'), - investment: new Big('2684.05'), - value: new Big('3190.5') // 15 * 212.7 - }, - { - date: '2021-06-05', - grossPerformance: new Big('507.65'), - netPerformance: new Big('507.65'), - investment: new Big('2684.05'), - value: new Big('3191.7') // 15 * 212.78 - }, - { - date: '2021-06-06', - grossPerformance: new Big('508.85'), - netPerformance: new Big('508.85'), - investment: new Big('2684.05'), - value: new Big('3192.9') // 15 * 212.86 - }, - { - date: '2021-06-07', - grossPerformance: new Big('510.05'), - netPerformance: new Big('510.05'), - investment: new Big('2684.05'), - value: new Big('3194.1') // 15 * 212.94 - }, - { - date: '2021-06-08', - grossPerformance: new Big('511.25'), - netPerformance: new Big('511.25'), - investment: new Big('2684.05'), - value: new Big('3195.3') // 15 * 213.02 - }, - { - date: '2021-06-09', - grossPerformance: new Big('512.45'), - netPerformance: new Big('512.45'), - investment: new Big('2684.05'), - value: new Big('3196.5') // 15 * 213.1 - }, - { - date: '2021-06-10', - grossPerformance: new Big('513.65'), - netPerformance: new Big('513.65'), - investment: new Big('2684.05'), - value: new Big('3197.7') // 15 * 213.18 - }, - { - date: '2021-06-11', - grossPerformance: new Big('514.85'), - netPerformance: new Big('514.85'), - investment: new Big('2684.05'), - value: new Big('3198.9') // 15 * 213.26 - }, - { - date: '2021-06-12', - grossPerformance: new Big('516.05'), - netPerformance: new Big('516.05'), - investment: new Big('2684.05'), - value: new Big('3200.1') // 15 * 213.34 - }, - { - date: '2021-06-13', - grossPerformance: new Big('517.25'), - netPerformance: new Big('517.25'), - investment: new Big('2684.05'), - value: new Big('3201.3') // 15 * 213.42 - }, - { - date: '2021-06-14', - grossPerformance: new Big('518.45'), - netPerformance: new Big('518.45'), - investment: new Big('2684.05'), - value: new Big('3202.5') // 15 * 213.5 - }, - { - date: '2021-06-15', - grossPerformance: new Big('519.65'), - netPerformance: new Big('519.65'), - investment: new Big('2684.05'), - value: new Big('3203.7') // 15 * 213.58 - }, - { - date: '2021-06-16', - grossPerformance: new Big('520.85'), - netPerformance: new Big('520.85'), - investment: new Big('2684.05'), - value: new Big('3204.9') // 15 * 213.66 - }, - { - date: '2021-06-17', - grossPerformance: new Big('522.05'), - netPerformance: new Big('522.05'), - investment: new Big('2684.05'), - value: new Big('3206.1') // 15 * 213.74 - }, - { - date: '2021-06-18', - grossPerformance: new Big('523.25'), - netPerformance: new Big('523.25'), - investment: new Big('2684.05'), - value: new Big('3207.3') // 15 * 213.82 - }, - { - date: '2021-06-19', - grossPerformance: new Big('524.45'), - netPerformance: new Big('524.45'), - investment: new Big('2684.05'), - value: new Big('3208.5') // 15 * 213.9 - }, - { - date: '2021-06-20', - grossPerformance: new Big('525.65'), - netPerformance: new Big('525.65'), - investment: new Big('2684.05'), - value: new Big('3209.7') // 15 * 213.98 - }, - { - date: '2021-06-21', - grossPerformance: new Big('526.85'), - netPerformance: new Big('526.85'), - investment: new Big('2684.05'), - value: new Big('3210.9') // 15 * 214.06 - }, - { - date: '2021-06-22', - grossPerformance: new Big('528.05'), - netPerformance: new Big('528.05'), - investment: new Big('2684.05'), - value: new Big('3212.1') // 15 * 214.14 - }, - { - date: '2021-06-23', - grossPerformance: new Big('529.25'), - netPerformance: new Big('529.25'), - investment: new Big('2684.05'), - value: new Big('3213.3') // 15 * 214.22 - }, - { - date: '2021-06-24', - grossPerformance: new Big('530.45'), - netPerformance: new Big('530.45'), - investment: new Big('2684.05'), - value: new Big('3214.5') // 15 * 214.3 - }, - { - date: '2021-06-25', - grossPerformance: new Big('531.65'), - netPerformance: new Big('531.65'), - investment: new Big('2684.05'), - value: new Big('3215.7') // 15 * 214.38 - }, - { - date: '2021-06-26', - grossPerformance: new Big('532.85'), - netPerformance: new Big('532.85'), - investment: new Big('2684.05'), - value: new Big('3216.9') // 15 * 214.46 - }, - { - date: '2021-06-27', - grossPerformance: new Big('534.05'), - netPerformance: new Big('534.05'), - investment: new Big('2684.05'), - value: new Big('3218.1') // 15 * 214.54 - }, - { - date: '2021-06-28', - grossPerformance: new Big('535.25'), - netPerformance: new Big('535.25'), - investment: new Big('2684.05'), - value: new Big('3219.3') // 15 * 214.62 - }, - { - date: '2021-06-29', - grossPerformance: new Big('536.45'), - netPerformance: new Big('536.45'), - investment: new Big('2684.05'), - value: new Big('3220.5') // 15 * 214.7 - }, - { - date: '2021-06-30', - grossPerformance: new Big('537.65'), - netPerformance: new Big('537.65'), - investment: new Big('2684.05'), - value: new Big('3221.7') // 15 * 214.78 - } - ]) - ); - }); - - it('with mixed portfolio', async () => { - const portfolioCalculator = new PortfolioCalculator( - currentRateService, - 'USD' - ); - portfolioCalculator.setTransactionPoints([ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - }, - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - } - ] - } - ]); - const timelineSpecification: TimelineSpecification[] = [ - { - start: '2019-01-01', - accuracy: 'year' - } - ]; - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2020-01-01' - ); - const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; - - expect(timeline).toEqual([ - { - date: '2019-01-01', - grossPerformance: new Big('0'), - netPerformance: new Big('0'), - investment: new Big('0'), - value: new Big('0') - }, - { - date: '2020-01-01', - grossPerformance: new Big('267.2'), - netPerformance: new Big('267.2'), - investment: new Big('11553.75'), - value: new Big('11820.95') // 10 * 171.1 + 5 * 2021.99 - } - ]); - }); - }); - describe('annualized performance percentage', () => { - const portfolioCalculator = new PortfolioCalculator( + const portfolioCalculator = new PortfolioCalculator({ currentRateService, - 'USD' - ); + currency: 'USD', + orders: [] + }); it('Get annualized performance', async () => { expect( @@ -2685,351 +71,3 @@ describe('PortfolioCalculator', () => { }); }); }); - -const ordersMixedSymbols: PortfolioOrder[] = [ - { - date: '2017-01-03', - name: 'Tesla, Inc.', - quantity: new Big('50'), - symbol: 'TSLA', - type: 'BUY', - unitPrice: new Big('42.97'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2017-07-01', - name: 'Bitcoin USD', - quantity: new Big('0.5614682'), - symbol: 'BTCUSD', - type: 'BUY', - unitPrice: new Big('3562.089535970158'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2018-09-01', - name: 'Amazon.com, Inc.', - quantity: new Big('5'), - symbol: 'AMZN', - type: 'BUY', - unitPrice: new Big('2021.99'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - } -]; - -const ordersVTI: PortfolioOrder[] = [ - { - date: '2019-02-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('144.38'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2019-08-03', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('147.99'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2020-02-02', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('15'), - symbol: 'VTI', - type: 'SELL', - unitPrice: new Big('151.41'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2021-08-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('177.69'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - }, - { - date: '2021-02-01', - name: 'Vanguard Total Stock Market Index Fund ETF Shares', - quantity: new Big('10'), - symbol: 'VTI', - type: 'BUY', - unitPrice: new Big('203.15'), - currency: 'USD', - dataSource: DataSource.YAHOO, - fee: new Big(0) - } -]; - -const orderTslaTransactionPoint: TransactionPoint[] = [ - { - date: '2021-01-01', - items: [ - { - quantity: new Big('1'), - symbol: 'TSLA', - investment: new Big('719.46'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2021-01-01', - fee: new Big(0), - transactionCount: 1 - } - ] - } -]; - -const ordersVTITransactionPoints: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2019-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2020-02-02', - items: [ - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 3 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 4 - } - ] - }, - { - date: '2021-08-01', - items: [ - { - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 5 - } - ] - } -]; - -const transactionPointsBuyAndSell: TransactionPoint[] = [ - { - date: '2019-02-01', - items: [ - { - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 1 - } - ] - }, - { - date: '2019-08-03', - items: [ - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2019-09-01', - items: [ - { - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 2 - } - ] - }, - { - date: '2020-02-02', - items: [ - { - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 1 - }, - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 3 - } - ] - }, - { - date: '2020-08-02', - items: [ - { - quantity: new Big('0'), - symbol: 'AMZN', - investment: new Big('0'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 2 - }, - { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 3 - } - ] - }, - { - date: '2021-02-01', - items: [ - { - quantity: new Big('0'), - symbol: 'AMZN', - investment: new Big('0'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 2 - }, - { - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 4 - } - ] - }, - { - date: '2021-08-01', - items: [ - { - quantity: new Big('0'), - symbol: 'AMZN', - investment: new Big('0'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-09-01', - fee: new Big(0), - transactionCount: 2 - }, - { - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: 'USD', - dataSource: DataSource.YAHOO, - firstBuyDate: '2019-02-01', - fee: new Big(0), - transactionCount: 5 - } - ] - } -]; diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 2dd11e0eb..20b0a8709 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,15 +1,15 @@ 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 } from '@ghostfolio/common/interfaces'; import { Logger } from '@nestjs/common'; import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { addDays, + addMilliseconds, addMonths, addYears, - differenceInDays, endOfDay, format, isAfter, @@ -17,11 +17,12 @@ import { max, min } from 'date-fns'; -import { flatten, isNumber } from 'lodash'; +import { first, flatten, isNumber, sortBy } from 'lodash'; import { CurrentRateService } from './current-rate.service'; import { CurrentPositions } from './interfaces/current-positions.interface'; import { GetValueObject } from './interfaces/get-value-object.interface'; +import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { TimelinePeriod } from './interfaces/timeline-period.interface'; import { @@ -32,22 +33,39 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in import { TransactionPoint } from './interfaces/transaction-point.interface'; export class PortfolioCalculator { - private transactionPoints: TransactionPoint[]; + private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = + true; + + private static readonly ENABLE_LOGGING = false; - public constructor( - private currentRateService: CurrentRateService, - private currency: string - ) {} + private currency: string; + private currentRateService: CurrentRateService; + private orders: PortfolioOrder[]; + private transactionPoints: TransactionPoint[]; - public computeTransactionPoints(orders: PortfolioOrder[]) { - orders.sort((a, b) => a.date.localeCompare(b.date)); + public constructor({ + currency, + currentRateService, + orders + }: { + currency: string; + currentRateService: CurrentRateService; + orders: PortfolioOrder[]; + }) { + this.currency = currency; + this.currentRateService = currentRateService; + this.orders = orders; + + this.orders.sort((a, b) => a.date.localeCompare(b.date)); + } + public computeTransactionPoints() { this.transactionPoints = []; const symbols: { [symbol: string]: TransactionPointSymbol } = {}; let lastDate: string = null; let lastTransactionPoint: TransactionPoint = null; - for (const order of orders) { + for (const order of this.orders) { const currentDate = order.date; let currentTransactionPointItem: TransactionPointSymbol; @@ -140,7 +158,6 @@ export class PortfolioCalculator { hasErrors: false, grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - netAnnualizedPerformance: new Big(0), netPerformance: new Big(0), netPerformancePercentage: new Big(0), positions: [], @@ -195,6 +212,7 @@ export class PortfolioCalculator { const marketSymbolMap: { [date: string]: { [symbol: string]: Big }; } = {}; + for (const marketSymbol of marketSymbols) { const date = format(marketSymbol.date, DATE_FORMAT); if (!marketSymbolMap[date]) { @@ -207,112 +225,37 @@ export class PortfolioCalculator { } } - let hasErrors = false; - const startString = format(start, DATE_FORMAT); - - const holdingPeriodReturns: { [symbol: string]: Big } = {}; - const netHoldingPeriodReturns: { [symbol: string]: Big } = {}; - const grossPerformance: { [symbol: string]: Big } = {}; - const netPerformance: { [symbol: string]: Big } = {}; const todayString = format(today, DATE_FORMAT); if (firstIndex > 0) { firstIndex--; } - const invalidSymbols = []; - const lastInvestments: { [symbol: string]: Big } = {}; - const lastQuantities: { [symbol: string]: Big } = {}; - const lastFees: { [symbol: string]: Big } = {}; const initialValues: { [symbol: string]: Big } = {}; - for (let i = firstIndex; i < this.transactionPoints.length; i++) { - const currentDate = - i === firstIndex ? startString : this.transactionPoints[i].date; - const nextDate = - i + 1 < this.transactionPoints.length - ? this.transactionPoints[i + 1].date - : todayString; - - const items = this.transactionPoints[i].items; - for (const item of items) { - if (!marketSymbolMap[nextDate]?.[item.symbol]) { - invalidSymbols.push(item.symbol); - hasErrors = true; - Logger.warn( - `Missing value for symbol ${item.symbol} at ${nextDate}`, - 'PortfolioCalculator' - ); - continue; - } - let lastInvestment: Big = new Big(0); - let lastQuantity: Big = item.quantity; - if (lastInvestments[item.symbol] && lastQuantities[item.symbol]) { - lastInvestment = item.investment.minus(lastInvestments[item.symbol]); - lastQuantity = lastQuantities[item.symbol]; - } - - const itemValue = marketSymbolMap[currentDate]?.[item.symbol]; - let initialValue = itemValue?.mul(lastQuantity); - let investedValue = itemValue?.mul(item.quantity); - const isFirstOrderAndIsStartBeforeCurrentDate = - i === firstIndex && - isBefore(parseDate(this.transactionPoints[i].date), start); - const lastFee: Big = lastFees[item.symbol] ?? new Big(0); - const fee = isFirstOrderAndIsStartBeforeCurrentDate - ? new Big(0) - : item.fee.minus(lastFee); - if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) { - initialValue = item.investment; - investedValue = item.investment; - } - if (i === firstIndex || !initialValues[item.symbol]) { - initialValues[item.symbol] = initialValue; - } - if (!item.quantity.eq(0)) { - if (!initialValue) { - invalidSymbols.push(item.symbol); - hasErrors = true; - Logger.warn( - `Missing value for symbol ${item.symbol} at ${currentDate}`, - 'PortfolioCalculator' - ); - continue; - } + const positions: TimelinePosition[] = []; + let hasAnySymbolMetricsErrors = false; - const cashFlow = lastInvestment; - const endValue = marketSymbolMap[nextDate][item.symbol].mul( - item.quantity - ); + const errors: ResponseError['errors'] = []; - const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow)); - holdingPeriodReturns[item.symbol] = ( - holdingPeriodReturns[item.symbol] ?? new Big(1) - ).mul(holdingPeriodReturn); - grossPerformance[item.symbol] = ( - grossPerformance[item.symbol] ?? new Big(0) - ).plus(endValue.minus(investedValue)); + for (const item of lastTransactionPoint.items) { + const marketValue = marketSymbolMap[todayString]?.[item.symbol]; - const netHoldingPeriodReturn = endValue.div( - initialValue.plus(cashFlow).plus(fee) - ); - netHoldingPeriodReturns[item.symbol] = ( - netHoldingPeriodReturns[item.symbol] ?? new Big(1) - ).mul(netHoldingPeriodReturn); - netPerformance[item.symbol] = ( - netPerformance[item.symbol] ?? new Big(0) - ).plus(endValue.minus(investedValue).minus(fee)); - } - lastInvestments[item.symbol] = item.investment; - lastQuantities[item.symbol] = item.quantity; - lastFees[item.symbol] = item.fee; - } - } + const { + grossPerformance, + grossPerformancePercentage, + hasErrors, + initialValue, + netPerformance, + netPerformancePercentage + } = this.getSymbolMetrics({ + marketSymbolMap, + start, + symbol: item.symbol + }); - const positions: TimelinePosition[] = []; + hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; + initialValues[item.symbol] = initialValue; - for (const item of lastTransactionPoint.items) { - const marketValue = marketSymbolMap[todayString]?.[item.symbol]; - const isValid = invalidSymbols.indexOf(item.symbol) === -1; positions.push({ averagePrice: item.quantity.eq(0) ? new Big(0) @@ -320,31 +263,33 @@ export class PortfolioCalculator { currency: item.currency, dataSource: item.dataSource, firstBuyDate: item.firstBuyDate, - grossPerformance: isValid - ? grossPerformance[item.symbol] ?? null + grossPerformance: !hasErrors ? grossPerformance ?? null : null, + grossPerformancePercentage: !hasErrors + ? grossPerformancePercentage ?? null : null, - grossPerformancePercentage: - isValid && holdingPeriodReturns[item.symbol] - ? holdingPeriodReturns[item.symbol].minus(1) - : null, investment: item.investment, marketPrice: marketValue?.toNumber() ?? null, - netPerformance: isValid ? netPerformance[item.symbol] ?? null : null, - netPerformancePercentage: - isValid && netHoldingPeriodReturns[item.symbol] - ? netHoldingPeriodReturns[item.symbol].minus(1) - : null, + netPerformance: !hasErrors ? netPerformance ?? null : null, + netPerformancePercentage: !hasErrors + ? netPerformancePercentage ?? null + : null, quantity: item.quantity, 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: hasErrors || overall.hasErrors + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; } @@ -462,20 +407,16 @@ export class PortfolioCalculator { 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 netAnnualizedPerformance = new Big(0); - - // use Date.now() to use the mock for today - const today = new Date(Date.now()); + let sumOfWeights = new Big(0); + let totalInvestment = new Big(0); for (const currentPosition of positions) { if (currentPosition.marketPrice) { @@ -485,36 +426,34 @@ export class PortfolioCalculator { } 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) - ); - netAnnualizedPerformance = netAnnualizedPerformance.plus( - this.getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays( - today, - parseDate(currentPosition.firstBuyDate) - ), - netPerformancePercent: currentPosition.netPerformancePercentage - }).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( @@ -525,13 +464,12 @@ export class PortfolioCalculator { } } - if (!completeInitialValue.eq(0)) { - grossPerformancePercentage = - grossPerformancePercentage.div(completeInitialValue); - netPerformancePercentage = - netPerformancePercentage.div(completeInitialValue); - netAnnualizedPerformance = - netAnnualizedPerformance.div(completeInitialValue); + if (sumOfWeights.gt(0)) { + grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights); + netPerformancePercentage = netPerformancePercentage.div(sumOfWeights); + } else { + grossPerformancePercentage = new Big(0); + netPerformancePercentage = new Big(0); } return { @@ -539,7 +477,6 @@ export class PortfolioCalculator { grossPerformance, grossPerformancePercentage, hasErrors, - netAnnualizedPerformance, netPerformance, netPerformancePercentage, totalInvestment @@ -693,6 +630,356 @@ export class PortfolioCalculator { } } + private getSymbolMetrics({ + marketSymbolMap, + start, + symbol + }: { + marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + }; + start: Date; + symbol: string; + }) { + let orders: PortfolioOrderItem[] = this.orders.filter((order) => { + return order.symbol === symbol; + }); + + if (orders.length <= 0) { + return { + hasErrors: false, + initialValue: new Big(0), + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0) + }; + } + + const dateOfFirstTransaction = new Date(first(orders).date); + const endDate = new Date(Date.now()); + + const unitPriceAtStartDate = + marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; + + const unitPriceAtEndDate = + marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol]; + + if ( + !unitPriceAtEndDate || + (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) + ) { + return { + hasErrors: true, + initialValue: new Big(0), + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0) + }; + } + + 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); + let grossPerformanceAtStartDate = new Big(0); + let grossPerformanceFromSells = new Big(0); + let initialValue: Big; + let investmentAtStartDate: Big; + 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); + let valueAtStartDate: Big; + + // Add a synthetic order at the start and the end date + orders.push({ + symbol, + currency: null, + date: format(start, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'start', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtStartDate + }); + + orders.push({ + symbol, + currency: null, + date: format(endDate, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'end', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtEndDate + }); + + // Sort orders so that the start and end placeholder order are at the right + // position + orders = sortBy(orders, (order) => { + let sortIndex = new Date(order.date); + + if (order.itemType === 'start') { + sortIndex = addMilliseconds(sortIndex, -1); + } + + if (order.itemType === 'end') { + sortIndex = addMilliseconds(sortIndex, 1); + } + + return sortIndex.getTime(); + }); + + const indexOfStartOrder = orders.findIndex((order) => { + 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 + ); + + if (!investmentAtStartDate && i >= indexOfStartOrder) { + investmentAtStartDate = totalInvestment ?? new Big(0); + valueAtStartDate = valueOfInvestmentBeforeTransaction; + } + + const transactionInvestment = order.quantity + .mul(order.unitPrice) + .mul(this.getFactor(order.type)); + + totalInvestment = totalInvestment.plus(transactionInvestment); + + if (i >= indexOfStartOrder && 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); + + totalUnits = totalUnits.plus( + order.quantity.mul(this.getFactor(order.type)) + ); + + const valueOfInvestment = totalUnits.mul(order.unitPrice); + + const grossPerformanceFromSell = + order.type === TypeOfOrder.SELL + ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) + : new Big(0); + + grossPerformanceFromSells = grossPerformanceFromSells.plus( + grossPerformanceFromSell + ); + + totalInvestmentWithGrossPerformanceFromSell = + totalInvestmentWithGrossPerformanceFromSell + .plus(transactionInvestment) + .plus(grossPerformanceFromSell); + + lastAveragePrice = totalUnits.eq(0) + ? new Big(0) + : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); + + const newGrossPerformance = valueOfInvestment + .minus(totalInvestmentWithGrossPerformanceFromSell) + .plus(grossPerformanceFromSells); + + if ( + i > indexOfStartOrder && + !lastValueOfInvestmentBeforeTransaction + .plus(lastTransactionInvestment) + .eq(0) + ) { + const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction + .minus( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ) + .div( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ); + + timeWeightedGrossPerformancePercentage = + timeWeightedGrossPerformancePercentage.mul( + new Big(1).plus(grossHoldingPeriodReturn) + ); + + const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction + .minus(fees.minus(feesAtStartDate)) + .minus( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ) + .div( + lastValueOfInvestmentBeforeTransaction.plus( + lastTransactionInvestment + ) + ); + + timeWeightedNetPerformancePercentage = + timeWeightedNetPerformancePercentage.mul( + new Big(1).plus(netHoldingPeriodReturn) + ); + } + + grossPerformance = newGrossPerformance; + + lastTransactionInvestment = transactionInvestment; + + lastValueOfInvestmentBeforeTransaction = + valueOfInvestmentBeforeTransaction; + + if (order.itemType === 'start') { + feesAtStartDate = fees; + grossPerformanceAtStartDate = grossPerformance; + } + } + + timeWeightedGrossPerformancePercentage = + timeWeightedGrossPerformancePercentage.minus(1); + + timeWeightedNetPerformancePercentage = + timeWeightedNetPerformancePercentage.minus(1); + + const totalGrossPerformance = grossPerformance.minus( + grossPerformanceAtStartDate + ); + + const totalNetPerformance = grossPerformance + .minus(grossPerformanceAtStartDate) + .minus(fees.minus(feesAtStartDate)); + + const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( + maxTotalInvestment.minus(investmentAtStartDate) + ); + + const grossPerformancePercentage = + PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || + averagePriceAtStartDate.eq(0) || + averagePriceAtEndDate.eq(0) || + orders[indexOfStartOrder].unitPrice.eq(0) + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + // lower than the average buying price results in a positive + // performance even if the market price stays constant + 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 = + PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || + averagePriceAtStartDate.eq(0) || + averagePriceAtEndDate.eq(0) || + orders[indexOfStartOrder].unitPrice.eq(0) + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + // lower than the average buying price results in a positive + // performance even if the market price stays constant + unitPriceAtEndDate + .minus(feesPerUnit) + .div(averagePriceAtEndDate) + .div( + orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) + ) + .minus(1); + + if (PortfolioCalculator.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, + grossPerformancePercentage, + netPerformancePercentage, + hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), + netPerformance: totalNetPerformance, + grossPerformance: totalGrossPerformance + }; + } + private isNextItemActive( timelineSpecification: TimelineSpecification[], currentDate: Date, diff --git a/apps/api/src/app/portfolio/portfolio-service.strategy.ts b/apps/api/src/app/portfolio/portfolio-service.strategy.ts deleted file mode 100644 index a85b28852..000000000 --- a/apps/api/src/app/portfolio/portfolio-service.strategy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { RequestWithUser } from '@ghostfolio/common/types'; -import { Inject, Injectable } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; - -import { PortfolioService } from './portfolio.service'; -import { PortfolioServiceNew } from './portfolio.service-new'; - -@Injectable() -export class PortfolioServiceStrategy { - public constructor( - private readonly portfolioService: PortfolioService, - private readonly portfolioServiceNew: PortfolioServiceNew, - @Inject(REQUEST) private readonly request: RequestWithUser - ) {} - - public get(newCalculationEngine?: boolean) { - if ( - newCalculationEngine || - this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true - ) { - return this.portfolioServiceNew; - } - - return this.portfolioService; - } -} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a47f43035..ad06dbb52 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -38,7 +38,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioPositions } from './interfaces/portfolio-positions.interface'; -import { PortfolioServiceStrategy } from './portfolio-service.strategy'; +import { PortfolioService } from './portfolio.service'; @Controller('portfolio') export class PortfolioController { @@ -46,7 +46,7 @@ export class PortfolioController { private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly portfolioServiceStrategy: PortfolioServiceStrategy, + private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @@ -57,9 +57,10 @@ export class PortfolioController { @Headers('impersonation-id') impersonationId: string, @Query('range') range ): Promise { - const historicalDataContainer = await this.portfolioServiceStrategy - .get() - .getChart(impersonationId, range); + const historicalDataContainer = await this.portfolioService.getChart( + impersonationId, + range + ); let chartData = historicalDataContainer.items; @@ -109,9 +110,11 @@ export class PortfolioController { let hasError = false; const { accounts, holdings, hasErrors } = - await this.portfolioServiceStrategy - .get(true) - .getDetails(impersonationId, this.request.user.id, range); + await this.portfolioService.getDetails( + impersonationId, + this.request.user.id, + range + ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { hasError = true; @@ -174,9 +177,9 @@ export class PortfolioController { ); } - let investments = await this.portfolioServiceStrategy - .get() - .getInvestments(impersonationId); + let investments = await this.portfolioService.getInvestments( + impersonationId + ); if ( impersonationId || @@ -203,9 +206,10 @@ export class PortfolioController { @Headers('impersonation-id') impersonationId: string, @Query('range') range ): Promise { - const performanceInformation = await this.portfolioServiceStrategy - .get() - .getPerformance(impersonationId, range); + const performanceInformation = await this.portfolioService.getPerformance( + impersonationId, + range + ); if ( impersonationId || @@ -228,9 +232,10 @@ export class PortfolioController { @Headers('impersonation-id') impersonationId: string, @Query('range') range ): Promise { - const result = await this.portfolioServiceStrategy - .get() - .getPositions(impersonationId, range); + const result = await this.portfolioService.getPositions( + impersonationId, + range + ); if ( impersonationId || @@ -270,9 +275,10 @@ export class PortfolioController { hasDetails = user.subscription.type === 'Premium'; } - const { holdings } = await this.portfolioServiceStrategy - .get(true) - .getDetails(access.userId, access.userId); + const { holdings } = await this.portfolioService.getDetails( + access.userId, + access.userId + ); const portfolioPublicDetails: PortfolioPublicDetails = { hasDetails, @@ -324,9 +330,7 @@ export class PortfolioController { ); } - let summary = await this.portfolioServiceStrategy - .get() - .getSummary(impersonationId); + let summary = await this.portfolioService.getSummary(impersonationId); if ( impersonationId || @@ -360,9 +364,11 @@ export class PortfolioController { @Param('dataSource') dataSource, @Param('symbol') symbol ): Promise { - let position = await this.portfolioServiceStrategy - .get() - .getPosition(dataSource, impersonationId, symbol); + let position = await this.portfolioService.getPosition( + dataSource, + impersonationId, + symbol + ); if (position) { if ( @@ -403,6 +409,6 @@ export class PortfolioController { ); } - return await this.portfolioServiceStrategy.get().getReport(impersonationId); + return await this.portfolioService.getReport(impersonationId); } } diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 5204f1795..7e6dfe88d 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -13,15 +13,13 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.mod import { Module } from '@nestjs/common'; import { CurrentRateService } from './current-rate.service'; -import { PortfolioServiceStrategy } from './portfolio-service.strategy'; import { PortfolioController } from './portfolio.controller'; import { PortfolioService } from './portfolio.service'; -import { PortfolioServiceNew } from './portfolio.service-new'; import { RulesService } from './rules.service'; @Module({ controllers: [PortfolioController], - exports: [PortfolioServiceStrategy], + exports: [PortfolioService], imports: [ AccessModule, ConfigurationModule, @@ -39,8 +37,6 @@ import { RulesService } from './rules.service'; AccountService, CurrentRateService, PortfolioService, - PortfolioServiceNew, - PortfolioServiceStrategy, RulesService ] }) diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts deleted file mode 100644 index e3b9e8536..000000000 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ /dev/null @@ -1,1324 +0,0 @@ -import { AccountService } from '@ghostfolio/api/app/account/account.service'; -import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; -import { OrderService } from '@ghostfolio/api/app/order/order.service'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; -import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; -import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; -import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; -import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; -import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; -import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; -import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; -import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-initial-investment'; -import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; -import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment'; -import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; -import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; -import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; -import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; -import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { - ASSET_SUB_CLASS_EMERGENCY_FUND, - UNKNOWN_KEY, - baseCurrency -} from '@ghostfolio/common/config'; -import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; -import { - Accounts, - PortfolioDetails, - PortfolioPerformanceResponse, - PortfolioReport, - PortfolioSummary, - Position, - TimelinePosition -} from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; -import type { - AccountWithValue, - DateRange, - Market, - OrderWithAccount, - RequestWithUser -} from '@ghostfolio/common/types'; -import { Inject, Injectable } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; -import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client'; -import Big from 'big.js'; -import { - differenceInDays, - endOfToday, - format, - isAfter, - isBefore, - max, - parse, - parseISO, - setDayOfYear, - startOfDay, - subDays, - subYears -} from 'date-fns'; -import { isEmpty, sortBy } from 'lodash'; - -import { - HistoricalDataContainer, - HistoricalDataItem, - PortfolioPositionDetail -} from './interfaces/portfolio-position-detail.interface'; -import { PortfolioCalculatorNew } from './portfolio-calculator-new'; -import { RulesService } from './rules.service'; - -const developedMarkets = require('../../assets/countries/developed-markets.json'); -const emergingMarkets = require('../../assets/countries/emerging-markets.json'); - -@Injectable() -export class PortfolioServiceNew { - public constructor( - private readonly accountService: AccountService, - private readonly currentRateService: CurrentRateService, - private readonly dataProviderService: DataProviderService, - private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly impersonationService: ImpersonationService, - private readonly orderService: OrderService, - @Inject(REQUEST) private readonly request: RequestWithUser, - private readonly rulesService: RulesService, - private readonly symbolProfileService: SymbolProfileService, - private readonly userService: UserService - ) {} - - public async getAccounts(aUserId: string): Promise { - const [accounts, details] = await Promise.all([ - this.accountService.accounts({ - include: { Order: true, Platform: true }, - orderBy: { name: 'asc' }, - where: { userId: aUserId } - }), - this.getDetails(aUserId, aUserId) - ]); - - const userCurrency = this.request.user.Settings.currency; - - return accounts.map((account) => { - let transactionCount = 0; - - for (const order of account.Order) { - if (!order.isDraft) { - transactionCount += 1; - } - } - - const valueInBaseCurrency = details.accounts[account.id]?.current ?? 0; - - const result = { - ...account, - transactionCount, - valueInBaseCurrency, - balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ), - value: this.exchangeRateDataService.toCurrency( - valueInBaseCurrency, - userCurrency, - account.currency - ) - }; - - delete result.Order; - - return result; - }); - } - - public async getAccountsWithAggregations(aUserId: string): Promise { - const accounts = await this.getAccounts(aUserId); - let totalBalanceInBaseCurrency = new Big(0); - let totalValueInBaseCurrency = new Big(0); - let transactionCount = 0; - - for (const account of accounts) { - totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus( - account.balanceInBaseCurrency - ); - totalValueInBaseCurrency = totalValueInBaseCurrency.plus( - account.valueInBaseCurrency - ); - transactionCount += account.transactionCount; - } - - return { - accounts, - transactionCount, - totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), - totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber() - }; - } - - public async getInvestments( - aImpersonationId: string - ): Promise { - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - - const { portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId, - includeDrafts: true - }); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: this.request.user.Settings.currency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.setTransactionPoints(transactionPoints); - if (transactionPoints.length === 0) { - return []; - } - - const investments = portfolioCalculator.getInvestments().map((item) => { - return { - date: item.date, - investment: item.investment.toNumber() - }; - }); - - // Add investment of today - const investmentOfToday = investments.filter((investment) => { - return investment.date === format(new Date(), DATE_FORMAT); - }); - - if (investmentOfToday.length <= 0) { - const pastInvestments = investments.filter((investment) => { - return isBefore(parseDate(investment.date), new Date()); - }); - const lastInvestment = pastInvestments[pastInvestments.length - 1]; - - investments.push({ - date: format(new Date(), DATE_FORMAT), - investment: lastInvestment?.investment ?? 0 - }); - } - - return sortBy(investments, (investment) => { - return investment.date; - }); - } - - public async getChart( - aImpersonationId: string, - aDateRange: DateRange = 'max' - ): Promise { - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - - const { portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId - }); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: this.request.user.Settings.currency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.setTransactionPoints(transactionPoints); - if (transactionPoints.length === 0) { - return { - isAllTimeHigh: false, - isAllTimeLow: false, - items: [] - }; - } - let portfolioStart = parse( - transactionPoints[0].date, - DATE_FORMAT, - new Date() - ); - - // Get start date for the full portfolio because of because of the - // min and max calculation - portfolioStart = this.getStartDate('max', portfolioStart); - - const timelineSpecification: TimelineSpecification[] = [ - { - start: format(portfolioStart, DATE_FORMAT), - accuracy: 'day' - } - ]; - - const timelineInfo = await portfolioCalculator.calculateTimeline( - timelineSpecification, - format(new Date(), DATE_FORMAT) - ); - - const timeline = timelineInfo.timelinePeriods; - - const items = timeline - .filter((timelineItem) => timelineItem !== null) - .map((timelineItem) => ({ - date: timelineItem.date, - marketPrice: timelineItem.value, - value: timelineItem.netPerformance.toNumber() - })); - - let lastItem = null; - if (timeline.length > 0) { - lastItem = timeline[timeline.length - 1]; - } - - let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq( - lastItem?.netPerformance - ); - let isAllTimeLow = timelineInfo.minNetPerformance?.eq( - lastItem?.netPerformance - ); - if (isAllTimeHigh && isAllTimeLow) { - isAllTimeHigh = false; - isAllTimeLow = false; - } - - portfolioStart = startOfDay( - this.getStartDate( - aDateRange, - parse(transactionPoints[0].date, DATE_FORMAT, new Date()) - ) - ); - - return { - isAllTimeHigh, - isAllTimeLow, - items: items.filter((item) => { - // Filter items of date range - return !isAfter(portfolioStart, parseDate(item.date)); - }) - }; - } - - public async getDetails( - aImpersonationId: string, - aUserId: string, - aDateRange: DateRange = 'max' - ): Promise { - const userId = await this.getUserId(aImpersonationId, aUserId); - const user = await this.userService.user({ id: userId }); - - const emergencyFund = new Big( - (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 - ); - const userCurrency = - this.request.user?.Settings?.currency ?? - user.Settings?.currency ?? - baseCurrency; - - const { orders, portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId - }); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: userCurrency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.setTransactionPoints(transactionPoints); - - const portfolioStart = parseDate( - transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) - ); - const startDate = this.getStartDate(aDateRange, portfolioStart); - const currentPositions = await portfolioCalculator.getCurrentPositions( - startDate - ); - - const cashDetails = await this.accountService.getCashDetails( - userId, - userCurrency - ); - - const holdings: PortfolioDetails['holdings'] = {}; - const totalInvestment = currentPositions.totalInvestment.plus( - cashDetails.balanceInBaseCurrency - ); - const totalValue = currentPositions.currentValue.plus( - cashDetails.balanceInBaseCurrency - ); - - const dataGatheringItems = currentPositions.positions.map((position) => { - return { - dataSource: position.dataSource, - symbol: position.symbol - }; - }); - const symbols = currentPositions.positions.map( - (position) => position.symbol - ); - - const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.getQuotes(dataGatheringItems), - this.symbolProfileService.getSymbolProfiles(symbols) - ]); - - const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; - for (const symbolProfile of symbolProfiles) { - symbolProfileMap[symbolProfile.symbol] = symbolProfile; - } - - const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; - for (const position of currentPositions.positions) { - portfolioItemsNow[position.symbol] = position; - } - - for (const item of currentPositions.positions) { - if (item.quantity.lte(0)) { - // Ignore positions without any quantity - continue; - } - - const value = item.quantity.mul(item.marketPrice); - const symbolProfile = symbolProfileMap[item.symbol]; - const dataProviderResponse = dataProviderResponses[item.symbol]; - - const markets: { [key in Market]: number } = { - developedMarkets: 0, - emergingMarkets: 0, - otherMarkets: 0 - }; - - for (const country of symbolProfile.countries) { - if (developedMarkets.includes(country.code)) { - markets.developedMarkets = new Big(markets.developedMarkets) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - markets.emergingMarkets = new Big(markets.emergingMarkets) - .plus(country.weight) - .toNumber(); - } else { - markets.otherMarkets = new Big(markets.otherMarkets) - .plus(country.weight) - .toNumber(); - } - } - - holdings[item.symbol] = { - markets, - allocationCurrent: value.div(totalValue).toNumber(), - allocationInvestment: item.investment.div(totalInvestment).toNumber(), - assetClass: symbolProfile.assetClass, - assetSubClass: symbolProfile.assetSubClass, - countries: symbolProfile.countries, - currency: item.currency, - dataSource: symbolProfile.dataSource, - grossPerformance: item.grossPerformance?.toNumber() ?? 0, - grossPerformancePercent: - item.grossPerformancePercentage?.toNumber() ?? 0, - investment: item.investment.toNumber(), - marketPrice: item.marketPrice, - marketState: dataProviderResponse.marketState, - name: symbolProfile.name, - netPerformance: item.netPerformance?.toNumber() ?? 0, - netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, - quantity: item.quantity.toNumber(), - sectors: symbolProfile.sectors, - symbol: item.symbol, - transactionCount: item.transactionCount, - value: value.toNumber() - }; - } - - const cashPositions = await this.getCashPositions({ - cashDetails, - emergencyFund, - userCurrency, - investment: totalInvestment, - value: totalValue - }); - - for (const symbol of Object.keys(cashPositions)) { - holdings[symbol] = cashPositions[symbol]; - } - - const accounts = await this.getValueOfAccounts( - orders, - portfolioItemsNow, - userCurrency, - userId - ); - - return { accounts, holdings, hasErrors: currentPositions.hasErrors }; - } - - public async getPosition( - aDataSource: DataSource, - aImpersonationId: string, - aSymbol: string - ): Promise { - const userCurrency = this.request.user.Settings.currency; - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - - const orders = ( - await this.orderService.getOrders({ userCurrency, userId }) - ).filter(({ SymbolProfile }) => { - return ( - SymbolProfile.dataSource === aDataSource && - SymbolProfile.symbol === aSymbol - ); - }); - - if (orders.length <= 0) { - return { - averagePrice: undefined, - firstBuyDate: undefined, - grossPerformance: undefined, - grossPerformancePercent: undefined, - historicalData: [], - investment: undefined, - marketPrice: undefined, - maxPrice: undefined, - minPrice: undefined, - netPerformance: undefined, - netPerformancePercent: undefined, - orders: [], - quantity: undefined, - SymbolProfile: undefined, - transactionCount: undefined, - value: undefined - }; - } - - const positionCurrency = orders[0].SymbolProfile.currency; - const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ - aSymbol - ]); - - const portfolioOrders: PortfolioOrder[] = orders - .filter((order) => { - return order.type === 'BUY' || order.type === 'SELL'; - }) - .map((order) => ({ - 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.SymbolProfile.symbol, - type: order.type, - unitPrice: new Big(order.unitPrice) - })); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: positionCurrency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.computeTransactionPoints(); - const transactionPoints = portfolioCalculator.getTransactionPoints(); - - const portfolioStart = parseDate(transactionPoints[0].date); - const currentPositions = await portfolioCalculator.getCurrentPositions( - portfolioStart - ); - - const position = currentPositions.positions.find( - (item) => item.symbol === aSymbol - ); - - if (position) { - const { - averagePrice, - currency, - dataSource, - firstBuyDate, - marketPrice, - quantity, - transactionCount - } = position; - - // Convert investment, gross and net performance to currency of user - const investment = this.exchangeRateDataService.toCurrency( - position.investment?.toNumber(), - currency, - userCurrency - ); - const grossPerformance = this.exchangeRateDataService.toCurrency( - position.grossPerformance?.toNumber(), - currency, - userCurrency - ); - const netPerformance = this.exchangeRateDataService.toCurrency( - position.netPerformance?.toNumber(), - currency, - userCurrency - ); - - const historicalData = await this.dataProviderService.getHistorical( - [{ dataSource, symbol: aSymbol }], - 'day', - parseISO(firstBuyDate), - new Date() - ); - - const historicalDataArray: HistoricalDataItem[] = []; - let maxPrice = Math.max(orders[0].unitPrice, marketPrice); - let minPrice = Math.min(orders[0].unitPrice, marketPrice); - - if (!historicalData?.[aSymbol]?.[firstBuyDate]) { - // Add historical entry for buy date, if no historical data available - historicalDataArray.push({ - averagePrice: orders[0].unitPrice, - date: firstBuyDate, - value: orders[0].unitPrice - }); - } - - if (historicalData[aSymbol]) { - let j = -1; - for (const [date, { marketPrice }] of Object.entries( - historicalData[aSymbol] - )) { - while ( - j + 1 < transactionPoints.length && - !isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date)) - ) { - j++; - } - let currentAveragePrice = 0; - const currentSymbol = transactionPoints[j].items.find( - (item) => item.symbol === aSymbol - ); - if (currentSymbol) { - currentAveragePrice = currentSymbol.quantity.eq(0) - ? 0 - : currentSymbol.investment.div(currentSymbol.quantity).toNumber(); - } - - historicalDataArray.push({ - date, - averagePrice: currentAveragePrice, - value: marketPrice - }); - - maxPrice = Math.max(marketPrice ?? 0, maxPrice); - minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); - } - } - - return { - firstBuyDate, - grossPerformance, - investment, - marketPrice, - maxPrice, - minPrice, - netPerformance, - orders, - SymbolProfile, - transactionCount, - averagePrice: averagePrice.toNumber(), - grossPerformancePercent: - position.grossPerformancePercentage?.toNumber(), - historicalData: historicalDataArray, - netPerformancePercent: position.netPerformancePercentage?.toNumber(), - quantity: quantity.toNumber(), - value: this.exchangeRateDataService.toCurrency( - quantity.mul(marketPrice).toNumber(), - currency, - userCurrency - ) - }; - } else { - const currentData = await this.dataProviderService.getQuotes([ - { dataSource: DataSource.YAHOO, symbol: aSymbol } - ]); - const marketPrice = currentData[aSymbol]?.marketPrice; - - let historicalData = await this.dataProviderService.getHistorical( - [{ dataSource: DataSource.YAHOO, symbol: aSymbol }], - 'day', - portfolioStart, - new Date() - ); - - if (isEmpty(historicalData)) { - historicalData = await this.dataProviderService.getHistoricalRaw( - [{ dataSource: DataSource.YAHOO, symbol: aSymbol }], - portfolioStart, - new Date() - ); - } - - const historicalDataArray: HistoricalDataItem[] = []; - let maxPrice = marketPrice; - let minPrice = marketPrice; - - for (const [date, { marketPrice }] of Object.entries( - historicalData[aSymbol] - )) { - historicalDataArray.push({ - date, - value: marketPrice - }); - - maxPrice = Math.max(marketPrice ?? 0, maxPrice); - minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); - } - - return { - marketPrice, - maxPrice, - minPrice, - orders, - SymbolProfile, - averagePrice: 0, - firstBuyDate: undefined, - grossPerformance: undefined, - grossPerformancePercent: undefined, - historicalData: historicalDataArray, - investment: 0, - netPerformance: undefined, - netPerformancePercent: undefined, - quantity: 0, - transactionCount: undefined, - value: 0 - }; - } - } - - public async getPositions( - aImpersonationId: string, - aDateRange: DateRange = 'max' - ): Promise<{ hasErrors: boolean; positions: Position[] }> { - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - - const { portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId - }); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: this.request.user.Settings.currency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - if (transactionPoints?.length <= 0) { - return { - hasErrors: false, - positions: [] - }; - } - - portfolioCalculator.setTransactionPoints(transactionPoints); - - const portfolioStart = parseDate(transactionPoints[0].date); - const startDate = this.getStartDate(aDateRange, portfolioStart); - const currentPositions = await portfolioCalculator.getCurrentPositions( - startDate - ); - - const positions = currentPositions.positions.filter( - (item) => !item.quantity.eq(0) - ); - const dataGatheringItem = positions.map((position) => { - return { - dataSource: position.dataSource, - symbol: position.symbol - }; - }); - const symbols = positions.map((position) => position.symbol); - - const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.getQuotes(dataGatheringItem), - this.symbolProfileService.getSymbolProfiles(symbols) - ]); - - const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; - for (const symbolProfile of symbolProfiles) { - symbolProfileMap[symbolProfile.symbol] = symbolProfile; - } - - return { - hasErrors: currentPositions.hasErrors, - positions: positions.map((position) => { - return { - ...position, - assetClass: symbolProfileMap[position.symbol].assetClass, - averagePrice: new Big(position.averagePrice).toNumber(), - grossPerformance: position.grossPerformance?.toNumber() ?? null, - grossPerformancePercentage: - position.grossPerformancePercentage?.toNumber() ?? null, - investment: new Big(position.investment).toNumber(), - marketState: - dataProviderResponses[position.symbol]?.marketState ?? - MarketState.delayed, - name: symbolProfileMap[position.symbol].name, - netPerformance: position.netPerformance?.toNumber() ?? null, - netPerformancePercentage: - position.netPerformancePercentage?.toNumber() ?? null, - quantity: new Big(position.quantity).toNumber() - }; - }) - }; - } - - public async getPerformance( - aImpersonationId: string, - aDateRange: DateRange = 'max' - ): Promise { - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - - const { portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId - }); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: this.request.user.Settings.currency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - if (transactionPoints?.length <= 0) { - return { - hasErrors: false, - performance: { - currentGrossPerformance: 0, - currentGrossPerformancePercent: 0, - currentNetPerformance: 0, - currentNetPerformancePercent: 0, - currentValue: 0 - } - }; - } - - portfolioCalculator.setTransactionPoints(transactionPoints); - - const portfolioStart = parseDate(transactionPoints[0].date); - const startDate = this.getStartDate(aDateRange, portfolioStart); - const currentPositions = await portfolioCalculator.getCurrentPositions( - startDate - ); - - const hasErrors = currentPositions.hasErrors; - const currentValue = currentPositions.currentValue.toNumber(); - const currentGrossPerformance = currentPositions.grossPerformance; - let currentGrossPerformancePercent = - currentPositions.grossPerformancePercentage; - const currentNetPerformance = currentPositions.netPerformance; - let currentNetPerformancePercent = - currentPositions.netPerformancePercentage; - - if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) { - // If algebraic sign is different, harmonize it - currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1); - } - - if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) { - // If algebraic sign is different, harmonize it - currentNetPerformancePercent = currentNetPerformancePercent.mul(-1); - } - - return { - errors: currentPositions.errors, - hasErrors: currentPositions.hasErrors || hasErrors, - performance: { - currentValue, - currentGrossPerformance: currentGrossPerformance.toNumber(), - currentGrossPerformancePercent: - currentGrossPerformancePercent.toNumber(), - currentNetPerformance: currentNetPerformance.toNumber(), - currentNetPerformancePercent: currentNetPerformancePercent.toNumber() - } - }; - } - - public async getReport(impersonationId: string): Promise { - const currency = this.request.user.Settings.currency; - const userId = await this.getUserId(impersonationId, this.request.user.id); - - const { orders, portfolioOrders, transactionPoints } = - await this.getTransactionPoints({ - userId - }); - - if (isEmpty(orders)) { - return { - rules: {} - }; - } - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.setTransactionPoints(transactionPoints); - - const portfolioStart = parseDate(transactionPoints[0].date); - const currentPositions = await portfolioCalculator.getCurrentPositions( - portfolioStart - ); - - const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; - for (const position of currentPositions.positions) { - portfolioItemsNow[position.symbol] = position; - } - const accounts = await this.getValueOfAccounts( - orders, - portfolioItemsNow, - currency, - userId - ); - return { - rules: { - accountClusterRisk: await this.rulesService.evaluate( - [ - new AccountClusterRiskInitialInvestment( - this.exchangeRateDataService, - accounts - ), - new AccountClusterRiskCurrentInvestment( - this.exchangeRateDataService, - accounts - ), - new AccountClusterRiskSingleAccount( - this.exchangeRateDataService, - accounts - ) - ], - { baseCurrency: currency } - ), - currencyClusterRisk: await this.rulesService.evaluate( - [ - new CurrencyClusterRiskBaseCurrencyInitialInvestment( - this.exchangeRateDataService, - currentPositions - ), - new CurrencyClusterRiskBaseCurrencyCurrentInvestment( - this.exchangeRateDataService, - currentPositions - ), - new CurrencyClusterRiskInitialInvestment( - this.exchangeRateDataService, - currentPositions - ), - new CurrencyClusterRiskCurrentInvestment( - this.exchangeRateDataService, - currentPositions - ) - ], - { baseCurrency: currency } - ), - fees: await this.rulesService.evaluate( - [ - new FeeRatioInitialInvestment( - this.exchangeRateDataService, - currentPositions.totalInvestment.toNumber(), - this.getFees(orders).toNumber() - ) - ], - { baseCurrency: currency } - ) - } - }; - } - - public async getSummary(aImpersonationId: string): Promise { - const userCurrency = this.request.user.Settings.currency; - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const user = await this.userService.user({ id: userId }); - - const performanceInformation = await this.getPerformance(aImpersonationId); - - const { balanceInBaseCurrency } = await this.accountService.getCashDetails( - userId, - userCurrency - ); - const orders = await this.orderService.getOrders({ - userCurrency, - userId - }); - const dividend = this.getDividend(orders).toNumber(); - const emergencyFund = new Big( - (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 - ); - const fees = this.getFees(orders).toNumber(); - const firstOrderDate = orders[0]?.date; - const items = this.getItems(orders).toNumber(); - - const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); - const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); - - const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); - const committedFunds = new Big(totalBuy).minus(totalSell); - - const netWorth = new Big(balanceInBaseCurrency) - .plus(performanceInformation.performance.currentValue) - .plus(items) - .toNumber(); - - const daysInMarket = differenceInDays(new Date(), firstOrderDate); - - const annualizedPerformancePercent = new PortfolioCalculatorNew({ - currency: userCurrency, - currentRateService: this.currentRateService, - orders: [] - }) - .getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent: new Big( - performanceInformation.performance.currentNetPerformancePercent - ) - }) - ?.toNumber(); - - return { - ...performanceInformation.performance, - annualizedPerformancePercent, - cash, - dividend, - fees, - firstOrderDate, - items, - netWorth, - totalBuy, - totalSell, - committedFunds: committedFunds.toNumber(), - emergencyFund: emergencyFund.toNumber(), - ordersCount: orders.filter((order) => { - return order.type === 'BUY' || order.type === 'SELL'; - }).length - }; - } - - private async getCashPositions({ - cashDetails, - emergencyFund, - investment, - userCurrency, - value - }: { - cashDetails: CashDetails; - emergencyFund: Big; - investment: Big; - value: Big; - userCurrency: string; - }) { - const cashPositions: PortfolioDetails['holdings'] = {}; - - for (const account of cashDetails.accounts) { - const convertedBalance = this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ); - - if (convertedBalance === 0) { - continue; - } - - if (cashPositions[account.currency]) { - cashPositions[account.currency].investment += convertedBalance; - cashPositions[account.currency].value += convertedBalance; - } else { - cashPositions[account.currency] = { - allocationCurrent: 0, - allocationInvestment: 0, - assetClass: AssetClass.CASH, - assetSubClass: AssetClass.CASH, - countries: [], - currency: account.currency, - dataSource: undefined, - grossPerformance: 0, - grossPerformancePercent: 0, - investment: convertedBalance, - marketPrice: 0, - marketState: MarketState.open, - name: account.currency, - netPerformance: 0, - netPerformancePercent: 0, - quantity: 0, - sectors: [], - symbol: account.currency, - transactionCount: 0, - value: convertedBalance - }; - } - } - - if (emergencyFund.gt(0)) { - cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = { - ...cashPositions[userCurrency], - assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND, - investment: emergencyFund.toNumber(), - name: ASSET_SUB_CLASS_EMERGENCY_FUND, - symbol: ASSET_SUB_CLASS_EMERGENCY_FUND, - value: emergencyFund.toNumber() - }; - - cashPositions[userCurrency].investment = new Big( - cashPositions[userCurrency].investment - ) - .minus(emergencyFund) - .toNumber(); - cashPositions[userCurrency].value = new Big( - cashPositions[userCurrency].value - ) - .minus(emergencyFund) - .toNumber(); - } - - for (const symbol of Object.keys(cashPositions)) { - // Calculate allocations for each currency - cashPositions[symbol].allocationCurrent = new Big( - cashPositions[symbol].value - ) - .div(value) - .toNumber(); - cashPositions[symbol].allocationInvestment = new Big( - cashPositions[symbol].investment - ) - .div(investment) - .toNumber(); - } - - return cashPositions; - } - - private getDividend(orders: OrderWithAccount[], date = new Date(0)) { - return orders - .filter((order) => { - // Filter out all orders before given date and type dividend - return ( - isBefore(date, new Date(order.date)) && - order.type === TypeOfOrder.DIVIDEND - ); - }) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.SymbolProfile.currency, - this.request.user.Settings.currency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - - private getFees(orders: OrderWithAccount[], date = new Date(0)) { - return orders - .filter((order) => { - // Filter out all orders before given date - return isBefore(date, new Date(order.date)); - }) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - order.fee, - order.SymbolProfile.currency, - this.request.user.Settings.currency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - - private getItems(orders: OrderWithAccount[], date = new Date(0)) { - return orders - .filter((order) => { - // Filter out all orders before given date and type item - return ( - isBefore(date, new Date(order.date)) && - order.type === TypeOfOrder.ITEM - ); - }) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.SymbolProfile.currency, - this.request.user.Settings.currency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); - } - - private getStartDate(aDateRange: DateRange, portfolioStart: Date) { - switch (aDateRange) { - case '1d': - portfolioStart = max([portfolioStart, subDays(new Date(), 1)]); - break; - case 'ytd': - portfolioStart = max([portfolioStart, setDayOfYear(new Date(), 1)]); - break; - case '1y': - portfolioStart = max([portfolioStart, subYears(new Date(), 1)]); - break; - case '5y': - portfolioStart = max([portfolioStart, subYears(new Date(), 5)]); - break; - } - return portfolioStart; - } - - private async getTransactionPoints({ - includeDrafts = false, - userId - }: { - includeDrafts?: boolean; - userId: string; - }): Promise<{ - transactionPoints: TransactionPoint[]; - orders: OrderWithAccount[]; - portfolioOrders: PortfolioOrder[]; - }> { - const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; - - const orders = await this.orderService.getOrders({ - includeDrafts, - userCurrency, - userId, - types: ['BUY', 'SELL'] - }); - - if (orders.length <= 0) { - return { transactionPoints: [], orders: [], portfolioOrders: [] }; - } - - const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ - currency: order.SymbolProfile.currency, - dataSource: order.SymbolProfile.dataSource, - date: format(order.date, DATE_FORMAT), - fee: new Big( - this.exchangeRateDataService.toCurrency( - order.fee, - order.SymbolProfile.currency, - userCurrency - ) - ), - name: order.SymbolProfile?.name, - quantity: new Big(order.quantity), - symbol: order.SymbolProfile.symbol, - type: order.type, - unitPrice: new Big( - this.exchangeRateDataService.toCurrency( - order.unitPrice, - order.SymbolProfile.currency, - userCurrency - ) - ) - })); - - const portfolioCalculator = new PortfolioCalculatorNew({ - currency: userCurrency, - currentRateService: this.currentRateService, - orders: portfolioOrders - }); - - portfolioCalculator.computeTransactionPoints(); - - return { - transactionPoints: portfolioCalculator.getTransactionPoints(), - orders, - portfolioOrders - }; - } - - private async getValueOfAccounts( - orders: OrderWithAccount[], - portfolioItemsNow: { [p: string]: TimelinePosition }, - userCurrency: string, - userId: string - ) { - const accounts: PortfolioDetails['accounts'] = {}; - - const currentAccounts = await this.accountService.getAccounts(userId); - - for (const account of currentAccounts) { - const ordersByAccount = orders.filter(({ accountId }) => { - return accountId === account.id; - }); - - accounts[account.id] = { - balance: account.balance, - currency: account.currency, - current: account.balance, - name: account.name, - original: account.balance - }; - - for (const order of ordersByAccount) { - let currentValueOfSymbol = - order.quantity * - portfolioItemsNow[order.SymbolProfile.symbol].marketPrice; - let originalValueOfSymbol = order.quantity * order.unitPrice; - - if (order.type === 'SELL') { - currentValueOfSymbol *= -1; - originalValueOfSymbol *= -1; - } - - if (accounts[order.Account?.id || UNKNOWN_KEY]?.current) { - accounts[order.Account?.id || UNKNOWN_KEY].current += - currentValueOfSymbol; - accounts[order.Account?.id || UNKNOWN_KEY].original += - originalValueOfSymbol; - } else { - accounts[order.Account?.id || UNKNOWN_KEY] = { - balance: 0, - currency: order.Account?.currency, - current: currentValueOfSymbol, - name: account.name, - original: originalValueOfSymbol - }; - } - } - } - - return accounts; - } - - private async getUserId(aImpersonationId: string, aUserId: string) { - const impersonationUserId = - await this.impersonationService.validateImpersonationId( - aImpersonationId, - aUserId - ); - - return impersonationUserId || aUserId; - } - - private getTotalByType( - orders: OrderWithAccount[], - currency: string, - type: TypeOfOrder - ) { - return orders - .filter( - (order) => !isAfter(order.date, endOfToday()) && order.type === type - ) - .map((order) => { - return this.exchangeRateDataService.toCurrency( - order.quantity * order.unitPrice, - order.SymbolProfile.currency, - currency - ); - }) - .reduce((previous, current) => previous + current, 0); - } -} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f8e617432..a7dc8adf7 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -5,7 +5,6 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; -import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator'; import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; @@ -41,6 +40,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in import type { AccountWithValue, DateRange, + Market, OrderWithAccount, RequestWithUser } from '@ghostfolio/common/types'; @@ -49,6 +49,7 @@ import { REQUEST } from '@nestjs/core'; import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { + differenceInDays, endOfToday, format, isAfter, @@ -68,8 +69,12 @@ import { HistoricalDataItem, PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; +import { PortfolioCalculator } from './portfolio-calculator'; import { RulesService } from './rules.service'; +const developedMarkets = require('../../assets/countries/developed-markets.json'); +const emergingMarkets = require('../../assets/countries/emerging-markets.json'); + @Injectable() export class PortfolioService { public constructor( @@ -159,15 +164,18 @@ export class PortfolioService { ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - this.request.user.Settings.currency - ); + const { portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId, + includeDrafts: true + }); - const { transactionPoints } = await this.getTransactionPoints({ - userId, - includeDrafts: true + const portfolioCalculator = new PortfolioCalculator({ + currency: this.request.user.Settings.currency, + currentRateService: this.currentRateService, + orders: portfolioOrders }); + portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { return []; @@ -208,12 +216,17 @@ export class PortfolioService { ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - this.request.user.Settings.currency - ); + const { portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId + }); + + const portfolioCalculator = new PortfolioCalculator({ + currency: this.request.user.Settings.currency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); - const { transactionPoints } = await this.getTransactionPoints({ userId }); portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { return { @@ -302,13 +315,16 @@ export class PortfolioService { this.request.user?.Settings?.currency ?? user.Settings?.currency ?? baseCurrency; - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - userCurrency - ); - const { orders, transactionPoints } = await this.getTransactionPoints({ - userId + const { orders, portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId + }); + + const portfolioCalculator = new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + orders: portfolioOrders }); portfolioCalculator.setTransactionPoints(transactionPoints); @@ -368,7 +384,31 @@ export class PortfolioService { const value = item.quantity.mul(item.marketPrice); const symbolProfile = symbolProfileMap[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol]; + + const markets: { [key in Market]: number } = { + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + + for (const country of symbolProfile.countries) { + if (developedMarkets.includes(country.code)) { + markets.developedMarkets = new Big(markets.developedMarkets) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + markets.emergingMarkets = new Big(markets.emergingMarkets) + .plus(country.weight) + .toNumber(); + } else { + markets.otherMarkets = new Big(markets.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + holdings[item.symbol] = { + markets, allocationCurrent: value.div(totalValue).toNumber(), allocationInvestment: item.investment.div(totalInvestment).toNumber(), assetClass: symbolProfile.assetClass, @@ -474,11 +514,13 @@ export class PortfolioService { unitPrice: new Big(order.unitPrice) })); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - positionCurrency - ); - portfolioCalculator.computeTransactionPoints(portfolioOrders); + const portfolioCalculator = new PortfolioCalculator({ + currency: positionCurrency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); + + portfolioCalculator.computeTransactionPoints(); const transactionPoints = portfolioCalculator.getTransactionPoints(); const portfolioStart = parseDate(transactionPoints[0].date); @@ -657,12 +699,16 @@ export class PortfolioService { ): Promise<{ hasErrors: boolean; positions: Position[] }> { const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - this.request.user.Settings.currency - ); + const { portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId + }); - const { transactionPoints } = await this.getTransactionPoints({ userId }); + const portfolioCalculator = new PortfolioCalculator({ + currency: this.request.user.Settings.currency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); if (transactionPoints?.length <= 0) { return { @@ -730,18 +776,21 @@ export class PortfolioService { ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - this.request.user.Settings.currency - ); + const { portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId + }); - const { transactionPoints } = await this.getTransactionPoints({ userId }); + const portfolioCalculator = new PortfolioCalculator({ + currency: this.request.user.Settings.currency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); if (transactionPoints?.length <= 0) { return { hasErrors: false, performance: { - annualizedPerformancePercent: 0, currentGrossPerformance: 0, currentGrossPerformancePercent: 0, currentNetPerformance: 0, @@ -760,26 +809,34 @@ export class PortfolioService { ); const hasErrors = currentPositions.hasErrors; - const annualizedPerformancePercent = - currentPositions.netAnnualizedPerformance.toNumber(); const currentValue = currentPositions.currentValue.toNumber(); - const currentGrossPerformance = - currentPositions.grossPerformance.toNumber(); - const currentGrossPerformancePercent = - currentPositions.grossPerformancePercentage.toNumber(); - const currentNetPerformance = currentPositions.netPerformance.toNumber(); - const currentNetPerformancePercent = - currentPositions.netPerformancePercentage.toNumber(); + const currentGrossPerformance = currentPositions.grossPerformance; + let currentGrossPerformancePercent = + currentPositions.grossPerformancePercentage; + const currentNetPerformance = currentPositions.netPerformance; + let currentNetPerformancePercent = + currentPositions.netPerformancePercentage; + + if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) { + // If algebraic sign is different, harmonize it + currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1); + } + + if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) { + // If algebraic sign is different, harmonize it + currentNetPerformancePercent = currentNetPerformancePercent.mul(-1); + } return { + errors: currentPositions.errors, hasErrors: currentPositions.hasErrors || hasErrors, performance: { - annualizedPerformancePercent, - currentGrossPerformance, - currentGrossPerformancePercent, - currentNetPerformance, - currentNetPerformancePercent, - currentValue + currentValue, + currentGrossPerformance: currentGrossPerformance.toNumber(), + currentGrossPerformancePercent: + currentGrossPerformancePercent.toNumber(), + currentNetPerformance: currentNetPerformance.toNumber(), + currentNetPerformancePercent: currentNetPerformancePercent.toNumber() } }; } @@ -788,9 +845,10 @@ export class PortfolioService { const currency = this.request.user.Settings.currency; const userId = await this.getUserId(impersonationId, this.request.user.id); - const { orders, transactionPoints } = await this.getTransactionPoints({ - userId - }); + const { orders, portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId + }); if (isEmpty(orders)) { return { @@ -798,10 +856,12 @@ export class PortfolioService { }; } - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - currency - ); + const portfolioCalculator = new PortfolioCalculator({ + currency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); + portfolioCalculator.setTransactionPoints(transactionPoints); const portfolioStart = parseDate(transactionPoints[0].date); @@ -907,8 +967,24 @@ export class PortfolioService { .plus(items) .toNumber(); + const daysInMarket = differenceInDays(new Date(), firstOrderDate); + + const annualizedPerformancePercent = new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + orders: [] + }) + .getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent: new Big( + performanceInformation.performance.currentNetPerformancePercent + ) + }) + ?.toNumber(); + return { ...performanceInformation.performance, + annualizedPerformancePercent, cash, dividend, fees, @@ -917,8 +993,6 @@ export class PortfolioService { netWorth, totalBuy, totalSell, - annualizedPerformancePercent: - performanceInformation.performance.annualizedPerformancePercent, committedFunds: committedFunds.toNumber(), emergencyFund: emergencyFund.toNumber(), ordersCount: orders.filter((order) => { @@ -937,8 +1011,8 @@ export class PortfolioService { cashDetails: CashDetails; emergencyFund: Big; investment: Big; - userCurrency: string; value: Big; + userCurrency: string; }) { const cashPositions: PortfolioDetails['holdings'] = {}; @@ -1111,6 +1185,7 @@ export class PortfolioService { }): Promise<{ transactionPoints: TransactionPoint[]; orders: OrderWithAccount[]; + portfolioOrders: PortfolioOrder[]; }> { const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; @@ -1122,7 +1197,7 @@ export class PortfolioService { }); if (orders.length <= 0) { - return { transactionPoints: [], orders: [] }; + return { transactionPoints: [], orders: [], portfolioOrders: [] }; } const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ @@ -1149,14 +1224,18 @@ export class PortfolioService { ) })); - const portfolioCalculator = new PortfolioCalculator( - this.currentRateService, - userCurrency - ); - portfolioCalculator.computeTransactionPoints(portfolioOrders); + const portfolioCalculator = new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + orders: portfolioOrders + }); + + portfolioCalculator.computeTransactionPoints(); + return { transactionPoints: portfolioCalculator.getTransactionPoints(), - orders + orders, + portfolioOrders }; } diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts index ef3b03f1b..8f8878079 100644 --- a/apps/api/src/app/user/interfaces/user-settings.interface.ts +++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts @@ -1,6 +1,5 @@ export interface UserSettings { emergencyFund?: number; locale?: string; - isNewCalculationEngine?: boolean; isRestrictedView?: boolean; } diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index eaa41464a..b09e904df 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -5,10 +5,6 @@ export class UpdateUserSettingDto { @IsOptional() emergencyFund?: number; - @IsBoolean() - @IsOptional() - isNewCalculationEngine?: boolean; - @IsBoolean() @IsOptional() isRestrictedView?: boolean; 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 072d91482..743d9c568 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -222,24 +222,6 @@ export class AccountPageComponent implements OnDestroy, OnInit { }); } - public onNewCalculationChange(aEvent: MatSlideToggleChange) { - this.dataService - .putUserSetting({ isNewCalculationEngine: aEvent.checked }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.userService.remove(); - - this.userService - .get() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((user) => { - this.user = user; - - this.changeDetectorRef.markForCheck(); - }); - }); - } - public onRedeemCoupon() { let couponCode = prompt('Please enter your coupon code:'); couponCode = couponCode?.trim(); diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index f1993a069..97af7d213 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -169,23 +169,6 @@ >
-
-
-
New Calculation Engine
-
Experimental
-
-
- -
-
From 56b169e1c432ae2285584b52552abb0e2adb2e58 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Apr 2022 10:28:07 +0200 Subject: [PATCH 248/337] Feature/make header background solid (#817) * Remove alpha * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/components/header/header.component.scss | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2fb77ee..a192fe654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switched to the new calculation engine - Improved the 4% rule in the _FIRE_ section +- Changed the background of the header to a solid color ## 1.133.0 - 07.04.2022 diff --git a/apps/client/src/app/components/header/header.component.scss b/apps/client/src/app/components/header/header.component.scss index affcc868a..32d125703 100644 --- a/apps/client/src/app/components/header/header.component.scss +++ b/apps/client/src/app/components/header/header.component.scss @@ -5,7 +5,7 @@ z-index: 999; .mat-toolbar { - background-color: rgba(var(--light-disabled-text)); + background-color: var(--light-background); .spacer { flex: 1 1 auto; @@ -27,6 +27,6 @@ :host-context(.is-dark-theme) { .mat-toolbar { - background-color: rgba(39, 39, 39, $alpha-disabled-text); + background-color: var(--dark-background); } } From 08405d14d5896caa061600e6fce5702848f7cdfb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Apr 2022 14:50:13 +0200 Subject: [PATCH 249/337] Release 1.134.0 (#818) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a192fe654..1c3baf6df 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.134.0 - 09.04.2022 ### Changed diff --git a/package.json b/package.json index 105a9b843..02f08bfa2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.133.0", + "version": "1.134.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 9b49ed77f7c725d4260c3e7af5d0bcc609e29dba Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Apr 2022 20:16:36 +0200 Subject: [PATCH 250/337] Feature/add support for thor chain (#819) * Add THORChain (RUNE-USD) * Update changelog --- CHANGELOG.md | 6 ++++++ .../services/cryptocurrency/custom-cryptocurrencies.json | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3baf6df..528015db9 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 the cryptocurrency _THORChain_ (`RUNE-USD`) + ## 1.134.0 - 09.04.2022 ### Changed diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index eff896ed6..9bc601500 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -6,6 +6,7 @@ "DOT": "Polkadot", "MATIC": "Polygon", "MINA": "Mina Protocol", + "RUNE": "THORChain", "SHIB": "Shiba Inu", "SOL": "Solana", "UNI3": "Uniswap" From d5ba6244032eddb492df6efdfd9534a27c8200f0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 10 Apr 2022 19:38:27 +0200 Subject: [PATCH 251/337] Feature/add support for terra (#820) * Add Terra (LUNA1-USD) * Update changelog --- CHANGELOG.md | 1 + .../api/src/services/cryptocurrency/custom-cryptocurrencies.json | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528015db9..1da5357aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for the cryptocurrency _Terra_ (`LUNA1-USD`) - Added support for the cryptocurrency _THORChain_ (`RUNE-USD`) ## 1.134.0 - 09.04.2022 diff --git a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json index 9bc601500..71eab866d 100644 --- a/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json +++ b/apps/api/src/services/cryptocurrency/custom-cryptocurrencies.json @@ -4,6 +4,7 @@ "ATOM": "Cosmos", "AVAX": "Avalanche", "DOT": "Polkadot", + "LUNA1": "Terra", "MATIC": "Polygon", "MINA": "Mina Protocol", "RUNE": "THORChain", From 23f2ac472e1f342a601f0ee624071f3126b8e06e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 10 Apr 2022 20:02:31 +0200 Subject: [PATCH 252/337] Feature/add fire calculator (#822) * Add fire calculator * Update changelog --- CHANGELOG.md | 1 + .../investment-chart.component.ts | 20 +- .../portfolio/fire/fire-page.component.ts | 6 +- .../app/pages/portfolio/fire/fire-page.html | 11 +- .../pages/portfolio/fire/fire-page.module.ts | 2 + libs/common/src/lib/helper.ts | 4 + .../fire-calculator.component.html | 65 +++++ .../fire-calculator.component.scss | 11 + .../fire-calculator.component.stories.ts | 48 ++++ .../fire-calculator.component.ts | 247 ++++++++++++++++++ .../fire-calculator/fire-calculator.module.ts | 28 ++ .../fire-calculator.service.ts | 49 ++++ libs/ui/src/lib/fire-calculator/index.ts | 1 + 13 files changed, 477 insertions(+), 16 deletions(-) create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.component.html create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.component.scss create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.module.ts create mode 100644 libs/ui/src/lib/fire-calculator/fire-calculator.service.ts create mode 100644 libs/ui/src/lib/fire-calculator/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da5357aa..e438eba52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a calculator to the _FIRE_ section - Added support for the cryptocurrency _Terra_ (`LUNA1-USD`) - Added support for the cryptocurrency _THORChain_ (`RUNE-USD`) diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index d21d0d9cb..e77c5b362 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -10,7 +10,10 @@ import { ViewChild } from '@angular/core'; import { primaryColorRgb } from '@ghostfolio/common/config'; -import { parseDate } from '@ghostfolio/common/helper'; +import { + parseDate, + transformTickToAbbreviation +} from '@ghostfolio/common/helper'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { Chart, @@ -148,19 +151,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { display: false }, ticks: { - display: true, - callback: (tickValue, index, ticks) => { - if (index === 0 || index === ticks.length - 1) { - // Only print last and first legend entry - if (typeof tickValue === 'number') { - return tickValue.toFixed(2); - } - - return tickValue; - } - - return ''; + callback: (value: number) => { + return transformTickToAbbreviation(value); }, + display: true, mirror: true, z: 1 } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 1ca20dd11..30faa2230 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; import Big from 'big.js'; +import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -14,6 +14,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './fire-page.html' }) export class FirePageComponent implements OnDestroy, OnInit { + public deviceType: string; public fireWealth: Big; public isLoading = false; public user: User; @@ -28,7 +29,7 @@ export class FirePageComponent implements OnDestroy, OnInit { public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, - private impersonationStorageService: ImpersonationStorageService, + private deviceService: DeviceDetectorService, private userService: UserService ) {} @@ -37,6 +38,7 @@ export class FirePageComponent implements OnDestroy, OnInit { */ public ngOnInit() { this.isLoading = true; + this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.dataService .fetchPortfolioSummary() diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index 4aa0f31ed..ebbfccb64 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -2,7 +2,7 @@

FIRE

-
+

4% Rule

+
+

Calculator

+ +
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts index 86fb0a953..0887da8fb 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -11,6 +12,7 @@ import { FirePageComponent } from './fire-page.component'; imports: [ CommonModule, FirePageRoutingModule, + GfFireCalculatorModule, GfValueModule, NgxSkeletonLoaderModule ], diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 2e45d40cd..351643732 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -176,3 +176,7 @@ export function parseDate(date: string) { export function prettifySymbol(aSymbol: string): string { return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, ''); } + +export function transformTickToAbbreviation(value: number) { + return value < 1000000 ? `${value / 1000}K` : `${value / 1000000}M`; +} diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html new file mode 100644 index 000000000..99273da11 --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html @@ -0,0 +1,65 @@ +
+
+
+
+ + + + Savings Rate + + {{ currency }} per month + + + + Investment Horizon + + years + + + + Annual Interest Rate + + % + + + +
+
+
+
+ + +
+
+
+
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss new file mode 100644 index 000000000..e02c91e3d --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; + + .chart-container { + aspect-ratio: 16 / 9; + + ngx-skeleton-loader { + height: 100%; + } + } +} diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts new file mode 100644 index 000000000..cce5727a8 --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts @@ -0,0 +1,48 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { baseCurrency, locale } from '@ghostfolio/common/config'; +import { Meta, Story, moduleMetadata } from '@storybook/angular'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { GfValueModule } from '../value'; + +import { FireCalculatorComponent } from './fire-calculator.component'; +import { FireCalculatorService } from './fire-calculator.service'; + +export default { + title: 'FIRE Calculator', + component: FireCalculatorComponent, + decorators: [ + moduleMetadata({ + declarations: [FireCalculatorComponent], + imports: [ + CommonModule, + FormsModule, + GfValueModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + NgxSkeletonLoaderModule, + NoopAnimationsModule, + ReactiveFormsModule + ], + providers: [FireCalculatorService] + }) + ] +} as Meta; + +const Template: Story = ( + args: FireCalculatorComponent +) => ({ + props: args +}); + +export const Simple = Template.bind({}); +Simple.args = { + currency: baseCurrency, + fireWealth: 0, + locale: locale +}; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts new file mode 100644 index 000000000..13fa76d2d --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -0,0 +1,247 @@ +import 'chartjs-adapter-date-fns'; + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + OnDestroy, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; +import { + BarController, + BarElement, + CategoryScale, + Chart, + LinearScale, + Tooltip +} from 'chart.js'; + +import { FireCalculatorService } from './fire-calculator.service'; +import { Subject, takeUntil } from 'rxjs'; +import { transformTickToAbbreviation } from '@ghostfolio/common/helper'; + +@Component({ + selector: 'gf-fire-calculator', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './fire-calculator.component.html', + styleUrls: ['./fire-calculator.component.scss'] +}) +export class FireCalculatorComponent + implements AfterViewInit, OnChanges, OnDestroy +{ + @Input() currency: string; + @Input() deviceType: string; + @Input() fireWealth: number; + @Input() locale: string; + + @ViewChild('chartCanvas') chartCanvas; + + public calculatorForm = this.formBuilder.group({ + annualInterestRate: new FormControl(), + paymentPerPeriod: new FormControl(), + principalInvestmentAmount: new FormControl(), + time: new FormControl() + }); + public chart: Chart; + public isLoading = true; + public projectedTotalAmount: number; + + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private fireCalculatorService: FireCalculatorService, + private formBuilder: FormBuilder + ) { + Chart.register( + BarController, + BarElement, + CategoryScale, + LinearScale, + Tooltip + ); + + this.calculatorForm.setValue({ + annualInterestRate: 5, + paymentPerPeriod: 500, + principalInvestmentAmount: 0, + time: 10 + }); + + this.calculatorForm.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.initialize(); + }); + } + + public ngAfterViewInit() { + if (this.fireWealth >= 0) { + setTimeout(() => { + // Wait for the chartCanvas + this.calculatorForm.patchValue({ + principalInvestmentAmount: this.fireWealth + }); + this.calculatorForm.get('principalInvestmentAmount').disable(); + + this.changeDetectorRef.markForCheck(); + }); + } + } + + public ngOnChanges() { + if (this.fireWealth >= 0) { + setTimeout(() => { + // Wait for the chartCanvas + this.calculatorForm.patchValue({ + principalInvestmentAmount: this.fireWealth + }); + this.calculatorForm.get('principalInvestmentAmount').disable(); + + this.changeDetectorRef.markForCheck(); + }); + } + } + + public ngOnDestroy() { + this.chart?.destroy(); + + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private initialize() { + this.isLoading = true; + + const chartData = this.getChartData(); + + if (this.chartCanvas) { + if (this.chart) { + this.chart.data.labels = chartData.labels; + this.chart.data.datasets[0].data = chartData.datasets[0].data; + this.chart.data.datasets[1].data = chartData.datasets[1].data; + + this.chart.update(); + } else { + this.chart = new Chart(this.chartCanvas.nativeElement, { + data: chartData, + options: { + plugins: { + tooltip: { + callbacks: { + label: (context) => { + let label = context.dataset.label || ''; + + if (label) { + label += ': '; + } + + if (context.parsed.y !== null) { + label += new Intl.NumberFormat(this.locale, { + currency: this.currency, + currencyDisplay: 'code', + style: 'currency' + }).format(context.parsed.y); + } + + return label; + } + } + } + }, + responsive: true, + scales: { + x: { + grid: { + display: false + }, + stacked: true + }, + y: { + display: this.deviceType !== 'mobile', + grid: { + display: false + }, + stacked: true, + ticks: { + callback: (value: number) => { + return transformTickToAbbreviation(value); + } + } + } + } + }, + type: 'bar' + }); + } + } + + this.isLoading = false; + } + + private getChartData() { + const currentYear = new Date().getFullYear(); + const labels = []; + + // Principal investment amount + const P: number = + this.calculatorForm.get('principalInvestmentAmount').value || 0; + + // Payment per period + const PMT: number = parseFloat( + this.calculatorForm.get('paymentPerPeriod').value + ); + + // Annual interest rate + const r: number = this.calculatorForm.get('annualInterestRate').value / 100; + + // Time + const t: number = parseFloat(this.calculatorForm.get('time').value); + + for (let year = currentYear; year < currentYear + t; year++) { + labels.push(year); + } + + const datasetInterest = { + backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, + data: [], + label: 'Interest' + }; + + const datasetPrincipal = { + backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, + data: [], + label: 'Principal' + }; + + for (let period = 1; period <= t; period++) { + const { interest, principal, totalAmount } = + this.fireCalculatorService.calculateCompoundInterest({ + P, + period, + PMT, + r + }); + + datasetPrincipal.data.push(principal.toNumber()); + datasetInterest.data.push(interest.toNumber()); + + if (period === t - 1) { + this.projectedTotalAmount = totalAmount.toNumber(); + } + } + + return { + labels, + datasets: [datasetPrincipal, datasetInterest] + }; + } +} diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.module.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.module.ts new file mode 100644 index 000000000..44fd48c2e --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { GfValueModule } from '../value'; +import { FireCalculatorComponent } from './fire-calculator.component'; +import { FireCalculatorService } from './fire-calculator.service'; + +@NgModule({ + declarations: [FireCalculatorComponent], + exports: [FireCalculatorComponent], + imports: [ + CommonModule, + FormsModule, + GfValueModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + NgxSkeletonLoaderModule, + ReactiveFormsModule + ], + providers: [FireCalculatorService] +}) +export class GfFireCalculatorModule {} diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts new file mode 100644 index 000000000..2a20b8a25 --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import Big from 'big.js'; + +@Injectable() +export class FireCalculatorService { + private readonly COMPOUND_PERIOD = 12; + private readonly CONTRIBUTION_PERIOD = 12; + + /** + * @constructor + */ + public constructor() {} + + public calculateCompoundInterest({ + P, + period, + PMT, + r + }: { + P: number; + period: number; + PMT: number; + r: number; + }) { + let interest = new Big(0); + const principal = new Big(P).plus( + new Big(PMT).mul(this.CONTRIBUTION_PERIOD).mul(period) + ); + let totalAmount = principal; + + if (r) { + const compoundInterestForPrincipal = new Big(1) + .plus(new Big(r).div(this.COMPOUND_PERIOD)) + .pow(new Big(this.COMPOUND_PERIOD).mul(period).toNumber()); + const compoundInterest = new Big(P).mul(compoundInterestForPrincipal); + const contributionInterest = new Big( + new Big(PMT).mul(compoundInterestForPrincipal.minus(1)) + ).div(new Big(r).div(this.CONTRIBUTION_PERIOD)); + interest = compoundInterest.plus(contributionInterest).minus(principal); + totalAmount = compoundInterest.plus(contributionInterest); + } + + return { + interest, + principal, + totalAmount + }; + } +} diff --git a/libs/ui/src/lib/fire-calculator/index.ts b/libs/ui/src/lib/fire-calculator/index.ts new file mode 100644 index 000000000..aea6c656a --- /dev/null +++ b/libs/ui/src/lib/fire-calculator/index.ts @@ -0,0 +1 @@ +export * from './fire-calculator.module'; From 6cfd052781661c3170f394c9d00b12ec2e1a9408 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 10 Apr 2022 20:03:39 +0200 Subject: [PATCH 253/337] Release 1.135.0 (#823) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e438eba52..c46357392 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.135.0 - 10.04.2022 ### Added diff --git a/package.json b/package.json index 02f08bfa2..7e7115067 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.134.0", + "version": "1.135.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 70723f8d5f9e8d35f6f8fad89547d4a40033d79e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:10:45 +0200 Subject: [PATCH 254/337] Bugfix/fix projected total amount in fire calculator (#825) * Fix calculation of projected total amount * Update changelog --- CHANGELOG.md | 6 ++++++ .../ui/src/lib/fire-calculator/fire-calculator.component.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46357392..fbc0cf20a 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 + +- Fixed an issue with the calculation of the projected total amount in the _FIRE_ calculator + ## 1.135.0 - 10.04.2022 ### Added diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 13fa76d2d..50132f4f5 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -234,7 +234,7 @@ export class FireCalculatorComponent datasetPrincipal.data.push(principal.toNumber()); datasetInterest.data.push(interest.toNumber()); - if (period === t - 1) { + if (period === t) { this.projectedTotalAmount = totalAmount.toNumber(); } } From aef91d3e30f2c8cdd6deb64fe45a8a169fc489d0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 12 Apr 2022 18:04:48 +0200 Subject: [PATCH 255/337] Feature/improve label in summary (#827) * Improve label * Update changelog --- CHANGELOG.md | 4 ++++ .../portfolio-summary/portfolio-summary.component.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc0cf20a..f8b050cdb 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 +### Changed + +- Changed the _Total_ label to _Total Assets_ in the portfolio summary tab on the home page + ### Fixed - Fixed an issue with the calculation of the projected total amount in the _FIRE_ calculator diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 5206aaedc..577d41741 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -119,7 +119,7 @@

-
Total
+
Total Assets
Date: Tue, 12 Apr 2022 19:57:23 +0200 Subject: [PATCH 256/337] Bugfix/fix loading state in fire calculator (#824) * Fix loading state * Update changelog --- CHANGELOG.md | 1 + .../fire-calculator/fire-calculator.component.stories.ts | 2 +- .../src/lib/fire-calculator/fire-calculator.component.ts | 9 +++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b050cdb..a7d161fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with the calculation of the projected total amount in the _FIRE_ calculator +- Fixed an issue with the loading state of the _FIRE_ calculator ## 1.135.0 - 10.04.2022 diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts index cce5727a8..6a451d20b 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts @@ -7,8 +7,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { baseCurrency, locale } from '@ghostfolio/common/config'; import { Meta, Story, moduleMetadata } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { GfValueModule } from '../value'; +import { GfValueModule } from '../value'; import { FireCalculatorComponent } from './fire-calculator.component'; import { FireCalculatorService } from './fire-calculator.service'; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 50132f4f5..b8c2c6141 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -12,6 +12,7 @@ import { } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; +import { transformTickToAbbreviation } from '@ghostfolio/common/helper'; import { BarController, BarElement, @@ -20,10 +21,10 @@ import { LinearScale, Tooltip } from 'chart.js'; +import { isNumber } from 'lodash'; +import { Subject, takeUntil } from 'rxjs'; import { FireCalculatorService } from './fire-calculator.service'; -import { Subject, takeUntil } from 'rxjs'; -import { transformTickToAbbreviation } from '@ghostfolio/common/helper'; @Component({ selector: 'gf-fire-calculator', @@ -84,7 +85,7 @@ export class FireCalculatorComponent } public ngAfterViewInit() { - if (this.fireWealth >= 0) { + if (isNumber(this.fireWealth) && this.fireWealth >= 0) { setTimeout(() => { // Wait for the chartCanvas this.calculatorForm.patchValue({ @@ -98,7 +99,7 @@ export class FireCalculatorComponent } public ngOnChanges() { - if (this.fireWealth >= 0) { + if (isNumber(this.fireWealth) && this.fireWealth >= 0) { setTimeout(() => { // Wait for the chartCanvas this.calculatorForm.patchValue({ From f1feb04f2901bdc7b85a14f55b143ee09e0b1afb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 13 Apr 2022 07:46:35 +0200 Subject: [PATCH 257/337] Release 1.136.0 (#829) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d161fa4..4bbf1dd57 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.136.0 - 13.04.2022 ### Changed diff --git a/package.json b/package.json index 7e7115067..d7349203c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.135.0", + "version": "1.136.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 8526b5a0279ef1a9e538695f920a45978cee80f4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 15 Apr 2022 10:53:40 +0200 Subject: [PATCH 258/337] Feature/export draft activities as ics (#830) * Export draft activities as ICS * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/export/export.service.ts | 4 +- .../position-detail-dialog.component.ts | 10 ++-- .../transactions-page.component.ts | 34 +++++++++-- .../transactions/transactions-page.html | 1 + .../src/app/services/ics/ics.service.ts | 59 +++++++++++++++++++ libs/common/src/lib/helper.ts | 27 ++++++--- .../src/lib/interfaces/export.interface.ts | 11 +++- .../activities-table.component.html | 11 ++++ .../activities-table.component.ts | 17 ++++++ 10 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 apps/client/src/app/services/ics/ics.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bbf1dd57..2eff35556 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 to export future activities (drafts) as `.ics` files + ## 1.136.0 - 13.04.2022 ### Changed diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 007429a38..74cdf14f1 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -42,6 +42,7 @@ export class ExportService { accountId, date, fee, + id, quantity, SymbolProfile, type, @@ -49,13 +50,14 @@ export class ExportService { }) => { return { accountId, - date, fee, + id, quantity, type, unitPrice, currency: SymbolProfile.currency, dataSource: SymbolProfile.dataSource, + date: date.toISOString(), symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol }; } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index db4cbf471..55efc0249 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -211,14 +211,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit { ) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - downloadAsFile( - data, - `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( + downloadAsFile({ + content: data, + fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, - 'text/plain' - ); + format: 'json' + }); }); } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index ba73739b7..61683fd75 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -7,6 +7,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; @@ -50,6 +51,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private icsService: IcsService, private impersonationStorageService: ImpersonationStorageService, private importTransactionsService: ImportTransactionsService, private route: ActivatedRoute, @@ -152,14 +154,36 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - downloadAsFile( - data, - `ghostfolio-export-${format( + for (const activity of data.activities) { + delete activity.id; + } + + downloadAsFile({ + content: data, + fileName: `ghostfolio-export-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, - 'text/plain' - ); + format: 'json' + }); + }); + } + + public onExportDrafts(activityIds?: string[]) { + this.dataService + .fetchExport(activityIds) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + downloadAsFile({ + content: this.icsService.transformActivitiesToIcsContent( + data.activities + ), + fileName: `ghostfolio-drafts-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.ics`, + format: 'string' + }); }); } diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index 0df0171b9..31d29b1df 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -15,6 +15,7 @@ (activityToClone)="onCloneTransaction($event)" (activityToUpdate)="onUpdateTransaction($event)" (export)="onExport($event)" + (exportDrafts)="onExportDrafts($event)" (import)="onImport()" >
diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts new file mode 100644 index 000000000..7329ea663 --- /dev/null +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { capitalize } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; +import { Type } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; + +@Injectable({ + providedIn: 'root' +}) +export class IcsService { + private readonly ICS_DATE_FORMAT = 'yyyyMMdd'; + + public constructor() {} + + public transformActivitiesToIcsContent( + aActivities: Export['activities'] + ): string { + const header = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Ghostfolio//NONSGML v1.0//EN' + ]; + const events = aActivities.map((activity) => { + return this.getEvent({ + date: parseISO(activity.date), + id: activity.id, + symbol: activity.symbol, + type: activity.type + }); + }); + const footer = ['END:VCALENDAR']; + + return [...header, ...events, ...footer].join('\n'); + } + + private getEvent({ + date, + id, + symbol, + type + }: { + date: Date; + id: string; + symbol: string; + type: Type; + }) { + const today = format(new Date(), this.ICS_DATE_FORMAT); + + return [ + 'BEGIN:VEVENT', + `UID:${id}`, + `DTSTAMP:${today}T000000`, + `DTSTART;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, + `DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, + `SUMMARY:${capitalize(type)} ${symbol}`, + 'END:VEVENT' + ].join('\n'); + } +} diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 351643732..ad47abfdd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -12,17 +12,28 @@ export function decodeDataSource(encodedDataSource: string) { return Buffer.from(encodedDataSource, 'hex').toString(); } -export function downloadAsFile( - aContent: unknown, - aFileName: string, - aContentType: string -) { +export function downloadAsFile({ + content, + contentType = 'text/plain', + fileName, + format +}: { + content: unknown; + contentType?: string; + fileName: string; + format: 'json' | 'string'; +}) { const a = document.createElement('a'); - const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { - type: aContentType + + if (format === 'json') { + content = JSON.stringify(content, undefined, ' '); + } + + const file = new Blob([content], { + type: contentType }); a.href = URL.createObjectURL(file); - a.download = aFileName; + a.download = fileName; a.click(); } diff --git a/libs/common/src/lib/interfaces/export.interface.ts b/libs/common/src/lib/interfaces/export.interface.ts index 48ddd2c98..37dbfba79 100644 --- a/libs/common/src/lib/interfaces/export.interface.ts +++ b/libs/common/src/lib/interfaces/export.interface.ts @@ -5,5 +5,14 @@ export interface Export { date: string; version: string; }; - activities: Partial[]; + activities: (Omit< + Order, + | 'accountUserId' + | 'createdAt' + | 'date' + | 'isDraft' + | 'symbolProfileId' + | 'updatedAt' + | 'userId' + > & { date: string; symbol: string })[]; } 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 cbcb7da9c..7cbfffe05 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -356,11 +356,22 @@ *ngIf="hasPermissionToExportActivities" class="align-items-center d-flex" mat-menu-item + [disabled]="dataSource.data.length === 0" (click)="onExport()" > Export + 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 52bc841ff..98ca9e2bd 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -56,6 +56,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { @Output() activityToClone = new EventEmitter(); @Output() activityToUpdate = new EventEmitter(); @Output() export = new EventEmitter(); + @Output() exportDrafts = new EventEmitter(); @Output() import = new EventEmitter(); @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @@ -68,6 +69,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public endOfToday = endOfToday(); public filters$: Subject = new BehaviorSubject([]); public filters: Observable = this.filters$.asObservable(); + public hasDrafts = false; public isAfter = isAfter; public isLoading = true; public isUUID = isUUID; @@ -198,6 +200,18 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { } } + public onExportDrafts() { + this.exportDrafts.emit( + this.dataSource.filteredData + .filter((activity) => { + return activity.isDraft; + }) + .map((activity) => { + return activity.id; + }) + ); + } + public onImport() { this.import.emit(); } @@ -234,6 +248,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.filters$.next(this.allFilters); + this.hasDrafts = this.dataSource.data.some((activity) => { + return activity.isDraft === true; + }); this.totalFees = this.getTotalFees(); this.totalValue = this.getTotalValue(); } From 65e062ad26c31a7d0d499f53a922fd7b27d81d47 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:39:33 +0200 Subject: [PATCH 259/337] Simplify search (#828) * Simplify search * Update changelog --- CHANGELOG.md | 4 +++ .../yahoo-finance/yahoo-finance.service.ts | 28 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eff35556..8070c4526 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 to export future activities (drafts) as `.ics` files +### Changed + +- Migrated the search functionality to `yahoo-finance2` + ## 1.136.0 - 13.04.2022 ### Changed 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 ef4328fc8..b5a0932fb 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 @@ -16,7 +16,6 @@ import { 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'; @@ -25,8 +24,6 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa @Injectable() export class YahooFinanceService implements DataProviderInterface { - private readonly yahooFinanceHostname = 'https://query1.finance.yahoo.com'; - public constructor( private readonly cryptocurrencyService: CryptocurrencyService ) {} @@ -244,16 +241,7 @@ export class YahooFinanceService implements DataProviderInterface { const items: LookupItem[] = []; try { - const get = bent( - `${this.yahooFinanceHostname}/v1/finance/search?q=${encodeURIComponent( - aQuery - )}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`, - 'GET', - 'json', - 200 - ); - - const searchResult = await get(); + const searchResult = await yahooFinance.search(aQuery); const quotes = searchResult.quotes .filter((quote) => { @@ -279,20 +267,24 @@ export class YahooFinanceService implements DataProviderInterface { return true; }); - const marketData = await this.getQuotes( + const marketData = await yahooFinance.quote( quotes.map(({ symbol }) => { return symbol; }) ); - for (const [symbol, value] of Object.entries(marketData)) { - const quote = quotes.find((currentQuote: any) => { - return currentQuote.symbol === symbol; + for (const marketDataItem of marketData) { + const quote = quotes.find((currentQuote) => { + return currentQuote.symbol === marketDataItem.symbol; }); + const symbol = this.convertFromYahooFinanceSymbol( + marketDataItem.symbol + ); + items.push({ symbol, - currency: value.currency, + currency: marketDataItem.currency, dataSource: this.getName(), name: quote?.longname || quote?.shortname || symbol }); From ba7c98d32509e940adee6c3ae2b500b994950a9f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 15 Apr 2022 18:56:23 +0200 Subject: [PATCH 260/337] Add test case for BUY and SELL (partially) (#826) * Add test case for BUY and SELL (partially) * Fix investment calculation for sell activities * Do not show total value if sell activity * Update changelog --- CHANGELOG.md | 4 + .../portfolio/current-rate.service.mock.ts | 7 ++ ...ulator-novn-buy-and-sell-partially.spec.ts | 96 +++++++++++++++++++ .../src/app/portfolio/portfolio-calculator.ts | 25 +++-- .../activities-table.component.html | 2 + .../activities-table.component.ts | 2 +- 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8070c4526..57b344467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migrated the search functionality to `yahoo-finance2` +### Fixed + +- Fixed an issue in the average price / investment calculation for sell activities + ## 1.136.0 - 13.04.2022 ### Changed diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 124a27f45..c91ed9d9a 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -20,6 +20,13 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; + case 'NOVN.SW': + if (isSameDay(parseDate('2022-04-11'), date)) { + return { marketPrice: 87.8 }; + } + + return { marketPrice: 0 }; + default: return { marketPrice: 0 }; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts new file mode 100644 index 000000000..d215f9e1e --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -0,0 +1,96 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculator } from './portfolio-calculator'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy and sell', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + currency: 'CHF', + orders: [ + { + currency: 'CHF', + date: '2022-03-07', + dataSource: 'YAHOO', + fee: new Big(1.3), + name: 'Novartis AG', + quantity: new Big(2), + symbol: 'NOVN.SW', + type: 'BUY', + unitPrice: new Big(75.8) + }, + { + currency: 'CHF', + date: '2022-04-08', + dataSource: 'YAHOO', + fee: new Big(2.95), + name: 'Novartis AG', + quantity: new Big(1), + symbol: 'NOVN.SW', + type: 'SELL', + unitPrice: new Big(85.73) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-04-11').getTime()); + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2022-03-07') + ); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValue: new Big('87.8'), + errors: [], + grossPerformance: new Big('21.93'), + grossPerformancePercentage: new Big('0.14465699208443271768'), + hasErrors: false, + netPerformance: new Big('17.68'), + netPerformancePercentage: new Big('0.11662269129287598945'), + positions: [ + { + averagePrice: new Big('75.80'), + currency: 'CHF', + dataSource: 'YAHOO', + firstBuyDate: '2022-03-07', + grossPerformance: new Big('21.93'), + grossPerformancePercentage: new Big('0.14465699208443271768'), + investment: new Big('75.80'), + netPerformance: new Big('17.68'), + netPerformancePercentage: new Big('0.11662269129287598945'), + marketPrice: 87.8, + quantity: new Big('1'), + symbol: 'NOVN.SW', + transactionCount: 2 + } + ], + totalInvestment: new Big('75.80') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 20b0a8709..4f8631a3f 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -77,17 +77,30 @@ export class PortfolioCalculator { const newQuantity = order.quantity .mul(factor) .plus(oldAccumulatedSymbol.quantity); + + let investment = new Big(0); + + if (newQuantity.gt(0)) { + if (order.type === 'BUY') { + investment = oldAccumulatedSymbol.investment.plus( + order.quantity.mul(unitPrice) + ); + } else if (order.type === 'SELL') { + const averagePrice = oldAccumulatedSymbol.investment.div( + oldAccumulatedSymbol.quantity + ); + investment = oldAccumulatedSymbol.investment.minus( + order.quantity.mul(averagePrice) + ); + } + } + currentTransactionPointItem = { + investment, currency: order.currency, dataSource: order.dataSource, fee: order.fee.plus(oldAccumulatedSymbol.fee), firstBuyDate: oldAccumulatedSymbol.firstBuyDate, - investment: newQuantity.eq(0) - ? new Big(0) - : unitPrice - .mul(order.quantity) - .mul(factor) - .plus(oldAccumulatedSymbol.investment), quantity: newQuantity, symbol: order.symbol, transactionCount: oldAccumulatedSymbol.transactionCount + 1 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 7cbfffe05..a7449a0be 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -271,6 +271,7 @@
Date: Fri, 15 Apr 2022 18:58:33 +0200 Subject: [PATCH 261/337] Release 1.137.0 (#831) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b344467..766834f67 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.137.0 - 15.04.2022 ### Added diff --git a/package.json b/package.json index d7349203c..2b589ebeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.136.0", + "version": "1.137.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From ec2ecab7519975260b81cab59fae51dc979fd807 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 Apr 2022 10:21:32 +0200 Subject: [PATCH 262/337] Clean up name (#834) --- .../rakuten-rapid-api/rakuten-rapid-api.service.ts | 2 -- 1 file changed, 2 deletions(-) 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 a15636956..3cb04dfa3 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 @@ -17,8 +17,6 @@ import { format, subMonths, subWeeks, subYears } from 'date-fns'; @Injectable() export class RakutenRapidApiService implements DataProviderInterface { - public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index'; - public constructor( private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService From 6e7cf0380b4271763fdd19e5572d5c96c98b382a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 Apr 2022 11:33:01 +0200 Subject: [PATCH 263/337] Feature/export single draft (#835) * Export single draft * Update changelog --- CHANGELOG.md | 8 ++++++- .../accounts-table.component.html | 9 ++++---- .../components/admin-users/admin-users.html | 4 ++-- .../transactions-page.component.ts | 8 +++---- .../src/app/services/ics/ics.service.ts | 5 ++-- .../activities-table.component.html | 23 ++++++++++++++----- .../activities-table.component.ts | 4 ++++ 7 files changed, 42 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 766834f67..f115e0206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,17 @@ 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 to export a single future activity (draft) as an `.ics` file + ## 1.137.0 - 15.04.2022 ### Added -- Added support to export future activities (drafts) as `.ics` files +- Added support to export future activities (drafts) as an `.ics` file ### Changed 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 5e8fcdba9..cf8446e42 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 @@ -194,16 +194,17 @@ - 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 9556f99cb..8982e2291 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -68,12 +68,12 @@ diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 61683fd75..9e94b305e 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -178,10 +178,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { content: this.icsService.transformActivitiesToIcsContent( data.activities ), - fileName: `ghostfolio-drafts-${format( - parseISO(data.meta.date), - 'yyyyMMddHHmm' - )}.ics`, + contentType: 'text/calendar', + fileName: `ghostfolio-draft${ + data.activities.length > 1 ? 's' : '' + }-${format(parseISO(data.meta.date), 'yyyyMMddHHmm')}.ics`, format: 'string' }); }); diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts index 7329ea663..bad9a514c 100644 --- a/apps/client/src/app/services/ics/ics.service.ts +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -9,6 +9,7 @@ import { format, parseISO } from 'date-fns'; }) export class IcsService { private readonly ICS_DATE_FORMAT = 'yyyyMMdd'; + private readonly ICS_LINE_BREAK = '\r\n'; public constructor() {} @@ -30,7 +31,7 @@ export class IcsService { }); const footer = ['END:VCALENDAR']; - return [...header, ...events, ...footer].join('\n'); + return [...header, ...events, ...footer].join(this.ICS_LINE_BREAK); } private getEvent({ @@ -54,6 +55,6 @@ export class IcsService { `DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, `SUMMARY:${capitalize(type)} ${symbol}`, 'END:VEVENT' - ].join('\n'); + ].join(this.ICS_LINE_BREAK); } } 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 a7449a0be..cd21ca88c 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -387,14 +387,25 @@ - - - + 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 c76d4c6a8..3025bdc8b 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -200,6 +200,10 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { } } + public onExportDraft(aActivityId: string) { + this.exportDrafts.emit([aActivityId]); + } + public onExportDrafts() { this.exportDrafts.emit( this.dataSource.filteredData From 638ae3f7fa06d82b64611ffd5326788941fcce49 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 Apr 2022 15:35:31 +0200 Subject: [PATCH 264/337] Feature/add boringly getting rich guide to resources page (#836) * Add "Boringly Getting Rich" guide * Update changelog --- CHANGELOG.md | 1 + .../app/pages/resources/resources-page.html | 196 ++++++++++-------- .../app/pages/resources/resources-page.scss | 12 +- 3 files changed, 110 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f115e0206..83b8db7a0 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 ### Added - Added support to export a single future activity (draft) as an `.ics` file +- Added the _Boringly Getting Rich_ guide to the resources section ## 1.137.0 - 15.04.2022 diff --git a/apps/client/src/app/pages/resources/resources-page.html b/apps/client/src/app/pages/resources/resources-page.html index e501fe87d..d97cf8952 100644 --- a/apps/client/src/app/pages/resources/resources-page.html +++ b/apps/client/src/app/pages/resources/resources-page.html @@ -1,105 +1,117 @@
-

Resources

- - -

Market

-
-
-
-
Fear & Greed Index
-
- The fear and greed index was developed by CNNMoney to - measure the primary emotions (fear and greed) that influence - how much investors are willing to pay for stocks. -
- -
+

Resources

+

Guides

+
+
+
+

Boringly Getting Rich

+
+ The Boringly Getting Rich guide supports you to get started + with investing. It introduces a strategy utilizing a broadly + diversified, low-cost portfolio excluding the risks of individual + stocks.
-
-
-
Inflation Chart
-
- Inflation Chart helps you find the intrinsic value of stock - markets, stock prices, goods and services by adjusting them to - the amount of the money supply (M0, M1, M2) or price of other - goods (food or oil). -
- -
+
-

Glossary

-
-
- -
-
Buy and Hold
-
- Buy and hold is a passive investment strategy where you buy - assets and hold them for a long period regardless of - fluctuations in the market. -
- -
+
+
+

Market

+
+
+
+

Fear & Greed Index

+
+ The fear and greed index was developed by CNNMoney to + measure the primary emotions (fear and greed) that influence how + much investors are willing to pay for stocks.
-
- -
-
Dollar-Cost Averaging (DCA)
-
- Dollar-cost averaging is an investment strategy where you - split the total amount to be invested across periodic - purchases of a target asset to reduce the impact of volatility - on the overall purchase. -
- -
+ -
- -
-
Financial Independence
-
- Financial independence is the status of having enough income, - for example with a passive income like dividends, to cover - your living expenses for the rest of your life. -
- -
+
+
+
+
+

Inflation Chart

+
+ Inflation Chart helps you find the intrinsic value of stock + markets, stock prices, goods and services by adjusting them to the + amount of the money supply (M0, M1, M2) or price of other goods + (food or oil). +
+ +
+
+
+

Glossary

+
+
+
+

Buy and Hold

+
+ Buy and hold is a passive investment strategy where you buy assets + and hold them for a long period regardless of fluctuations in the + market. +
+ +
+
+
+
+

Dollar-Cost Averaging (DCA)

+
+ Dollar-cost averaging is an investment strategy where you split + the total amount to be invested across periodic purchases of a + target asset to reduce the impact of volatility on the overall + purchase. +
+ +
+
+
+
+

Financial Independence

+
+ Financial independence is the status of having enough income, for + example with a passive income like dividends, to cover your living + expenses for the rest of your life. +
+
- - +
+
diff --git a/apps/client/src/app/pages/resources/resources-page.scss b/apps/client/src/app/pages/resources/resources-page.scss index 01a013378..4a8680714 100644 --- a/apps/client/src/app/pages/resources/resources-page.scss +++ b/apps/client/src/app/pages/resources/resources-page.scss @@ -2,14 +2,12 @@ color: rgb(var(--dark-primary-text)); display: block; - .mat-card { - a { - color: rgba(var(--palette-primary-500), 1); - font-weight: 500; + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; - &:hover { - color: rgba(var(--palette-primary-300), 1); - } + &:hover { + color: rgba(var(--palette-primary-300), 1); } } } From 0d8362ca8fac53c21f404f3b4c90b8aa8c816862 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 Apr 2022 21:01:55 +0200 Subject: [PATCH 265/337] Feature/separate deposit and savings in fire calculator (#837) * Separate deposit and savings * Update changelog --- CHANGELOG.md | 4 +++ .../fire-calculator.component.ts | 30 ++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b8db7a0..14f863c44 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 - Added support to export a single future activity (draft) as an `.ics` file - Added the _Boringly Getting Rich_ guide to the resources section +### Changed + +- Separated the deposit and savings in the chart of the the _FIRE_ calculator + ## 1.137.0 - 15.04.2022 ### Added diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index b8c2c6141..0c3092a60 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -11,7 +11,7 @@ import { ViewChild } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; -import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; +import { primaryColorRgb } from '@ghostfolio/common/config'; import { transformTickToAbbreviation } from '@ghostfolio/common/helper'; import { BarController, @@ -21,6 +21,7 @@ import { LinearScale, Tooltip } from 'chart.js'; +import * as Color from 'color'; import { isNumber } from 'lodash'; import { Subject, takeUntil } from 'rxjs'; @@ -211,16 +212,30 @@ export class FireCalculatorComponent labels.push(year); } + const datasetDeposit = { + backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, + data: [], + label: 'Deposit' + }; + const datasetInterest = { - backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, + backgroundColor: Color( + `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` + ) + .lighten(0.5) + .hex(), data: [], label: 'Interest' }; - const datasetPrincipal = { - backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, + const datasetSavings = { + backgroundColor: Color( + `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})` + ) + .lighten(0.25) + .hex(), data: [], - label: 'Principal' + label: 'Savings' }; for (let period = 1; period <= t; period++) { @@ -232,8 +247,9 @@ export class FireCalculatorComponent r }); - datasetPrincipal.data.push(principal.toNumber()); + datasetDeposit.data.push(this.fireWealth); datasetInterest.data.push(interest.toNumber()); + datasetSavings.data.push(principal.minus(this.fireWealth).toNumber()); if (period === t) { this.projectedTotalAmount = totalAmount.toNumber(); @@ -242,7 +258,7 @@ export class FireCalculatorComponent return { labels, - datasets: [datasetPrincipal, datasetInterest] + datasets: [datasetDeposit, datasetSavings, datasetInterest] }; } } From bd4608e521c98f0cd5ff90584fdc99810e74fd07 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 Apr 2022 21:03:28 +0200 Subject: [PATCH 266/337] Release 1.138.0 (#838) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f863c44..077931c1e 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.138.0 - 16.04.2022 ### Added diff --git a/package.json b/package.json index 2b589ebeb..7f31f1733 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.137.0", + "version": "1.138.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 8ae041faa086e709c11f92793d3f7fa1dac96bd8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 10:31:16 +0200 Subject: [PATCH 267/337] Bugfix/fix issue in fire calculator after changing investment horizon (#839) * Properly update chart datasets and improve tooltip * Update changelog --- CHANGELOG.md | 12 +++++++++- .../fire-calculator.component.ts | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 077931c1e..f4ca9347e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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 total amount to the tooltip in the chart of the _FIRE_ calculator + +### Fixed + +- Fixed an issue with changing the investment horizon in the chart of the _FIRE_ calculator + ## 1.138.0 - 16.04.2022 ### Added @@ -14,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Separated the deposit and savings in the chart of the the _FIRE_ calculator +- Separated the deposit and savings in the chart of the _FIRE_ calculator ## 1.137.0 - 15.04.2022 diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 0c3092a60..dc194206c 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -128,8 +128,10 @@ export class FireCalculatorComponent if (this.chartCanvas) { if (this.chart) { this.chart.data.labels = chartData.labels; - this.chart.data.datasets[0].data = chartData.datasets[0].data; - this.chart.data.datasets[1].data = chartData.datasets[1].data; + + for (let index = 0; index < this.chart.data.datasets.length; index++) { + this.chart.data.datasets[index].data = chartData.datasets[index].data; + } this.chart.update(); } else { @@ -138,7 +140,24 @@ export class FireCalculatorComponent options: { plugins: { tooltip: { + itemSort: (a, b) => { + // Reverse order + return b.datasetIndex - a.datasetIndex; + }, + mode: 'index', callbacks: { + footer: (items) => { + const totalAmount = items.reduce( + (a, b) => a + b.parsed.y, + 0 + ); + + return `Total Amount: ${new Intl.NumberFormat(this.locale, { + currency: this.currency, + currencyDisplay: 'code', + style: 'currency' + }).format(totalAmount)}`; + }, label: (context) => { let label = context.dataset.label || ''; From 82ede2fe32f71dbd940a2e55965e3469b176537b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 10:49:02 +0200 Subject: [PATCH 268/337] Bugfix/fix fear and greed data source (#840) * Fix data source of Fear & Greed Index * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/info/info.service.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ca9347e..59ecfeb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with changing the investment horizon in the chart of the _FIRE_ calculator +- Fixed the data source of the _Fear & Greed Index_ (market mood) ## 1.138.0 - 16.04.2022 diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 67bd62a62..60df05c51 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -52,9 +52,15 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - info.fearAndGreedDataSource = encodeDataSource( - ghostfolioFearAndGreedIndexDataSource - ); + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true + ) { + info.fearAndGreedDataSource = encodeDataSource( + ghostfolioFearAndGreedIndexDataSource + ); + } else { + info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource; + } } if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { From 056b318d86541c2820b33f50c8fac2531bc98991 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 11:31:16 +0200 Subject: [PATCH 269/337] Bugfix/fix end date in ics files (#841) * Fix end date * Update changelog --- CHANGELOG.md | 1 + .../pages/portfolio/transactions/transactions-page.component.ts | 2 +- apps/client/src/app/services/ics/ics.service.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ecfeb7e..67bb0e886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with changing the investment horizon in the chart of the _FIRE_ calculator +- Fixed an issue with the end dates in the `.ics` file of the future activities (drafts) export - Fixed the data source of the _Fear & Greed Index_ (market mood) ## 1.138.0 - 16.04.2022 diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 9e94b305e..1717dbbda 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -181,7 +181,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { contentType: 'text/calendar', fileName: `ghostfolio-draft${ data.activities.length > 1 ? 's' : '' - }-${format(parseISO(data.meta.date), 'yyyyMMddHHmm')}.ics`, + }-${format(parseISO(data.meta.date), 'yyyyMMddHHmmss')}.ics`, format: 'string' }); }); diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts index bad9a514c..1163fa786 100644 --- a/apps/client/src/app/services/ics/ics.service.ts +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -52,7 +52,6 @@ export class IcsService { `UID:${id}`, `DTSTAMP:${today}T000000`, `DTSTART;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, - `DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`, `SUMMARY:${capitalize(type)} ${symbol}`, 'END:VEVENT' ].join(this.ICS_LINE_BREAK); From 90efc2ac51ecd79f08bc31df6335bee78256f042 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 11:57:57 +0200 Subject: [PATCH 270/337] Feature/beautify etf names in asset profile (#842) * Beautify ETF names * Update changelog --- CHANGELOG.md | 4 +++ .../yahoo-finance/yahoo-finance.service.ts | 27 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bb0e886..0fcba5e49 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 total amount to the tooltip in the chart of the _FIRE_ calculator +### Changed + +- Beautified the ETF names in the symbol profile + ### Fixed - Fixed an issue with changing the investment horizon in the chart of the _FIRE_ calculator 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 b5a0932fb..b5d8e1ebd 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 @@ -20,7 +20,10 @@ import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; import yahooFinance from 'yahoo-finance2'; -import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; +import type { + Price, + QuoteSummaryResult +} from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -89,8 +92,7 @@ export class YahooFinanceService implements DataProviderInterface { response.assetSubClass = assetSubClass; response.currency = assetProfile.price.currency; response.dataSource = this.getName(); - response.name = - assetProfile.price.longName || assetProfile.price.shortName || symbol; + response.name = this.formatName(assetProfile); response.symbol = aSymbol; if ( @@ -296,6 +298,25 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } + private formatName(aAssetProfile: QuoteSummaryResult) { + let name = aAssetProfile.price.longName; + + if (name) { + name = name.replace('iShares ETF (CH) - ', ''); + name = name.replace('iShares III Public Limited Company - ', ''); + name = name.replace('iShares VI Public Limited Company - ', ''); + name = name.replace('iShares VII PLC - ', ''); + name = name.replace('Multi Units Luxembourg - ', ''); + name = name.replace('VanEck ETFs N.V. - ', ''); + name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', ''); + name = name.replace('Vanguard Funds Public Limited Company - ', ''); + name = name.replace('Vanguard Index Funds - ', ''); + name = name.replace('Xtrackers (IE) Plc - ', ''); + } + + return name || aAssetProfile.price.shortName || aAssetProfile.price.symbol; + } + private parseAssetClass(aPrice: Price): { assetClass: AssetClass; assetSubClass: AssetSubClass; From 9f597cbff1a0bd96e1053dc6e4f2223c011466d2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 11:59:16 +0200 Subject: [PATCH 271/337] Release 1.139.0 (#843) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fcba5e49..d3cf8f653 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.139.0 - 18.04.2022 ### Added diff --git a/package.json b/package.json index 7f31f1733..209d2d4b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.138.0", + "version": "1.139.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From f3271ab1ff69b59f31d498c774b915bb171bcb7d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 17:10:00 +0200 Subject: [PATCH 272/337] Feature/upgrade yahoo finance2 to version 2.3.1 (#844) * Upgrade yahoo-finance2 to version 2.3.1 * Update changelog --- CHANGELOG.md | 6 ++++++ package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3cf8f653..fb47ae1bd 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 `yahoo-finance2` from version `2.3.0` to `2.3.1` + ## 1.139.0 - 18.04.2022 ### Added diff --git a/package.json b/package.json index 209d2d4b3..4770d5384 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", - "yahoo-finance2": "2.3.0", + "yahoo-finance2": "2.3.1", "zone.js": "0.11.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index cb313769c..0fcd743b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18836,10 +18836,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.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.0.tgz#81bd76732dfd38aa5d7019a97caf0f938c0127c2" - integrity sha512-7oj8n/WJH9MtX+q99WbHdjEVPdobTX8IyYjg7v4sDOh4f9ByT2Frxmp+Uj+rctrO0EiiD9QWTuwV4h8AemGuCg== +yahoo-finance2@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.1.tgz#d2cffbef78f6974e4e6a40487cc08ab133dc9fc5" + integrity sha512-QTXiiWgfrpVbSylchBgLqESZz+8+SyyDSqntjfZHxMIHa6d14xq+biNNDIeYd5SylcZ9Vt4zLmZXHN7EdLM1pA== dependencies: ajv "8.10.0" ajv-formats "2.1.1" From 7c58c5fb7f87554a1cdc2a8aeb26c79526230c87 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:20:30 +0200 Subject: [PATCH 273/337] Setup funding.yml (#847) --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..57ee8ad6f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://www.buymeacoffee.com/ghostfolio'] From 83ebacbb06ae66c291d9135889d1a6b2046ac4bd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:32:54 +0200 Subject: [PATCH 274/337] Add Buy me a coffee link (#848) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dbb047861..3c8d98e53 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,8 @@ Ghostfolio is **100% free** and **open source**. We encourage and support an act Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you. +If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**. + ## License © 2022 [Ghostfolio](https://ghostfol.io) From 0e4c39d14548401c72aa3759cda5212f38901386 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 19 Apr 2022 17:06:12 +0200 Subject: [PATCH 275/337] Feature/reuse value component in ghostfolio in numbers section (#846) * Reuse value component * Update changelog --- CHANGELOG.md | 5 ++ .../src/app/pages/about/about-page.html | 61 ++++++++++--------- .../src/app/pages/about/about-page.module.ts | 2 + libs/ui/src/lib/value/value.component.html | 9 +-- libs/ui/src/lib/value/value.component.scss | 4 ++ libs/ui/src/lib/value/value.component.ts | 1 + 6 files changed, 50 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb47ae1bd..59c2229d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for sub-labels in the value component + ### Changed +- Reused the value component in the _Ghostfolio in Numbers_ section of the about page - Upgraded `yahoo-finance2` from version `2.3.0` to `2.3.1` ## 1.139.0 - 18.04.2022 diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index 90c1632a6..eb9da1593 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -109,38 +109,39 @@
-

{{ statistics?.activeUsers1d || '-' }}

-
- Active Users (Last 24 hours) -
+
-

{{ statistics?.newUsers30d ?? '-' }}

-
- New Users (Last 30 days) -
+
-

{{ statistics?.activeUsers30d ?? '-' }}

-
- Active Users (Last 30 days) -
+
@@ -148,10 +149,11 @@ class="d-block" href="https://github.com/ghostfolio/ghostfolio/graphs/contributors" > -

- {{ statistics?.gitHubContributors ?? '-' }} -

-
Contributors on GitHub
+
@@ -159,8 +161,11 @@ class="d-block" href="https://github.com/ghostfolio/ghostfolio/stargazers" > -

{{ statistics?.gitHubStargazers ?? '-' }}

-
Stars on GitHub
+
diff --git a/apps/client/src/app/pages/about/about-page.module.ts b/apps/client/src/app/pages/about/about-page.module.ts index 2ca669fde..f0003e3fb 100644 --- a/apps/client/src/app/pages/about/about-page.module.ts +++ b/apps/client/src/app/pages/about/about-page.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { AboutPageRoutingModule } from './about-page-routing.module'; import { AboutPageComponent } from './about-page.component'; @@ -12,6 +13,7 @@ import { AboutPageComponent } from './about-page.component'; imports: [ AboutPageRoutingModule, CommonModule, + GfValueModule, MatButtonModule, MatCardModule ], diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index 026eb2794..8ed9c9bdf 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -10,14 +10,14 @@
{{ formattedValue }}%
@@ -36,7 +36,7 @@
{{ formattedValue | titlecase }} @@ -45,7 +45,8 @@
- {{ label }} + {{ label }} + {{ subLabel }}
{{ label }} diff --git a/libs/ui/src/lib/value/value.component.scss b/libs/ui/src/lib/value/value.component.scss index 7452006e0..04c8fe3ff 100644 --- a/libs/ui/src/lib/value/value.component.scss +++ b/libs/ui/src/lib/value/value.component.scss @@ -2,4 +2,8 @@ display: flex; flex-direction: column; font-variant-numeric: tabular-nums; + + .h2 { + line-height: 1; + } } diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 2004065db..8919b1455 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -25,6 +25,7 @@ export class ValueComponent implements OnChanges { @Input() position = ''; @Input() precision: number | undefined; @Input() size: 'large' | 'medium' | 'small' = 'small'; + @Input() subLabel = ''; @Input() value: number | string = ''; public absoluteValue = 0; From 20358d9105d21f02aa66d82ae7ac35ce70bc4452 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 21 Apr 2022 23:07:19 +0200 Subject: [PATCH 276/337] Feature/persist savings rate (#849) * Persist savings rate * Update changelog --- CHANGELOG.md | 1 + .../src/app/user/update-user-setting.dto.ts | 4 ++ .../portfolio/fire/fire-page.component.ts | 7 ++++ .../app/pages/portfolio/fire/fire-page.html | 4 +- .../fire-calculator.component.ts | 40 +++++++++++++++---- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c2229d7..49d2b2129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Reused the value component in the _Ghostfolio in Numbers_ section of the about page +- Persisted the savings rate in the _FIRE_ calculator - Upgraded `yahoo-finance2` from version `2.3.0` to `2.3.1` ## 1.139.0 - 18.04.2022 diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index b09e904df..b97dd287e 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -12,4 +12,8 @@ export class UpdateUserSettingDto { @IsString() @IsOptional() locale?: string; + + @IsNumber() + @IsOptional() + savingsRate?: number; } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 30faa2230..bd4175f93 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -68,6 +68,13 @@ export class FirePageComponent implements OnDestroy, OnInit { }); } + public onSavingsRateChange(savingsRate: number) { + this.dataService + .putUserSetting({ savingsRate }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index ebbfccb64..0aa0962e4 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -41,7 +41,7 @@ [value]="withdrawalRatePerMonth?.toNumber()" > per month, based on your investment of + >, based on your total assets of
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index dc194206c..256bdd6a1 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -5,9 +5,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + EventEmitter, Input, OnChanges, OnDestroy, + Output, ViewChild } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; @@ -40,6 +42,9 @@ export class FireCalculatorComponent @Input() deviceType: string; @Input() fireWealth: number; @Input() locale: string; + @Input() savingsRate = 0; + + @Output() savingsRateChanged = new EventEmitter(); @ViewChild('chartCanvas') chartCanvas; @@ -73,7 +78,7 @@ export class FireCalculatorComponent this.calculatorForm.setValue({ annualInterestRate: 5, - paymentPerPeriod: 500, + paymentPerPeriod: this.savingsRate, principalInvestmentAmount: 0, time: 10 }); @@ -83,15 +88,28 @@ export class FireCalculatorComponent .subscribe(() => { this.initialize(); }); + + this.calculatorForm + .get('paymentPerPeriod') + .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((savingsRate) => { + this.savingsRateChanged.emit(savingsRate); + }); } public ngAfterViewInit() { if (isNumber(this.fireWealth) && this.fireWealth >= 0) { setTimeout(() => { // Wait for the chartCanvas - this.calculatorForm.patchValue({ - principalInvestmentAmount: this.fireWealth - }); + this.calculatorForm.patchValue( + { + principalInvestmentAmount: this.fireWealth, + paymentPerPeriod: this.savingsRate ?? 0 + }, + { + emitEvent: false + } + ); this.calculatorForm.get('principalInvestmentAmount').disable(); this.changeDetectorRef.markForCheck(); @@ -103,9 +121,15 @@ export class FireCalculatorComponent if (isNumber(this.fireWealth) && this.fireWealth >= 0) { setTimeout(() => { // Wait for the chartCanvas - this.calculatorForm.patchValue({ - principalInvestmentAmount: this.fireWealth - }); + this.calculatorForm.patchValue( + { + principalInvestmentAmount: this.fireWealth, + paymentPerPeriod: this.savingsRate ?? 0 + }, + { + emitEvent: false + } + ); this.calculatorForm.get('principalInvestmentAmount').disable(); this.changeDetectorRef.markForCheck(); @@ -152,7 +176,7 @@ export class FireCalculatorComponent 0 ); - return `Total Amount: ${new Intl.NumberFormat(this.locale, { + return `Total: ${new Intl.NumberFormat(this.locale, { currency: this.currency, currencyDisplay: 'code', style: 'currency' From beb12637cef8e563706d3deb5215d263d4879069 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Apr 2022 19:29:18 +0200 Subject: [PATCH 277/337] Bugfix/fix total calculation for sell and dividend (#850) * Fix calculation for sell and dividend activities * Update changelog --- CHANGELOG.md | 4 + ...-or-update-transaction-dialog.component.ts | 108 +++++++++++------- .../create-or-update-transaction-dialog.html | 4 +- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d2b2129..747526656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Persisted the savings rate in the _FIRE_ calculator - Upgraded `yahoo-finance2` from version `2.3.0` to `2.3.1` +### Fixed + +- Fixed the calculation of the total value for sell and dividend activities in the create or edit transaction dialog + ## 1.139.0 - 18.04.2022 ### Added 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 134adcdc5..920120900 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 @@ -46,6 +46,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { public filteredLookupItemsObservable: Observable; public isLoading = false; public platforms: { id: string; name: string }[]; + public total = 0; public Validators = Validators; private unsubscribeSubject = new Subject(); @@ -89,6 +90,25 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { unitPrice: [this.data.activity?.unitPrice, Validators.required] }); + this.activityForm.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + if ( + this.activityForm.controls['type'].value === 'BUY' || + this.activityForm.controls['type'].value === 'ITEM' + ) { + this.total = + this.activityForm.controls['quantity'].value * + this.activityForm.controls['unitPrice'].value + + this.activityForm.controls['fee'].value ?? 0; + } else { + this.total = + this.activityForm.controls['quantity'].value * + this.activityForm.controls['unitPrice'].value - + this.activityForm.controls['fee'].value ?? 0; + } + }); + this.filteredLookupItemsObservable = this.activityForm.controls[ 'searchSymbol' ].valueChanges.pipe( @@ -100,9 +120,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { const filteredLookupItemsObservable = this.dataService.fetchSymbols(query); - filteredLookupItemsObservable.subscribe((filteredLookupItems) => { - this.filteredLookupItems = filteredLookupItems; - }); + filteredLookupItemsObservable + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((filteredLookupItems) => { + this.filteredLookupItems = filteredLookupItems; + }); return filteredLookupItemsObservable; } @@ -111,45 +133,47 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { }) ); - this.activityForm.controls['type'].valueChanges.subscribe((type: Type) => { - if (type === 'ITEM') { - this.activityForm.controls['accountId'].removeValidators( - Validators.required - ); - this.activityForm.controls['accountId'].updateValueAndValidity(); - this.activityForm.controls['currency'].setValue( - this.data.user.settings.baseCurrency - ); - this.activityForm.controls['dataSource'].removeValidators( - Validators.required - ); - this.activityForm.controls['dataSource'].updateValueAndValidity(); - this.activityForm.controls['name'].setValidators(Validators.required); - this.activityForm.controls['name'].updateValueAndValidity(); - this.activityForm.controls['quantity'].setValue(1); - this.activityForm.controls['searchSymbol'].removeValidators( - Validators.required - ); - this.activityForm.controls['searchSymbol'].updateValueAndValidity(); - } else { - this.activityForm.controls['accountId'].setValidators( - Validators.required - ); - this.activityForm.controls['accountId'].updateValueAndValidity(); - this.activityForm.controls['dataSource'].setValidators( - Validators.required - ); - this.activityForm.controls['dataSource'].updateValueAndValidity(); - this.activityForm.controls['name'].removeValidators( - Validators.required - ); - this.activityForm.controls['name'].updateValueAndValidity(); - this.activityForm.controls['searchSymbol'].setValidators( - Validators.required - ); - this.activityForm.controls['searchSymbol'].updateValueAndValidity(); - } - }); + this.activityForm.controls['type'].valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((type: Type) => { + if (type === 'ITEM') { + this.activityForm.controls['accountId'].removeValidators( + Validators.required + ); + this.activityForm.controls['accountId'].updateValueAndValidity(); + this.activityForm.controls['currency'].setValue( + this.data.user.settings.baseCurrency + ); + this.activityForm.controls['dataSource'].removeValidators( + Validators.required + ); + this.activityForm.controls['dataSource'].updateValueAndValidity(); + this.activityForm.controls['name'].setValidators(Validators.required); + this.activityForm.controls['name'].updateValueAndValidity(); + this.activityForm.controls['quantity'].setValue(1); + this.activityForm.controls['searchSymbol'].removeValidators( + Validators.required + ); + this.activityForm.controls['searchSymbol'].updateValueAndValidity(); + } else { + this.activityForm.controls['accountId'].setValidators( + Validators.required + ); + this.activityForm.controls['accountId'].updateValueAndValidity(); + this.activityForm.controls['dataSource'].setValidators( + Validators.required + ); + this.activityForm.controls['dataSource'].updateValueAndValidity(); + this.activityForm.controls['name'].removeValidators( + Validators.required + ); + this.activityForm.controls['name'].updateValueAndValidity(); + this.activityForm.controls['searchSymbol'].setValidators( + Validators.required + ); + this.activityForm.controls['searchSymbol'].updateValueAndValidity(); + } + }); this.activityForm.controls['type'].setValue(this.data.activity?.type); diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html index 212b3c245..3ce720b37 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -138,9 +138,9 @@
From 1b03ddc586abf6e85781c6c9085c8310db9936a0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Apr 2022 21:27:55 +0200 Subject: [PATCH 278/337] Feature/add symbol profile overrides model (#851) * Add symbol profile overrides model * Update changelog --- CHANGELOG.md | 5 ++ .../src/services/symbol-profile.service.ts | 48 +++++++++++++++---- .../migration.sql | 15 ++++++ .../migration.sql | 2 + prisma/schema.prisma | 43 +++++++++++------ 5 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 prisma/migrations/20220422174935_added_symbol_profile_overrides/migration.sql create mode 100644 prisma/migrations/20220422183831_added_commodity_to_asset_sub_class/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 747526656..67e0c9380 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 ### Added - Added support for sub-labels in the value component +- Added a symbol profile overrides model for manual adjustments ### Changed @@ -21,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the calculation of the total value for sell and dividend activities in the create or edit transaction dialog +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.139.0 - 18.04.2022 ### Added diff --git a/apps/api/src/services/symbol-profile.service.ts b/apps/api/src/services/symbol-profile.service.ts index b45c44af9..8b119de78 100644 --- a/apps/api/src/services/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile.service.ts @@ -4,7 +4,12 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Injectable } from '@nestjs/common'; -import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; +import { + DataSource, + Prisma, + SymbolProfile, + SymbolProfileOverrides +} from '@prisma/client'; import { continents, countries } from 'countries-list'; import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface'; @@ -36,6 +41,7 @@ export class SymbolProfileService { ): Promise { return this.prismaService.symbolProfile .findMany({ + include: { SymbolProfileOverrides: true }, where: { symbol: { in: symbols @@ -45,14 +51,38 @@ export class SymbolProfileService { .then((symbolProfiles) => this.getSymbols(symbolProfiles)); } - private getSymbols(symbolProfiles: SymbolProfile[]): EnhancedSymbolProfile[] { - return symbolProfiles.map((symbolProfile) => ({ - ...symbolProfile, - countries: this.getCountries(symbolProfile), - scraperConfiguration: this.getScraperConfiguration(symbolProfile), - sectors: this.getSectors(symbolProfile), - symbolMapping: this.getSymbolMapping(symbolProfile) - })); + private getSymbols( + symbolProfiles: (SymbolProfile & { + SymbolProfileOverrides: SymbolProfileOverrides; + })[] + ): EnhancedSymbolProfile[] { + return symbolProfiles.map((symbolProfile) => { + const item = { + ...symbolProfile, + countries: this.getCountries(symbolProfile), + scraperConfiguration: this.getScraperConfiguration(symbolProfile), + sectors: this.getSectors(symbolProfile), + symbolMapping: this.getSymbolMapping(symbolProfile) + }; + + if (item.SymbolProfileOverrides) { + item.assetClass = + item.SymbolProfileOverrides.assetClass ?? item.assetClass; + item.assetSubClass = + item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass; + item.countries = + (item.SymbolProfileOverrides.sectors as unknown as Country[]) ?? + item.countries; + item.name = item.SymbolProfileOverrides?.name ?? item.name; + item.sectors = + (item.SymbolProfileOverrides.sectors as unknown as Sector[]) ?? + item.sectors; + + delete item.SymbolProfileOverrides; + } + + return item; + }); } private getCountries(symbolProfile: SymbolProfile): Country[] { diff --git a/prisma/migrations/20220422174935_added_symbol_profile_overrides/migration.sql b/prisma/migrations/20220422174935_added_symbol_profile_overrides/migration.sql new file mode 100644 index 000000000..b0af4d5e6 --- /dev/null +++ b/prisma/migrations/20220422174935_added_symbol_profile_overrides/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "SymbolProfileOverrides" ( + "assetClass" "AssetClass", + "assetSubClass" "AssetSubClass", + "countries" JSONB, + "name" TEXT, + "sectors" JSONB, + "symbolProfileId" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SymbolProfileOverrides_pkey" PRIMARY KEY ("symbolProfileId") +); + +-- AddForeignKey +ALTER TABLE "SymbolProfileOverrides" ADD CONSTRAINT "SymbolProfileOverrides_symbolProfileId_fkey" FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20220422183831_added_commodity_to_asset_sub_class/migration.sql b/prisma/migrations/20220422183831_added_commodity_to_asset_sub_class/migration.sql new file mode 100644 index 000000000..17e69547f --- /dev/null +++ b/prisma/migrations/20220422183831_added_commodity_to_asset_sub_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetSubClass" ADD VALUE 'COMMODITY'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6dea69159..7ffca1690 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -112,25 +112,37 @@ model Settings { } model SymbolProfile { - assetClass AssetClass? - assetSubClass AssetSubClass? - countries Json? - createdAt DateTime @default(now()) - currency String - dataSource DataSource - id String @id @default(uuid()) - name String? - Order Order[] - updatedAt DateTime @updatedAt - scraperConfiguration Json? - sectors Json? - symbol String - symbolMapping Json? - url String? + assetClass AssetClass? + assetSubClass AssetSubClass? + countries Json? + createdAt DateTime @default(now()) + currency String + dataSource DataSource + id String @id @default(uuid()) + name String? + Order Order[] + updatedAt DateTime @updatedAt + scraperConfiguration Json? + sectors Json? + symbol String + symbolMapping Json? + SymbolProfileOverrides SymbolProfileOverrides? + url String? @@unique([dataSource, symbol]) } +model SymbolProfileOverrides { + assetClass AssetClass? + assetSubClass AssetSubClass? + countries Json? + name String? + sectors Json? + SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) + symbolProfileId String @id + updatedAt DateTime @updatedAt +} + model Subscription { createdAt DateTime @default(now()) expiresAt DateTime @@ -176,6 +188,7 @@ enum AssetClass { enum AssetSubClass { BOND + COMMODITY CRYPTOCURRENCY ETF MUTUALFUND From bbe9183fb0bfacc6cffe3b33b9731ddc4632a1fc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Apr 2022 21:29:08 +0200 Subject: [PATCH 279/337] Release 1.140.0 (#852) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e0c9380..131f9a516 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.140.0 - 22.04.2022 ### Added diff --git a/package.json b/package.json index 4770d5384..8b77754c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.139.0", + "version": "1.140.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 4a75c6d483ba1a24287eafce468f2ecc72c6ff6a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 22 Apr 2022 21:45:50 +0200 Subject: [PATCH 280/337] Release 1.140.1 (#853) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 131f9a516..022994af1 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). -## 1.140.0 - 22.04.2022 +## 1.140.1 - 22.04.2022 ### Added diff --git a/package.json b/package.json index 8b77754c6..0928a913d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.140.0", + "version": "1.140.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From e9a46cb2246de1369b6bb323cdbd4b888bd4db16 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Apr 2022 09:52:22 +0200 Subject: [PATCH 281/337] Release 1.140.2 (#854) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 022994af1..3f6c61ca3 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). -## 1.140.1 - 22.04.2022 +## 1.140.2 - 22.04.2022 ### Added diff --git a/package.json b/package.json index 0928a913d..4f415a506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.140.1", + "version": "1.140.2", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 283f054ee22f050da17841226f8fd5629759f555 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Apr 2022 13:38:41 +0200 Subject: [PATCH 282/337] Feature/upgrade prisma to version 3.12.0 (#856) * Upgrade prisma to version 3.12.0 * Update changelog --- CHANGELOG.md | 6 ++++++ package.json | 4 ++-- yarn.lock | 36 ++++++++++++++++++------------------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6c61ca3..b0aca3bd0 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 `prisma` from version `3.11.1` to `3.12.0` + ## 1.140.2 - 22.04.2022 ### Added diff --git a/package.json b/package.json index 4f415a506..bbcca09c8 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.5", - "@prisma/client": "3.11.1", + "@prisma/client": "3.12.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -109,7 +109,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.11.1", + "prisma": "3.12.0", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 0fcd743b4..250fbd811 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3487,22 +3487,22 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@prisma/client@3.11.1": - version "3.11.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7" - integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng== +"@prisma/client@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12" + integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw== dependencies: - "@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" -"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9" - integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ== +"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886" + integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw== -"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c" - integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw== +"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45" + integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -15334,12 +15334,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prisma@3.11.1: - version "3.11.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028" - integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg== +prisma@3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd" + integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg== dependencies: - "@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1" From edca05f54204e77f1fb858cc7f1767772002944b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Apr 2022 16:48:23 +0200 Subject: [PATCH 283/337] Feature/change get started url of public page (#857) * Change url * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/pages/public/public-page.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0aca3bd0..413e5bda7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Changed the url of the _Get Started_ link to `https://ghostfol.io` on the public page - Upgraded `prisma` from version `3.11.1` to `3.12.0` ## 1.140.2 - 22.04.2022 diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 5af657d67..b4f7e7295 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -119,7 +119,7 @@ Ghostfolio empowers you to keep track of your wealth.

From 8f61f7c1697acb6a79a361af3e02ad5dd857b959 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Apr 2022 19:22:20 +0200 Subject: [PATCH 284/337] Feature/extract activities table filter component (#858) * Extract activities table component * Update changelog --- CHANGELOG.md | 1 + .../activities-filter.component.html | 33 +++++ .../activities-filter.component.scss | 22 ++++ .../activities-filter.component.ts | 108 +++++++++++++++ .../activities-filter.module.ts | 24 ++++ .../activities-table.component.html | 41 +----- .../activities-table.component.scss | 15 --- .../activities-table.component.ts | 124 +++++------------- .../activities-table.module.ts | 10 +- 9 files changed, 228 insertions(+), 150 deletions(-) create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.html create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.scss create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.ts create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 413e5bda7..43fb36495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extracted the activities table filter to a dedicated component - Changed the url of the _Get Started_ link to `https://ghostfol.io` on the public page - Upgraded `prisma` from version `3.11.1` to `3.12.0` diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html new file mode 100644 index 000000000..ff39a6750 --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -0,0 +1,33 @@ + + + + + {{ searchKeyword | gfSymbol }} + + + + + + + {{ filter | gfSymbol }} + + + diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.scss b/libs/ui/src/lib/activities-filter/activities-filter.component.scss new file mode 100644 index 000000000..f40444914 --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.scss @@ -0,0 +1,22 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + ::ng-deep { + .mat-form-field-infix { + border-top: 0 solid transparent !important; + } + } + + .mat-chip { + cursor: pointer; + min-height: 1.5rem !important; + } +} + +:host-context(.is-dark-theme) { + .mat-form-field { + color: rgba(var(--light-primary-text)); + } +} diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts new file mode 100644 index 000000000..6e8eea64f --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -0,0 +1,108 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-activities-filter', + styleUrls: ['./activities-filter.component.scss'], + templateUrl: './activities-filter.component.html' +}) +export class ActivitiesFilterComponent implements OnChanges, OnDestroy { + @Input() allFilters: string[]; + @Input() placeholder: string; + + @Output() valueChanged = new EventEmitter(); + + @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('searchInput') searchInput: ElementRef; + + public filters$: Subject = new BehaviorSubject([]); + public filters: Observable = this.filters$.asObservable(); + public searchControl = new FormControl(); + public searchKeywords: string[] = []; + public separatorKeysCodes: number[] = [ENTER, COMMA]; + + private unsubscribeSubject = new Subject(); + + public constructor() { + this.searchControl.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((keyword) => { + if (keyword) { + const filterValue = keyword.toLowerCase(); + this.filters$.next( + this.allFilters.filter( + (filter) => filter.toLowerCase().indexOf(filterValue) === 0 + ) + ); + } else { + this.filters$.next(this.allFilters); + } + }); + } + + public ngOnChanges() { + if (this.allFilters) { + this.updateFilter(); + } + } + + public addKeyword({ input, value }: MatChipInputEvent): void { + if (value?.trim()) { + this.searchKeywords.push(value.trim()); + this.updateFilter(); + } + + // Reset the input value + if (input) { + input.value = ''; + } + + this.searchControl.setValue(null); + } + + public keywordSelected(event: MatAutocompleteSelectedEvent): void { + this.searchKeywords.push(event.option.viewValue); + this.updateFilter(); + this.searchInput.nativeElement.value = ''; + this.searchControl.setValue(null); + } + + public removeKeyword(keyword: string): void { + const index = this.searchKeywords.indexOf(keyword); + + if (index >= 0) { + this.searchKeywords.splice(index, 1); + this.updateFilter(); + } + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private updateFilter() { + this.filters$.next(this.allFilters); + + this.valueChanged.emit(this.searchKeywords); + } +} diff --git a/libs/ui/src/lib/activities-filter/activities-filter.module.ts b/libs/ui/src/lib/activities-filter/activities-filter.module.ts new file mode 100644 index 000000000..a192296fd --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatInputModule } from '@angular/material/input'; +import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; + +import { ActivitiesFilterComponent } from './activities-filter.component'; + +@NgModule({ + declarations: [ActivitiesFilterComponent], + exports: [ActivitiesFilterComponent], + imports: [ + CommonModule, + GfSymbolModule, + MatAutocompleteModule, + MatChipsModule, + MatInputModule, + ReactiveFormsModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfActivitiesFilterModule {} 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 cd21ca88c..a047053dd 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,40 +1,9 @@ - - - - - {{ searchKeyword | gfSymbol }} - - - - - - - {{ filter | gfSymbol }} - - - + [placeholder]="placeholder" + (valueChanged)="updateFilter($event)" +>
(); @Output() import = new EventEmitter(); - @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; - @ViewChild('searchInput') searchInput: ElementRef; @ViewChild(MatSort) sort: MatSort; + public allFilters: string[]; public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; public displayedColumns = []; public endOfToday = endOfToday(); - public filters$: Subject = new BehaviorSubject([]); - public filters: Observable = this.filters$.asObservable(); public hasDrafts = false; public isAfter = isAfter; public isLoading = true; @@ -77,59 +66,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public routeQueryParams: Subscription; public searchControl = new FormControl(); public searchKeywords: string[] = []; - public separatorKeysCodes: number[] = [ENTER, COMMA]; public totalFees: number; public totalValue: number; - private allFilters: string[]; private unsubscribeSubject = new Subject(); - public constructor(private router: Router) { - this.searchControl.valueChanges - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((keyword) => { - if (keyword) { - const filterValue = keyword.toLowerCase(); - this.filters$.next( - this.allFilters.filter( - (filter) => filter.toLowerCase().indexOf(filterValue) === 0 - ) - ); - } else { - this.filters$.next(this.allFilters); - } - }); - } - - public addKeyword({ input, value }: MatChipInputEvent): void { - if (value?.trim()) { - this.searchKeywords.push(value.trim()); - this.updateFilter(); - } - - // Reset the input value - if (input) { - input.value = ''; - } - - this.searchControl.setValue(null); - } - - public removeKeyword(keyword: string): void { - const index = this.searchKeywords.indexOf(keyword); - - if (index >= 0) { - this.searchKeywords.splice(index, 1); - this.updateFilter(); - } - } - - public keywordSelected(event: MatAutocompleteSelectedEvent): void { - this.searchKeywords.push(event.option.viewValue); - this.updateFilter(); - this.searchInput.nativeElement.value = ''; - this.searchControl.setValue(null); - } + public constructor(private router: Router) {} public ngOnChanges() { this.displayedColumns = [ @@ -230,28 +172,23 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.activityToUpdate.emit(aActivity); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - - private updateFilter() { - this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR); - const lowercaseSearchKeywords = this.searchKeywords.map((keyword) => + public updateFilter(filters: string[] = []) { + this.dataSource.filter = filters.join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((keyword) => keyword.trim().toLowerCase() ); this.placeholder = lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; + this.searchKeywords = filters; + this.allFilters = this.getSearchableFieldValues(this.activities).filter( (item) => { return !lowercaseSearchKeywords.includes(item.trim().toLowerCase()); } ); - this.filters$.next(this.allFilters); - this.hasDrafts = this.dataSource.data.some((activity) => { return activity.isDraft === true; }); @@ -259,6 +196,31 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.totalValue = this.getTotalValue(); } + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private getFilterableValues( + activity: OrderWithAccount, + fieldValues: Set = new Set() + ): string[] { + fieldValues.add(activity.Account?.name); + fieldValues.add(activity.Account?.Platform?.name); + fieldValues.add(activity.SymbolProfile.currency); + + if (!isUUID(activity.SymbolProfile.symbol)) { + fieldValues.add(activity.SymbolProfile.symbol); + } + + fieldValues.add(activity.type); + fieldValues.add(format(activity.date, 'yyyy')); + + return [...fieldValues].filter((item) => { + return item !== undefined; + }); + } + private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { const fieldValues = new Set(); @@ -287,26 +249,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { }); } - private getFilterableValues( - activity: OrderWithAccount, - fieldValues: Set = new Set() - ): string[] { - fieldValues.add(activity.Account?.name); - fieldValues.add(activity.Account?.Platform?.name); - fieldValues.add(activity.SymbolProfile.currency); - - if (!isUUID(activity.SymbolProfile.symbol)) { - fieldValues.add(activity.SymbolProfile.symbol); - } - - fieldValues.add(activity.type); - fieldValues.add(format(activity.date, 'yyyy')); - - return [...fieldValues].filter((item) => { - return item !== undefined; - }); - } - private getTotalFees() { let totalFees = new Big(0); diff --git a/libs/ui/src/lib/activities-table/activities-table.module.ts b/libs/ui/src/lib/activities-table/activities-table.module.ts index 56947d7e9..2d009fb4f 100644 --- a/libs/ui/src/lib/activities-table/activities-table.module.ts +++ b/libs/ui/src/lib/activities-table/activities-table.module.ts @@ -1,16 +1,13 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; -import { MatChipsModule } from '@angular/material/chips'; -import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { RouterModule } from '@angular/router'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; +import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -22,19 +19,16 @@ import { ActivitiesTableComponent } from './activities-table.component'; exports: [ActivitiesTableComponent], imports: [ CommonModule, + GfActivitiesFilterModule, GfNoTransactionsInfoModule, GfSymbolIconModule, GfSymbolModule, GfValueModule, - MatAutocompleteModule, MatButtonModule, - MatChipsModule, - MatInputModule, MatMenuModule, MatSortModule, MatTableModule, NgxSkeletonLoaderModule, - ReactiveFormsModule, RouterModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] From ea89ca5734f3c48a6e79346dc2ed6e2e83c40f0b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Apr 2022 09:35:01 +0200 Subject: [PATCH 285/337] Feature/simplify ids in database schema (#861) * Simplify ids in database schema * Access * Order * Subscription * Update changelog --- CHANGELOG.md | 5 +++ apps/api/src/app/access/access.controller.ts | 11 +++--- apps/api/src/app/order/order.controller.ts | 36 +++++++------------ .../migration.sql | 14 ++++++++ prisma/schema.prisma | 20 ++++------- 5 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 prisma/migrations/20220424064155_changed_various_ids_with_multiple_fields/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 43fb36495..ff5c940bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extracted the activities table filter to a dedicated component - Changed the url of the _Get Started_ link to `https://ghostfol.io` on the public page +- Simplified `@@id` using multiple fields with `@id` in the database schema of (`Access`, `Order`, `Subscription`) - Upgraded `prisma` from version `3.11.1` to `3.12.0` +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.140.2 - 22.04.2022 ### Added diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 405c53e68..a778d8b57 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -78,8 +78,12 @@ export class AccessController { @Delete(':id') @UseGuards(AuthGuard('jwt')) public async deleteAccess(@Param('id') id: string): Promise { + const access = await this.accessService.access({ id }); + if ( - !hasPermission(this.request.user.permissions, permissions.deleteAccess) + !hasPermission(this.request.user.permissions, permissions.deleteAccess) || + !access || + access.userId !== this.request.user.id ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -88,10 +92,7 @@ export class AccessController { } return this.accessService.deleteAccess({ - id_userId: { - id, - userId: this.request.user.id - } + id }); } } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 740676950..73fdd67f8 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -42,8 +42,12 @@ export class OrderController { @Delete(':id') @UseGuards(AuthGuard('jwt')) public async deleteOrder(@Param('id') id: string): Promise { + const order = await this.orderService.order({ id }); + if ( - !hasPermission(this.request.user.permissions, permissions.deleteOrder) + !hasPermission(this.request.user.permissions, permissions.deleteOrder) || + !order || + order.userId !== this.request.user.id ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -52,10 +56,7 @@ export class OrderController { } return this.orderService.deleteOrder({ - id_userId: { - id, - userId: this.request.user.id - } + id }); } @@ -135,23 +136,15 @@ export class OrderController { @UseGuards(AuthGuard('jwt')) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { - if ( - !hasPermission(this.request.user.permissions, permissions.updateOrder) - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - const originalOrder = await this.orderService.order({ - id_userId: { - id, - userId: this.request.user.id - } + id }); - if (!originalOrder) { + if ( + !hasPermission(this.request.user.permissions, permissions.updateOrder) || + !originalOrder || + originalOrder.userId !== this.request.user.id + ) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), StatusCodes.FORBIDDEN @@ -183,10 +176,7 @@ export class OrderController { User: { connect: { id: this.request.user.id } } }, where: { - id_userId: { - id, - userId: this.request.user.id - } + id } }); } diff --git a/prisma/migrations/20220424064155_changed_various_ids_with_multiple_fields/migration.sql b/prisma/migrations/20220424064155_changed_various_ids_with_multiple_fields/migration.sql new file mode 100644 index 000000000..19d8d862a --- /dev/null +++ b/prisma/migrations/20220424064155_changed_various_ids_with_multiple_fields/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Access" DROP CONSTRAINT "Access_pkey", +ADD CONSTRAINT "Access_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "MarketData" ADD CONSTRAINT "MarketData_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "Order" DROP CONSTRAINT "Order_pkey", +ADD CONSTRAINT "Order_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_pkey", +ADD CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7ffca1690..e220b4ebb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,12 +13,10 @@ model Access { createdAt DateTime @default(now()) GranteeUser User? @relation(fields: [granteeUserId], name: "accessGet", references: [id]) granteeUserId String? - id String @default(uuid()) + id String @id @default(uuid()) updatedAt DateTime @updatedAt User User @relation(fields: [userId], name: "accessGive", references: [id]) userId String - - @@id([id, userId]) } model Account { @@ -61,7 +59,7 @@ model MarketData { createdAt DateTime @default(now()) dataSource DataSource date DateTime - id String @default(uuid()) + id String @id @default(uuid()) symbol String marketPrice Float @@ -76,7 +74,7 @@ model Order { createdAt DateTime @default(now()) date DateTime fee Float - id String @default(uuid()) + id String @id @default(uuid()) isDraft Boolean @default(false) quantity Float SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) @@ -86,8 +84,6 @@ model Order { updatedAt DateTime @updatedAt User User @relation(fields: [userId], references: [id]) userId String - - @@id([id, userId]) } model Platform { @@ -138,20 +134,18 @@ model SymbolProfileOverrides { countries Json? name String? sectors Json? - SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) - symbolProfileId String @id - updatedAt DateTime @updatedAt + SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) + symbolProfileId String @id + updatedAt DateTime @updatedAt } model Subscription { createdAt DateTime @default(now()) expiresAt DateTime - id String @default(uuid()) + id String @id @default(uuid()) updatedAt DateTime @updatedAt User User @relation(fields: [userId], references: [id]) userId String - - @@id([id, userId]) } model User { From bad9d17c44217ae5631cd9a4ffdb7ccb3161cc7a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:23:03 +0200 Subject: [PATCH 286/337] Setup allocations page and endpoint (#859) * Setup tagging system * Update changelog --- CHANGELOG.md | 4 ++ apps/api/src/app/info/info.module.ts | 4 +- apps/api/src/app/info/info.service.ts | 7 ++- apps/api/src/app/order/order.service.ts | 14 ++++++ .../src/app/portfolio/portfolio.controller.ts | 12 +++-- .../src/app/portfolio/portfolio.service.ts | 13 ++++-- apps/api/src/app/user/user.controller.ts | 2 +- apps/api/src/app/user/user.module.ts | 4 +- apps/api/src/app/user/user.service.ts | 14 +++++- apps/api/src/services/tag/tag.module.ts | 11 +++++ apps/api/src/services/tag/tag.service.ts | 30 +++++++++++++ .../allocations/allocations-page.component.ts | 44 +++++++++++++------ .../allocations/allocations-page.html | 6 +++ .../allocations/allocations-page.module.ts | 2 + apps/client/src/app/services/data.service.ts | 10 ++++- .../src/lib/interfaces/info-item.interface.ts | 2 + .../src/lib/interfaces/user.interface.ts | 3 +- prisma/schema.prisma | 7 +++ 18 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 apps/api/src/services/tag/tag.module.ts create mode 100644 apps/api/src/services/tag/tag.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5c940bd..d795f9058 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 a tagging system for activities + ### Changed - Extracted the activities table filter to a dedicated component diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 5828ac963..338747ebc 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -26,7 +27,8 @@ import { InfoService } from './info.service'; PrismaModule, PropertyModule, RedisCacheModule, - SymbolProfileModule + SymbolProfileModule, + TagModule ], providers: [InfoService] }) diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 60df05c51..032b05f27 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DEMO_USER_ID, PROPERTY_IS_READ_ONLY_MODE, @@ -33,7 +34,8 @@ export class InfoService { private readonly jwtService: JwtService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, - private readonly redisCacheService: RedisCacheService + private readonly redisCacheService: RedisCacheService, + private readonly tagService: TagService ) {} public async get(): Promise { @@ -105,7 +107,8 @@ export class InfoService { demoAuthToken: this.getDemoAuthToken(), lastDataGathering: await this.getLastDataGathering(), statistics: await this.getStatistics(), - subscriptions: await this.getSubscriptions() + subscriptions: await this.getSubscriptions(), + tags: await this.tagService.get() }; } diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index ee01b3092..131b09b15 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -152,11 +152,13 @@ export class OrderService { public async getOrders({ includeDrafts = false, + tags, types, userCurrency, userId }: { includeDrafts?: boolean; + tags?: string[]; types?: TypeOfOrder[]; userCurrency: string; userId: string; @@ -167,6 +169,18 @@ export class OrderService { where.isDraft = false; } + if (tags?.length > 0) { + where.tags = { + some: { + OR: tags.map((tag) => { + return { + name: tag + }; + }) + } + }; + } + if (types) { where.OR = types.map((type) => { return { diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index ad06dbb52..9f9b20c56 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -105,7 +105,8 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers('impersonation-id') impersonationId: string, - @Query('range') range + @Query('range') range, + @Query('tags') tags?: string ): Promise { let hasError = false; @@ -113,7 +114,8 @@ export class PortfolioController { await this.portfolioService.getDetails( impersonationId, this.request.user.id, - range + range, + tags?.split(',') ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -159,7 +161,11 @@ export class PortfolioController { this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic'; - return { accounts, hasError, holdings: isBasicUser ? {} : holdings }; + return { + hasError, + accounts: tags ? {} : accounts, + holdings: isBasicUser ? {} : holdings + }; } @Get('investments') diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a7dc8adf7..86fc1398f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -303,7 +303,8 @@ export class PortfolioService { public async getDetails( aImpersonationId: string, aUserId: string, - aDateRange: DateRange = 'max' + aDateRange: DateRange = 'max', + tags?: string[] ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); const user = await this.userService.user({ id: userId }); @@ -318,6 +319,7 @@ export class PortfolioService { const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ + tags, userId }); @@ -441,8 +443,10 @@ export class PortfolioService { value: totalValue }); - for (const symbol of Object.keys(cashPositions)) { - holdings[symbol] = cashPositions[symbol]; + if (tags === undefined) { + for (const symbol of Object.keys(cashPositions)) { + holdings[symbol] = cashPositions[symbol]; + } } const accounts = await this.getValueOfAccounts( @@ -1178,9 +1182,11 @@ export class PortfolioService { private async getTransactionPoints({ includeDrafts = false, + tags, userId }: { includeDrafts?: boolean; + tags?: string[]; userId: string; }): Promise<{ transactionPoints: TransactionPoint[]; @@ -1191,6 +1197,7 @@ export class PortfolioService { const orders = await this.orderService.getOrders({ includeDrafts, + tags, userCurrency, userId, types: ['BUY', 'SELL'] diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 5bd14cfaa..e79f61dd4 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -34,7 +34,7 @@ import { UserService } from './user.service'; export class UserController { public constructor( private readonly configurationService: ConfigurationService, - private jwtService: JwtService, + private readonly jwtService: JwtService, private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index 976f5b6c3..6a705524f 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -2,6 +2,7 @@ import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscriptio import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -19,7 +20,8 @@ import { UserService } from './user.service'; }), PrismaModule, PropertyModule, - SubscriptionModule + SubscriptionModule, + TagModule ], providers: [UserService] }) diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index feed46434..0995ace2f 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -2,6 +2,7 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscripti import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { PROPERTY_IS_READ_ONLY_MODE, baseCurrency, @@ -13,7 +14,6 @@ import { hasRole, permissions } from '@ghostfolio/common/permissions'; -import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable } from '@nestjs/common'; import { Prisma, Role, User, ViewMode } from '@prisma/client'; @@ -30,7 +30,8 @@ export class UserService { private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, - private readonly subscriptionService: SubscriptionService + private readonly subscriptionService: SubscriptionService, + private readonly tagService: TagService ) {} public async getUser( @@ -51,12 +52,21 @@ export class UserService { orderBy: { User: { alias: 'asc' } }, where: { GranteeUser: { id } } }); + let tags = await this.tagService.getByUser(id); + + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + subscription.type === 'Basic' + ) { + tags = []; + } return { alias, id, permissions, subscription, + tags, access: access.map((accessItem) => { return { alias: accessItem.User.alias, diff --git a/apps/api/src/services/tag/tag.module.ts b/apps/api/src/services/tag/tag.module.ts new file mode 100644 index 000000000..32d905884 --- /dev/null +++ b/apps/api/src/services/tag/tag.module.ts @@ -0,0 +1,11 @@ +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; +import { Module } from '@nestjs/common'; + +import { TagService } from './tag.service'; + +@Module({ + exports: [TagService], + imports: [PrismaModule], + providers: [TagService] +}) +export class TagModule {} diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts new file mode 100644 index 000000000..534a6e73d --- /dev/null +++ b/apps/api/src/services/tag/tag.service.ts @@ -0,0 +1,30 @@ +import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TagService { + public constructor(private readonly prismaService: PrismaService) {} + + public async get() { + return this.prismaService.tag.findMany({ + orderBy: { + name: 'asc' + } + }); + } + + public async getByUser(userId: string) { + return this.prismaService.tag.findMany({ + orderBy: { + name: 'asc' + }, + where: { + orders: { + some: { + userId + } + } + } + }); + } +} 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 da5e07025..ed8d9e199 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 @@ -48,6 +48,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { { label: 'Initial', value: 'original' }, { label: 'Current', value: 'current' } ]; + public placeholder = ''; public portfolioDetails: PortfolioDetails; public positions: { [symbol: string]: Pick< @@ -73,6 +74,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; + public tags: string[] = []; public user: User; @@ -120,29 +122,22 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.hasImpersonationId = !!aId; }); - this.dataService - .fetchPortfolioDetails({}) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((portfolioDetails) => { - this.portfolioDetails = portfolioDetails; - - this.initializeAnalysisData(this.period); - - this.changeDetectorRef.markForCheck(); - }); - this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { this.user = state.user; + this.tags = this.user.tags.map((tag) => { + return tag.name; + }); + this.changeDetectorRef.markForCheck(); } }); } - public initializeAnalysisData(aPeriod: string) { + public initialize() { this.accounts = {}; this.continents = { [UNKNOWN_KEY]: { @@ -185,6 +180,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: 0 } }; + } + + public initializeAnalysisData(aPeriod: string) { + this.initialize(); for (const [id, { current, name, original }] of Object.entries( this.portfolioDetails.accounts @@ -305,7 +304,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } } - if (position.assetClass === AssetClass.EQUITY) { + if (position.dataSource) { this.symbols[prettifySymbol(symbol)] = { dataSource: position.dataSource, name: position.name, @@ -342,6 +341,25 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } } + public onUpdateFilters(tags: string[] = []) { + this.update(tags); + } + + public update(tags?: string[]) { + this.initialize(); + + this.dataService + .fetchPortfolioDetails({ tags }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((portfolioDetails) => { + this.portfolioDetails = portfolioDetails; + + this.initializeAnalysisData(this.period); + + this.changeDetectorRef.markForCheck(); + }); + } + 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 dac241b1d..eebf007d6 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -2,6 +2,12 @@

Allocations

+
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts index 62dd05e4e..fc8791775 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts @@ -4,6 +4,7 @@ import { MatCardModule } from '@angular/material/card'; import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module'; import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; +import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -16,6 +17,7 @@ import { AllocationsPageComponent } from './allocations-page.component'; imports: [ AllocationsPageRoutingModule, CommonModule, + GfActivitiesFilterModule, GfPortfolioProportionChartModule, GfPositionsTableModule, GfToggleModule, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index b51a00ce8..c59982824 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -182,9 +182,15 @@ export class DataService { ); } - public fetchPortfolioDetails(aParams: { [param: string]: any }) { + public fetchPortfolioDetails({ tags }: { tags?: string[] }) { + let params = new HttpParams(); + + if (tags?.length > 0) { + params = params.append('tags', tags.join(',')); + } + return this.http.get('/api/v1/portfolio/details', { - params: aParams + params }); } diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 443bb061a..f9d53b186 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,3 +1,4 @@ +import { Tag } from '@prisma/client'; import { Statistics } from './statistics.interface'; import { Subscription } from './subscription.interface'; @@ -13,4 +14,5 @@ export interface InfoItem { stripePublicKey?: string; subscriptions: Subscription[]; systemMessage?: string; + tags: Tag[]; } diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index 71288ccdb..a6bd3dab0 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -1,5 +1,5 @@ import { Access } from '@ghostfolio/api/app/user/interfaces/access.interface'; -import { Account } from '@prisma/client'; +import { Account, Tag } from '@prisma/client'; import { UserSettings } from './user-settings.interface'; @@ -14,4 +14,5 @@ export interface User { expiresAt?: Date; type: 'Basic' | 'Premium'; }; + tags: Tag[]; } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e220b4ebb..db2ee93c8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -79,6 +79,7 @@ model Order { quantity Float SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id]) symbolProfileId String + tags Tag[] type Type unitPrice Float updatedAt DateTime @updatedAt @@ -148,6 +149,12 @@ model Subscription { userId String } +model Tag { + id String @id @default(uuid()) + name String @unique + orders Order[] +} + model User { Access Access[] @relation("accessGet") AccessGive Access[] @relation(name: "accessGive") From 9a2ea0a4ed063644eb1d65942a3efc61b1a826e0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:24:21 +0200 Subject: [PATCH 287/337] Release 1.141.0 (#862) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d795f9058..aa685b4f7 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.141.0 - 24.04.2022 ### Added diff --git a/package.json b/package.json index bbcca09c8..7b6b45c13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.140.2", + "version": "1.141.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 8ff811ed28a55e68f993602971872f59e2324a09 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Apr 2022 17:27:00 +0200 Subject: [PATCH 288/337] Release/1.141.1 (#863) --- CHANGELOG.md | 10 +++++++ package.json | 2 +- .../migration.sql | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20220424152051_added_tags_to_order/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index aa685b4f7..494fb2233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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.141.1 - 24.04.2022 + +### Added + +- Added the database migration + +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.141.0 - 24.04.2022 ### Added diff --git a/package.json b/package.json index 7b6b45c13..2b7fba001 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.141.0", + "version": "1.141.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { diff --git a/prisma/migrations/20220424152051_added_tags_to_order/migration.sql b/prisma/migrations/20220424152051_added_tags_to_order/migration.sql new file mode 100644 index 000000000..2df084652 --- /dev/null +++ b/prisma/migrations/20220424152051_added_tags_to_order/migration.sql @@ -0,0 +1,28 @@ +-- CreateTable +CREATE TABLE "Tag" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_OrderToTag" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "_OrderToTag_AB_unique" ON "_OrderToTag"("A", "B"); + +-- CreateIndex +CREATE INDEX "_OrderToTag_B_index" ON "_OrderToTag"("B"); + +-- AddForeignKey +ALTER TABLE "_OrderToTag" ADD FOREIGN KEY ("A") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_OrderToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; From c61a415fb2c40e96622aae81be84197ab191e6e2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 25 Apr 2022 18:12:42 +0200 Subject: [PATCH 289/337] Bugfix/change date to utc in data gathering service (#867) * Change date to UTC * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/services/data-gathering.service.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494fb2233..fec9b353c 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 + +- Changed the date to UTC in the data gathering service + ## 1.141.1 - 24.04.2022 ### Added diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index c3a7f64c7..d878b014b 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -377,7 +377,14 @@ export class DataGatheringService { data: { dataSource, symbol, - date: currentDate, + date: new Date( + Date.UTC( + getYear(currentDate), + getMonth(currentDate), + getDate(currentDate), + 0 + ) + ), marketPrice: lastMarketPrice } }); From b7bbc029ac61e14ac5a1eda5568cbe942d98fd5b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 25 Apr 2022 22:37:34 +0200 Subject: [PATCH 290/337] Feature/render tags in dialogs (#864) * Render tags * Update changelog --- CHANGELOG.md | 5 +++ apps/api/src/app/order/order.service.ts | 3 +- .../portfolio-position-detail.interface.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 18 ++++++++- .../position-detail-dialog.component.ts | 5 ++- .../position-detail-dialog.html | 40 ++++++++++++------- .../position-detail-dialog.module.ts | 2 + ...-or-update-transaction-dialog.component.ts | 1 + .../create-or-update-transaction-dialog.html | 12 ++++++ ...ate-or-update-transaction-dialog.module.ts | 2 + .../src/lib/types/order-with-account.type.ts | 3 +- 11 files changed, 73 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec9b353c..4960394cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the tags to the create or edit transaction dialog +- Added the tags to the position detail dialog + ### Changed - Changed the date to UTC in the data gathering service diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 131b09b15..981fc4108 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -202,7 +202,8 @@ export class OrderService { } }, // eslint-disable-next-line @typescript-eslint/naming-convention - SymbolProfile: true + SymbolProfile: true, + tags: true }, orderBy: { date: 'asc' } }) diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 99c7c6911..5f2425d7a 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,5 +1,6 @@ import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { OrderWithAccount } from '@ghostfolio/common/types'; +import { Tag } from '@prisma/client'; export interface PortfolioPositionDetail { averagePrice: number; @@ -16,6 +17,7 @@ export interface PortfolioPositionDetail { orders: OrderWithAccount[]; quantity: number; SymbolProfile: EnhancedSymbolProfile; + tags: Tag[]; transactionCount: number; value: number; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 86fc1398f..ecc0ea20e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -46,7 +46,12 @@ import type { } from '@ghostfolio/common/types'; import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client'; +import { + AssetClass, + DataSource, + Tag, + Type as TypeOfOrder +} from '@prisma/client'; import Big from 'big.js'; import { differenceInDays, @@ -62,7 +67,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty, sortBy } from 'lodash'; +import { isEmpty, sortBy, uniqBy } from 'lodash'; import { HistoricalDataContainer, @@ -476,8 +481,11 @@ export class PortfolioService { ); }); + let tags: Tag[] = []; + if (orders.length <= 0) { return { + tags, averagePrice: undefined, firstBuyDate: undefined, grossPerformance: undefined, @@ -504,6 +512,8 @@ export class PortfolioService { const portfolioOrders: PortfolioOrder[] = orders .filter((order) => { + tags = tags.concat(order.tags); + return order.type === 'BUY' || order.type === 'SELL'; }) .map((order) => ({ @@ -518,6 +528,8 @@ export class PortfolioService { unitPrice: new Big(order.unitPrice) })); + tags = uniqBy(tags, 'id'); + const portfolioCalculator = new PortfolioCalculator({ currency: positionCurrency, currentRateService: this.currentRateService, @@ -626,6 +638,7 @@ export class PortfolioService { netPerformance, orders, SymbolProfile, + tags, transactionCount, averagePrice: averagePrice.toNumber(), grossPerformancePercent: @@ -682,6 +695,7 @@ export class PortfolioService { minPrice, orders, SymbolProfile, + tags, averagePrice: 0, firstBuyDate: undefined, grossPerformance: undefined, diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 55efc0249..3baa8c0e5 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -11,7 +11,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; -import { SymbolProfile } from '@prisma/client'; +import { SymbolProfile, Tag } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -48,6 +48,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { [name: string]: { name: string; value: number }; }; public SymbolProfile: SymbolProfile; + public tags: Tag[]; public transactionCount: number; public value: number; @@ -83,6 +84,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { orders, quantity, SymbolProfile, + tags, transactionCount, value }) => { @@ -115,6 +117,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.quantity = quantity; this.sectors = {}; this.SymbolProfile = SymbolProfile; + this.tags = tags; this.transactionCount = transactionCount; this.value = value; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index c832c07b1..a5b946e04 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -194,21 +194,31 @@
- +
+
Activities
+ +
+ +
+
Tags
+ + {{ tag.name }} + +
+
+ + Tags + + + {{ tag.name }} + + + +
Date: Mon, 25 Apr 2022 22:37:56 +0200 Subject: [PATCH 291/337] Add orderBy statement to make debugging easier (#868) --- apps/api/src/services/data-gathering.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index d878b014b..b64afbde4 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -544,6 +544,7 @@ export class DataGatheringService { await this.prismaService.marketData.groupBy({ _count: true, by: ['symbol'], + orderBy: [{ symbol: 'asc' }], where: { date: { gt: startDate } } From 899fa0370e85dcb04415d15cddc1c68f9967a854 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 25 Apr 2022 22:39:08 +0200 Subject: [PATCH 292/337] Feature/improve users table of admin control panel (#866) * Improve users table * Update changelog --- CHANGELOG.md | 1 + .../admin-users/admin-users.component.ts | 17 ++++++++++--- .../components/admin-users/admin-users.html | 25 ++++++++++++++----- .../admin-users/admin-users.module.ts | 3 ++- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4960394cc..1ef179de7 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 ### Changed - Changed the date to UTC in the data gathering service +- Reused the value component in the users table of the admin control panel ## 1.141.1 - 24.04.2022 diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 5a5a96a70..a27af3a3a 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { AdminData } from '@ghostfolio/common/interfaces'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { AdminData, User } from '@ghostfolio/common/interfaces'; import { differenceInSeconds, formatDistanceToNowStrict, @@ -15,6 +16,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-users.html' }) export class AdminUsersComponent implements OnDestroy, OnInit { + public user: User; public users: AdminData['users']; private unsubscribeSubject = new Subject(); @@ -24,8 +26,17 @@ export class AdminUsersComponent implements OnDestroy, OnInit { */ public constructor( private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService - ) {} + private dataService: DataService, + private userService: UserService + ) { + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + } + }); + } /** * Initializes the controller 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 8982e2291..fab2e3010 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -45,14 +45,27 @@
- - -
{{ formatDistanceToNow(userItem.createdAt) }} - {{ userItem.accountCount }} + + - {{ userItem.transactionCount }} + + - {{ userItem.engagement | number: '1.0-0' }} + + {{ formatDistanceToNow(userItem.lastActivity) }} diff --git a/apps/client/src/app/components/admin-users/admin-users.module.ts b/apps/client/src/app/components/admin-users/admin-users.module.ts index c59c632e4..44b9696a3 100644 --- a/apps/client/src/app/components/admin-users/admin-users.module.ts +++ b/apps/client/src/app/components/admin-users/admin-users.module.ts @@ -2,13 +2,14 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { AdminUsersComponent } from './admin-users.component'; @NgModule({ declarations: [AdminUsersComponent], exports: [], - imports: [CommonModule, MatButtonModule, MatMenuModule], + imports: [CommonModule, GfValueModule, MatButtonModule, MatMenuModule], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfAdminUsersModule {} From b4bc72c6f9030acd8830117e930b6a2f757fa87d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 25 Apr 2022 22:41:02 +0200 Subject: [PATCH 293/337] Release 1.142.0 (#869) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef179de7..9af9d9490 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.142.0 - 25.04.2022 ### Added diff --git a/package.json b/package.json index 2b7fba001..e9b44e268 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.141.1", + "version": "1.142.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From b6cd007ad431676a4c86e9abb5283130f231c772 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 26 Apr 2022 22:31:53 +0200 Subject: [PATCH 294/337] Release/1.143.0 (#871) * Release 1.143.0 * Improve filtering by tags --- CHANGELOG.md | 6 +++ .../allocations/allocations-page.component.ts | 43 ++++++++++--------- .../allocations/allocations-page.html | 3 +- .../activities-filter.component.html | 5 +++ .../activities-filter.component.scss | 8 ++++ .../activities-filter.component.ts | 9 ++-- .../activities-filter.module.ts | 2 + .../portfolio-proportion-chart.component.ts | 8 +--- package.json | 2 +- 9 files changed, 55 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af9d9490..11e0c65d9 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.143.0 - 26.04.2022 + +### Changed + +- Improved the filtering by tags + ## 1.142.0 - 25.04.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 ed8d9e199..b8842cea0 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 @@ -17,7 +17,7 @@ import { Market, ToggleOption } from '@ghostfolio/common/types'; import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; @Component({ host: { class: 'page' }, @@ -39,7 +39,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; + public filters$ = new Subject(); public hasImpersonationId: boolean; + public isLoading = false; public markets: { [key in Market]: { name: string; value: number }; }; @@ -122,6 +124,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.hasImpersonationId = !!aId; }); + this.filters$ + .pipe( + distinctUntilChanged(), + switchMap((tags) => { + this.isLoading = true; + + return this.dataService.fetchPortfolioDetails({ tags }); + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe((portfolioDetails) => { + this.portfolioDetails = portfolioDetails; + + this.initializeAnalysisData(this.period); + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -341,25 +363,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } } - public onUpdateFilters(tags: string[] = []) { - this.update(tags); - } - - public update(tags?: string[]) { - this.initialize(); - - this.dataService - .fetchPortfolioDetails({ tags }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((portfolioDetails) => { - this.portfolioDetails = portfolioDetails; - - this.initializeAnalysisData(this.period); - - this.changeDetectorRef.markForCheck(); - }); - } - 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 eebf007d6..74b9330e2 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -4,9 +4,10 @@

Allocations

diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html index ff39a6750..98c40d4b8 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -30,4 +30,9 @@ {{ filter | gfSymbol }} + diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.scss b/libs/ui/src/lib/activities-filter/activities-filter.component.scss index f40444914..363005aab 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.scss +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.scss @@ -7,6 +7,10 @@ .mat-form-field-infix { border-top: 0 solid transparent !important; } + + .mat-spinner circle { + stroke: rgba(var(--dark-dividers)); + } } .mat-chip { @@ -19,4 +23,8 @@ .mat-form-field { color: rgba(var(--light-primary-text)); } + + .mat-spinner circle { + stroke: rgba(var(--light-dividers)); + } } diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index 6e8eea64f..72d083c90 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -8,6 +8,7 @@ import { OnChanges, OnDestroy, Output, + SimpleChanges, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; @@ -27,6 +28,7 @@ import { takeUntil } from 'rxjs/operators'; }) export class ActivitiesFilterComponent implements OnChanges, OnDestroy { @Input() allFilters: string[]; + @Input() isLoading: boolean; @Input() placeholder: string; @Output() valueChanged = new EventEmitter(); @@ -59,8 +61,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { }); } - public ngOnChanges() { - if (this.allFilters) { + public ngOnChanges(changes: SimpleChanges) { + if (changes.allFilters?.currentValue) { this.updateFilter(); } } @@ -103,6 +105,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { private updateFilter() { this.filters$.next(this.allFilters); - this.valueChanged.emit(this.searchKeywords); + // Emit an array with a new reference + this.valueChanged.emit([...this.searchKeywords]); } } diff --git a/libs/ui/src/lib/activities-filter/activities-filter.module.ts b/libs/ui/src/lib/activities-filter/activities-filter.module.ts index a192296fd..66b8800da 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.module.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.module.ts @@ -4,6 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatChipsModule } from '@angular/material/chips'; import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { ActivitiesFilterComponent } from './activities-filter.component'; @@ -17,6 +18,7 @@ import { ActivitiesFilterComponent } from './activities-filter.component'; MatAutocompleteModule, MatChipsModule, MatInputModule, + MatProgressSpinnerModule, ReactiveFormsModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] 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 207c9b3ca..a81e585cd 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 @@ -192,13 +192,8 @@ export class PortfolioProportionChartComponent // Reuse color item.color = this.colorMap[symbol]; } else { - const color = + item.color = this.getColorPalette()[index % this.getColorPalette().length]; - - // Store color for reuse - this.colorMap[symbol] = color; - - item.color = color; } }); @@ -265,6 +260,7 @@ export class PortfolioProportionChartComponent this.chart = new Chart(this.chartCanvas.nativeElement, { data, options: { + animation: false, cutout: '70%', layout: { padding: this.showLabels === true ? 100 : 0 diff --git a/package.json b/package.json index e9b44e268..9749f12d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.142.0", + "version": "1.143.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1f0bd5a7db2e976da492ac7f1d7bbef0053ff154 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:30:57 +0200 Subject: [PATCH 295/337] Bugfix/fix color of spinner in filter component (#873) * Fix color for dark mode * Update changelog --- CHANGELOG.md | 6 ++++++ .../activities-filter.component.scss | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e0c65d9..922ec6848 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 + +- Fixed the color of the spinner in the activities filter component (dark mode) + ## 1.143.0 - 26.04.2022 ### Changed diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.scss b/libs/ui/src/lib/activities-filter/activities-filter.component.scss index 363005aab..d859ac16d 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.scss +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.scss @@ -8,8 +8,10 @@ border-top: 0 solid transparent !important; } - .mat-spinner circle { - stroke: rgba(var(--dark-dividers)); + .mat-spinner { + circle { + stroke: rgba(var(--dark-dividers)); + } } } @@ -24,7 +26,11 @@ color: rgba(var(--light-primary-text)); } - .mat-spinner circle { - stroke: rgba(var(--light-dividers)); + ::ng-deep { + .mat-spinner { + circle { + stroke: rgba(var(--light-dividers)); + } + } } } From 4dc76817ce8fd86eb741efb8a105dd745d87d1c7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 29 Apr 2022 13:10:45 +0200 Subject: [PATCH 296/337] Bugfix/fix import validation for numbers equal zero (#875) * Fix import validation for numbers equal 0 * Update changelog --- CHANGELOG.md | 1 + .../app/services/import-transactions.service.ts | 8 ++++---- test/import/ok.csv | 1 + test/import/ok.json | 17 +++++++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 922ec6848..039c54432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed the import validation for numbers equal 0 - Fixed the color of the spinner in the activities filter component (dark mode) ## 1.143.0 - 26.04.2022 diff --git a/apps/client/src/app/services/import-transactions.service.ts b/apps/client/src/app/services/import-transactions.service.ts index 8557fe0af..1d5670a32 100644 --- a/apps/client/src/app/services/import-transactions.service.ts +++ b/apps/client/src/app/services/import-transactions.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Account, DataSource, Type } from '@prisma/client'; import { parse } from 'date-fns'; -import { isNumber } from 'lodash'; +import { isFinite } from 'lodash'; import { parse as csvToJson } from 'papaparse'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -185,7 +185,7 @@ export class ImportTransactionsService { item = this.lowercaseKeys(item); for (const key of ImportTransactionsService.FEE_KEYS) { - if ((item[key] || item[key] === 0) && isNumber(item[key])) { + if (isFinite(item[key])) { return item[key]; } } @@ -208,7 +208,7 @@ export class ImportTransactionsService { item = this.lowercaseKeys(item); for (const key of ImportTransactionsService.QUANTITY_KEYS) { - if (item[key] && isNumber(item[key])) { + if (isFinite(item[key])) { return item[key]; } } @@ -288,7 +288,7 @@ export class ImportTransactionsService { item = this.lowercaseKeys(item); for (const key of ImportTransactionsService.UNIT_PRICE_KEYS) { - if (item[key] && isNumber(item[key])) { + if (isFinite(item[key])) { return item[key]; } } diff --git a/test/import/ok.csv b/test/import/ok.csv index ddefbc93f..2d27422ab 100644 --- a/test/import/ok.csv +++ b/test/import/ok.csv @@ -2,3 +2,4 @@ Date,Code,Currency,Price,Quantity,Action,Fee 17/11/2021,MSFT,USD,0.62,5,dividend,0.00 16/09/2021,MSFT,USD,298.580,5,buy,19.00 01/01/2022,Penthouse Apartment,USD,500000.0,1,item,0.00 +06/06/2050,MSFT,USD,0.00,0,buy,0.00 diff --git a/test/import/ok.json b/test/import/ok.json index ae08aee73..63961be74 100644 --- a/test/import/ok.json +++ b/test/import/ok.json @@ -5,34 +5,43 @@ }, "activities": [ { - "accountId": null, - "date": "2021-12-31T23:00:00.000Z", + "fee": 0, + "quantity": 0, + "type": "BUY", + "unitPrice": 0, + "currency": "USD", + "dataSource": "YAHOO", + "date": "2050-06-05T22:00:00.000Z", + "symbol": "MSFT" + }, + { "fee": 0, "quantity": 1, "type": "ITEM", "unitPrice": 500000, "currency": "USD", "dataSource": "MANUAL", + "date": "2021-12-31T22:00:00.000Z", "symbol": "Penthouse Apartment" }, { - "date": "2021-11-16T23:00:00.000Z", "fee": 0, "quantity": 5, "type": "DIVIDEND", "unitPrice": 0.62, "currency": "USD", "dataSource": "YAHOO", + "date": "2021-11-16T22:00:00.000Z", "symbol": "MSFT" }, { - "date": "2021-09-15T22:00:00.000Z", "fee": 19, "quantity": 5, "type": "BUY", "unitPrice": 298.58, "currency": "USD", "dataSource": "YAHOO", + "date": "2021-09-15T22:00:00.000Z", "symbol": "MSFT" } ] From 04044f8720ddae9165a203b48ad8ca295d3fec44 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 09:55:24 +0200 Subject: [PATCH 297/337] Support futures (#845) * Support futures * Upgrade yahoo-finance2 to version 2.3.2 * Update changelog --- CHANGELOG.md | 8 +++ .../yahoo-finance/yahoo-finance.service.ts | 51 +++++++++++++++---- package.json | 2 +- yarn.lock | 8 +-- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 039c54432..4ed0c80a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for commodities (via futures) + +### Changed + +- Upgraded `yahoo-finance2` from version `2.3.1` to `2.3.2` + ### Fixed - Fixed the import validation for numbers equal 0 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 b5d8e1ebd..3d07c833b 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 @@ -20,10 +20,7 @@ import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; import yahooFinance from 'yahoo-finance2'; -import type { - Price, - QuoteSummaryResult -} from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; +import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -92,7 +89,12 @@ export class YahooFinanceService implements DataProviderInterface { response.assetSubClass = assetSubClass; response.currency = assetProfile.price.currency; response.dataSource = this.getName(); - response.name = this.formatName(assetProfile); + response.name = this.formatName({ + longName: assetProfile.price.longName, + quoteType: assetProfile.price.quoteType, + shortName: assetProfile.price.shortName, + symbol: assetProfile.price.symbol + }); response.symbol = aSymbol; if ( @@ -247,7 +249,7 @@ export class YahooFinanceService implements DataProviderInterface { const quotes = searchResult.quotes .filter((quote) => { - // filter out undefined symbols + // Filter out undefined symbols return quote.symbol; }) .filter(({ quoteType, symbol }) => { @@ -256,7 +258,7 @@ export class YahooFinanceService implements DataProviderInterface { this.cryptocurrencyService.isCryptocurrency( symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency) )) || - ['EQUITY', 'ETF', 'MUTUALFUND'].includes(quoteType) + ['EQUITY', 'ETF', 'FUTURE', 'MUTUALFUND'].includes(quoteType) ); }) .filter(({ quoteType, symbol }) => { @@ -264,6 +266,9 @@ export class YahooFinanceService implements DataProviderInterface { // Only allow cryptocurrencies in base currency to avoid having redundancy in the database. // Transactions need to be converted manually to the base currency before return symbol.includes(baseCurrency); + } else if (quoteType === 'FUTURE') { + // Allow GC=F, but not MGC=F + return symbol.length === 4; } return true; @@ -288,7 +293,12 @@ export class YahooFinanceService implements DataProviderInterface { symbol, currency: marketDataItem.currency, dataSource: this.getName(), - name: quote?.longname || quote?.shortname || symbol + name: this.formatName({ + longName: quote.longname, + quoteType: quote.quoteType, + shortName: quote.shortname, + symbol: quote.symbol + }) }); } } catch (error) { @@ -298,8 +308,18 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private formatName(aAssetProfile: QuoteSummaryResult) { - let name = aAssetProfile.price.longName; + private formatName({ + longName, + quoteType, + shortName, + symbol + }: { + longName: Price['longName']; + quoteType: Price['quoteType']; + shortName: Price['shortName']; + symbol: Price['symbol']; + }) { + let name = longName; if (name) { name = name.replace('iShares ETF (CH) - ', ''); @@ -314,7 +334,12 @@ export class YahooFinanceService implements DataProviderInterface { name = name.replace('Xtrackers (IE) Plc - ', ''); } - return name || aAssetProfile.price.shortName || aAssetProfile.price.symbol; + if (quoteType === 'FUTURE') { + // "Gold Jun 22" -> "Gold" + name = shortName?.slice(0, -6); + } + + return name || shortName || symbol; } private parseAssetClass(aPrice: Price): { @@ -337,6 +362,10 @@ export class YahooFinanceService implements DataProviderInterface { assetClass = AssetClass.EQUITY; assetSubClass = AssetSubClass.ETF; break; + case 'future': + assetClass = AssetClass.COMMODITY; + assetSubClass = AssetSubClass.COMMODITY; + break; case 'mutualfund': assetClass = AssetClass.EQUITY; assetSubClass = AssetSubClass.MUTUALFUND; diff --git a/package.json b/package.json index 9749f12d9..27b5210ac 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", - "yahoo-finance2": "2.3.1", + "yahoo-finance2": "2.3.2", "zone.js": "0.11.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 250fbd811..de5b59b06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18836,10 +18836,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.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.1.tgz#d2cffbef78f6974e4e6a40487cc08ab133dc9fc5" - integrity sha512-QTXiiWgfrpVbSylchBgLqESZz+8+SyyDSqntjfZHxMIHa6d14xq+biNNDIeYd5SylcZ9Vt4zLmZXHN7EdLM1pA== +yahoo-finance2@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.2.tgz#3643b5e14f752b1d5546427d760d515401ac2ac7" + integrity sha512-xOBaamD/mXN9ruc3TBOEhEIhP/N+efWo/wQf2PyYtB6N68iSGZVDt+4vdJN7ra+iCVq7FnSHTDH3K4Cvqy63lQ== dependencies: ajv "8.10.0" ajv-formats "2.1.1" From c2a1cbd20ffd4b805f3ce00e719f8948183df66f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 10:48:02 +0200 Subject: [PATCH 298/337] Feature/improve layout of position detail dialog (#877) * Improve layout * Update changelog --- CHANGELOG.md | 1 + .../position-detail-dialog.html | 55 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ed0c80a7..3c10402f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the layout of the position detail dialog - Upgraded `yahoo-finance2` from version `2.3.1` to `2.3.2` ### Fixed diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index a5b946e04..169e47390 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -168,7 +168,7 @@
-
Sectors
+
Sectors
-
Countries
+
Countries
- -
-
Activities
- -
+
+
+
Activities
+ +
+
-
-
Tags
- - {{ tag.name }} - +
+
+
Tags
+ + {{ tag.name }} + +
+
From 751256f15880d7498d695129bfec26cb890e67c5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 11:49:58 +0200 Subject: [PATCH 299/337] Feature/add support for real estate and precious metal (#878) * Add support for real estate and precious metal * Update changelog --- CHANGELOG.md | 5 +++++ .../yahoo-finance/yahoo-finance.service.ts | 10 ++++++++++ .../migration.sql | 2 ++ .../migration.sql | 2 ++ prisma/schema.prisma | 2 ++ 5 files changed, 21 insertions(+) create mode 100644 prisma/migrations/20220430083454_added_real_estate_to_asset_class/migration.sql create mode 100644 prisma/migrations/20220430083631_added_precious_metal_to_asset_sub_class/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c10402f1..b829416ac 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 ### Added - Added support for commodities (via futures) +- Added support for real estate ### Changed @@ -21,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the import validation for numbers equal 0 - Fixed the color of the spinner in the activities filter component (dark mode) +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.143.0 - 26.04.2022 ### Changed 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 3d07c833b..0f0683ac8 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 @@ -365,6 +365,16 @@ export class YahooFinanceService implements DataProviderInterface { case 'future': assetClass = AssetClass.COMMODITY; assetSubClass = AssetSubClass.COMMODITY; + + if ( + aPrice?.shortName?.toLowerCase()?.startsWith('gold') || + aPrice?.shortName?.toLowerCase()?.startsWith('palladium') || + aPrice?.shortName?.toLowerCase()?.startsWith('platinum') || + aPrice?.shortName?.toLowerCase()?.startsWith('silver') + ) { + assetSubClass = AssetSubClass.PRECIOUS_METAL; + } + break; case 'mutualfund': assetClass = AssetClass.EQUITY; diff --git a/prisma/migrations/20220430083454_added_real_estate_to_asset_class/migration.sql b/prisma/migrations/20220430083454_added_real_estate_to_asset_class/migration.sql new file mode 100644 index 000000000..32707aff7 --- /dev/null +++ b/prisma/migrations/20220430083454_added_real_estate_to_asset_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetClass" ADD VALUE 'REAL_ESTATE'; diff --git a/prisma/migrations/20220430083631_added_precious_metal_to_asset_sub_class/migration.sql b/prisma/migrations/20220430083631_added_precious_metal_to_asset_sub_class/migration.sql new file mode 100644 index 000000000..149f87b72 --- /dev/null +++ b/prisma/migrations/20220430083631_added_precious_metal_to_asset_sub_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetSubClass" ADD VALUE 'PRECIOUS_METAL'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index db2ee93c8..a267ab811 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -185,6 +185,7 @@ enum AssetClass { COMMODITY EQUITY FIXED_INCOME + REAL_ESTATE } enum AssetSubClass { @@ -193,6 +194,7 @@ enum AssetSubClass { CRYPTOCURRENCY ETF MUTUALFUND + PRECIOUS_METAL STOCK } From e9e9f1a1243a04b6a31b5c374cabd09735886f7d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 11:51:49 +0200 Subject: [PATCH 300/337] Release 1.144.0 (#879) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b829416ac..3167941a7 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.144.0 - 30.04.2022 ### Added diff --git a/package.json b/package.json index 27b5210ac..f13c26f52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.143.0", + "version": "1.144.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 01103f3db4d254350e8696b8c5ea25fa8a129d0c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 21:47:10 +0200 Subject: [PATCH 301/337] Feature/add asset and asset sub class to wealth items form (#880) * Add asset and asset sub class * Update changelog --- CHANGELOG.md | 6 ++++ apps/api/src/app/order/create-order.dto.ts | 14 +++++++-- apps/api/src/app/order/order.controller.ts | 5 ++++ apps/api/src/app/order/order.service.ts | 27 ++++++++++++++--- apps/api/src/app/order/update-order.dto.ts | 20 +++++++++++-- ...-or-update-transaction-dialog.component.ts | 9 ++++-- .../create-or-update-transaction-dialog.html | 30 +++++++++++++++++++ 7 files changed, 99 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3167941a7..3906b0f31 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 + +- Extended the form to set the asset and asset sub class for (wealth) items + ## 1.144.0 - 30.04.2022 ### Added diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index b826166b0..3211fc4ed 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -1,4 +1,4 @@ -import { DataSource, Type } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; import { IsEnum, IsISO8601, @@ -10,14 +10,22 @@ import { export class CreateOrderDto { @IsString() @IsOptional() - accountId: string; + accountId?: string; + + @IsEnum(AssetClass, { each: true }) + @IsOptional() + assetClass?: AssetClass; + + @IsEnum(AssetSubClass, { each: true }) + @IsOptional() + assetSubClass?: AssetSubClass; @IsString() currency: string; @IsEnum(DataSource, { each: true }) @IsOptional() - dataSource: DataSource; + dataSource?: DataSource; @IsISO8601() date: string; diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 73fdd67f8..73c546d83 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -171,6 +171,11 @@ export class OrderController { dataSource: data.dataSource, symbol: data.symbol } + }, + update: { + assetClass: data.assetClass, + assetSubClass: data.assetSubClass, + name: data.symbol } }, User: { connect: { id: this.request.user.id } } diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 981fc4108..762b414ba 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -6,7 +6,14 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client'; +import { + AssetClass, + AssetSubClass, + DataSource, + Order, + Prisma, + Type as TypeOfOrder +} from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; import { v4 as uuidv4 } from 'uuid'; @@ -55,6 +62,8 @@ export class OrderService { public async createOrder( data: Prisma.OrderCreateInput & { accountId?: string; + assetClass?: AssetClass; + assetSubClass?: AssetSubClass; currency?: string; dataSource?: DataSource; symbol?: string; @@ -77,6 +86,8 @@ export class OrderService { }; if (data.type === 'ITEM') { + const assetClass = data.assetClass; + const assetSubClass = data.assetSubClass; const currency = data.SymbolProfile.connectOrCreate.create.currency; const dataSource: DataSource = 'MANUAL'; const id = uuidv4(); @@ -84,6 +95,8 @@ export class OrderService { Account = undefined; data.id = id; + data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; + data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass; data.SymbolProfile.connectOrCreate.create.currency = currency; data.SymbolProfile.connectOrCreate.create.dataSource = dataSource; data.SymbolProfile.connectOrCreate.create.name = name; @@ -120,6 +133,8 @@ export class OrderService { await this.cacheService.flush(); delete data.accountId; + delete data.assetClass; + delete data.assetSubClass; delete data.currency; delete data.dataSource; delete data.symbol; @@ -232,6 +247,8 @@ export class OrderService { where }: { data: Prisma.OrderUpdateInput & { + assetClass?: AssetClass; + assetSubClass?: AssetSubClass; currency?: string; dataSource?: DataSource; symbol?: string; @@ -245,10 +262,10 @@ export class OrderService { let isDraft = false; if (data.type === 'ITEM') { - const name = data.SymbolProfile.connect.dataSource_symbol.symbol; - - data.SymbolProfile = { update: { name } }; + delete data.SymbolProfile.connect; } else { + delete data.SymbolProfile.update; + isDraft = isAfter(data.date as Date, endOfToday()); if (!isDraft) { @@ -265,6 +282,8 @@ export class OrderService { await this.cacheService.flush(); + delete data.assetClass; + delete data.assetSubClass; delete data.currency; delete data.dataSource; delete data.symbol; diff --git a/apps/api/src/app/order/update-order.dto.ts b/apps/api/src/app/order/update-order.dto.ts index 180da6dbf..0ad46180d 100644 --- a/apps/api/src/app/order/update-order.dto.ts +++ b/apps/api/src/app/order/update-order.dto.ts @@ -1,10 +1,24 @@ -import { DataSource, Type } from '@prisma/client'; -import { IsISO8601, IsNumber, IsOptional, IsString } from 'class-validator'; +import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; +import { + IsEnum, + IsISO8601, + IsNumber, + IsOptional, + IsString +} from 'class-validator'; export class UpdateOrderDto { @IsOptional() @IsString() - accountId: string; + accountId?: string; + + @IsEnum(AssetClass, { each: true }) + @IsOptional() + assetClass?: AssetClass; + + @IsEnum(AssetSubClass, { each: true }) + @IsOptional() + assetSubClass?: AssetSubClass; @IsString() currency: string; 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 07f7bdf81..b4794a043 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 @@ -13,7 +13,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { Type } from '@prisma/client'; +import { AssetClass, AssetSubClass, Type } from '@prisma/client'; import { isUUID } from 'class-validator'; import { isString } from 'lodash'; import { EMPTY, Observable, Subject } from 'rxjs'; @@ -39,7 +39,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { @ViewChild('autocomplete') autocomplete; public activityForm: FormGroup; - + public assetClasses = Object.keys(AssetClass); + public assetSubClasses = Object.keys(AssetSubClass); public currencies: string[] = []; public currentMarketPrice = null; public filteredLookupItems: LookupItem[]; @@ -67,6 +68,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { this.activityForm = this.formBuilder.group({ accountId: [this.data.activity?.accountId, Validators.required], + assetClass: [this.data.activity?.SymbolProfile?.assetClass], + assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], currency: [ this.data.activity?.SymbolProfile?.currency, Validators.required @@ -234,6 +237,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy { public onSubmit() { const activity: CreateOrderDto | UpdateOrderDto = { accountId: this.activityForm.controls['accountId'].value, + assetClass: this.activityForm.controls['assetClass'].value, + assetSubClass: this.activityForm.controls['assetSubClass'].value, currency: this.activityForm.controls['currency'].value, date: this.activityForm.controls['date'].value, dataSource: this.activityForm.controls['dataSource'].value, diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html index 19181009f..b57068453 100644 --- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html +++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -134,6 +134,36 @@ > +
+ + Asset Class + + + {{ assetClass }} + + +
+
+ + Asset Sub-Class + + + {{ assetSubClass }} + + +
From f6f62db830ec13b8fb74fe77e4d1b7c35d343ee3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 30 Apr 2022 22:16:13 +0200 Subject: [PATCH 302/337] Feature/add support for private equity (#881) * Add support for private equity * Update changelog --- CHANGELOG.md | 5 +++++ .../migration.sql | 2 ++ prisma/schema.prisma | 1 + 3 files changed, 8 insertions(+) create mode 100644 prisma/migrations/20220430193522_added_private_equity_to_asset_sub_class/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3906b0f31..a09b7cad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for private equity - Extended the form to set the asset and asset sub class for (wealth) items +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.144.0 - 30.04.2022 ### Added diff --git a/prisma/migrations/20220430193522_added_private_equity_to_asset_sub_class/migration.sql b/prisma/migrations/20220430193522_added_private_equity_to_asset_sub_class/migration.sql new file mode 100644 index 000000000..6578247d8 --- /dev/null +++ b/prisma/migrations/20220430193522_added_private_equity_to_asset_sub_class/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AssetSubClass" ADD VALUE 'PRIVATE_EQUITY'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a267ab811..c5a03dc93 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -195,6 +195,7 @@ enum AssetSubClass { ETF MUTUALFUND PRECIOUS_METAL + PRIVATE_EQUITY STOCK } From ce6b5fb7cb2fe1135544994b78c0f9c6a4ba508a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 1 May 2022 08:38:57 +0200 Subject: [PATCH 303/337] Bugfix/fix tooltip in proportion chart after update (#882) * Keep tooltip configuration up to date * Update changelog --- CHANGELOG.md | 4 + .../portfolio-proportion-chart.component.ts | 92 ++++++++++--------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a09b7cad1..0f2d4c10a 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 - Added support for private equity - Extended the form to set the asset and asset sub class for (wealth) items +### Fixed + +- Fixed the tooltip update in the portfolio proportion chart component + ### Todo - Apply data migration (`yarn database:migrate`) 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 a81e585cd..94130e60e 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 @@ -15,7 +15,7 @@ import { getTextColor } from '@ghostfolio/common/helper'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; -import { Tooltip } from 'chart.js'; +import { ChartConfiguration, Tooltip } from 'chart.js'; import { LinearScale } from 'chart.js'; import { ArcElement } from 'chart.js'; import { DoughnutController } from 'chart.js'; @@ -215,7 +215,7 @@ export class PortfolioProportionChartComponent }); }); - const datasets = [ + const datasets: ChartConfiguration['data']['datasets'] = [ { backgroundColor: chartDataSorted.map(([, item]) => { return item.color; @@ -247,7 +247,7 @@ export class PortfolioProportionChartComponent datasets[0].data[0] = Number.MAX_SAFE_INTEGER; } - const data = { + const data: ChartConfiguration['data'] = { datasets, labels }; @@ -255,6 +255,8 @@ export class PortfolioProportionChartComponent if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins.tooltip = + this.getTooltipPluginConfiguration(data); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -302,46 +304,7 @@ export class PortfolioProportionChartComponent } }, legend: { display: false }, - tooltip: { - callbacks: { - label: (context) => { - const labelIndex = - (data.datasets[context.datasetIndex - 1]?.data?.length ?? - 0) + context.dataIndex; - 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; - - let sum = 0; - context.dataset.data.map((item) => { - sum += item; - }); - - const percentage = (context.parsed * 100) / sum; - - if (context.raw === Number.MAX_SAFE_INTEGER) { - return 'No data available'; - } else if (this.isInPercent) { - return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; - } else { - const value = context.raw; - return [ - `${name ?? symbol}`, - `${value.toLocaleString(this.locale, { - maximumFractionDigits: 2, - minimumFractionDigits: 2 - })} ${this.baseCurrency} (${percentage.toFixed(2)}%)` - ]; - } - } - } - } + tooltip: this.getTooltipPluginConfiguration(data) } }, plugins: [ChartDataLabels], @@ -373,4 +336,47 @@ export class PortfolioProportionChartComponent '#cc5de8' // grape 5 ]; } + + private getTooltipPluginConfiguration(data: ChartConfiguration['data']) { + return { + callbacks: { + label: (context) => { + const labelIndex = + (data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) + + context.dataIndex; + 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; + + let sum = 0; + for (const item of context.dataset.data) { + sum += item; + } + + const percentage = (context.parsed * 100) / sum; + + if (context.raw === Number.MAX_SAFE_INTEGER) { + return 'No data available'; + } else if (this.isInPercent) { + return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; + } else { + const value = context.raw; + return [ + `${name ?? symbol}`, + `${value.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${this.baseCurrency} (${percentage.toFixed(2)}%)` + ]; + } + } + } + }; + } } From 16dd8f7652a083385fd1834a8874f1d70e9c86f7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 May 2022 11:44:29 +0200 Subject: [PATCH 304/337] Feature/refactor filters with interface (#883) * Refactor filtering with an interface * Filter by accounts * Update changelog --- CHANGELOG.md | 5 ++ apps/api/src/app/order/order.service.ts | 29 +++++-- .../src/app/portfolio/portfolio.controller.ts | 30 +++++-- .../src/app/portfolio/portfolio.service.ts | 15 ++-- .../allocations/allocations-page.component.ts | 29 +++++-- .../allocations/allocations-page.html | 3 +- apps/client/src/app/services/data.service.ts | 36 ++++++++- .../src/lib/interfaces/filter.interface.ts | 5 ++ libs/common/src/lib/interfaces/index.ts | 2 + .../activities-filter.component.html | 12 +-- .../activities-filter.component.ts | 71 ++++++++++------- .../activities-table.component.html | 3 +- .../activities-table.component.ts | 79 +++++++++++-------- 13 files changed, 221 insertions(+), 98 deletions(-) create mode 100644 libs/common/src/lib/interfaces/filter.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2d4c10a..f4e37b189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for filtering by accounts on the allocations page - Added support for private equity - Extended the form to set the asset and asset sub class for (wealth) items +### Changed + +- Refactored the filtering (activities table and allocations page) + ### Fixed - Fixed the tooltip update in the portfolio proportion chart component diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 762b414ba..ed5bbe0cd 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; +import { Filter } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { @@ -16,6 +17,7 @@ import { } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; +import { groupBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Activity } from './interfaces/activities.interface'; @@ -166,31 +168,44 @@ export class OrderService { } public async getOrders({ + filters, includeDrafts = false, - tags, types, userCurrency, userId }: { + filters?: Filter[]; includeDrafts?: boolean; - tags?: string[]; types?: TypeOfOrder[]; userCurrency: string; userId: string; }): Promise { const where: Prisma.OrderWhereInput = { userId }; + const { account: filtersByAccount, tag: filtersByTag } = groupBy( + filters, + (filter) => { + return filter.type; + } + ); + + if (filtersByAccount?.length > 0) { + where.accountId = { + in: filtersByAccount.map(({ id }) => { + return id; + }) + }; + } + if (includeDrafts === false) { where.isDraft = false; } - if (tags?.length > 0) { + if (filtersByTag?.length > 0) { where.tags = { some: { - OR: tags.map((tag) => { - return { - name: tag - }; + OR: filtersByTag.map(({ id }) => { + return { id }; }) } }; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 9f9b20c56..37252b8fc 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -11,6 +11,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { baseCurrency } from '@ghostfolio/common/config'; import { parseDate } from '@ghostfolio/common/helper'; import { + Filter, PortfolioChart, PortfolioDetails, PortfolioInvestments, @@ -19,7 +20,7 @@ import { PortfolioReport, PortfolioSummary } from '@ghostfolio/common/interfaces'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import { Controller, Get, @@ -105,17 +106,36 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Query('tags') tags?: string + @Query('accounts') filterByAccounts?: string, + @Query('range') range?: DateRange, + @Query('tags') filterByTags?: string ): Promise { let hasError = false; + const accountIds = filterByAccounts?.split(',') ?? []; + const tagIds = filterByTags?.split(',') ?? []; + + const filters: Filter[] = [ + ...accountIds.map((accountId) => { + return { + id: accountId, + type: 'account' + }; + }), + ...tagIds.map((tagId) => { + return { + id: tagId, + type: 'tag' + }; + }) + ]; + const { accounts, holdings, hasErrors } = await this.portfolioService.getDetails( impersonationId, this.request.user.id, range, - tags?.split(',') + filters ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -163,7 +183,7 @@ export class PortfolioController { return { hasError, - accounts: tags ? {} : accounts, + accounts: filters ? {} : accounts, holdings: isBasicUser ? {} : holdings }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index ecc0ea20e..795f516ee 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -29,6 +29,7 @@ import { import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, + Filter, PortfolioDetails, PortfolioPerformanceResponse, PortfolioReport, @@ -309,7 +310,7 @@ export class PortfolioService { aImpersonationId: string, aUserId: string, aDateRange: DateRange = 'max', - tags?: string[] + aFilters?: Filter[] ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); const user = await this.userService.user({ id: userId }); @@ -324,8 +325,8 @@ export class PortfolioService { const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ - tags, - userId + userId, + filters: aFilters }); const portfolioCalculator = new PortfolioCalculator({ @@ -448,7 +449,7 @@ export class PortfolioService { value: totalValue }); - if (tags === undefined) { + if (aFilters === undefined) { for (const symbol of Object.keys(cashPositions)) { holdings[symbol] = cashPositions[symbol]; } @@ -1195,12 +1196,12 @@ export class PortfolioService { } private async getTransactionPoints({ + filters, includeDrafts = false, - tags, userId }: { + filters?: Filter[]; includeDrafts?: boolean; - tags?: string[]; userId: string; }): Promise<{ transactionPoints: TransactionPoint[]; @@ -1210,8 +1211,8 @@ export class PortfolioService { const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const orders = await this.orderService.getOrders({ + filters, includeDrafts, - tags, userCurrency, userId, types: ['BUY', 'SELL'] 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 b8842cea0..1c20dc518 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 @@ -8,6 +8,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { prettifySymbol } from '@ghostfolio/common/helper'; import { + Filter, PortfolioDetails, PortfolioPosition, UniqueAsset, @@ -32,6 +33,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; + public allFilters: Filter[]; public continents: { [code: string]: { name: string; value: number }; }; @@ -39,7 +41,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; - public filters$ = new Subject(); + public filters$ = new Subject(); public hasImpersonationId: boolean; public isLoading = false; public markets: { @@ -76,7 +78,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; - public tags: string[] = []; public user: User; @@ -127,10 +128,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.filters$ .pipe( distinctUntilChanged(), - switchMap((tags) => { + switchMap((filters) => { this.isLoading = true; - return this.dataService.fetchPortfolioDetails({ tags }); + return this.dataService.fetchPortfolioDetails({ filters }); }), takeUntil(this.unsubscribeSubject) ) @@ -150,10 +151,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - this.tags = this.user.tags.map((tag) => { - return tag.name; + const accountFilters: Filter[] = this.user.accounts.map( + ({ id, name }) => { + return { + id: id, + label: name, + type: 'account' + }; + } + ); + + const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { + return { + id, + label: name, + type: 'tag' + }; }); + this.allFilters = [...accountFilters, ...tagFilters]; + this.changeDetectorRef.markForCheck(); } }); 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 74b9330e2..07418a4af 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -3,9 +3,8 @@

Allocations

diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index c59982824..7679c154f 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -19,6 +19,7 @@ import { AdminData, AdminMarketData, Export, + Filter, InfoItem, PortfolioChart, PortfolioDetails, @@ -33,7 +34,7 @@ import { permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { DataSource, Order as OrderModel } from '@prisma/client'; import { parseISO } from 'date-fns'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, groupBy } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -182,11 +183,38 @@ export class DataService { ); } - public fetchPortfolioDetails({ tags }: { tags?: string[] }) { + public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { let params = new HttpParams(); - if (tags?.length > 0) { - params = params.append('tags', tags.join(',')); + if (filters?.length > 0) { + const { account: filtersByAccount, tag: filtersByTag } = groupBy( + filters, + (filter) => { + return filter.type; + } + ); + + if (filtersByAccount) { + params = params.append( + 'accounts', + filtersByAccount + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByTag) { + params = params.append( + 'tags', + filtersByTag + .map(({ id }) => { + return id; + }) + .join(',') + ); + } } return this.http.get('/api/v1/portfolio/details', { diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts new file mode 100644 index 000000000..e6d5bb108 --- /dev/null +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -0,0 +1,5 @@ +export interface Filter { + id: string; + label?: string; + type: 'account' | 'tag'; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d2ad50742..0b20b8f23 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { } from './admin-market-data.interface'; import { Coupon } from './coupon.interface'; import { Export } from './export.interface'; +import { Filter } from './filter.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioDetails } from './portfolio-details.interface'; @@ -38,6 +39,7 @@ export { AdminMarketDataItem, Coupon, Export, + Filter, InfoItem, PortfolioChart, PortfolioDetails, diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html index 98c40d4b8..e0eb5da87 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -2,13 +2,13 @@ - {{ searchKeyword | gfSymbol }} + {{ filter.label | gfSymbol }} - {{ filter | gfSymbol }} + {{ filter.label | gfSymbol }} (); + @Output() valueChanged = new EventEmitter(); @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @ViewChild('searchInput') searchInput: ElementRef; - public filters$: Subject = new BehaviorSubject([]); - public filters: Observable = this.filters$.asObservable(); + public filters$: Subject = new BehaviorSubject([]); + public filters: Observable = this.filters$.asObservable(); public searchControl = new FormControl(); - public searchKeywords: string[] = []; + public selectedFilters: Filter[] = []; public separatorKeysCodes: number[] = [ENTER, COMMA]; private unsubscribeSubject = new Subject(); @@ -47,16 +48,23 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { public constructor() { this.searchControl.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((keyword) => { - if (keyword) { - const filterValue = keyword.toLowerCase(); + .subscribe((currentFilter: string) => { + if (currentFilter) { this.filters$.next( - this.allFilters.filter( - (filter) => filter.toLowerCase().indexOf(filterValue) === 0 - ) + this.allFilters + .filter((filter) => { + // Filter selected filters + return !this.selectedFilters.some((selectedFilter) => { + return selectedFilter.id === filter.id; + }); + }) + .filter((filter) => { + return filter.label + .toLowerCase() + .startsWith(currentFilter?.toLowerCase()); + }) + .sort((a, b) => a.label.localeCompare(b.label)) ); - } else { - this.filters$.next(this.allFilters); } }); } @@ -67,9 +75,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { } } - public addKeyword({ input, value }: MatChipInputEvent): void { + public onAddFilter({ input, value }: MatChipInputEvent): void { if (value?.trim()) { - this.searchKeywords.push(value.trim()); this.updateFilter(); } @@ -81,20 +88,19 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { this.searchControl.setValue(null); } - public keywordSelected(event: MatAutocompleteSelectedEvent): void { - this.searchKeywords.push(event.option.viewValue); + public onRemoveFilter(aFilter: Filter): void { + this.selectedFilters = this.selectedFilters.filter((filter) => { + return filter.id !== aFilter.id; + }); + this.updateFilter(); - this.searchInput.nativeElement.value = ''; - this.searchControl.setValue(null); } - public removeKeyword(keyword: string): void { - const index = this.searchKeywords.indexOf(keyword); - - if (index >= 0) { - this.searchKeywords.splice(index, 1); - this.updateFilter(); - } + public onSelectFilter(event: MatAutocompleteSelectedEvent): void { + this.selectedFilters.push(event.option.value); + this.updateFilter(); + this.searchInput.nativeElement.value = ''; + this.searchControl.setValue(null); } public ngOnDestroy() { @@ -103,9 +109,18 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { } private updateFilter() { - this.filters$.next(this.allFilters); + this.filters$.next( + this.allFilters + .filter((filter) => { + // Filter selected filters + return !this.selectedFilters.some((selectedFilter) => { + return selectedFilter.id === filter.id; + }); + }) + .sort((a, b) => a.label.localeCompare(b.label)) + ); // Emit an array with a new reference - this.valueChanged.emit([...this.searchKeywords]); + this.valueChanged.emit([...this.selectedFilters]); } } 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 a047053dd..60948d60d 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,8 +1,9 @@
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 03e8cf048..f2fd54bb8 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -14,13 +14,13 @@ import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { getDateFormatString } from '@ghostfolio/common/helper'; -import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import Big from 'big.js'; import { isUUID } from 'class-validator'; import { endOfToday, format, isAfter } from 'date-fns'; import { isNumber } from 'lodash'; -import { Subject, Subscription } from 'rxjs'; +import { distinctUntilChanged, Subject, Subscription, takeUntil } from 'rxjs'; const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_STRING_SEPARATOR = ','; @@ -53,11 +53,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { @ViewChild(MatSort) sort: MatSort; - public allFilters: string[]; + public allFilters: Filter[]; public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; public displayedColumns = []; public endOfToday = endOfToday(); + public filters$ = new Subject(); public hasDrafts = false; public isAfter = isAfter; public isLoading = true; @@ -71,7 +72,13 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { private unsubscribeSubject = new Subject(); - public constructor(private router: Router) {} + public constructor(private router: Router) { + this.filters$ + .pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject)) + .subscribe((filters) => { + this.updateFilters(filters); + }); + } public ngOnChanges() { this.displayedColumns = [ @@ -95,11 +102,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { }); } - this.isLoading = true; - this.defaultDateFormat = getDateFormatString(this.locale); if (this.activities) { + this.allFilters = this.getSearchableFieldValues(this.activities).map( + (label) => { + return { label, id: label, type: 'tag' }; + } + ); + this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { const dataString = this.getFilterableValues(data) @@ -113,8 +124,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return contains; }; this.dataSource.sort = this.sort; - this.updateFilter(); - this.isLoading = false; + + this.updateFilters(); } } @@ -172,30 +183,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.activityToUpdate.emit(aActivity); } - public updateFilter(filters: string[] = []) { - this.dataSource.filter = filters.join(SEARCH_STRING_SEPARATOR); - const lowercaseSearchKeywords = filters.map((keyword) => - keyword.trim().toLowerCase() - ); - - this.placeholder = - lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; - - this.searchKeywords = filters; - - this.allFilters = this.getSearchableFieldValues(this.activities).filter( - (item) => { - return !lowercaseSearchKeywords.includes(item.trim().toLowerCase()); - } - ); - - this.hasDrafts = this.dataSource.data.some((activity) => { - return activity.isDraft === true; - }); - this.totalFees = this.getTotalFees(); - this.totalValue = this.getTotalValue(); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); @@ -280,4 +267,32 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return totalValue.toNumber(); } + + private updateFilters(filters: Filter[] = []) { + this.isLoading = true; + + this.dataSource.filter = filters + .map((filter) => { + return filter.label; + }) + .join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((filter) => { + return filter.label.trim().toLowerCase(); + }); + + this.placeholder = + lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; + + this.searchKeywords = filters.map((filter) => { + return filter.label; + }); + + this.hasDrafts = this.dataSource.filteredData.some((activity) => { + return activity.isDraft === true; + }); + this.totalFees = this.getTotalFees(); + this.totalValue = this.getTotalValue(); + + this.isLoading = false; + } } From 21173bed21cfabb1d8e4daa427b0191a0dffabc7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 May 2022 11:47:28 +0200 Subject: [PATCH 305/337] Release 1.145.0 (#884) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e37b189..901b50eba 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.145.0 - 07.05.2022 ### Added diff --git a/package.json b/package.json index f13c26f52..80e2b6672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.144.0", + "version": "1.145.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 5622c4cf7e0cdaec82400944552e323606fcf22e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 May 2022 14:11:42 +0200 Subject: [PATCH 306/337] Feature/harmonize no data available label (#885) * Harmonize label for UNKNOWN_KEY * Update changelog --- CHANGELOG.md | 6 ++++++ .../portfolio-proportion-chart.component.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 901b50eba..3299a24b5 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 + +- Harmonized the _No data available_ label in the portfolio proportion chart component + ## 1.145.0 - 07.05.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 94130e60e..78e56a8cc 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 @@ -349,7 +349,7 @@ export class PortfolioProportionChartComponent if (symbol === this.OTHER_KEY) { symbol = 'Other'; } else if (symbol === UNKNOWN_KEY) { - symbol = 'Unknown'; + symbol = 'No data available'; } const name = this.positions[symbol]?.name; From 40380346e678e7c8d0c38d8ca38db6c027d20df3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 May 2022 20:00:51 +0200 Subject: [PATCH 307/337] Feature/setup bull queue system (#886) * Setup @nestjs/bull and asset profile data gathering job * Update changelog --- CHANGELOG.md | 5 + apps/api/src/app/admin/admin.controller.ts | 36 +++- apps/api/src/app/admin/admin.service.ts | 2 +- apps/api/src/app/app.module.ts | 7 + apps/api/src/app/order/order.service.ts | 18 +- apps/api/src/services/cron.service.ts | 17 +- .../api/src/services/data-gathering.module.ts | 10 +- .../src/services/data-gathering.processor.ts | 27 +++ .../src/services/data-gathering.service.ts | 80 ++++---- libs/common/src/lib/config.ts | 4 + .../src/lib/interfaces/info-item.interface.ts | 1 + .../activities-table.component.ts | 2 +- package.json | 3 + yarn.lock | 182 +++++++++++++++++- 14 files changed, 332 insertions(+), 62 deletions(-) create mode 100644 apps/api/src/services/data-gathering.processor.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3299a24b5..250884cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Set up a queue for the data gathering jobs + ### Changed +- Migrated the asset profile data gathering to the queue design pattern - Harmonized the _No data available_ label in the portfolio proportion chart component ## 1.145.0 - 07.05.2022 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 8ce4def5e..c67b443c3 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,6 +1,10 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; +import { + DATA_GATHERING_QUEUE, + GATHER_ASSET_PROFILE_PROCESS +} from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, @@ -8,6 +12,7 @@ import { } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; +import { InjectQueue } from '@nestjs/bull'; import { Body, Controller, @@ -23,6 +28,7 @@ import { import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { DataSource, MarketData } from '@prisma/client'; +import { Queue } from 'bull'; import { isDate } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -33,6 +39,8 @@ import { UpdateMarketDataDto } from './update-market-data.dto'; export class AdminController { public constructor( private readonly adminService: AdminService, + @InjectQueue(DATA_GATHERING_QUEUE) + private readonly dataGatheringQueue: Queue, private readonly dataGatheringService: DataGatheringService, private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser @@ -71,10 +79,16 @@ export class AdminController { ); } - await this.dataGatheringService.gatherProfileData(); - this.dataGatheringService.gatherMax(); + const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); - return; + for (const { dataSource, symbol } of uniqueAssets) { + await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, { + dataSource, + symbol + }); + } + + this.dataGatheringService.gatherMax(); } @Post('gather/profile-data') @@ -92,9 +106,14 @@ export class AdminController { ); } - this.dataGatheringService.gatherProfileData(); + const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); - return; + for (const { dataSource, symbol } of uniqueAssets) { + await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, { + dataSource, + symbol + }); + } } @Post('gather/profile-data/:dataSource/:symbol') @@ -115,9 +134,10 @@ export class AdminController { ); } - this.dataGatheringService.gatherProfileData([{ dataSource, symbol }]); - - return; + await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, { + dataSource, + symbol + }); } @Post('gather/:dataSource/:symbol') diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index c1be0bedb..40a15afc2 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -15,7 +15,7 @@ import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; -import { DataSource, Property } from '@prisma/client'; +import { Property } from '@prisma/client'; import { differenceInDays } from 'date-fns'; @Injectable() diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index e3d8a3bea..f3b85fc9b 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -9,6 +9,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data- import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module'; +import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; @@ -36,6 +37,12 @@ import { UserModule } from './user/user.module'; AccountModule, AuthDeviceModule, AuthModule, + BullModule.forRoot({ + redis: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT, 10) + } + }), CacheModule, ConfigModule.forRoot(), ConfigurationModule, diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index ed5bbe0cd..aceb8de06 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -4,8 +4,13 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; +import { + DATA_GATHERING_QUEUE, + GATHER_ASSET_PROFILE_PROCESS +} from '@ghostfolio/common/config'; import { Filter } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; +import { InjectQueue } from '@nestjs/bull'; import { Injectable } from '@nestjs/common'; import { AssetClass, @@ -16,6 +21,7 @@ import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; +import { Queue } from 'bull'; import { endOfToday, isAfter } from 'date-fns'; import { groupBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @@ -27,6 +33,8 @@ export class OrderService { public constructor( private readonly accountService: AccountService, private readonly cacheService: CacheService, + @InjectQueue(DATA_GATHERING_QUEUE) + private readonly dataGatheringQueue: Queue, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly dataGatheringService: DataGatheringService, private readonly prismaService: PrismaService, @@ -112,12 +120,10 @@ export class OrderService { data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase(); } - await this.dataGatheringService.gatherProfileData([ - { - dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, - symbol: data.SymbolProfile.connectOrCreate.create.symbol - } - ]); + await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, { + dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, + symbol: data.SymbolProfile.connectOrCreate.create.symbol + }); const isDraft = isAfter(data.date as Date, endOfToday()); diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index b1ca2a513..40051b9ce 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -1,5 +1,11 @@ +import { + DATA_GATHERING_QUEUE, + GATHER_ASSET_PROFILE_PROCESS +} from '@ghostfolio/common/config'; +import { InjectQueue } from '@nestjs/bull'; import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; +import { Queue } from 'bull'; import { DataGatheringService } from './data-gathering.service'; import { ExchangeRateDataService } from './exchange-rate-data.service'; @@ -8,6 +14,8 @@ import { TwitterBotService } from './twitter-bot/twitter-bot.service'; @Injectable() export class CronService { public constructor( + @InjectQueue(DATA_GATHERING_QUEUE) + private readonly dataGatheringQueue: Queue, private readonly dataGatheringService: DataGatheringService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly twitterBotService: TwitterBotService @@ -30,6 +38,13 @@ export class CronService { @Cron(CronExpression.EVERY_WEEKEND) public async runEveryWeekend() { - await this.dataGatheringService.gatherProfileData(); + const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); + + for (const { dataSource, symbol } of uniqueAssets) { + await this.dataGatheringQueue.add(GATHER_ASSET_PROFILE_PROCESS, { + dataSource, + symbol + }); + } } } diff --git a/apps/api/src/services/data-gathering.module.ts b/apps/api/src/services/data-gathering.module.ts index f458e3bda..e8e98058c 100644 --- a/apps/api/src/services/data-gathering.module.ts +++ b/apps/api/src/services/data-gathering.module.ts @@ -3,13 +3,19 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; +import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config'; +import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; +import { DataGatheringProcessor } from './data-gathering.processor'; import { ExchangeRateDataModule } from './exchange-rate-data.module'; import { SymbolProfileModule } from './symbol-profile.module'; @Module({ imports: [ + BullModule.registerQueue({ + name: DATA_GATHERING_QUEUE + }), ConfigurationModule, DataEnhancerModule, DataProviderModule, @@ -17,7 +23,7 @@ import { SymbolProfileModule } from './symbol-profile.module'; PrismaModule, SymbolProfileModule ], - providers: [DataGatheringService], - exports: [DataEnhancerModule, DataGatheringService] + providers: [DataGatheringProcessor, DataGatheringService], + exports: [BullModule, DataEnhancerModule, DataGatheringService] }) export class DataGatheringModule {} diff --git a/apps/api/src/services/data-gathering.processor.ts b/apps/api/src/services/data-gathering.processor.ts new file mode 100644 index 000000000..de8d8eb4e --- /dev/null +++ b/apps/api/src/services/data-gathering.processor.ts @@ -0,0 +1,27 @@ +import { + DATA_GATHERING_QUEUE, + GATHER_ASSET_PROFILE_PROCESS +} from '@ghostfolio/common/config'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { Process, Processor } from '@nestjs/bull'; +import { Injectable, Logger } from '@nestjs/common'; +import { Job } from 'bull'; + +import { DataGatheringService } from './data-gathering.service'; + +@Injectable() +@Processor(DATA_GATHERING_QUEUE) +export class DataGatheringProcessor { + public constructor( + private readonly dataGatheringService: DataGatheringService + ) {} + + @Process(GATHER_ASSET_PROFILE_PROCESS) + public async gatherAssetProfile(job: Job) { + try { + await this.dataGatheringService.gatherAssetProfiles([job.data]); + } catch (error) { + Logger.error(error, 'DataGatheringProcessor'); + } + } +} diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index b64afbde4..61adaa19e 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -226,28 +226,29 @@ export class DataGatheringService { } } - public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) { + public async gatherAssetProfiles(aUniqueAssets?: UniqueAsset[]) { + let uniqueAssets = aUniqueAssets?.filter((dataGatheringItem) => { + return dataGatheringItem.dataSource !== 'MANUAL'; + }); + + if (!uniqueAssets) { + uniqueAssets = await this.getUniqueAssets(); + } + Logger.log( - 'Profile data gathering has been started.', + `Asset profile data gathering has been started for ${uniqueAssets + .map(({ dataSource, symbol }) => { + return `${symbol} (${dataSource})`; + }) + .join(',')}.`, 'DataGatheringService' ); - console.time('data-gathering-profile'); - - let dataGatheringItems = aDataGatheringItems?.filter( - (dataGatheringItem) => { - return dataGatheringItem.dataSource !== 'MANUAL'; - } - ); - - if (!dataGatheringItems) { - dataGatheringItems = await this.getSymbolsProfileData(); - } const assetProfiles = await this.dataProviderService.getAssetProfiles( - dataGatheringItems + uniqueAssets ); const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( - dataGatheringItems.map(({ symbol }) => { + uniqueAssets.map(({ symbol }) => { return symbol; }) ); @@ -322,10 +323,13 @@ export class DataGatheringService { } Logger.log( - 'Profile data gathering has been completed.', + `Asset profile data gathering has been completed for ${uniqueAssets + .map(({ dataSource, symbol }) => { + return `${symbol} (${dataSource})`; + }) + .join(',')}.`, 'DataGatheringService' ); - console.timeEnd('data-gathering-profile'); } public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { @@ -508,6 +512,27 @@ export class DataGatheringService { return [...currencyPairsToGather, ...symbolProfilesToGather]; } + public async getUniqueAssets(): Promise { + const symbolProfiles = await this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }] + }); + + return symbolProfiles + .filter(({ dataSource }) => { + return ( + dataSource !== DataSource.GHOSTFOLIO && + dataSource !== DataSource.MANUAL && + dataSource !== DataSource.RAKUTEN + ); + }) + .map(({ dataSource, symbol }) => { + return { + dataSource, + symbol + }; + }); + } + public async reset() { Logger.log('Data gathering has been reset.', 'DataGatheringService'); @@ -584,27 +609,6 @@ export class DataGatheringService { return [...currencyPairsToGather, ...symbolProfilesToGather]; } - private async getSymbolsProfileData(): Promise { - const symbolProfiles = await this.prismaService.symbolProfile.findMany({ - orderBy: [{ symbol: 'asc' }] - }); - - 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() { const lastDataGathering = await this.getLastDataGathering(); diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 133dbeef6..cc709e96f 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -44,8 +44,12 @@ export const warnColorRgb = { export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND'; +export const DATA_GATHERING_QUEUE = 'DATA_GATHERING_QUEUE'; + export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; +export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE'; + export const PROPERTY_COUPONS = 'COUPONS'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index f9d53b186..b8119e34d 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,4 +1,5 @@ import { Tag } from '@prisma/client'; + import { Statistics } from './statistics.interface'; import { Subscription } from './subscription.interface'; 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 f2fd54bb8..959a4e867 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -20,7 +20,7 @@ import Big from 'big.js'; import { isUUID } from 'class-validator'; import { endOfToday, format, isAfter } from 'date-fns'; import { isNumber } from 'lodash'; -import { distinctUntilChanged, Subject, Subscription, takeUntil } from 'rxjs'; +import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs'; const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_STRING_SEPARATOR = ','; diff --git a/package.json b/package.json index 80e2b6672..9974a1c3f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@angular/router": "13.2.2", "@codewithdan/observable-store": "2.2.11", "@dinero.js/currencies": "2.0.0-alpha.8", + "@nestjs/bull": "0.5.5", "@nestjs/common": "8.2.3", "@nestjs/config": "1.1.3", "@nestjs/core": "8.2.3", @@ -82,6 +83,7 @@ "bent": "7.3.12", "big.js": "6.1.1", "bootstrap": "4.6.0", + "bull": "4.8.2", "cache-manager": "3.4.3", "cache-manager-redis-store": "2.0.0", "chart.js": "3.7.0", @@ -146,6 +148,7 @@ "@storybook/builder-webpack5": "6.4.18", "@storybook/manager-webpack5": "6.4.18", "@types/big.js": "6.1.2", + "@types/bull": "3.15.8", "@types/cache-manager": "3.4.2", "@types/color": "3.0.2", "@types/google-spreadsheet": "3.1.5", diff --git a/yarn.lock b/yarn.lock index de5b59b06..5f0c38d5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3012,6 +3012,21 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@nestjs/bull-shared@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@nestjs/bull-shared/-/bull-shared-0.0.5.tgz#5cd9907fdc99cdd2a2088406bcae086e13918c4a" + integrity sha512-MPkl1q7N/ZlzLq4SC/NWdZ+7pIOF9x70+92xBcUPx7J6mewqVR1gzADHzEexErZEgg+K/n5nwJGe+BLlYFTUgg== + dependencies: + tslib "2.3.1" + +"@nestjs/bull@0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@nestjs/bull/-/bull-0.5.5.tgz#1ee9b7d8b78a52af58c4654de8d765a2a2b16862" + integrity sha512-g42/KH8YJmJE60kQVzQucWm5xEfyOj8iwXp2s3Ox3Yu6B/mmzNmqmXLnACnaD15q3WtH84vOhME41CJANxbQeQ== + dependencies: + "@nestjs/bull-shared" "^0.0.5" + tslib "2.3.1" + "@nestjs/common@8.2.3": version "8.2.3" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-8.2.3.tgz#77ac1ef59c36dd9ae131f5c5ddabfc6258802d1c" @@ -4753,6 +4768,14 @@ dependencies: "@types/node" "*" +"@types/bull@3.15.8": + version "3.15.8" + resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.8.tgz#ae2139f94490d740b37c8da5d828ce75dd82ce7c" + integrity sha512-8DbSPMSsZH5PWPnGEkAZLYgJEH4ghHJNKF7LB6Wr5R0/v6g+Vs+JoaA7kcvLtHE936xg2WpFPkaoaJgExOmKDw== + dependencies: + "@types/ioredis" "*" + "@types/redis" "^2.8.0" + "@types/cache-manager@3.4.2": version "3.4.2" resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.2.tgz#d57e7e5e6374d1037bdce753a05c9703e4483401" @@ -4880,6 +4903,13 @@ dependencies: "@types/node" "*" +"@types/ioredis@*": + version "4.28.10" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" + integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== + dependencies: + "@types/node" "*" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -5084,6 +5114,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/redis@^2.8.0": + version "2.8.32" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" + integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" @@ -6945,6 +6982,21 @@ builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= +bull@4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.2.tgz#0d02fe389777abe29d50fd46d123bc62e074cfcd" + integrity sha512-S7CNIL9+vsbLKwOGkUI6mawY5iABKQJLZn5a7KPnxAZrDhFXkrxsHHXLCKUR/+Oqys3Vk5ElWdj0SLtK84b1Nw== + dependencies: + cron-parser "^4.2.1" + debuglog "^1.0.0" + get-port "^5.1.1" + ioredis "^4.28.5" + lodash "^4.17.21" + msgpackr "^1.5.2" + p-timeout "^3.2.0" + semver "^7.3.2" + uuid "^8.3.0" + busboy@^0.2.11: version "0.2.14" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" @@ -7500,6 +7552,11 @@ clsx@^1.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -7978,6 +8035,13 @@ critters@0.0.16: postcss "^8.3.7" pretty-bytes "^5.3.0" +cron-parser@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.4.0.tgz#829d67f9e68eb52fa051e62de0418909f05db983" + integrity sha512-TrE5Un4rtJaKgmzPewh67yrER5uKM0qI9hGLDBfWb8GGRe9pn/SDkhVrdHa4z7h0SeyeNxnQnogws/H+AQANQA== + dependencies: + luxon "^1.28.0" + cron@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/cron/-/cron-1.7.2.tgz#2ea1f35c138a07edac2ac5af5084ed6fee5723db" @@ -8303,6 +8367,11 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" +debuglog@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -8422,7 +8491,7 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -denque@^1.5.0: +denque@^1.1.0, denque@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== @@ -10284,6 +10353,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -11325,6 +11399,23 @@ ionicons@5.5.1: dependencies: "@stencil/core" "^2.5.0" +ioredis@^4.28.5: + version "4.28.5" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" + integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -13041,6 +13132,16 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash.get@4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -13056,6 +13157,11 @@ lodash.includes@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -13171,6 +13277,11 @@ lru-cache@^7.3.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.3.1.tgz#7702e80694ec2bf19865567a469f2b081fcf53f5" integrity sha512-nX1x4qUrKqwbIAhv4s9et4FIUVzNOpeY07bsjGUy8gwJrXH/wScImSQqXErmo/b2jZY2r0mohbLA9zVj7u1cNw== +luxon@^1.28.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" + integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== + magic-string@0.25.7, magic-string@^0.25.0: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -13681,6 +13792,57 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract-darwin-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-1.1.0.tgz#d590dffac6b90edc3ab53392f7ec5668ed94638c" + integrity sha512-s1kHoT12tS2cCQOv+Wl3I+/cYNJXBPtwQqGA+dPYoXmchhXiE0Nso+BIfvQ5PxbmAyjj54Q5o7PnLTqVquNfZA== + +msgpackr-extract-darwin-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-1.1.0.tgz#568cbdf5e819ac120659c02b0dbaabf483523ee3" + integrity sha512-yx/H/i12IKg4eWGu/eKdKzJD4jaYvvujQSaVmeOMCesbSQnWo5X6YR9TFjoiNoU9Aexk1KufzL9gW+1DozG1yw== + +msgpackr-extract-linux-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-1.1.0.tgz#c0a30e6687cea4f79115f5762c5fdff90e4a20d4" + integrity sha512-AxFle3fHNwz2V4CYDIGFxI6o/ZuI0lBKg0uHI8EcCMUmDE5mVAUWYge5WXmORVvb8sVWyVgFlmi3MTu4Ve6tNQ== + +msgpackr-extract-linux-arm@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-1.1.0.tgz#38e8db873b6b3986558bde4d7bb15eacc8743a9e" + integrity sha512-0VvSCqi12xpavxl14gMrauwIzHqHbmSChUijy/uo3mpjB1Pk4vlisKpZsaOZvNJyNKj0ACi5jYtbWnnOd7hYGw== + +msgpackr-extract-linux-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-1.1.0.tgz#8c44ca5211d9fa6af77be64a8e687c0be0491ce7" + integrity sha512-O+XoyNFWpdB8oQL6O/YyzffPpmG5rTNrr1nKLW70HD2ENJUhcITzbV7eZimHPzkn8LAGls1tBaMTHQezTBpFOw== + +msgpackr-extract-win32-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-1.1.0.tgz#7bf9bd258e334668842c7532e5e40a60ca3325d7" + integrity sha512-6AJdM5rNsL4yrskRfhujVSPEd6IBpgvsnIT/TPowKNLQ62iIdryizPY2PJNFiW3AJcY249AHEiDBXS1cTDPxzA== + +msgpackr-extract@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.1.4.tgz#665037c1470f225d01d2d735dad0334fff5faae6" + integrity sha512-WQbHvsThprXh+EqZYy+SQFEs7z6bNM7a0vgirwUfwUcphWGT2mdPcpyLCNiRsN6w5q5VKJUMblHY+tNEyceb9Q== + dependencies: + node-gyp-build-optional-packages "^4.3.2" + optionalDependencies: + msgpackr-extract-darwin-arm64 "1.1.0" + msgpackr-extract-darwin-x64 "1.1.0" + msgpackr-extract-linux-arm "1.1.0" + msgpackr-extract-linux-arm64 "1.1.0" + msgpackr-extract-linux-x64 "1.1.0" + msgpackr-extract-win32-x64 "1.1.0" + +msgpackr@^1.5.2: + version "1.5.7" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.5.7.tgz#53b3fd0e7afdf4184a594881a18832df9422b660" + integrity sha512-Hsa80i8W4BiObSMHslfnwC+CC1CYHZzoXJZn0+3EvoCEOgt3c5QlXhdcjgFk2aZxMgpV8aUFZqJyQUCIp4UrzA== + optionalDependencies: + msgpackr-extract "^1.1.4" + multer@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b" @@ -13864,6 +14026,11 @@ node-forge@^1.2.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== +node-gyp-build-optional-packages@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz#82de9bdf9b1ad042457533afb2f67469dc2264bb" + integrity sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA== + node-gyp-build@^4.2.2: version "4.2.3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" @@ -14445,7 +14612,7 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^2.0.0: +p-map@^2.0.0, p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -14472,7 +14639,7 @@ p-retry@^4.5.0: "@types/retry" "^0.12.0" retry "^0.13.1" -p-timeout@^3.1.0: +p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -15811,7 +15978,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redis-commands@^1.7.0: +redis-commands@1.7.0, redis-commands@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== @@ -16948,6 +17115,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -18166,7 +18338,7 @@ uuid-browser@^3.1.0: resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" integrity sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA= -uuid@8.3.2, uuid@^8.3.2: +uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== From 57314d62eebc53fb29d2d24318b6d880aae3bf53 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 7 May 2022 22:33:57 +0200 Subject: [PATCH 308/337] Feature/improve allocations page with no filter (#887) * Improve accounts for no filters * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/portfolio.controller.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 250884cdc..193819859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Migrated the asset profile data gathering to the queue design pattern +- Improved the allocations page with no filtering - Harmonized the _No data available_ label in the portfolio proportion chart component ## 1.145.0 - 07.05.2022 diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 37252b8fc..25021492e 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -183,7 +183,7 @@ export class PortfolioController { return { hasError, - accounts: filters ? {} : accounts, + accounts: filters.length === 0 ? accounts : {}, holdings: isBasicUser ? {} : holdings }; } From 34cbdd7c2a79e6818ef0bff0cc8de17e58d4a3e7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 09:26:33 +0200 Subject: [PATCH 309/337] Upgrade angular, Nx and storybook (#888) * Upgrade angular, Nx and storybook * Update changelog --- CHANGELOG.md | 5 +- angular.json | 8 +- apps/api/{jest.config.js => jest.config.ts} | 5 +- apps/api/tsconfig.app.json | 2 +- apps/api/tsconfig.spec.json | 2 +- .../client/{jest.config.js => jest.config.ts} | 5 +- apps/client/tsconfig.app.json | 3 +- apps/client/tsconfig.editor.json | 3 +- apps/client/tsconfig.spec.json | 2 +- jest.config.js => jest.config.ts | 0 jest.preset.js => jest.preset.ts | 0 .../common/{jest.config.js => jest.config.ts} | 5 +- libs/common/tsconfig.lib.json | 2 +- libs/common/tsconfig.spec.json | 3 +- libs/ui/.storybook/tsconfig.json | 2 +- libs/ui/{jest.config.js => jest.config.ts} | 5 +- .../fire-calculator.component.ts | 4 + libs/ui/tsconfig.lib.json | 3 +- libs/ui/tsconfig.spec.json | 2 +- package.json | 68 +- yarn.lock | 3076 +++++++++-------- 21 files changed, 1714 insertions(+), 1491 deletions(-) rename apps/api/{jest.config.js => jest.config.ts} (83%) rename apps/client/{jest.config.js => jest.config.ts} (86%) rename jest.config.js => jest.config.ts (100%) rename jest.preset.js => jest.preset.ts (100%) rename libs/common/{jest.config.js => jest.config.ts} (72%) rename libs/ui/{jest.config.js => jest.config.ts} (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 193819859..a31f3d6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migrated the asset profile data gathering to the queue design pattern - Improved the allocations page with no filtering - Harmonized the _No data available_ label in the portfolio proportion chart component +- Upgraded `angular` from version `13.2.2` to `13.3.6` +- Upgraded `Nx` from version `13.8.5` to `14.1.4` +- Upgraded `storybook` from version `6.4.18` to `6.4.22` ## 1.145.0 - 07.05.2022 @@ -427,7 +430,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Upgraded `angular` from version `13.1.2` to `13.2.3` +- Upgraded `angular` from version `13.1.2` to `13.2.2` - Upgraded `Nx` from version `13.4.1` to `13.8.1` - Upgraded `storybook` from version `6.4.9` to `6.4.18` diff --git a/angular.json b/angular.json index 52f3e9cfe..e8c179ff2 100644 --- a/angular.json +++ b/angular.json @@ -47,7 +47,7 @@ "test": { "builder": "@nrwl/jest:jest", "options": { - "jestConfig": "apps/api/jest.config.js", + "jestConfig": "apps/api/jest.config.ts", "passWithNoTests": true }, "outputs": ["coverage/apps/api"] @@ -180,7 +180,7 @@ "test": { "builder": "@nrwl/jest:jest", "options": { - "jestConfig": "apps/client/jest.config.js", + "jestConfig": "apps/client/jest.config.ts", "passWithNoTests": true }, "outputs": ["coverage/apps/client"] @@ -225,7 +225,7 @@ "builder": "@nrwl/jest:jest", "outputs": ["coverage/libs/common"], "options": { - "jestConfig": "libs/common/jest.config.js", + "jestConfig": "libs/common/jest.config.ts", "passWithNoTests": true } } @@ -247,7 +247,7 @@ "builder": "@nrwl/jest:jest", "outputs": ["coverage/libs/ui"], "options": { - "jestConfig": "libs/ui/jest.config.js", + "jestConfig": "libs/ui/jest.config.ts", "passWithNoTests": true } }, diff --git a/apps/api/jest.config.js b/apps/api/jest.config.ts similarity index 83% rename from apps/api/jest.config.js rename to apps/api/jest.config.ts index c46248946..9f0ebed54 100644 --- a/apps/api/jest.config.js +++ b/apps/api/jest.config.ts @@ -1,6 +1,6 @@ module.exports = { displayName: 'api', - preset: '../../jest.preset.js', + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' @@ -12,5 +12,6 @@ module.exports = { moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../../coverage/apps/api', testTimeout: 10000, - testEnvironment: 'node' + testEnvironment: 'node', + preset: '../../jest.preset.ts' }; diff --git a/apps/api/tsconfig.app.json b/apps/api/tsconfig.app.json index 62d262ff8..44e62fa9f 100644 --- a/apps/api/tsconfig.app.json +++ b/apps/api/tsconfig.app.json @@ -6,6 +6,6 @@ "emitDecoratorMetadata": true, "target": "es2015" }, - "exclude": ["**/*.spec.ts", "**/*.test.ts"], + "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], "include": ["**/*.ts"] } diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json index 07c165e42..148da8555 100644 --- a/apps/api/tsconfig.spec.json +++ b/apps/api/tsconfig.spec.json @@ -5,5 +5,5 @@ "module": "commonjs", "types": ["jest", "node"] }, - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] } diff --git a/apps/client/jest.config.js b/apps/client/jest.config.ts similarity index 86% rename from apps/client/jest.config.js rename to apps/client/jest.config.ts index 15d6e93be..f8f7806f2 100644 --- a/apps/client/jest.config.js +++ b/apps/client/jest.config.ts @@ -1,6 +1,6 @@ module.exports = { displayName: 'client', - preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { 'ts-jest': { @@ -17,5 +17,6 @@ module.exports = { transform: { '^.+.(ts|mjs|js|html)$': 'jest-preset-angular' }, - transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'] + transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], + preset: '../../jest.preset.ts' }; diff --git a/apps/client/tsconfig.app.json b/apps/client/tsconfig.app.json index 1796f77c9..a6bd36a5e 100644 --- a/apps/client/tsconfig.app.json +++ b/apps/client/tsconfig.app.json @@ -5,5 +5,6 @@ "types": ["node"], "typeRoots": ["../node_modules/@types"] }, - "files": ["src/main.ts", "src/polyfills.ts"] + "files": ["src/main.ts", "src/polyfills.ts"], + "exclude": ["jest.config.ts"] } diff --git a/apps/client/tsconfig.editor.json b/apps/client/tsconfig.editor.json index 20c4afdbf..1bf3c0a74 100644 --- a/apps/client/tsconfig.editor.json +++ b/apps/client/tsconfig.editor.json @@ -3,5 +3,6 @@ "include": ["**/*.ts"], "compilerOptions": { "types": ["jest", "node"] - } + }, + "exclude": ["jest.config.ts"] } diff --git a/apps/client/tsconfig.spec.json b/apps/client/tsconfig.spec.json index bc90aab09..b6347c6f6 100644 --- a/apps/client/tsconfig.spec.json +++ b/apps/client/tsconfig.spec.json @@ -6,5 +6,5 @@ "types": ["jest", "node"] }, "files": ["src/test-setup.ts"], - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] } diff --git a/jest.config.js b/jest.config.ts similarity index 100% rename from jest.config.js rename to jest.config.ts diff --git a/jest.preset.js b/jest.preset.ts similarity index 100% rename from jest.preset.js rename to jest.preset.ts diff --git a/libs/common/jest.config.js b/libs/common/jest.config.ts similarity index 72% rename from libs/common/jest.config.js rename to libs/common/jest.config.ts index 28f7b3d55..f650355ff 100644 --- a/libs/common/jest.config.js +++ b/libs/common/jest.config.ts @@ -1,6 +1,6 @@ module.exports = { displayName: 'common', - preset: '../../jest.preset.js', + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, @@ -8,5 +8,6 @@ module.exports = { '^.+\\.[tj]sx?$': 'ts-jest' }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - coverageDirectory: '../../coverage/libs/common' + coverageDirectory: '../../coverage/libs/common', + preset: '../../jest.preset.ts' }; diff --git a/libs/common/tsconfig.lib.json b/libs/common/tsconfig.lib.json index c96f54f33..95cc0c01c 100644 --- a/libs/common/tsconfig.lib.json +++ b/libs/common/tsconfig.lib.json @@ -5,5 +5,5 @@ "types": [] }, "include": ["**/*.ts"], - "exclude": ["**/*.spec.ts", "**/*.test.ts"] + "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"] } diff --git a/libs/common/tsconfig.spec.json b/libs/common/tsconfig.spec.json index 682964419..46f9467f3 100644 --- a/libs/common/tsconfig.spec.json +++ b/libs/common/tsconfig.spec.json @@ -14,6 +14,7 @@ "**/*.test.js", "**/*.spec.jsx", "**/*.test.jsx", - "**/*.d.ts" + "**/*.d.ts", + "jest.config.ts" ] } diff --git a/libs/ui/.storybook/tsconfig.json b/libs/ui/.storybook/tsconfig.json index 0b48a7813..535432f06 100644 --- a/libs/ui/.storybook/tsconfig.json +++ b/libs/ui/.storybook/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "emitDecoratorMetadata": true }, - "exclude": ["../**/*.spec.ts", "../**/*.test.ts"], + "exclude": ["../**/*.spec.ts", "../**/*.test.ts", "jest.config.ts"], "include": ["../src/**/*", "*.js"] } diff --git a/libs/ui/jest.config.js b/libs/ui/jest.config.ts similarity index 94% rename from libs/ui/jest.config.js rename to libs/ui/jest.config.ts index 6992f0fda..79ad3a73c 100644 --- a/libs/ui/jest.config.js +++ b/libs/ui/jest.config.ts @@ -1,6 +1,6 @@ module.exports = { displayName: 'ui', - preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { 'ts-jest': { @@ -17,5 +17,6 @@ module.exports = { 'jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment' - ] + ], + preset: '../../jest.preset.ts' }; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 256bdd6a1..ed3810961 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -178,6 +178,8 @@ export class FireCalculatorComponent return `Total: ${new Intl.NumberFormat(this.locale, { currency: this.currency, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: Only supported from ES2020 or later currencyDisplay: 'code', style: 'currency' }).format(totalAmount)}`; @@ -192,6 +194,8 @@ export class FireCalculatorComponent if (context.parsed.y !== null) { label += new Intl.NumberFormat(this.locale, { currency: this.currency, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: Only supported from ES2020 or later currencyDisplay: 'code', style: 'currency' }).format(context.parsed.y); diff --git a/libs/ui/tsconfig.lib.json b/libs/ui/tsconfig.lib.json index 0ec636637..4e6eb88b9 100644 --- a/libs/ui/tsconfig.lib.json +++ b/libs/ui/tsconfig.lib.json @@ -14,7 +14,8 @@ "**/*.spec.ts", "**/*.test.ts", "**/*.stories.ts", - "**/*.stories.js" + "**/*.stories.js", + "jest.config.ts" ], "include": ["**/*.ts"] } diff --git a/libs/ui/tsconfig.spec.json b/libs/ui/tsconfig.spec.json index bc90aab09..b6347c6f6 100644 --- a/libs/ui/tsconfig.spec.json +++ b/libs/ui/tsconfig.spec.json @@ -6,5 +6,5 @@ "types": ["jest", "node"] }, "files": ["src/test-setup.ts"], - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] } diff --git a/package.json b/package.json index 9974a1c3f..8bf08dae0 100644 --- a/package.json +++ b/package.json @@ -50,16 +50,16 @@ "workspace-generator": "nx workspace-generator" }, "dependencies": { - "@angular/animations": "13.2.2", - "@angular/cdk": "13.2.2", - "@angular/common": "13.2.2", - "@angular/compiler": "13.2.2", - "@angular/core": "13.2.2", - "@angular/forms": "13.2.2", - "@angular/material": "13.2.2", - "@angular/platform-browser": "13.2.2", - "@angular/platform-browser-dynamic": "13.2.2", - "@angular/router": "13.2.2", + "@angular/animations": "13.3.6", + "@angular/cdk": "13.3.6", + "@angular/common": "13.3.6", + "@angular/compiler": "13.3.6", + "@angular/core": "13.3.6", + "@angular/forms": "13.3.6", + "@angular/material": "13.3.6", + "@angular/platform-browser": "13.3.6", + "@angular/platform-browser-dynamic": "13.3.6", + "@angular/router": "13.3.6", "@codewithdan/observable-store": "2.2.11", "@dinero.js/currencies": "2.0.0-alpha.8", "@nestjs/bull": "0.5.5", @@ -71,7 +71,7 @@ "@nestjs/platform-express": "8.2.3", "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", - "@nrwl/angular": "13.8.5", + "@nrwl/angular": "14.1.4", "@prisma/client": "3.12.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", @@ -124,35 +124,36 @@ "zone.js": "0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "13.2.3", + "@angular-devkit/build-angular": "13.3.5", "@angular-eslint/eslint-plugin": "13.0.1", "@angular-eslint/eslint-plugin-template": "13.0.1", "@angular-eslint/template-parser": "13.0.1", - "@angular/cli": "13.2.3", - "@angular/compiler-cli": "13.2.2", - "@angular/language-service": "13.2.2", - "@angular/localize": "13.2.2", + "@angular/cli": "13.3.5", + "@angular/compiler-cli": "13.3.6", + "@angular/language-service": "13.3.6", + "@angular/localize": "13.3.6", "@nestjs/schematics": "8.0.5", "@nestjs/testing": "8.2.3", - "@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", - "@storybook/manager-webpack5": "6.4.18", + "@nrwl/cli": "14.1.4", + "@nrwl/cypress": "14.1.4", + "@nrwl/eslint-plugin-nx": "14.1.4", + "@nrwl/jest": "14.1.4", + "@nrwl/nest": "14.1.4", + "@nrwl/node": "14.1.4", + "@nrwl/nx-cloud": "14.0.3", + "@nrwl/storybook": "14.1.4", + "@nrwl/workspace": "14.1.4", + "@storybook/addon-essentials": "6.4.22", + "@storybook/angular": "6.4.22", + "@storybook/builder-webpack5": "6.4.22", + "@storybook/core-server": "6.4.22", + "@storybook/manager-webpack5": "6.4.22", "@types/big.js": "6.1.2", "@types/bull": "3.15.8", "@types/cache-manager": "3.4.2", "@types/color": "3.0.2", "@types/google-spreadsheet": "3.1.5", - "@types/jest": "27.0.2", + "@types/jest": "27.4.1", "@types/lodash": "4.14.174", "@types/node": "14.14.33", "@types/passport-google-oauth20": "2.0.11", @@ -168,14 +169,15 @@ "import-sort-cli": "6.0.0", "import-sort-parser-typescript": "6.0.0", "import-sort-style-module": "6.0.0", - "jest": "27.2.3", + "jest": "27.5.1", "jest-preset-angular": "11.1.1", + "nx": "14.1.4", "prettier": "2.5.1", "replace-in-file": "6.2.0", "rimraf": "3.0.2", - "ts-jest": "27.0.5", + "ts-jest": "27.1.4", "ts-node": "9.1.1", - "typescript": "4.5.5" + "typescript": "4.6.4" }, "engines": { "node": ">=14" diff --git a/yarn.lock b/yarn.lock index 5f0c38d5e..cb8212c4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,31 +2,31 @@ # yarn lockfile v1 -"@ampproject/remapping@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-1.1.1.tgz#e220d0a5288b07afd6392a584d15921839e9da32" - integrity sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw== +"@ampproject/remapping@2.2.0", "@ampproject/remapping@^2.0.0", "@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - sourcemap-codec "1.4.8" + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" -"@angular-devkit/architect@0.1302.3": - version "0.1302.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1302.3.tgz#0e71cc2d737be6828d4072f53872aa2b17940870" - integrity sha512-0m8jMKrFfIqsYt33zTUwSmyekyfuS67hna08RQ6USjzWQSE3z4S8ulCUARSjM6AzdMblX+whfy56nJUpT17NSA== +"@angular-devkit/architect@0.1303.5": + version "0.1303.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1303.5.tgz#845a4df25e94d64e75943e16b085413e362e7b21" + integrity sha512-ZF5Vul8UqwDSwYPxJ4YvdG7lmciJZ1nncyt9Dbk0swxw4MGdy0ZIf+91o318qUn/5JrttQ7ZCYoCZJCjYOSBtw== dependencies: - "@angular-devkit/core" "13.2.3" + "@angular-devkit/core" "13.3.5" rxjs "6.6.7" -"@angular-devkit/build-angular@13.2.3": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-13.2.3.tgz#03d9bb0f3a620a9125c2794e801c7071a9fea7e7" - integrity sha512-cZ2gRcMRgW3t1WCeP+2D/wmr2M+BR/RICAh0wL9irIdypWAzIFt3Z2+2R/HmgAAxoEkdUMIfB9AnkYmwRVgFeA== +"@angular-devkit/build-angular@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-13.3.5.tgz#81fca9708609dac52c7664b46d4d5612b1a216ad" + integrity sha512-6ZQ788U0vT7KqMZeOsNQxP01IhOpxlbKonxK2fZNju8e+Ha2K77yV9A9XMbmcUGWRRHCOFvUEaJhvxDFsunESg== dependencies: - "@ampproject/remapping" "1.1.1" - "@angular-devkit/architect" "0.1302.3" - "@angular-devkit/build-webpack" "0.1302.3" - "@angular-devkit/core" "13.2.3" + "@ampproject/remapping" "2.2.0" + "@angular-devkit/architect" "0.1303.5" + "@angular-devkit/build-webpack" "0.1303.5" + "@angular-devkit/core" "13.3.5" "@babel/core" "7.16.12" "@babel/generator" "7.16.8" "@babel/helper-annotate-as-pure" "7.16.7" @@ -37,7 +37,7 @@ "@babel/runtime" "7.16.7" "@babel/template" "7.16.7" "@discoveryjs/json-ext" "0.5.6" - "@ngtools/webpack" "13.2.3" + "@ngtools/webpack" "13.3.5" ansi-colors "4.1.1" babel-loader "8.2.3" babel-plugin-istanbul "6.1.1" @@ -48,7 +48,7 @@ core-js "3.20.3" critters "0.0.16" css-loader "6.5.1" - esbuild-wasm "0.14.14" + esbuild-wasm "0.14.22" glob "7.2.0" https-proxy-agent "5.0.0" inquirer "8.2.0" @@ -56,10 +56,10 @@ karma-source-map-support "1.4.0" less "4.1.2" less-loader "10.2.0" - license-webpack-plugin "4.0.1" + license-webpack-plugin "4.0.2" loader-utils "3.2.0" mini-css-extract-plugin "2.5.3" - minimatch "3.0.4" + minimatch "3.0.5" open "8.4.0" ora "5.4.1" parse5-html-rewriting-stream "6.0.1" @@ -71,31 +71,31 @@ regenerator-runtime "0.13.9" resolve-url-loader "5.0.0" rxjs "6.6.7" - sass "1.49.0" + sass "1.49.9" sass-loader "12.4.0" semver "7.3.5" source-map-loader "3.0.1" source-map-support "0.5.21" stylus "0.56.0" stylus-loader "6.2.0" - terser "5.10.0" + terser "5.11.0" text-table "0.2.0" tree-kill "1.2.2" tslib "2.3.1" - webpack "5.67.0" + webpack "5.70.0" webpack-dev-middleware "5.3.0" webpack-dev-server "4.7.3" webpack-merge "5.8.0" webpack-subresource-integrity "5.1.0" optionalDependencies: - esbuild "0.14.14" + esbuild "0.14.22" -"@angular-devkit/build-webpack@0.1302.3": - version "0.1302.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1302.3.tgz#a1b43f0d61048e91d25f43e2ec14c1545b070655" - integrity sha512-+JYH1lWU0UOjaWYxpoR2VLsdcb6nG9Gv+M1gH+kT0r2sAKOFaHnrksbOvca3EhDoaMa2b9LSGEE0OcSHWnN+eQ== +"@angular-devkit/build-webpack@0.1303.5": + version "0.1303.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1303.5.tgz#241ac7019011411ef4c6d9e63a55643199f6b1c0" + integrity sha512-EI7scRGKPw9Rg4LypUSTf7JM3lE1imTVxY8mY6gqNkRWnvsb5+kptJQ+gK+VZSom/URcPFbN40lJYwgmZBNPeA== dependencies: - "@angular-devkit/architect" "0.1302.3" + "@angular-devkit/architect" "0.1303.5" rxjs "6.6.7" "@angular-devkit/core@13.0.2": @@ -110,10 +110,10 @@ rxjs "6.6.7" source-map "0.7.3" -"@angular-devkit/core@13.2.3": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.2.3.tgz#a5770c7a5e50d1b09e5a41ccb7cf0af140a9e168" - integrity sha512-/47RA8qmWzeS60xSdaprIn1MiSv0Iw83t0M9/ENH7irFS5vMAq62NCcwiWXH59pZmvvLbF+7xy/RgYUZLr4nHQ== +"@angular-devkit/core@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.3.5.tgz#c5f32f4f99b5cad8df9cf3cf4da9c4b1335c1155" + integrity sha512-w7vzK4VoYP9rLgxJ2SwEfrkpKybdD+QgQZlsDBzT0C6Ebp7b4gkNcNVFo8EiZvfDl6Yplw2IAP7g7fs3STn0hQ== dependencies: ajv "8.9.0" ajv-formats "2.1.1" @@ -133,12 +133,12 @@ ora "5.4.1" rxjs "6.6.7" -"@angular-devkit/schematics@13.2.3", "@angular-devkit/schematics@~13.2.0": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.2.3.tgz#c2acb68ba798f4ab115bee73f078d98f5b20d21c" - integrity sha512-+dyC4iKV0huvpjiuz4uyjLNK3FsCIp/Ghv5lXvhG6yok/dCAubsJItJOxi6G16aVCzG/E9zbsDfm9fNMyVOkgQ== +"@angular-devkit/schematics@13.3.5", "@angular-devkit/schematics@~13.3.0": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-13.3.5.tgz#9cb03ac99ee14173a6fa00fd7ca94fa42600c163" + integrity sha512-0N/kL/Vfx0yVAEwa3HYxNx9wYb+G9r1JrLjJQQzDp+z9LtcojNf7j3oey6NXrDUs1WjVZOa/AIdRl3/DuaoG5w== dependencies: - "@angular-devkit/core" "13.2.3" + "@angular-devkit/core" "13.3.5" jsonc-parser "3.0.0" magic-string "0.25.7" ora "5.4.1" @@ -183,31 +183,31 @@ "@angular-eslint/bundled-angular-compiler" "13.0.1" "@typescript-eslint/experimental-utils" "5.3.0" -"@angular/animations@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-13.2.2.tgz#b6af6962a721d73a3832d6f4891dc644bee50ed7" - integrity sha512-qX8LAMuCJaueHBVyuwKtqunx96G0Dr26k7y5Z03VTcscYst4Ib4V2d4i5dwn3HS82DehFdO86cm3Hi2PqE/qww== +"@angular/animations@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-13.3.6.tgz#a889e1422d30ce8d0b5e70072ab43143966d8d6d" + integrity sha512-uPIj5Fm5zRAmrHatmHr21LnL2rZNdL08aNIrIlPJxmjekvUUYphfRHwuhNQKS97uucWnlxGgMB/R6iAiisn5Hg== dependencies: tslib "^2.3.0" -"@angular/cdk@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-13.2.2.tgz#4d41b1da6e92e3bc80d28aabe2049fa7ca40f5b1" - integrity sha512-cT5DIaz+NI9IGb3X61Wh26+L6zdRcOXT1BP37iRbK2Qa2qM8/0VNeK6hrBBIblyoHKR/WUmRlS8XYf6mmArpZw== +"@angular/cdk@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-13.3.6.tgz#4b62d33f82bd4cddaae78c53d2485ca385a26bae" + integrity sha512-sPwEGCHARxuUMzPLRiiN51GQP0P39ev+eL06NcyYXZ3AxyZIH5N/PWmGKf7EDOXIof4ata13jsIeW38mFe+wvg== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^5.0.0" -"@angular/cli@13.2.3": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-13.2.3.tgz#4a1694025bf0ac589aeaec17a6ba560647cf07b0" - integrity sha512-QsakxpdQuO67u4fQNuOASqabYUO9gJb/5CpUGpWbuBzru0/9CMEF1CtXoF4EoDiwa5sJMirz3SJMKhtzFlv1cQ== +"@angular/cli@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-13.3.5.tgz#908ff97f59836d1dbd79d6cae85337ddc5f9ff37" + integrity sha512-FrPg86cfmm0arWZInt55muCTpcQSNlvoViVrIVkyqSN06GoyCAQ2zn6/OYJnx/XAg/XvXTbygL+58c0WXuOaiA== dependencies: - "@angular-devkit/architect" "0.1302.3" - "@angular-devkit/core" "13.2.3" - "@angular-devkit/schematics" "13.2.3" - "@schematics/angular" "13.2.3" + "@angular-devkit/architect" "0.1303.5" + "@angular-devkit/core" "13.3.5" + "@angular-devkit/schematics" "13.3.5" + "@schematics/angular" "13.3.5" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" debug "4.3.3" @@ -224,33 +224,33 @@ symbol-observable "4.0.0" uuid "8.3.2" -"@angular/common@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-13.2.2.tgz#3c787a53acf69633902e804c141c39a3455d85d3" - integrity sha512-56C/bheNLKtTCyQUZCiYtKbBIZN9jj6rjFILPtJCGls3cBCxp7t9tIdoLiQG/wVQRmaxdj1ioLT+sCWz7mLtQw== +"@angular/common@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-13.3.6.tgz#94dcbab315cd4c197b1a613d7c16f57fa088b004" + integrity sha512-IHWzyNzrvzdE23P7KX3qDgmT+xGpmXKaMLDEM9s8l2opKcMerjdullrYXwLfjFQr9pybnn+uE9fzj9dp0DCmzg== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-13.2.2.tgz#f2c4c207fdbcc274c7a153b3506c3b8f99fc6f59" - integrity sha512-tuOIcEEKVIht+mKrj0rtX3I8gc+ByPjzpCZhFQRggxM6xbKJIToO1zERbEGKrZ+sUJ6BB5KLvscDy+Pddy3b8w== +"@angular/compiler-cli@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-13.3.6.tgz#d2b11b27f9c593e9e14da7622c4f65f0d842249b" + integrity sha512-m3XKkMIfD6ERUNuIwsNpLkIQq/yHKyCBrCFb+V+XBVsEkpl9pfLGJEpg3ETa5y+EdRbIYnPSWmLQDM+taD1FDA== dependencies: - "@babel/core" "^7.8.6" + "@babel/core" "^7.17.2" chokidar "^3.0.0" convert-source-map "^1.5.1" dependency-graph "^0.11.0" - magic-string "^0.25.0" + magic-string "^0.26.0" reflect-metadata "^0.1.2" semver "^7.0.0" sourcemap-codec "^1.4.8" tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.2.2.tgz#89999576906f4d65b58ecff38666c71809ddee45" - integrity sha512-XXQtB0/e7pR2LPrHmpEiTU72SX4xxHGy91vYWIj1JCjSn0fYF7vtHzSJPXDvkbnkNow/PXXzJJYaU1ctdMZPcA== +"@angular/compiler@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.3.6.tgz#c9fee7d0d6d92139fbc1f785eea60dcce96f1cd7" + integrity sha512-HnAttP3Ds+0GwFAA4ZnY13Y29xURCZdZoljb7+ZZHDmtBesRwX8Y1TwLx6rNnqqakNr3WEXyf65bbFc7ICrXRg== dependencies: tslib "^2.3.0" @@ -259,10 +259,10 @@ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== -"@angular/core@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-13.2.2.tgz#30d2cd6ed3d74d90071c135def7163b12137eb7f" - integrity sha512-zpctw0BxIVOsRFnckchK15SD1L8tzhf5GzwIDaM6+VylDQj1uYkm8mvAjJTQZyUuApomoFet2Rfj7XQPV+cNSQ== +"@angular/core@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-13.3.6.tgz#2b6c041ce6314e54d7ba62f883ac10f091bf75e0" + integrity sha512-Le9kWVny8jT8UXoWUFFF641howw8h//PK+juDHDU2M6DpR24zxnBKdTwVvjjtCyfMBMiC50JY5YRnYfpaEC27w== dependencies: tslib "^2.3.0" @@ -271,52 +271,52 @@ resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== -"@angular/forms@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-13.2.2.tgz#cdc4f249a85160eab003104d7050f8797a2038a7" - integrity sha512-T61W4Ay9X9qhxjc6lLqpNFeHrGKwg2mqdsZ3zIm/c7oKo37mgl9TB5kkrtnS+205r3N2hF4ICnGFZ4a/egUP/g== +"@angular/forms@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-13.3.6.tgz#c6a97ae4835288fa17ade643a026ad673253eddf" + integrity sha512-Sn29oKvyduMmB2GizXs04hOub/iqOPpSZFxEdTFmKVOjEidv76rcHKcHUby2Qwyal22+z1wVQNn3w4l8rySx0A== dependencies: tslib "^2.3.0" -"@angular/language-service@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-13.2.2.tgz#4de8742dc7603a331cb39be065f5a30d0bdc0fad" - integrity sha512-2P5+wRsbHgpI2rVeFwnsLWxyntUiw8kG9Tqh5BkVDqtQovbYtzFiaMkf5TFz/g938JBBgeRQzvXr1kQhEidAWQ== +"@angular/language-service@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-13.3.6.tgz#2dbb3a0a560659cf5cf3bf8b2b1c9a77adaf0229" + integrity sha512-T1ckdV2/oF2My4mO2MUMqP33S4jhjQ46gs1IR1si1bdKoRzaWmWb/T1H5rV2dfoDiah7rmlk1i1H1VJpSyTBWA== -"@angular/localize@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-13.2.2.tgz#a19ee864e362baf1d538d56b9021e7d71d7293c9" - integrity sha512-W/deDIxAFlv0euq7hxYPRCvj2DtDTsid5mRqyjEaVr+eYaLPnD8EElZuKuQEDeo7rWkCjljEpBrFsXwC5FZ8cw== +"@angular/localize@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-13.3.6.tgz#f6307c02b407487cec39157a8bd7a587d943f5d6" + integrity sha512-atSE02+EyLHCUjp8uNd0fxAIKa/qlHkfRh0MYeKdG5YRnG5qyS19cv0AVRYPM7K4HffUfpXKPJg8RMQ8CdDi4g== dependencies: - "@babel/core" "7.8.6" + "@babel/core" "7.17.2" glob "7.2.0" yargs "^17.2.1" -"@angular/material@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-13.2.2.tgz#d673f68b0c9357ada2ee82ffeaf11f10f5d49b27" - integrity sha512-YAjPp2+/wuEOPfkAxdRVdbWHiK4P3DgMZa9qP/NizN2lTXNrftEfD09ZlPIFMZRnnExezJ2LnO7eyELpc1VSKg== +"@angular/material@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-13.3.6.tgz#95a55f37c40b39d3593efe02b5e59f864e31a72f" + integrity sha512-V+3Fs9JK+7KlcdJG/Oa/IQuLHC8WYzuL8ffH1Q06ANSzGxSjsAHs4FZQpKUpVBoL3E+p9Yz+zkKe91k5L62VoQ== dependencies: tslib "^2.3.0" -"@angular/platform-browser-dynamic@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.2.2.tgz#9a50fb79b96b62c13e65c996727542c5174e9246" - integrity sha512-lj6xwat0StLp+ROFqXU62upwHQhlxaQi0djhrS+DGKUK0Xu9bkBeaSCfBFgS78jPm1SwL8Xztu9/vuDAHLRrqw== +"@angular/platform-browser-dynamic@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.3.6.tgz#5a962fcfd0db7600b2ff78b272b24f166d03b1f4" + integrity sha512-FcQlodqhjnxaa7b9Gskfi9Lk8L3970lRuFSXtzJmeASyqnTj9UaLkE5kpSJqh07Ulxf2UFU4SD/nP1QChbvPuA== dependencies: tslib "^2.3.0" -"@angular/platform-browser@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-13.2.2.tgz#e603c32c3550970211b004df3e227e0d2b5088cd" - integrity sha512-M7gWC8fFCPc/CRcHCzqe/j7WzwAUMeKt9vwlK633XnesHBoqZdYgbb3YHHc6WPVU0YI09Nb/Hm5sezEKmjUmPg== +"@angular/platform-browser@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-13.3.6.tgz#59b89346034f24d0f07c1c3dd1474fb6c1c199b5" + integrity sha512-ilzlMQe7Vg5fMnzUoe01SwBCjruJy2xna/jdXND4xHyntk2bUtOHOBnkwCiN3uAWDg228iS2tWKDbYGKoiRYmQ== dependencies: tslib "^2.3.0" -"@angular/router@13.2.2": - version "13.2.2" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-13.2.2.tgz#ba637da8963e5905520622f02357c4eda2f3da68" - integrity sha512-dt2b9/kGJAkmOqUmUD3aKlp4pGpdqLwB0zmhUYF3ktNEcQaPf4ZjWT/4jhy09gFL+TKOHG5OQW9GxBbhWI4bSg== +"@angular/router@13.3.6": + version "13.3.6" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-13.3.6.tgz#1ee2113f6e77513777824fedcae0beb1ed4b6974" + integrity sha512-XN3kaDdrqq/2JJbVaOs5pznsfOPwaqO99TADmWtwHlrrqBk7DUW730YijaCE6HZCZSjFpykUGVz3J1aXSULU2Q== dependencies: tslib "^2.3.0" @@ -361,6 +361,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== +"@babel/compat-data@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" + integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -404,28 +409,28 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/core@7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.6.tgz#27d7df9258a45c2e686b6f18b6c659e563aa4636" - integrity sha512-Sheg7yEJD51YHAvLEV/7Uvw95AeWqYPL3Vk3zGujJKIhJ+8oLw2ALaf3hbucILhKsgSoADOvtKRJuNVdcJkOrg== +"@babel/core@7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.2.tgz#2c77fc430e95139d816d39b113b31bf40fb22337" + integrity sha512-R3VH5G42VSDolRHyUO4V2cfag8WHcZyxdq5Z/m8Xyb92lW/Erm/6kM+XtRFGf3Mulre3mveni2NHfEUws8wSvw== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.6" - "@babel/helpers" "^7.8.4" - "@babel/parser" "^7.8.6" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" + "@ampproject/remapping" "^2.0.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.0" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.17.2" + "@babel/parser" "^7.17.0" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.0" + "@babel/types" "^7.17.0" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.0" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" -"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.2.2", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.6": +"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.2.2", "@babel/core@^7.7.2", "@babel/core@^7.7.5": version "7.15.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.5.tgz#f8ed9ace730722544609f90c9bb49162dc3bf5b9" integrity sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg== @@ -467,6 +472,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.17.2", "@babel/core@^7.8.0": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.10.tgz#74ef0fbf56b7dfc3f198fc2d927f4f03e12f4b05" + integrity sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.10" + "@babel/helper-compilation-targets" "^7.17.10" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.10" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.10" + "@babel/types" "^7.17.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/generator@7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" @@ -485,7 +511,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.16.0", "@babel/generator@^7.8.6": +"@babel/generator@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== @@ -503,6 +529,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.10.tgz#c281fa35b0c349bbe9d02916f4ae08fc85ed7189" + integrity sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg== + dependencies: + "@babel/types" "^7.17.10" + "@jridgewell/gen-mapping" "^0.1.0" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@7.16.7", "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -563,6 +598,16 @@ browserslist "^4.17.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz#09c63106d47af93cf31803db6bc49fef354e2ebe" + integrity sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz#7f977c17bd12a5fba363cb19bea090394bf37d2e" @@ -708,6 +753,14 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + "@babel/helper-get-function-arity@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" @@ -834,6 +887,20 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + "@babel/helper-optimise-call-expression@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" @@ -940,6 +1007,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== + dependencies: + "@babel/types" "^7.17.0" + "@babel/helper-skip-transparent-expression-wrappers@^7.14.5", "@babel/helper-skip-transparent-expression-wrappers@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb" @@ -1029,7 +1103,7 @@ "@babel/traverse" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/helpers@^7.16.0", "@babel/helpers@^7.8.4": +"@babel/helpers@^7.16.0": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== @@ -1047,6 +1121,15 @@ "@babel/traverse" "^7.17.0" "@babel/types" "^7.17.0" +"@babel/helpers@^7.17.2", "@babel/helpers@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.9" + "@babel/types" "^7.17.0" + "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" @@ -1074,12 +1157,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0-beta.54", "@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5", "@babel/parser@^7.7.2": +"@babel/parser@^7.0.0-beta.54", "@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5": version "7.15.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.5.tgz#d33a58ca69facc05b26adfe4abebfed56c1c2dac" integrity sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg== -"@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3", "@babel/parser@^7.8.6": +"@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3": version "7.16.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== @@ -1089,6 +1172,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c" integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw== +"@babel/parser@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" + integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" @@ -2355,7 +2443,7 @@ "@babel/parser" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/template@^7.16.0", "@babel/template@^7.8.6": +"@babel/template@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== @@ -2364,7 +2452,7 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.1.0", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.7.2": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== @@ -2379,7 +2467,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3", "@babel/traverse@^7.8.6": +"@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== @@ -2410,7 +2498,23 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6": +"@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.10.tgz#1ee1a5ac39f4eac844e6cf855b35520e5eb6f8b5" + integrity sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.10" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.10" + "@babel/types" "^7.17.10" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.4.tgz#74eeb86dbd6748d2741396557b9860e57fce0a0d" integrity sha512-0f1HJFuGmmbrKTCZtbm3cU+b/AqdEYk5toj5iQur58xkVMlS0JWaKxTBSmCXd47uiN7vbcozAupm6Mvs80GNhw== @@ -2434,6 +2538,14 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.10.tgz#d35d7b4467e439fcf06d195f8100e0fea7fc82c4" + integrity sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -2681,47 +2793,47 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.2.2", "@jest/console@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.3.1.tgz#e8ea3a475d3f8162f23d69efbfaa9cbe486bee93" - integrity sha512-RkFNWmv0iui+qsOr/29q9dyfKTTT5DCuP31kUwg7rmOKPT/ozLeGLKJKVIiOfbiKyleUZKIrHwhmiZWVe8IMdw== +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.3.1" - jest-util "^27.3.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" -"@jest/core@^27.2.3", "@jest/core@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.3.1.tgz#04992ef1b58b17c459afb87ab56d81e63d386925" - integrity sha512-DMNE90RR5QKx0EA+wqe3/TNEwiRpOkhshKNxtLxd4rt3IZpCt+RSL+FoJsGeblRZmqdK4upHA/mKKGPPRAifhg== - dependencies: - "@jest/console" "^27.3.1" - "@jest/reporters" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.8.1" exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^27.3.0" - jest-config "^27.3.1" - jest-haste-map "^27.3.1" - jest-message-util "^27.3.1" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-resolve-dependencies "^27.3.1" - jest-runner "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" - jest-watcher "^27.3.1" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" @@ -2737,15 +2849,15 @@ "@types/node" "*" jest-mock "^27.1.0" -"@jest/environment@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.3.1.tgz#2182defbce8d385fd51c5e7c7050f510bd4c86b1" - integrity sha512-BCKCj4mOVLme6Tanoyc9k0ultp3pnmuyHw73UHRPeeZxirsU/7E3HC4le/VDb/SMzE1JcPnto+XBKFOcoiJzVw== +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== dependencies: - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.3.0" + jest-mock "^27.5.1" "@jest/fake-timers@^27.1.0": version "27.1.0" @@ -2759,126 +2871,86 @@ jest-mock "^27.1.0" jest-util "^27.1.0" -"@jest/fake-timers@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.3.1.tgz#1fad860ee9b13034762cdb94266e95609dfce641" - integrity sha512-M3ZFgwwlqJtWZ+QkBG5NmC23A9w+A6ZxNsO5nJxJsKYt4yguBd3i8TpjQz5NfCX91nEve1KqD9RA2Q+Q1uWqoA== +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^27.3.1" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" -"@jest/globals@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.3.1.tgz#ce1dfb03d379237a9da6c1b99ecfaca1922a5f9e" - integrity sha512-Q651FWiWQAIFiN+zS51xqhdZ8g9b88nGCobC87argAxA7nMfNQq0Q0i9zTfQYgLa6qFXk2cGANEqfK051CZ8Pg== +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== dependencies: - "@jest/environment" "^27.3.1" - "@jest/types" "^27.2.5" - expect "^27.3.1" + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" -"@jest/reporters@27.2.2": - version "27.2.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.2.2.tgz#e2d41cd9f8088676b81b9a9908cb1ba67bdbee78" - integrity sha512-ufwZ8XoLChEfPffDeVGroYbhbcYPom3zKDiv4Flhe97rr/o2IfUXoWkDUDoyJ3/V36RFIMjokSu0IJ/pbFtbHg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.2.2" - "@jest/test-result" "^27.2.2" - "@jest/transform" "^27.2.2" - "@jest/types" "^27.1.1" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^27.2.2" - jest-resolve "^27.2.2" - jest-util "^27.2.0" - jest-worker "^27.2.2" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^8.0.0" - -"@jest/reporters@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.3.1.tgz#28b5c1f5789481e23788048fa822ed15486430b9" - integrity sha512-m2YxPmL9Qn1emFVgZGEiMwDntDxRRQ2D58tiDQlwYTg5GvbFOKseYCcHtn0WsI8CG4vzPglo3nqbOiT8ySBT/w== +"@jest/reporters@27.5.1", "@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" + istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^27.3.1" - jest-resolve "^27.3.1" - jest-util "^27.3.1" - jest-worker "^27.3.1" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/source-map@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.6.tgz#be9e9b93565d49b0548b86e232092491fb60551f" - integrity sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g== +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== dependencies: callsites "^3.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" source-map "^0.6.0" -"@jest/test-result@27.2.2": - version "27.2.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.2.2.tgz#cd4ba1ca9b0521e463bd4b32349ba1842277563b" - integrity sha512-yENoDEoWlEFI7l5z7UYyJb/y5Q8RqbPd4neAVhKr6l+vVaQOPKf8V/IseSMJI9+urDUIxgssA7RGNyCRhGjZvw== - dependencies: - "@jest/console" "^27.2.2" - "@jest/types" "^27.1.1" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-result@^27.2.2", "@jest/test-result@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.3.1.tgz#89adee8b771877c69b3b8d59f52f29dccc300194" - integrity sha512-mLn6Thm+w2yl0opM8J/QnPTqrfS4FoXsXF2WIWJb2O/GBSyResL71BRuMYbYRsGt7ELwS5JGcEcGb52BNrumgg== +"@jest/test-result@27.5.1", "@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: - "@jest/console" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.2.2", "@jest/test-sequencer@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.3.1.tgz#4b3bde2dbb05ee74afdae608cf0768e3354683b1" - integrity sha512-siySLo07IMEdSjA4fqEnxfIX8lB/lWYsBPwNFtkOvsFQvmBrL3yj3k3uFNZv/JDyApTakRpxbKLJ3CT8UGVCrA== +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== dependencies: - "@jest/test-result" "^27.3.1" - graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-runtime "^27.3.1" + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" "@jest/transform@^26.6.2": version "26.6.2" @@ -2901,23 +2973,23 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/transform@^27.2.2", "@jest/transform@^27.3.1": - version "27.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.3.1.tgz#ff80eafbeabe811e9025e4b6f452126718455220" - integrity sha512-3fSvQ02kuvjOI1C1ssqMVBKJpZf6nwoCiSu00zAKh5nrp3SptNtZy/8s5deayHnqxhjD9CWDJ+yqQwuQ0ZafXQ== +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^27.2.5" - babel-plugin-istanbul "^6.0.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-regex-util "^27.0.6" - jest-util "^27.3.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" micromatch "^4.0.4" - pirates "^4.0.1" + pirates "^4.0.4" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" @@ -2944,10 +3016,10 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jest/types@^27.1.1", "@jest/types@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" - integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ== +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -2955,11 +3027,37 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.4" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72" integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg== +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.10" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.10.tgz#db436f0917d655393851bc258918c00226c9b183" + integrity sha512-Q0YbBd6OTsXm8Y21+YUSDXupHnodNC2M4O18jtd3iwJ3+vMZNdKGols0a9G6JOK0dcJ3IdUUHoh908ZI6qhk8Q== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mdx-js/loader@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4" @@ -3120,10 +3218,10 @@ optional "0.1.4" tslib "2.3.1" -"@ngtools/webpack@13.2.3": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-13.2.3.tgz#a4434eb5e2ec3cc04d7f714f20d5203114563f33" - integrity sha512-wooUZiV92QyoeFxkhqIwH/cfiAAAn+l8fEEuaaEIfJtpjpbShvvlboEVsqb28soeGiFJfLcmsZM3mUFgsG4QBQ== +"@ngtools/webpack@13.3.5": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-13.3.5.tgz#bb7dde18fb30919c49c4a25635b94381d19b7622" + integrity sha512-OaMZR0rO0ljBHamLwzddfZX03ijtpheUpjH5dNzMNyNrrpKgS4/3jTQ1wvs2j3zzKfKjOS12WG0905QFJYWG6g== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -3211,19 +3309,23 @@ node-gyp "^8.2.0" read-package-json-fast "^2.0.1" -"@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.5" - "@nrwl/devkit" "13.8.5" - "@nrwl/jest" "13.8.5" - "@nrwl/linter" "13.8.5" - "@nrwl/storybook" "13.8.5" +"@nrwl/angular@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-14.1.4.tgz#c3a61291632f94f85202c9a52ab76e4c64f04b19" + integrity sha512-ez4MYei2H/9rdk1NFJ2mu1IKoxxlGjwBZA9c4g4ifKkW9I37CCTAcwkEXGIXRpn0A0sq0Zx9tpS1mXV/vqFB6A== + dependencies: + "@angular-devkit/schematics" "~13.3.0" + "@nrwl/cypress" "14.1.4" + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/storybook" "14.1.4" + "@nrwl/workspace" "14.1.4" "@phenomnomnominal/tsquery" "4.1.1" - "@schematics/angular" "~13.2.0" + "@schematics/angular" "~13.3.0" + chalk "4.1.0" + chokidar "^3.5.1" + http-server "^14.1.0" ignore "^5.0.4" jasmine-marbles "~0.8.4" rxjs-for-await "0.0.2" @@ -3231,28 +3333,25 @@ ts-node "~9.1.1" tsconfig-paths "^3.9.0" tslib "^2.3.0" + webpack "^5.58.1" webpack-merge "5.7.3" -"@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== +"@nrwl/cli@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-14.1.4.tgz#4d896bbfcb49f058711dd47a67c422297db03be9" + integrity sha512-XjJe/bnOKDYvvY9IxKdETWTGekRu/cN9wThwwlo8xo4Tqy1VK0MfqpDDALrFYp739NG5Zyuhtbz5+Ko48RK75g== dependencies: - "@nrwl/tao" "13.8.5" - chalk "4.1.0" - enquirer "~2.3.6" - v8-compile-cache "2.3.0" - yargs-parser "20.0.0" + nx "14.1.4" -"@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== +"@nrwl/cypress@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-14.1.4.tgz#7ac38ead60afee62e1d253f958bba7f575e2b698" + integrity sha512-3uokpFludvqms8SWEG3vQyitM6zooHSx0yTKni0NuF0Ce4v359GOY00ijZLORUxla7y1jVe8b8x/BHxEem+ZmQ== dependencies: "@cypress/webpack-preprocessor" "^5.9.1" - "@nrwl/devkit" "13.8.5" - "@nrwl/linter" "13.8.5" - "@nrwl/workspace" "13.8.5" + "@nrwl/devkit" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/workspace" "14.1.4" chalk "4.1.0" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" @@ -3263,58 +3362,57 @@ tslib "^2.3.0" webpack-node-externals "^3.0.0" -"@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== +"@nrwl/devkit@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-14.1.4.tgz#7cabec88109ec8fec60b11aba7cbfddc4b9e771e" + integrity sha512-Q7/cDrULhqdbfPRp8N0x9y67r49ApzYG+CVEXI8VowwHFAI3zCr/PNlzsfuLL4l2Uj0jYOt+LPoNbFZuCLAQLA== dependencies: - "@nrwl/tao" "13.8.5" - ejs "^3.1.5" + ejs "^3.1.7" ignore "^5.0.4" rxjs "^6.5.4" semver "7.3.4" tslib "^2.3.0" -"@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== +"@nrwl/eslint-plugin-nx@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-14.1.4.tgz#4a9f7ae291d863fdc0e70b74d036b599c374e133" + integrity sha512-tuE+MXU2o9qRt8734iuuONC8PCnWt1g+iQ0Ywg8O7zwsnpsjNGWoeq5rcNc6C6FrnnbDJQt78aPbk1jzuwKGxw== dependencies: - "@nrwl/devkit" "13.8.5" - "@nrwl/workspace" "13.8.5" - "@typescript-eslint/experimental-utils" "~5.10.0" + "@nrwl/devkit" "14.1.4" + "@nrwl/workspace" "14.1.4" + "@typescript-eslint/experimental-utils" "~5.18.0" chalk "4.1.0" confusing-browser-globals "^1.0.9" -"@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== +"@nrwl/jest@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-14.1.4.tgz#c69ad97913fba78ae9eabce83c9cec324a29881b" + integrity sha512-Me8r3QY080gVtEtD3F3FkQI7NU0MJl1PjU+qEcBejk5+NJD31LtPoIW4XHYdep/dPC/Og+m+QWopKK97K7Yuaw== dependencies: - "@jest/reporters" "27.2.2" - "@jest/test-result" "27.2.2" - "@nrwl/devkit" "13.8.5" + "@jest/reporters" "27.5.1" + "@jest/test-result" "27.5.1" + "@nrwl/devkit" "14.1.4" chalk "4.1.0" identity-obj-proxy "3.0.0" - jest-config "27.2.2" - jest-resolve "27.2.2" - jest-util "27.2.0" + jest-config "27.5.1" + jest-resolve "27.5.1" + jest-util "27.5.1" resolve.exports "1.1.0" rxjs "^6.5.4" tslib "^2.3.0" -"@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== +"@nrwl/js@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/js/-/js-14.1.4.tgz#08f857c01fa343b45afe57be2bf05c7d8c976fbf" + integrity sha512-d3rgi/WuE1ZMWoKDtHQD9DJL918s6pQX7u3Jq8HyWt8w7bOxfwfmFs0b3J80tkQpgfBNTXWEaFdsml9+ZQhGTw== dependencies: - "@nrwl/devkit" "13.8.5" - "@nrwl/jest" "13.8.5" - "@nrwl/linter" "13.8.5" - "@nrwl/workspace" "13.8.5" + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/workspace" "14.1.4" "@parcel/watcher" "2.0.4" chalk "4.1.0" - fast-glob "^3.2.7" + fast-glob "3.2.7" fs-extra "^9.1.0" ignore "^5.0.4" js-tokens "^4.0.0" @@ -3322,46 +3420,46 @@ 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== +"@nrwl/linter@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-14.1.4.tgz#6e10da0b8e2ea3b38668cc1ac77b67b633c8be8f" + integrity sha512-DUChINM9aoR3nuoSjV2vMoaFrM6PwPamEnN0WFowO4x+DRpEdp4I5IPpoSCyXTnrQB9EUVWHBo0vgeYILtgKlA== dependencies: - "@nrwl/devkit" "13.8.5" - "@nrwl/jest" "13.8.5" + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" "@phenomnomnominal/tsquery" "4.1.1" tmp "~0.2.1" tslib "^2.3.0" -"@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== +"@nrwl/nest@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-14.1.4.tgz#0929b53540241e3ab8444757af976338a8b547dc" + integrity sha512-PpjhbnZW1nEJ368y12ePee2OxoZKkXV9Eur+n08seqaMcdhLCuHaa+LkHQpYT5LpP4WdLCiXBu0DZTIZjaeuaQ== dependencies: "@nestjs/schematics" "^8.0.0" - "@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" + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" + "@nrwl/js" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/node" "14.1.4" + +"@nrwl/node@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-14.1.4.tgz#c23337570a50a23747bd44cf50c832c3c0a4777e" + integrity sha512-Exrq+XDz1+tej+DzhOFl2buFgj5mhYixIj1Da0E6lBEb6x7i1DC8niaAOtU7c2YAQoofXNM4THQI+U7MIPo2Kw== + dependencies: + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" + "@nrwl/js" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/workspace" "14.1.4" chalk "4.1.0" copy-webpack-plugin "^9.0.1" enhanced-resolve "^5.8.3" fork-ts-checker-webpack-plugin "6.2.10" fs-extra "^9.1.0" glob "7.1.4" - license-webpack-plugin "4.0.0" + license-webpack-plugin "^4.0.2" rxjs "^6.5.4" rxjs-for-await "0.0.2" source-map-support "0.5.19" @@ -3376,51 +3474,46 @@ webpack-merge "^5.8.0" webpack-node-externals "^3.0.0" -"@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== +"@nrwl/nx-cloud@14.0.3": + version "14.0.3" + resolved "https://registry.yarnpkg.com/@nrwl/nx-cloud/-/nx-cloud-14.0.3.tgz#fda3d75274ca6e8f4a6f469b294a4b65b54bdc28" + integrity sha512-08t9vnRzA5SAkG6gp+NrAGauraepHH0v4IByO4NQ+7JJ7JMWw0nt6fp/70wofRmem7MGQ+Y/AIgIc5T0m89k+w== dependencies: - "@nrwl/cypress" "13.8.5" - "@nrwl/devkit" "13.8.5" - "@nrwl/linter" "13.8.5" - "@nrwl/workspace" "13.8.5" + axios "^0.21.1" + chalk "4.1.0" + node-machine-id "^1.1.12" + strip-json-comments "^3.1.1" + tar "6.1.11" + +"@nrwl/storybook@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-14.1.4.tgz#5c9f10afd1a8cae69f28fc364662379f70d05972" + integrity sha512-AymUiswW6ENXlhS0bMGNjPd+ff8/fW+u+S/+2KKN5kzAb4s7stfyB/UnX/1HXRrb/8kaahxElf3CUCmps+77ig== + dependencies: + "@nrwl/cypress" "14.1.4" + "@nrwl/devkit" "14.1.4" + "@nrwl/linter" "14.1.4" + "@nrwl/workspace" "14.1.4" 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.5": - version "13.8.5" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-13.8.5.tgz#223e93dbfe11b47c4c13a66cc9086c2f2572b1ae" - integrity sha512-ENT6wpxjSWBYKeLT0YueVFehlN1K2lJzgVOJTk4cQ0LbTw0fJCwcTe4ludiW4hPPTF7P5zzi0PmB9a4ss46tQg== +"@nrwl/tao@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-14.1.4.tgz#20902811c5712732d2944a8e0c4cde031e631694" + integrity sha512-Dk8/CM1/GTCYBuA5RJ59Vz1OJGK3Tf4KVE3QS7UpEkR/iWTPDmTtrcQmTLSgtX+tYgca5dMWHV9WfiJjKW2/ow== 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.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" + nx "14.1.4" -"@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== +"@nrwl/workspace@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-14.1.4.tgz#ec5bfb05d00e7f7bb9eb45b5821ca8f93dad8f86" + integrity sha512-J/mcdlOPu5dkGoXDrGA1pPdEGFqYepRgZSoR+fSWlJT0Z4nb5C9aaXnu6OPl/8qwYFzAmQ5xsrqaaXQqtUHpQQ== dependencies: - "@nrwl/cli" "13.8.5" - "@nrwl/devkit" "13.8.5" - "@nrwl/jest" "13.8.5" - "@nrwl/linter" "13.8.5" + "@nrwl/devkit" "14.1.4" + "@nrwl/jest" "14.1.4" + "@nrwl/linter" "14.1.4" "@parcel/watcher" "2.0.4" chalk "4.1.0" chokidar "^3.5.1" @@ -3435,13 +3528,14 @@ ignore "^5.0.4" minimatch "3.0.4" npm-run-path "^4.0.1" + nx "14.1.4" open "^8.4.0" rxjs "^6.5.4" semver "7.3.4" tmp "~0.2.1" tslib "^2.3.0" - yargs "15.4.1" - yargs-parser "20.0.0" + yargs "^17.4.0" + yargs-parser "21.0.1" "@nuxtjs/opencollective@0.3.2": version "0.3.2" @@ -3526,13 +3620,13 @@ dependencies: any-observable "^0.3.0" -"@schematics/angular@13.2.3", "@schematics/angular@~13.2.0": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-13.2.3.tgz#600bbe21bff5b090aaee17ba726410ee1328b40b" - integrity sha512-jloooGC7eco9AKxlIMMqFRptJYzZ0jNRBStWOp2dCISg6rmOKqpxbsHLtYFQIT1PnlomSxtKDAgYGQMDi9zhXw== +"@schematics/angular@13.3.5", "@schematics/angular@~13.3.0": + version "13.3.5" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-13.3.5.tgz#052e52cd6a58291a5566ddabb75d8c074cfe63b1" + integrity sha512-1Ovx0cq72ZaNCyTyRD8ebIwUzpqhEH9ypWF05bfBLq3J0LlZgewIMhPJSxKmwRC3NQB5DZIYEvD0uhzBIuHCCA== dependencies: - "@angular-devkit/core" "13.2.3" - "@angular-devkit/schematics" "13.2.3" + "@angular-devkit/core" "13.3.5" + "@angular-devkit/schematics" "13.3.5" jsonc-parser "3.0.0" "@simplewebauthn/browser@4.1.0": @@ -3588,17 +3682,17 @@ resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.8.0.tgz#87d950fbbf050dce1566f32ca48c98007239472b" integrity sha512-WazFGUMnbumg8ePNvej8cIOEcxvuZ0ugKQkkE1xFbDYcl7DgJd62MiG+bIqCcQlIdLEfhjAdoixxlFdJgrgjyA== -"@storybook/addon-actions@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.18.tgz#e997060e1b0af62f9f831301a56a3addfc1f1365" - integrity sha512-qPw5qfbWPmyOdaXxAVAbdVLVVE31gRrkH0ESUps+FXVNypRz1/0lJ6M2VrtOHMrFbGBl94SALdqsHOx6OYZKwg== +"@storybook/addon-actions@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.22.tgz#ec1b4332e76a8021dc0a1375dfd71a0760457588" + integrity sha512-t2w3iLXFul+R/1ekYxIEzUOZZmvEa7EzUAVAuCHP4i6x0jBnTTZ7sAIUVRaxVREPguH5IqI/2OklYhKanty2Yw== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.18" + "@storybook/theming" "6.4.22" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -3612,18 +3706,18 @@ util-deprecate "^1.0.2" uuid-browser "^3.1.0" -"@storybook/addon-backgrounds@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.18.tgz#178531ece3848de33b1aaea44af8cdd88da70314" - integrity sha512-LAonQO0s77CkbGx7l8qyeEevOBWDuYZKl9iJ0BSPogU48+4JVEYVSBiystIXPJXcGeEy+M0qFmwg5nHWjf9/cA== +"@storybook/addon-backgrounds@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.4.22.tgz#5d9dbff051eefc1ca6e6c7973c01d17fbef4c2f5" + integrity sha512-xQIV1SsjjRXP7P5tUoGKv+pul1EY8lsV7iBXQb5eGbp4AffBj3qoYBSZbX4uiazl21o0MQiQoeIhhaPVaFIIGg== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.18" + "@storybook/theming" "6.4.22" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" @@ -3631,28 +3725,28 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-controls@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.18.tgz#0b65658a141428e9625ddbe7dee0a761911cd04f" - integrity sha512-nP7JCiAES4S5mn8PYfmPZZG9VpsPV7eeQQRPuiPgdidhH93cmsW/FYj8V739lrm5QJc0JSI6uY/y9qHa9rh43w== +"@storybook/addon-controls@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.4.22.tgz#42c7f426eb7ba6d335e8e14369d6d13401878665" + integrity sha512-f/M/W+7UTEUnr/L6scBMvksq+ZA8GTfh3bomE5FtWyOyaFppq9k8daKAvdYNlzXAOrUUsoZVJDgpb20Z2VBiSQ== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-common" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-common" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.18" - "@storybook/store" "6.4.18" - "@storybook/theming" "6.4.18" + "@storybook/node-logger" "6.4.22" + "@storybook/store" "6.4.22" + "@storybook/theming" "6.4.22" core-js "^3.8.2" lodash "^4.17.21" ts-dedent "^2.0.0" -"@storybook/addon-docs@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.18.tgz#e1f969a63f286649e46a4846b56cc309bccd07c3" - integrity sha512-NcGcrW+2hrzoyWHEaDmw6wxqyV/FDsdLaOS0XZrIQuBaj1rve0IfA1jqggfNo8owqmXXGp8cQBnFbhRES1a7nQ== +"@storybook/addon-docs@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.4.22.tgz#19f22ede8ae31291069af7ab5abbc23fa269012b" + integrity sha512-9j+i+W+BGHJuRe4jUrqk6ubCzP4fc1xgFS2o8pakRiZgPn5kUQPdkticmsyh1XeEJifwhqjKJvkEDrcsleytDA== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -3663,21 +3757,21 @@ "@mdx-js/loader" "^1.6.22" "@mdx-js/mdx" "^1.6.22" "@mdx-js/react" "^1.6.22" - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/builder-webpack4" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/builder-webpack4" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.18" - "@storybook/node-logger" "6.4.18" - "@storybook/postinstall" "6.4.18" - "@storybook/preview-web" "6.4.18" - "@storybook/source-loader" "6.4.18" - "@storybook/store" "6.4.18" - "@storybook/theming" "6.4.18" + "@storybook/csf-tools" "6.4.22" + "@storybook/node-logger" "6.4.22" + "@storybook/postinstall" "6.4.22" + "@storybook/preview-web" "6.4.22" + "@storybook/source-loader" "6.4.22" + "@storybook/store" "6.4.22" + "@storybook/theming" "6.4.22" acorn "^7.4.1" acorn-jsx "^5.3.1" acorn-walk "^7.2.0" @@ -3701,116 +3795,116 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-essentials@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.18.tgz#0f8a90a53887bfd5dfc0d147d634bf0ae19a9a5d" - integrity sha512-AWKF0Gn7HagzB4ZbZdSXauJ8rgjbIB0Y1jgNCYtReZ//9QDSmF9yrFE0fLJi8O0WBHiQOTeV8Vj+yooGGWRRWQ== - dependencies: - "@storybook/addon-actions" "6.4.18" - "@storybook/addon-backgrounds" "6.4.18" - "@storybook/addon-controls" "6.4.18" - "@storybook/addon-docs" "6.4.18" - "@storybook/addon-measure" "6.4.18" - "@storybook/addon-outline" "6.4.18" - "@storybook/addon-toolbars" "6.4.18" - "@storybook/addon-viewport" "6.4.18" - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/node-logger" "6.4.18" +"@storybook/addon-essentials@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.4.22.tgz#6981c89e8b315cda7ce93b9bf74e98ca80aec00a" + integrity sha512-GTv291fqvWq2wzm7MruBvCGuWaCUiuf7Ca3kzbQ/WqWtve7Y/1PDsqRNQLGZrQxkXU0clXCqY1XtkTrtA3WGFQ== + dependencies: + "@storybook/addon-actions" "6.4.22" + "@storybook/addon-backgrounds" "6.4.22" + "@storybook/addon-controls" "6.4.22" + "@storybook/addon-docs" "6.4.22" + "@storybook/addon-measure" "6.4.22" + "@storybook/addon-outline" "6.4.22" + "@storybook/addon-toolbars" "6.4.22" + "@storybook/addon-viewport" "6.4.22" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/node-logger" "6.4.22" core-js "^3.8.2" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-measure@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.18.tgz#feaadddf5905c87f08230d4acec4e8f5f15bcd2e" - integrity sha512-a9bFiQ/QK/5guxWscwOJN+sszhClPTTn2pTX77SKs+bzZUmckCfneto4NUavsHpj/XTxjwAwidowewwqFV1jTQ== +"@storybook/addon-measure@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.4.22.tgz#5e2daac4184a4870b6b38ff71536109b7811a12a" + integrity sha512-CjDXoCNIXxNfXfgyJXPc0McjCcwN1scVNtHa9Ckr+zMjiQ8pPHY7wDZCQsG69KTqcWHiVfxKilI82456bcHYhQ== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" -"@storybook/addon-outline@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.18.tgz#64ded86bd0ed2dbfc083fcfc050967acfb8222b7" - integrity sha512-FyADdeD7x/25OkjCR7oIXDgrlwM5RB0tbslC0qrRMxKXSjZFTJjr3Qwge0bg8I9QbxDRz+blVzBv3xIhOiDNzQ== +"@storybook/addon-outline@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.4.22.tgz#7a2776344785f7deab83338fbefbefd5e6cfc8cf" + integrity sha512-VIMEzvBBRbNnupGU7NV0ahpFFb6nKVRGYWGREjtABdFn2fdKr1YicOHFe/3U7hRGjb5gd+VazSvyUvhaKX9T7Q== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-toolbars@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.18.tgz#2de4a4e8bc17301055f9e2cae75887bdb6f4efc0" - integrity sha512-Vvj8mvorZhoXvYDuUUKqFpcsNNkVJZmhojdLZ4ULPcvjN3z5MWGwtlJfV+9vkVmJWuR1WG93dVK1+PnitYDZAQ== +"@storybook/addon-toolbars@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.4.22.tgz#858a4e5939987c188c96ed374ebeea88bdd9e8de" + integrity sha512-FFyj6XDYpBBjcUu6Eyng7R805LUbVclEfydZjNiByAoDVyCde9Hb4sngFxn/T4fKAfBz/32HKVXd5iq4AHYtLg== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/theming" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/theming" "6.4.22" core-js "^3.8.2" regenerator-runtime "^0.13.7" -"@storybook/addon-viewport@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.18.tgz#7a2f61fdad488fac9fe399dba4f3b947fe70f6db" - integrity sha512-gWSJAdtUaVrpsbdBveFTkz4V3moGnKxS3iwR8djcIWhTqdRVJxGu0gFtxNpKvdOEMIqF4yNmXYj79oLuNZNkcQ== - dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" - "@storybook/theming" "6.4.18" +"@storybook/addon-viewport@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.4.22.tgz#381a2fc4764fe0851889994a5ba36c3121300c11" + integrity sha512-6jk0z49LemeTblez5u2bYXYr6U+xIdLbywe3G283+PZCBbEDE6eNYy2d2HDL+LbCLbezJBLYPHPalElphjJIcw== + dependencies: + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" + "@storybook/theming" "6.4.22" core-js "^3.8.2" global "^4.4.0" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.18.tgz#fc92a4a608680f2e182a5e896ed382792f6b774e" - integrity sha512-fd3S79P4jJCYZNA2JxA1Xnkj0UlHGQ4Vg72aroWy4OQFlgGQor1LgPfM6RaJ9rh/4k4BXYPXsS7wzI0UWKG3Lw== +"@storybook/addons@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.22.tgz#e165407ca132c2182de2d466b7ff7c5644b6ad7b" + integrity sha512-P/R+Jsxh7pawKLYo8MtE3QU/ilRFKbtCewV/T1o5U/gm8v7hKQdFz3YdRMAra4QuCY8bQIp7MKd2HrB5aH5a1A== dependencies: - "@storybook/api" "6.4.18" - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/api" "6.4.22" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.18" - "@storybook/theming" "6.4.18" + "@storybook/router" "6.4.22" + "@storybook/theming" "6.4.22" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/angular@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.18.tgz#05dc219600fa01718f179f867dd360b7dd559063" - integrity sha512-FfbtID1/2E6c9aEJwOpzZFCVeZcIfC6hI+VTYbR/AlUWukNJPCHwKSPwezpjds6TobjYghes93Lbsdgb4ZQL9g== +"@storybook/angular@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/angular/-/angular-6.4.22.tgz#a8216034fe4d1dd1ecadef8b985b88be295ac10b" + integrity sha512-O83TrTB/sBmB1n4dwQC2TGNH/5xDQExU269rrfEJGJG8wB/vJCi+R6Hqgx/q6ChH+OJKhVr79r41MEeXDHrCVg== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/core" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/core" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.18" + "@storybook/node-logger" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.18" + "@storybook/store" "6.4.22" "@types/webpack-env" "^1.16.0" autoprefixer "^9.8.6" core-js "^3.8.2" @@ -3833,18 +3927,18 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/api@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.18.tgz#92da2b69aeec712419bec9bab5c8434ff1776e97" - integrity sha512-tSbsHKklBysuSmw4T+cKzMj6mQh/42m9F8+2iJns2XG/IUKpMAzFg/9dlgCTW+ay6dJwsR79JGIc9ccIe4SMgQ== +"@storybook/api@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.22.tgz#d63f7ad3ffdd74af01ae35099bff4c39702cf793" + integrity sha512-lAVI3o2hKupYHXFTt+1nqFct942up5dHH6YD7SZZJGyW21dwKC3HK1IzCsTawq3fZAKkgWFgmOO649hKk60yKg== dependencies: - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.18" + "@storybook/router" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.18" + "@storybook/theming" "6.4.22" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -3856,10 +3950,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.18.tgz#8bae72b9e982d35a5a9f2b7f9af9d85a9c2dc966" - integrity sha512-N/OGjTnc7CpVoDnfoI49uMjAIpGqh2lWHFYNIWaUoG1DNnTt1nCc49hw9awjFc5KgaYOwJmVg1SYYE8Afssu+Q== +"@storybook/builder-webpack4@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.22.tgz#d3384b146e97a2b3a6357c6eb8279ff0f1c7f8f5" + integrity sha512-A+GgGtKGnBneRFSFkDarUIgUTI8pYFdLmUVKEAGdh2hL+vLXAz9A46sEY7C8LQ85XWa8TKy3OTDxqR4+4iWj3A== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3882,22 +3976,22 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/channel-postmessage" "6.4.18" - "@storybook/channels" "6.4.18" - "@storybook/client-api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/core-events" "6.4.18" - "@storybook/node-logger" "6.4.18" - "@storybook/preview-web" "6.4.18" - "@storybook/router" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/channel-postmessage" "6.4.22" + "@storybook/channels" "6.4.22" + "@storybook/client-api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/core-events" "6.4.22" + "@storybook/node-logger" "6.4.22" + "@storybook/preview-web" "6.4.22" + "@storybook/router" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.18" - "@storybook/theming" "6.4.18" - "@storybook/ui" "6.4.18" + "@storybook/store" "6.4.22" + "@storybook/theming" "6.4.22" + "@storybook/ui" "6.4.22" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -3931,10 +4025,10 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.2.2" -"@storybook/builder-webpack5@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.18.tgz#64e3325efc84831d0efded88d4b6b8e51acff1dd" - integrity sha512-VbLqGVROK9wJsFt2wcBojgY/VMVgoFVVdEYTs0BsZqmuYVBVnKxpkhpzJFnhulFGpIMx1NL5GKwbSNK3Pz7FWQ== +"@storybook/builder-webpack5@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.22.tgz#cd10b3b307f4a6f9c474d3b72ba5555055395681" + integrity sha512-vvQ0HgkIIVz+cmaCXIRor0UFZbGZqh4aV0ISSof60BjdW5ld+R+XCr/bdTU6Zg8b2fL9CXh7/LE6fImnIMpRIA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -3956,21 +4050,21 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/channel-postmessage" "6.4.18" - "@storybook/channels" "6.4.18" - "@storybook/client-api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/core-events" "6.4.18" - "@storybook/node-logger" "6.4.18" - "@storybook/preview-web" "6.4.18" - "@storybook/router" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/channel-postmessage" "6.4.22" + "@storybook/channels" "6.4.22" + "@storybook/client-api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/core-events" "6.4.22" + "@storybook/node-logger" "6.4.22" + "@storybook/preview-web" "6.4.22" + "@storybook/router" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.18" - "@storybook/theming" "6.4.18" + "@storybook/store" "6.4.22" + "@storybook/theming" "6.4.22" "@types/node" "^14.0.10" babel-loader "^8.0.0" babel-plugin-macros "^3.0.1" @@ -3994,51 +4088,51 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.4.1" -"@storybook/channel-postmessage@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.18.tgz#24547fe7cee599969fd62df22142ba7046099a8e" - integrity sha512-SKapUREPkqzKoBMpOJrZddE9PCR8CJkPTcDpjDqcRsTvToRWsux3pvzmuW4iGYnHNh+GQml7Rz9x85WfMIpfyQ== +"@storybook/channel-postmessage@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.22.tgz#8be0be1ea1e667a49fb0f09cdfdeeb4a45829637" + integrity sha512-gt+0VZLszt2XZyQMh8E94TqjHZ8ZFXZ+Lv/Mmzl0Yogsc2H+6VzTTQO4sv0IIx6xLbpgG72g5cr8VHsxW5kuDQ== dependencies: - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-websocket@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.18.tgz#cf3a03e88b983c2953cb76a40a964806790567c4" - integrity sha512-ROqNZAFB1gP9u8dmlM4KxykXHsd1ifunBgFY3ncQKeRi2Oh30OMVB2ZhNdoIF8i8X5ZBwSpId1o6nQhL2e/EJA== +"@storybook/channel-websocket@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.22.tgz#d541f69125873123c453757e2b879a75a9266c65" + integrity sha512-Bm/FcZ4Su4SAK5DmhyKKfHkr7HiHBui6PNutmFkASJInrL9wBduBfN8YQYaV7ztr8ezoHqnYRx8sj28jpwa6NA== dependencies: - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" core-js "^3.8.2" global "^4.4.0" telejson "^5.3.2" -"@storybook/channels@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.18.tgz#2907aca0039b5eb9ae305112f14c488c2621c2f6" - integrity sha512-Bh4l7VKKR2ImLbZ9XgL/DzT3lFv9+SLiCu1ozfpBZGHUCOLyHRnkG/h8wYvRkF9s3tpNwOtaCaqD1vkkZfr3uw== +"@storybook/channels@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.22.tgz#710f732763d63f063f615898ab1afbe74e309596" + integrity sha512-cfR74tu7MLah1A8Rru5sak71I+kH2e/sY6gkpVmlvBj4hEmdZp4Puj9PTeaKcMXh9DgIDPNA5mb8yvQH6VcyxQ== dependencies: core-js "^3.8.2" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.18.tgz#61c7c90f3f099e4d3bcc36576d2adbe2e5ef6eee" - integrity sha512-ua2Q692Fz2b3q5M/Qzjixg2LArwrcHGBmht06bNw/jrRfyFeTUHOhh5BT7LxSEetUgHATH/Y1GW40xza9rXFNw== +"@storybook/client-api@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.22.tgz#df14f85e7900b94354c26c584bab53a67c47eae9" + integrity sha512-sO6HJNtrrdit7dNXQcZMdlmmZG1k6TswH3gAyP/DoYajycrTwSJ6ovkarzkO+0QcJ+etgra4TEdTIXiGHBMe/A== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/channel-postmessage" "6.4.18" - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/channel-postmessage" "6.4.22" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.18" + "@storybook/store" "6.4.22" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" @@ -4053,23 +4147,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.18.tgz#4ad8ea7d67b17e5db8f15cffcc2f984df3479462" - integrity sha512-ciBaASMaB2ZPksbuyDbp3++5SZxbhcihEpl+RQcAVV8g+TUyBZKIcHt8HNHicTczz5my1EydZovMh1IkSBMICA== +"@storybook/client-logger@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.22.tgz#51abedb7d3c9bc21921aeb153ac8a19abc625cd6" + integrity sha512-LXhxh/lcDsdGnK8kimqfhu3C0+D2ylCSPPQNbU0IsLRmTfbpQYMdyl0XBjPdHiRVwlL7Gkw5OMjYemQgJ02zlw== dependencies: core-js "^3.8.2" global "^4.4.0" -"@storybook/components@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.18.tgz#1f3eba9ab69a09b9468af0126d6e7ab040655ca4" - integrity sha512-LAPKYWgB6S10Vzt0IWa1Ihf9EAuQOGxlqehTuxYLOwMOKbto8iEbGRse/XaQfxdZf/RbmOL4u+7nVRROWgOEjg== +"@storybook/components@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.22.tgz#4d425280240702883225b6a1f1abde7dc1a0e945" + integrity sha512-dCbXIJF9orMvH72VtAfCQsYbe57OP7fAADtR6YTwfCw9Sm1jFuZr8JbblQ1HcrXEoJG21nOyad3Hm5EYVb/sBw== dependencies: "@popperjs/core" "^2.6.0" - "@storybook/client-logger" "6.4.18" + "@storybook/client-logger" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.18" + "@storybook/theming" "6.4.22" "@types/color-convert" "^2.0.0" "@types/overlayscrollbars" "^1.12.0" "@types/react-syntax-highlighter" "11.0.5" @@ -4091,21 +4185,21 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.18.tgz#7f2feb961864dcf6de501a94a41900fd36b43657" - integrity sha512-F9CqW31Mr9Qde90uqPorpkiS+P7UteKYmdHlV0o0czeWaL+MEhpY+3pRJuRIIjX5C7Vc89TvljMqs37Khakmdg== - dependencies: - "@storybook/addons" "6.4.18" - "@storybook/channel-postmessage" "6.4.18" - "@storybook/channel-websocket" "6.4.18" - "@storybook/client-api" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" +"@storybook/core-client@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.22.tgz#9079eda8a9c8e6ba24b84962a749b1c99668cb2a" + integrity sha512-uHg4yfCBeM6eASSVxStWRVTZrAnb4FT6X6v/xDqr4uXCpCttZLlBzrSDwPBLNNLtCa7ntRicHM8eGKIOD5lMYQ== + dependencies: + "@storybook/addons" "6.4.22" + "@storybook/channel-postmessage" "6.4.22" + "@storybook/channel-websocket" "6.4.22" + "@storybook/client-api" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/preview-web" "6.4.18" - "@storybook/store" "6.4.18" - "@storybook/ui" "6.4.18" + "@storybook/preview-web" "6.4.22" + "@storybook/store" "6.4.22" + "@storybook/ui" "6.4.22" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" @@ -4117,10 +4211,10 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.18.tgz#0688a0a4a80cdbc161966c5a7ff49e755d64bbab" - integrity sha512-y4e43trNyQ3/v0Wpi240on7yVooUQvJBhJxOGEfcxAMRtcDa0ZCxHO4vAFX3k3voQOSmiXItXfJ1eGo/K+u0Fw== +"@storybook/core-common@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.22.tgz#b00fa3c0625e074222a50be3196cb8052dd7f3bf" + integrity sha512-PD3N/FJXPNRHeQS2zdgzYFtqPLdi3MLwAicbnw+U3SokcsspfsAuyYHZOYZgwO8IAEKy6iCc7TpBdiSJZ/vAKQ== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -4143,7 +4237,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.4.18" + "@storybook/node-logger" "6.4.22" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/pretty-hrtime" "^1.0.0" @@ -4172,29 +4266,29 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core-events@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.18.tgz#630a19425eb387c6134f29b967c30458c65f7ea8" - integrity sha512-lCT3l0rFs6CuVpD8+mwmj1lUTomGErySTxi0KmVd2AWQj8kJL90EfS0jHSU5JIXScDvuwXDXLLmvMfqNU+zHdg== +"@storybook/core-events@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.22.tgz#c09b0571951affd4254028b8958a4d8652700989" + integrity sha512-5GYY5+1gd58Gxjqex27RVaX6qbfIQmJxcbzbNpXGNSqwqAuIIepcV1rdCVm6I4C3Yb7/AQ3cN5dVbf33QxRIwA== dependencies: core-js "^3.8.2" -"@storybook/core-server@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.18.tgz#520935f7f330a734488e733ad4cf15a9556679b5" - integrity sha512-7e2QUtD8/TE14P9X/xsBDMBbNVi/etTtMKKhsG2TG25daRz+6qadbM9tNP0YwvIDk452cNYJkjflV48mf5+ZEA== +"@storybook/core-server@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.22.tgz#254409ec2ba49a78b23f5e4a4c0faea5a570a32b" + integrity sha512-wFh3e2fa0un1d4+BJP+nd3FVWUO7uHTqv3OGBfOmzQMKp4NU1zaBNdSQG7Hz6mw0fYPBPZgBjPfsJRwIYLLZyw== dependencies: "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-webpack4" "6.4.18" - "@storybook/core-client" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/builder-webpack4" "6.4.22" + "@storybook/core-client" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.18" - "@storybook/manager-webpack4" "6.4.18" - "@storybook/node-logger" "6.4.18" + "@storybook/csf-tools" "6.4.22" + "@storybook/manager-webpack4" "6.4.22" + "@storybook/node-logger" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.18" + "@storybook/store" "6.4.22" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" "@types/pretty-hrtime" "^1.0.0" @@ -4227,18 +4321,18 @@ webpack "4" ws "^8.2.3" -"@storybook/core@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.18.tgz#56f7bb0f20dbcfa205d860022b7bf30bf42fd472" - integrity sha512-7DTMAEXiBIwd1jgalbsZiXCiS2Be9MKKsr6GQdf3WaBm0WYV067oN9jcUY5IgNtJX06arT4Ykp+CGG/TR+sLlw== +"@storybook/core@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.22.tgz#cf14280d7831b41d5dea78f76b414bdfde5918f0" + integrity sha512-KZYJt7GM5NgKFXbPRZZZPEONZ5u/tE/cRbMdkn/zWN3He8+VP+65/tz8hbriI/6m91AWVWkBKrODSkeq59NgRA== dependencies: - "@storybook/core-client" "6.4.18" - "@storybook/core-server" "6.4.18" + "@storybook/core-client" "6.4.22" + "@storybook/core-server" "6.4.22" -"@storybook/csf-tools@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.18.tgz#cdd40b543f9bea79c1481c236868b8ea04af6bd7" - integrity sha512-KtwxKrkndEyvyAiBliI6m4yMFMvnyI4fOjU8t1qCit/0gjutOF5JxmmZ+H8FSI5dIyibEOzQmzHe0MyStAjCEQ== +"@storybook/csf-tools@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.22.tgz#f6d64bcea1b36114555972acae66a1dbe9e34b5c" + integrity sha512-LMu8MZAiQspJAtMBLU2zitsIkqQv7jOwX7ih5JrXlyaDticH7l2j6Q+1mCZNWUOiMTizj0ivulmUsSaYbpToSw== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -4265,20 +4359,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.18.tgz#5317c917dbdaf4cf8721647551a785eb13c04146" - integrity sha512-6oX1KrIJBoY4vdZiMftJNusv+Bm8pegVjdJ+aZcbr/41x7ufP3tu5UKebEXDH0UURXtLw0ffl+OgojewGdpC1Q== +"@storybook/manager-webpack4@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.22.tgz#eabd674beee901c7f755d9b679e9f969cbab636d" + integrity sha512-nzhDMJYg0vXdcG0ctwE6YFZBX71+5NYaTGkxg3xT7gbgnP1YFXn9gVODvgq3tPb3gcRapjyOIxUa20rV+r8edA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.18" - "@storybook/core-client" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/node-logger" "6.4.18" - "@storybook/theming" "6.4.18" - "@storybook/ui" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/core-client" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/node-logger" "6.4.22" + "@storybook/theming" "6.4.22" + "@storybook/ui" "6.4.22" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.0.0" @@ -4307,20 +4401,20 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/manager-webpack5@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.18.tgz#b8ec804e4a7b765ee40e0c728e6eebd76954611b" - integrity sha512-F3usxo5GKDbs+zMtiJsPFLvcJKteB6bp8sy6lK+++tFJWhlGaiebAE8pOghv7/LuEFzo1HS2NXcinb+9fG8hYA== +"@storybook/manager-webpack5@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.22.tgz#14b6c564692324e25084f699d599cd202659fe21" + integrity sha512-BMkOMselT4jOn7EQGt748FurM5ewtDfZtOQPCVK8MZX+HYE2AgjNOzm562TYODIxk12Fkhgj3EIz7GGMe1U3RA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.18" - "@storybook/core-client" "6.4.18" - "@storybook/core-common" "6.4.18" - "@storybook/node-logger" "6.4.18" - "@storybook/theming" "6.4.18" - "@storybook/ui" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/core-client" "6.4.22" + "@storybook/core-common" "6.4.22" + "@storybook/node-logger" "6.4.22" + "@storybook/theming" "6.4.22" + "@storybook/ui" "6.4.22" "@types/node" "^14.0.10" babel-loader "^8.0.0" case-sensitive-paths-webpack-plugin "^2.3.0" @@ -4346,10 +4440,10 @@ webpack-dev-middleware "^4.1.0" webpack-virtual-modules "^0.4.1" -"@storybook/node-logger@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.18.tgz#8759761ba7526b2fa03a1a08fe82d6d892d7a072" - integrity sha512-wY1qt4XOXtJJdQ+DrO3RijtiwVFqWuWetvCY4RV4lge5yk0FP5Q+MTpmjazYodAvGPUIP0LK9bvEDLwXa0JUfw== +"@storybook/node-logger@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.22.tgz#c4ec00f8714505f44eda7671bc88bb44abf7ae59" + integrity sha512-sUXYFqPxiqM7gGH7gBXvO89YEO42nA4gBicJKZjj9e+W4QQLrftjF9l+mAw2K0mVE10Bn7r4pfs5oEZ0aruyyA== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.1.0" @@ -4357,24 +4451,24 @@ npmlog "^5.0.1" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.18.tgz#e94350471fa3df98215ad3c8f3d0574a3a0a8e04" - integrity sha512-eS91pFvnuC1rFXMhDj3smXJ1OTwt2K5HS1+QtWi3NuE4XRvtdwDA/wZ4KQJWZszWuY/k2HgFfJYbQEumJxVrCQ== +"@storybook/postinstall@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.22.tgz#592c7406f197fd25a5644c3db7a87d9b5da77e85" + integrity sha512-LdIvA+l70Mp5FSkawOC16uKocefc+MZLYRHqjTjgr7anubdi6y7W4n9A7/Yw4IstZHoknfL88qDj/uK5N+Ahzw== dependencies: core-js "^3.8.2" -"@storybook/preview-web@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.18.tgz#47c908bf27d2089ccf3296c376a6f5b1e8674b5a" - integrity sha512-0x64uLdGhIOk9hIuRKTHFdP7+iEHyjAOi5U4jtwqFfDtG4n4zxEGSsUWij7pTR2rAYf7g2NWIbAM7qb1AqqcLQ== +"@storybook/preview-web@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.22.tgz#58bfc6492503ff4265b50f42a27ea8b0bfcf738a" + integrity sha512-sWS+sgvwSvcNY83hDtWUUL75O2l2LY/GTAS0Zp2dh3WkObhtuJ/UehftzPZlZmmv7PCwhb4Q3+tZDKzMlFxnKQ== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/channel-postmessage" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/channel-postmessage" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.18" + "@storybook/store" "6.4.22" ansi-to-html "^0.6.11" core-js "^3.8.2" global "^4.4.0" @@ -4386,12 +4480,12 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/router@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.18.tgz#8803dd78277f8602d6c11dae56f6229474dfa54c" - integrity sha512-itvSWHhG1X/NV1sMlwP1qKtF0HfiIaAHImr0LwQ2K2F6/CI11W68dJAs4WBUdwzA0+H0Joyu/2a/6mCQHcee1A== +"@storybook/router@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.22.tgz#e3cc5cd8595668a367e971efb9695bbc122ed95e" + integrity sha512-zeuE8ZgFhNerQX8sICQYNYL65QEi3okyzw7ynF58Ud6nRw4fMxSOHcj2T+nZCIU5ufozRL4QWD/Rg9P2s/HtLw== dependencies: - "@storybook/client-logger" "6.4.18" + "@storybook/client-logger" "6.4.22" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -4411,13 +4505,13 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.18.tgz#205423e56f7da752d64a0695f2b22ed94378e5d0" - integrity sha512-sjKvngCCYDbBwjjFTjAXO6VsAzKkjy+UctseeULXxEN3cKIsz/R3y7MrrN9yBrwyYcn0k3pqa9d9e3gE+Jv2Tw== +"@storybook/source-loader@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.4.22.tgz#c931b81cf1bd63f79b51bfa9311de7f5a04a7b77" + integrity sha512-O4RxqPgRyOgAhssS6q1Rtc8LiOvPBpC1EqhCYWRV3K+D2EjFarfQMpjgPj18hC+QzpUSfzoBZYqsMECewEuLNw== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/client-logger" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/client-logger" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" estraverse "^5.2.0" @@ -4427,14 +4521,14 @@ prettier ">=2.2.1 <=2.3.0" regenerator-runtime "^0.13.7" -"@storybook/store@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.18.tgz#3b693c9d5555d5cfc04e2318e104746d9d55ad66" - integrity sha512-Vl5oCs/9fP1gUgfgMHTBsnYbwAAoaR93/bzDBeOHI3eo5x9uzzJtA4zcRmEiKahR/wgwGacpWy90JrIX469PDQ== +"@storybook/store@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.22.tgz#f291fbe3639f14d25f875cac86abb209a97d4e2a" + integrity sha512-lrmcZtYJLc2emO+1l6AG4Txm9445K6Pyv9cGAuhOJ9Kks0aYe0YtvMkZVVry0RNNAIv6Ypz72zyKc/QK+tZLAQ== dependencies: - "@storybook/addons" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/core-events" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/core-events" "6.4.22" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" fast-deep-equal "^3.1.3" @@ -4448,15 +4542,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/theming@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.18.tgz#05365cc1d3dab5d71b80a82928fc5188106a0ed6" - integrity sha512-1o0w2eP+8sXUesdtXpZR4Yvayp1h3xvK7l9+wuHh+1uCy+EvD5UI9d1HvU5kt5fw7XAJJcInaVAmyAbpwct0TQ== +"@storybook/theming@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.22.tgz#19097eec0366447ddd0d6917b0e0f81d0ec5e51e" + integrity sha512-NVMKH/jxSPtnMTO4VCN1k47uztq+u9fWv4GSnzq/eezxdGg9ceGL4/lCrNGoNajht9xbrsZ4QvsJ/V2sVGM8wA== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.27" - "@storybook/client-logger" "6.4.18" + "@storybook/client-logger" "6.4.22" core-js "^3.8.2" deep-object-diff "^1.1.0" emotion-theming "^10.0.27" @@ -4466,21 +4560,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.4.18": - version "6.4.18" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.18.tgz#3ceaf6b317f8f2c1d7d1cdc49daaac7eaf10af6b" - integrity sha512-f2ckcLvEyA9CRcu6W2I2CyEbUnU4j3h5Nz0N40YZ2uRMVNQY2xPywAFZVySZIJAaum/5phDfnOD0Feap/Q6zVQ== +"@storybook/ui@6.4.22": + version "6.4.22" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.22.tgz#49badd7994465d78d984ca4c42533c1c22201c46" + integrity sha512-UVjMoyVsqPr+mkS1L7m30O/xrdIEgZ5SCWsvqhmyMUok3F3tRB+6M+OA5Yy+cIVfvObpA7MhxirUT1elCGXsWQ== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.4.18" - "@storybook/api" "6.4.18" - "@storybook/channels" "6.4.18" - "@storybook/client-logger" "6.4.18" - "@storybook/components" "6.4.18" - "@storybook/core-events" "6.4.18" - "@storybook/router" "6.4.18" + "@storybook/addons" "6.4.22" + "@storybook/api" "6.4.22" + "@storybook/channels" "6.4.22" + "@storybook/client-logger" "6.4.22" + "@storybook/components" "6.4.22" + "@storybook/core-events" "6.4.22" + "@storybook/router" "6.4.22" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.18" + "@storybook/theming" "6.4.22" copy-to-clipboard "^3.3.1" core-js "^3.8.2" core-js-pure "^3.8.2" @@ -4537,130 +4631,130 @@ 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-arm-eabi@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.178.tgz#8a2e1c6a81236e437981f1b2a0f7f74fddc9d24f" + integrity sha512-eWW8aSNe/X9xinM5Nb07mLjIGLXWFfUr7InxNrxHggRZLb1aUm29fUlYWPt+TWsj23uj8qJCmzryjFLUy+gDxA== "@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-android-arm64@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-android-arm64/-/core-android-arm64-1.2.178.tgz#c967e507c942c42f23f735d95bb47291eb029cad" + integrity sha512-en/WKIbwmu8Q6LFoaiCPqS9Lqnc5Or2oWQpWG3awN7rbn2OqwYgki76fnqgI4dkxURztqDPZ5Fxh8oGkrmYxWg== "@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-arm64@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.178.tgz#44a59af2338043a8218047a63915955744074138" + integrity sha512-P0jaGwMVVnTRaeo+6NXTnsqIFelYl+ZeZ1W0BSD+ymOcFlUhjpw1ADux1XiXTbGgloN4uzCmPxphrJO0b7rsag== "@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-darwin-x64@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.2.178.tgz#a94dc71a6f91c679dcc9908b2e8a27156cfe258c" + integrity sha512-ZSWe4Wcnwkrf0Eh/EeBsGRM38OCT6wl8ynWRXO6j5fb+S5cQx6KPpa6TrlQ+43jduzv44IizjnVanHXDB7eqww== "@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-freebsd-x64@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.178.tgz#97559d237aba5da3cb8d951e09872d397ae3a36a" + integrity sha512-pZCHOvSGJ0JecepsXN7sAesamsbmmkXQ2XHlOuPFRqyrmE7mrZ6NwgUKfXvGZ9kuE1xwqBLFPwco9IM/cNSl6g== "@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-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-arm-gnueabihf@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.178.tgz#a56cf9bfc5d15b1171d9161aca345e3abc3d1a11" + integrity sha512-5IqU+ILaMRW/u+Ww8ZFP18i0/TFFZbQ/3VS2rlRloXDKAQhYc9ZAwi4/jLccX+zcB7l5JzFuf+skqjsf+n8eFQ== "@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-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-gnu@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.178.tgz#e9b8ba073d5f59c97645d27c877b43400110cf86" + integrity sha512-1n3v6JzN7P/wNCewbKmxPNp06XYxUlXVpZKRgKM3JCq9jX1IzKcaK+5u0l0fk94PTTsiPCuPFi7RzLfMc/gtDA== "@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-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-arm64-musl@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.178.tgz#b1f8f16880e018df28d68f88b5b0e9086906413f" + integrity sha512-U6FHVkrp8AwkBNzvNG6vwYpPZBVz1L6leMb56S59eI3wib3Trz2pTlZnsGLPIFyjTqLOowhhFJ+TrpX65NiAaw== "@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-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-gnu@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.178.tgz#9a70d87e919b4d2c9a57d400ba28663f05814a3b" + integrity sha512-+0Lsrr7sO7CybsqQZ2o/CI9r2TGCGQ7cgemE1y6FULkY3lf2keCsch535Vbq3/emDD9ujoHiph87sIOIBHjEIw== "@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-linux-x64-musl@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.178.tgz#fbb195c9d3e980bc90555d716cc9c46cae08a761" + integrity sha512-tVYcPA+vSaeGCUOfIO27viYaHZhtW1MvD1gBqpD1AUrV9ufyCklihH5BlxOTuGww464lGCybKu2VbsoHBz2qIw== "@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-arm64-msvc@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.178.tgz#ac88b1b0e543398aa549007e5ce32ab344e2fb60" + integrity sha512-KgpApFeWUSBpUELz6PT4AaML4lW3ziz2kVlrZlLrUk5XRJv/J+KKHJ+iYQUnYCTOOeGI9vSVmzjfdEvYyIhB/Q== "@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-ia32-msvc@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.178.tgz#4c73d32641b892623736ce614b62da8de03f23fc" + integrity sha512-j3E0VxfewsEZYw2tSh4C4TlIehfvYKibcpnSsZTIjXMW3a3SSSvYtEus63TjbEj2IjtAXrZnyNPeuej9ioY8Gg== "@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-win32-x64-msvc@1.2.178": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.178.tgz#639a3138ed4a964fa1e4145c35c15a2c54dc1458" + integrity sha512-ocsdX9q2iFMJHj2wdhdNLwh26Ap0crMGbI6LVNYfngDFON7ywS5qOvjufHSgtIxARMVusUDjmehqRCmzd3Yr6w== "@swc/core@^1.2.119": version "1.2.138" @@ -4681,24 +4775,24 @@ "@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== +"@swc/core@^1.2.173": + version "1.2.178" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.2.178.tgz#7f6fac9763699a1e8c3b97af8a3c4d07122be741" + integrity sha512-c+Wu65ROsMiZdkL/VOpHIeiJv1w3jPnfZez41cD95yuXk+oiWDNvtWUqbU4l1x38eYKi9YlY6wPYpLBJdHA22w== 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" + "@swc/core-android-arm-eabi" "1.2.178" + "@swc/core-android-arm64" "1.2.178" + "@swc/core-darwin-arm64" "1.2.178" + "@swc/core-darwin-x64" "1.2.178" + "@swc/core-freebsd-x64" "1.2.178" + "@swc/core-linux-arm-gnueabihf" "1.2.178" + "@swc/core-linux-arm64-gnu" "1.2.178" + "@swc/core-linux-arm64-musl" "1.2.178" + "@swc/core-linux-x64-gnu" "1.2.178" + "@swc/core-linux-x64-musl" "1.2.178" + "@swc/core-win32-arm64-msvc" "1.2.178" + "@swc/core-win32-ia32-msvc" "1.2.178" + "@swc/core-win32-x64-msvc" "1.2.178" "@tootallnate/once@1": version "1.1.2" @@ -4823,6 +4917,14 @@ "@types/eslint" "*" "@types/estree" "*" +"@types/eslint-scope@^3.7.3": + version "3.7.3" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" + integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + "@types/eslint@*": version "7.28.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" @@ -4836,6 +4938,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + "@types/express-serve-static-core@*": version "4.17.28" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" @@ -4934,12 +5041,12 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.0.2": - version "27.0.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.2.tgz#ac383c4d4aaddd29bbf2b916d8d105c304a5fcd7" - integrity sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA== +"@types/jest@27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== dependencies: - jest-diff "^27.0.0" + jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": @@ -5285,12 +5392,12 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/experimental-utils@~5.10.0": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.10.2.tgz#dbb541e2070c7bd6e63d3e3a55b58be73a8fbb34" - integrity sha512-stRnIlxDduzxtaVLtEohESoXI1k7J6jvJHGyIkOT2pvXbg5whPM6f9tzJ51bJJxaJTdmvwgVFDNCopFRb2F5Gw== +"@typescript-eslint/experimental-utils@~5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.18.0.tgz#a6b5662e6b0452cb0e75a13662ce3b33cd1be59d" + integrity sha512-hypiw5N0aM2aH91/uMmG7RpyUH3PN/iOhilMwkMFZIbm/Bn/G3ZnbaYdSoAN4PG/XHQjdhBYLi0ZoRZsRYT4hA== dependencies: - "@typescript-eslint/utils" "5.10.2" + "@typescript-eslint/utils" "5.18.0" "@typescript-eslint/parser@5.4.0": version "5.4.0" @@ -5302,13 +5409,13 @@ "@typescript-eslint/typescript-estree" "5.4.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz#92c0bc935ec00f3d8638cdffb3d0e70c9b879639" - integrity sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw== +"@typescript-eslint/scope-manager@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.18.0.tgz#a7d7b49b973ba8cebf2a3710eefd457ef2fb5505" + integrity sha512-C0CZML6NyRDj+ZbMqh9FnPscg2PrzSaVQg3IpTmpe0NURMVBXlghGZgMYqBw07YW73i0MCqSDqv2SbywnCS8jQ== dependencies: - "@typescript-eslint/types" "5.10.2" - "@typescript-eslint/visitor-keys" "5.10.2" + "@typescript-eslint/types" "5.18.0" + "@typescript-eslint/visitor-keys" "5.18.0" "@typescript-eslint/scope-manager@5.3.0": version "5.3.0" @@ -5326,10 +5433,10 @@ "@typescript-eslint/types" "5.4.0" "@typescript-eslint/visitor-keys" "5.4.0" -"@typescript-eslint/types@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.2.tgz#604d15d795c4601fffba6ecb4587ff9fdec68ce8" - integrity sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w== +"@typescript-eslint/types@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e" + integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw== "@typescript-eslint/types@5.3.0": version "5.3.0" @@ -5341,13 +5448,13 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.4.0.tgz#b1c130f4b381b77bec19696c6e3366f9781ce8f2" integrity sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA== -"@typescript-eslint/typescript-estree@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz#810906056cd3ddcb35aa333fdbbef3713b0fe4a7" - integrity sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ== +"@typescript-eslint/typescript-estree@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474" + integrity sha512-wa+2VAhOPpZs1bVij9e5gyVu60ReMi/KuOx4LKjGx2Y3XTNUDJgQ+5f77D49pHtqef/klglf+mibuHs9TrPxdQ== dependencies: - "@typescript-eslint/types" "5.10.2" - "@typescript-eslint/visitor-keys" "5.10.2" + "@typescript-eslint/types" "5.18.0" + "@typescript-eslint/visitor-keys" "5.18.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" @@ -5380,24 +5487,24 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.2.tgz#1fcd37547c32c648ab11aea7173ec30060ee87a8" - integrity sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg== +"@typescript-eslint/utils@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855" + integrity sha512-+hFGWUMMri7OFY26TsOlGa+zgjEy1ssEipxpLjtl4wSll8zy85x0GrUSju/FHdKfVorZPYJLkF3I4XPtnCTewA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.10.2" - "@typescript-eslint/types" "5.10.2" - "@typescript-eslint/typescript-estree" "5.10.2" + "@typescript-eslint/scope-manager" "5.18.0" + "@typescript-eslint/types" "5.18.0" + "@typescript-eslint/typescript-estree" "5.18.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz#fdbf272d8e61c045d865bd6c8b41bea73d222f3d" - integrity sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q== +"@typescript-eslint/visitor-keys@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60" + integrity sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg== dependencies: - "@typescript-eslint/types" "5.10.2" + "@typescript-eslint/types" "5.18.0" eslint-visitor-keys "^3.0.0" "@typescript-eslint/visitor-keys@5.3.0": @@ -5760,6 +5867,11 @@ acorn@^8.2.4, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== +acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + acorn@^8.6.0: version "8.6.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" @@ -6295,11 +6407,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - async@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" @@ -6317,6 +6424,11 @@ async@^3.2.0: resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== +async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -6374,7 +6486,7 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" -axios@^0.21.4: +axios@^0.21.1, axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -6393,18 +6505,18 @@ axobject-query@^2.2.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -babel-jest@^27.2.2, babel-jest@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.3.1.tgz#0636a3404c68e07001e434ac4956d82da8a80022" - integrity sha512-SjIF8hh/ir0peae2D6S6ZKRhUy7q/DnpH7k/V6fT4Bgs/LXXUztOpX4G2tCgq8mLo5HA9mN6NmlFMeYtKmIsTQ== +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^27.2.0" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" slash "^3.0.0" babel-loader@8.2.3, babel-loader@^8.0.0: @@ -6455,7 +6567,7 @@ babel-plugin-extract-import-names@1.6.22: dependencies: "@babel/helper-plugin-utils" "7.10.4" -babel-plugin-istanbul@6.1.1: +babel-plugin-istanbul@6.1.1, babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== @@ -6477,10 +6589,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.2.0: - version "27.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.2.0.tgz#79f37d43f7e5c4fdc4b2ca3e10cc6cf545626277" - integrity sha512-TOux9khNKdi64mW+0OIhcmbAn75tTlzKhxmiNXevQaPbrBYK7YKjP1jl6NHTJ6XR5UgUrJbCnWlKVnJn29dfjw== +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -6584,12 +6696,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.2.0: - version "27.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.2.0.tgz#556bbbf340608fed5670ab0ea0c8ef2449fba885" - integrity sha512-z7MgQ3peBwN5L5aCqBKnF6iqdlvZvFUQynEhu0J+X9nHLU72jO3iY331lcYrg+AssJ8q7xsv5/3AICzVmJ/wvg== +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: - babel-plugin-jest-hoist "^27.2.0" + babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" bail@^1.0.0: @@ -6625,6 +6737,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + batch-processor@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" @@ -6695,7 +6814,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.1.0: +bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -6911,6 +7030,17 @@ browserslist@^4.19.1: node-releases "^2.0.1" picocolors "^1.0.0" +browserslist@^4.20.2: + version "4.20.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" + integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== + dependencies: + caniuse-lite "^1.0.30001332" + electron-to-chromium "^1.4.118" + escalade "^3.1.1" + node-releases "^2.0.3" + picocolors "^1.0.0" + 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" @@ -7181,6 +7311,11 @@ caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001299: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz#682ef3f4e617f1a177ad943de59775ed3032e511" integrity sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A== +caniuse-lite@^1.0.30001332: + version "1.0.30001338" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz#b5dd7a7941a51a16480bdf6ff82bded1628eec0d" + integrity sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -7211,7 +7346,7 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chalk@4, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: +chalk@4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -7515,15 +7650,6 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -7920,6 +8046,11 @@ cors@2.8.5: object-assign "^4" vary "^1" +corser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= + cosmiconfig@^5.0.5: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -8557,10 +8688,10 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff-sequences@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" - integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff@^4.0.1: version "4.0.2" @@ -8749,12 +8880,12 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^3.1.5: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== +ejs@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" + integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== dependencies: - jake "^10.6.1" + jake "^10.8.5" electron-to-chromium@^1.3.830: version "1.3.830" @@ -8766,6 +8897,11 @@ electron-to-chromium@^1.3.896: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.0.tgz#7456192519838f881e35e4038bf4ad2c36353e63" integrity sha512-+oXCt6SaIu8EmFTPx8wNGSB0tHQ5biDscnlf6Uxuz17e9CjzMRtGk9B8705aMPnj0iWr3iC74WuIkngCsLElmA== +electron-to-chromium@^1.4.118: + version "1.4.137" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" + integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== + electron-to-chromium@^1.4.17: version "1.4.30" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.30.tgz#0f75a1dce26dffbd5a0f7212e5b87fe0b61cbc76" @@ -8842,7 +8978,7 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -8874,6 +9010,14 @@ enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.9.2: + version "5.9.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" + integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.5, enquirer@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -9019,190 +9163,195 @@ esbuild-android-arm64@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-android-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz#fb051169a63307d958aec85ad596cfc7d7770303" + integrity sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ== 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-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz#615ea0a9de67b57a293a7128d7ac83ee307a856d" + integrity sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw== 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-darwin-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz#82054dcfcecb15ccfd237093b8008e7745a99ad9" + integrity sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw== 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-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz#778a818c5b078d5cdd6bb6c0e0797217d196999b" + integrity sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A== 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-freebsd-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz#18da93b9f3db2e036f72383bfe73b28b73bb332c" + integrity sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ== 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-32@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz#d0d5d9f5bb3536e17ac097e9512019c65b7c0234" + integrity sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg== 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-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz#2773d540971999ea7f38107ef92fca753f6a8c30" + integrity sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg== 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-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz#5d4480ce6d6bffab1dd76a23158f5a5ab33e7ba4" + integrity sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A== 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-arm@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz#c6391b3f7c8fa6d3b99a7e893ce0f45f3a921eef" + integrity sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g== 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-mips64le@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz#2c8dabac355c502e86c38f9f292b3517d8e181f3" + integrity sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ== 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-ppc64le@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz#69d71b2820d5c94306072dac6094bae38e77d1c0" + integrity sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng== + +esbuild-linux-riscv64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz#c0ec0fc3a23624deebf657781550d2329cec4213" + integrity sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ== 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-linux-s390x@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz#ec2af4572d63336cfb27f5a5c851fb1b6617dd91" + integrity sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw== 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-netbsd-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz#0e283278e9fdbaa7f0930f93ee113d7759cd865e" + integrity sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA== 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-openbsd-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz#2a73bba04e16d8ef278fbe2be85248e12a2f2cc2" + integrity sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA== 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-sunos-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz#8fe03513b8b2e682a6d79d5e3ca5849651a3c1d8" + integrity sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g== 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-wasm@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.14.22.tgz#9671d1355473b6688d00fe8ef6fa50274eff5465" + integrity sha512-FOSAM29GN1fWusw0oLMv6JYhoheDIh5+atC72TkJKfIUMID6yISlicoQSd9gsNSFsNBvABvtE2jR4JB1j4FkFw== 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-32@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz#a75df61e3e49df292a1842be8e877a3153ee644f" + integrity sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg== 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-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz#d06cf8bbe4945b8bf95a730d871e54a22f635941" + integrity sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw== 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-windows-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz#f8b1b05c548073be8413a5ecb12d7c2f6e717227" + integrity sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg== esbuild@0.14.11: version "0.14.11" @@ -9228,29 +9377,30 @@ esbuild@0.14.11: esbuild-windows-64 "0.14.11" esbuild-windows-arm64 "0.14.11" -esbuild@0.14.14: - version "0.14.14" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.14.tgz#3b99f20d628013c3e2ae90e67687e03f1d6eb071" - integrity sha512-aiK4ddv+uui0k52OqSHu4xxu+SzOim7Rlz4i25pMEiC8rlnGU0HJ9r+ZMfdWL5bzifg+nhnn7x4NSWTeehYblg== +esbuild@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.22.tgz#2b55fde89d7aa5aaaad791816d58ff9dfc5ed085" + integrity sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA== optionalDependencies: - esbuild-android-arm64 "0.14.14" - esbuild-darwin-64 "0.14.14" - esbuild-darwin-arm64 "0.14.14" - esbuild-freebsd-64 "0.14.14" - esbuild-freebsd-arm64 "0.14.14" - esbuild-linux-32 "0.14.14" - esbuild-linux-64 "0.14.14" - esbuild-linux-arm "0.14.14" - esbuild-linux-arm64 "0.14.14" - esbuild-linux-mips64le "0.14.14" - esbuild-linux-ppc64le "0.14.14" - esbuild-linux-s390x "0.14.14" - esbuild-netbsd-64 "0.14.14" - esbuild-openbsd-64 "0.14.14" - esbuild-sunos-64 "0.14.14" - esbuild-windows-32 "0.14.14" - esbuild-windows-64 "0.14.14" - esbuild-windows-arm64 "0.14.14" + esbuild-android-arm64 "0.14.22" + esbuild-darwin-64 "0.14.22" + esbuild-darwin-arm64 "0.14.22" + esbuild-freebsd-64 "0.14.22" + esbuild-freebsd-arm64 "0.14.22" + esbuild-linux-32 "0.14.22" + esbuild-linux-64 "0.14.22" + esbuild-linux-arm "0.14.22" + esbuild-linux-arm64 "0.14.22" + esbuild-linux-mips64le "0.14.22" + esbuild-linux-ppc64le "0.14.22" + esbuild-linux-riscv64 "0.14.22" + esbuild-linux-s390x "0.14.22" + esbuild-netbsd-64 "0.14.22" + esbuild-openbsd-64 "0.14.22" + esbuild-sunos-64 "0.14.22" + esbuild-windows-32 "0.14.22" + esbuild-windows-64 "0.14.22" + esbuild-windows-arm64 "0.14.22" escalade@^3.1.1: version "3.1.1" @@ -9644,17 +9794,15 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.3.1.tgz#d0f170b1f5c8a2009bab0beffd4bb94f043e38e7" - integrity sha512-MrNXV2sL9iDRebWPGOGFdPQRl2eDQNu/uhxIMShjjx74T6kC6jFIkmQ6OqXDtevjGUkyB2IT56RzDBqXf/QPCg== +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== dependencies: - "@jest/types" "^27.2.5" - ansi-styles "^5.0.0" - jest-get-type "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-regex-util "^27.0.6" + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" express@4.17.1, express@^4.17.1: version "4.17.1" @@ -10175,6 +10323,11 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" @@ -10334,7 +10487,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -10893,6 +11046,13 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-entities@^2.1.0, html-entities@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" @@ -11047,6 +11207,25 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-server@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.0.tgz#51d43e03cdbb94f328b24821cad2cefc6c6a2eed" + integrity sha512-5lYsIcZtf6pdR8tCtzAHTWrAveo4liUlJdWc7YafwK/maPgYHs+VNP6KpCClmUnSorJrARVMXqtT055zBv11Yg== + dependencies: + basic-auth "^2.0.1" + chalk "^4.1.2" + corser "^2.0.1" + he "^1.2.0" + html-encoding-sniffer "^3.0.0" + http-proxy "^1.18.1" + mime "^1.6.0" + minimist "^1.2.5" + opener "^1.5.1" + portfinder "^1.0.28" + secure-compare "3.0.1" + union "~0.5.0" + url-join "^4.0.1" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -11098,7 +11277,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2, iconv-lite@^0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -11948,7 +12127,7 @@ istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: +istanbul-lib-instrument@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== @@ -11969,6 +12148,17 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -11987,10 +12177,10 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -12013,13 +12203,13 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" - chalk "^2.4.2" + async "^3.2.3" + chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" @@ -12030,139 +12220,115 @@ jasmine-marbles@~0.8.4: dependencies: lodash "^4.17.20" -jest-changed-files@^27.3.0: - version "27.3.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.3.0.tgz#22a02cc2b34583fc66e443171dc271c0529d263c" - integrity sha512-9DJs9garMHv4RhylUMZgbdCJ3+jHSkpL9aaVKp13xtXAD80qLTLrqcDZL1PHA9dYA0bCI86Nv2BhkLpLhrBcPg== +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" execa "^5.0.0" throat "^6.0.1" -jest-circus@^27.2.2, jest-circus@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.3.1.tgz#1679e74387cbbf0c6a8b42de963250a6469e0797" - integrity sha512-v1dsM9II6gvXokgqq6Yh2jHCpfg7ZqV4jWY66u7npz24JnhP3NHxI0sKT7+ZMQ7IrOWHYAaeEllOySbDbWsiXw== +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: - "@jest/environment" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.3.1" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" throat "^6.0.1" -jest-cli@^27.2.3: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.3.1.tgz#b576f9d146ba6643ce0a162d782b40152b6b1d16" - integrity sha512-WHnCqpfK+6EvT62me6WVs8NhtbjAS4/6vZJnk7/2+oOr50cwAzG4Wxt6RXX0hu6m1169ZGMlhYYUNeKBXCph/Q== +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== dependencies: - "@jest/core" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" prompts "^2.0.1" yargs "^16.2.0" -jest-config@27.2.2: - version "27.2.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.2.2.tgz#970d8466c60ac106ac9d7d0b8dcf3ff150fa713a" - integrity sha512-2nhms3lp52ZpU0636bB6zIFHjDVtYxzFQIOHZjBFUeXcb6b41sEkWojbHaJ4FEIO44UbccTLa7tvNpiFCgPE7w== +jest-config@27.5.1, jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.2.2" - "@jest/types" "^27.1.1" - babel-jest "^27.2.2" - chalk "^4.0.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - is-ci "^3.0.0" - jest-circus "^27.2.2" - jest-environment-jsdom "^27.2.2" - jest-environment-node "^27.2.2" - jest-get-type "^27.0.6" - jest-jasmine2 "^27.2.2" - jest-regex-util "^27.0.6" - jest-resolve "^27.2.2" - jest-runner "^27.2.2" - jest-util "^27.2.0" - jest-validate "^27.2.2" - micromatch "^4.0.4" - pretty-format "^27.2.2" - -jest-config@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.3.1.tgz#cb3b7f6aaa8c0a7daad4f2b9573899ca7e09bbad" - integrity sha512-KY8xOIbIACZ/vdYCKSopL44I0xboxC751IX+DXL2+Wx6DKNycyEfV3rryC3BPm5Uq/BBqDoMrKuqLEUNJmMKKg== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.3.1" - "@jest/types" "^27.2.5" - babel-jest "^27.3.1" + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" - graceful-fs "^4.2.4" - jest-circus "^27.3.1" - jest-environment-jsdom "^27.3.1" - jest-environment-node "^27.3.1" - jest-get-type "^27.3.1" - jest-jasmine2 "^27.3.1" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-runner "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" micromatch "^4.0.4" - pretty-format "^27.3.1" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" -jest-diff@^27.0.0, jest-diff@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.3.1.tgz#d2775fea15411f5f5aeda2a5e02c2f36440f6d55" - integrity sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-docblock@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3" - integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA== +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: detect-newline "^3.0.0" -jest-each@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.3.1.tgz#14c56bb4f18dd18dc6bdd853919b5f16a17761ff" - integrity sha512-E4SwfzKJWYcvOYCjOxhZcxwL+AY0uFMvdCOwvzgutJiaiodFjkxQQDxHm8FQBeTqDnSmKsQWn7ldMRzTn2zJaQ== +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" chalk "^4.0.0" - jest-get-type "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" jest-environment-jsdom@^27.0.0: version "27.1.0" @@ -12177,40 +12343,35 @@ jest-environment-jsdom@^27.0.0: jest-util "^27.1.0" jsdom "^16.6.0" -jest-environment-jsdom@^27.2.2, jest-environment-jsdom@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.3.1.tgz#63ac36d68f7a9303494df783494856222b57f73e" - integrity sha512-3MOy8qMzIkQlfb3W1TfrD7uZHj+xx8Olix5vMENkj5djPmRqndMaXtpnaZkxmxM+Qc3lo+yVzJjzuXbCcZjAlg== +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== dependencies: - "@jest/environment" "^27.3.1" - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" jsdom "^16.6.0" -jest-environment-node@^27.2.2, jest-environment-node@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.3.1.tgz#af7d0eed04edafb740311b303f3fe7c8c27014bb" - integrity sha512-T89F/FgkE8waqrTSA7/ydMkcc52uYPgZZ6q8OaZgyiZkJb5QNNCF6oPZjH9IfPFfcc9uBWh1574N0kY0pSvTXw== +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== dependencies: - "@jest/environment" "^27.3.1" - "@jest/fake-timers" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.3.0" - jest-util "^27.3.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" -jest-get-type@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" - integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== - -jest-get-type@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff" - integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-haste-map@^26.6.2: version "26.6.2" @@ -12233,67 +12394,66 @@ jest-haste-map@^26.6.2: optionalDependencies: fsevents "^2.1.2" -jest-haste-map@^27.2.2, jest-haste-map@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee" - integrity sha512-lYfNZIzwPccDJZIyk9Iz5iQMM/MH56NIIcGj7AFU1YyA4ewWFBl8z+YPJuSCRML/ee2cCt2y3W4K3VXPT6Nhzg== +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.0.6" - jest-serializer "^27.0.6" - jest-util "^27.3.1" - jest-worker "^27.3.1" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.2.2, jest-jasmine2@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.3.1.tgz#df6d3d07c7dafc344feb43a0072a6f09458d32b0" - integrity sha512-WK11ZUetDQaC09w4/j7o4FZDUIp+4iYWH/Lik34Pv7ukL+DuXFGdnmmi7dT58J2ZYKFB5r13GyE0z3NPeyJmsg== +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.3.1" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^27.3.1" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-runtime "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - pretty-format "^27.3.1" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" throat "^6.0.1" -jest-leak-detector@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.3.1.tgz#7fb632c2992ef707a1e73286e1e704f9cc1772b2" - integrity sha512-78QstU9tXbaHzwlRlKmTpjP9k4Pvre5l0r8Spo4SbFFVy/4Abg9I6ZjHwjg2QyKEAMg020XcjP+UgLZIY50yEg== +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-matcher-utils@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.3.1.tgz#257ad61e54a6d4044e080d85dbdc4a08811e9c1c" - integrity sha512-hX8N7zXS4k+8bC1Aj0OWpGb7D3gIXxYvPNK1inP5xvE4ztbz3rc4AkI6jGVaerepBnfWB17FL5lWFJT3s7qo8w== +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" - jest-diff "^27.3.1" - jest-get-type "^27.3.1" - pretty-format "^27.3.1" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" jest-message-util@^27.1.0: version "27.1.0" @@ -12310,18 +12470,18 @@ jest-message-util@^27.1.0: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.3.1.tgz#f7c25688ad3410ab10bcb862bcfe3152345c6436" - integrity sha512-bh3JEmxsTZ/9rTm0jQrPElbY2+y48Rw2t47uMfByNyUVR+OfPh4anuyKsGqsNkXk/TI4JbLRZx+7p7Hdt6q1yg== +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^27.3.1" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" @@ -12333,12 +12493,12 @@ jest-mock@^27.1.0: "@jest/types" "^27.1.0" "@types/node" "*" -jest-mock@^27.3.0: - version "27.3.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.3.0.tgz#ddf0ec3cc3e68c8ccd489bef4d1f525571a1b867" - integrity sha512-ziZiLk0elZOQjD08bLkegBzv5hCABu/c8Ytx45nJKkysQwGaonvmTxwjLqEA4qGdasq9o2I8/HtdGMNnVsMTGw== +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -12364,111 +12524,90 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-regex-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" - integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== - -jest-resolve-dependencies@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.3.1.tgz#85b99bdbdfa46e2c81c6228fc4c91076f624f6e2" - integrity sha512-X7iLzY8pCiYOnvYo2YrK3P9oSE8/3N2f4pUZMJ8IUcZnT81vlSonya1KTO9ZfKGuC+svE6FHK/XOb8SsoRUV1A== - dependencies: - "@jest/types" "^27.2.5" - jest-regex-util "^27.0.6" - jest-snapshot "^27.3.1" +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve@27.2.2: - version "27.2.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.2.2.tgz#1bad93dbc6c20edb874e6720e82e4e48900b120b" - integrity sha512-tfbHcBs/hJTb3fPQ/3hLWR+TsLNTzzK98TU+zIAsrL9nNzWfWROwopUOmiSUqmHMZW5t9au/433kSF2/Af+tTw== +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: - "@jest/types" "^27.1.1" - chalk "^4.0.0" - escalade "^3.1.1" - graceful-fs "^4.2.4" - jest-haste-map "^27.2.2" - jest-pnp-resolver "^1.2.2" - jest-util "^27.2.0" - jest-validate "^27.2.2" - resolve "^1.20.0" - slash "^3.0.0" + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" -jest-resolve@^27.2.2, jest-resolve@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.3.1.tgz#0e5542172a1aa0270be6f66a65888647bdd74a3e" - integrity sha512-Dfzt25CFSPo3Y3GCbxynRBZzxq9AdyNN+x/v2IqYx6KVT5Z6me2Z/PsSGFSv3cOSUZqJ9pHxilao/I/m9FouLw== +jest-resolve@27.5.1, jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" jest-pnp-resolver "^1.2.2" - jest-util "^27.3.1" - jest-validate "^27.3.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.2.2, jest-runner@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.3.1.tgz#1d594dcbf3bd8600a7e839e790384559eaf96e3e" - integrity sha512-r4W6kBn6sPr3TBwQNmqE94mPlYVn7fLBseeJfo4E2uCTmAyDFm2O5DYAQAFP7Q3YfiA/bMwg8TVsciP7k0xOww== - dependencies: - "@jest/console" "^27.3.1" - "@jest/environment" "^27.3.1" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.0.6" - jest-environment-jsdom "^27.3.1" - jest-environment-node "^27.3.1" - jest-haste-map "^27.3.1" - jest-leak-detector "^27.3.1" - jest-message-util "^27.3.1" - jest-resolve "^27.3.1" - jest-runtime "^27.3.1" - jest-util "^27.3.1" - jest-worker "^27.3.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" source-map-support "^0.5.6" throat "^6.0.1" -jest-runtime@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.3.1.tgz#80fa32eb85fe5af575865ddf379874777ee993d7" - integrity sha512-qtO6VxPbS8umqhEDpjA4pqTkKQ1Hy4ZSi9mDVeE9Za7LKBo2LdW2jmT+Iod3XFaJqINikZQsn2wEi0j9wPRbLg== - dependencies: - "@jest/console" "^27.3.1" - "@jest/environment" "^27.3.1" - "@jest/globals" "^27.3.1" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.3.1" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" - "@types/yargs" "^16.0.0" +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" execa "^5.0.0" - exit "^0.1.2" glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.3.1" - jest-message-util "^27.3.1" - jest-mock "^27.3.0" - jest-regex-util "^27.0.6" - jest-resolve "^27.3.1" - jest-snapshot "^27.3.1" - jest-util "^27.3.1" - jest-validate "^27.3.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^16.2.0" jest-serializer@^26.6.2: version "26.6.2" @@ -12478,54 +12617,52 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-serializer@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1" - integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" -jest-snapshot@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.3.1.tgz#1da5c0712a252d70917d46c037054f5918c49ee4" - integrity sha512-APZyBvSgQgOT0XumwfFu7X3G5elj6TGhCBLbBdn3R1IzYustPGPE38F51dBWMQ8hRXa9je0vAdeVDtqHLvB6lg== +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== dependencies: "@babel/core" "^7.7.2" "@babel/generator" "^7.7.2" - "@babel/parser" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/transform" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.3.1" - graceful-fs "^4.2.4" - jest-diff "^27.3.1" - jest-get-type "^27.3.1" - jest-haste-map "^27.3.1" - jest-matcher-utils "^27.3.1" - jest-message-util "^27.3.1" - jest-resolve "^27.3.1" - jest-util "^27.3.1" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" natural-compare "^1.4.0" - pretty-format "^27.3.1" + pretty-format "^27.5.1" semver "^7.3.2" -jest-util@27.2.0: - version "27.2.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.2.0.tgz#bfccb85cfafae752257319e825a5b8d4ada470dc" - integrity sha512-T5ZJCNeFpqcLBpx+Hl9r9KoxBCUqeWlJ1Htli+vryigZVJ1vuLB9j35grEBASp4R13KFkV7jM52bBGnArpJN6A== +jest-util@27.5.1, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^27.1.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^3.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" picomatch "^2.2.3" jest-util@^26.6.2: @@ -12552,41 +12689,29 @@ jest-util@^27.0.0, jest-util@^27.1.0: is-ci "^3.0.0" picomatch "^2.2.3" -jest-util@^27.2.0, jest-util@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.3.1.tgz#a58cdc7b6c8a560caac9ed6bdfc4e4ff23f80429" - integrity sha512-8fg+ifEH3GDryLQf/eKZck1DEs2YuVPBCMOaHQxVVLmQwl/CDhWzrvChTX4efLZxGrw+AA0mSXv78cyytBt/uw== - dependencies: - "@jest/types" "^27.2.5" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.4" - picomatch "^2.2.3" - -jest-validate@^27.2.2, jest-validate@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.3.1.tgz#3a395d61a19cd13ae9054af8cdaf299116ef8a24" - integrity sha512-3H0XCHDFLA9uDII67Bwi1Vy7AqwA5HqEEjyy934lgVhtJ3eisw6ShOF1MDmRPspyikef5MyExvIm0/TuLzZ86Q== +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== dependencies: - "@jest/types" "^27.2.5" + "@jest/types" "^27.5.1" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.3.1" + jest-get-type "^27.5.1" leven "^3.1.0" - pretty-format "^27.3.1" + pretty-format "^27.5.1" -jest-watcher@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.3.1.tgz#ba5e0bc6aa843612b54ddb7f009d1cbff7e05f3e" - integrity sha512-9/xbV6chABsGHWh9yPaAGYVVKurWoP3ZMCv6h+O1v9/+pkOroigs6WzZ0e9gLP/njokUwM7yQhr01LKJVMkaZA== +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== dependencies: - "@jest/test-result" "^27.3.1" - "@jest/types" "^27.2.5" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.3.1" + jest-util "^27.5.1" string-length "^4.0.1" jest-worker@^26.5.0, jest-worker@^26.6.2: @@ -12607,16 +12732,7 @@ jest-worker@^27.0.6: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^27.2.2, jest-worker@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.3.1.tgz#0def7feae5b8042be38479799aeb7b5facac24b2" - integrity sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^27.4.5: +jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -12625,14 +12741,14 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@27.2.3: - version "27.2.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.3.tgz#9c2af9ce874a3eb202f83d92fbc1cc61ccc73248" - integrity sha512-r4ggA29J5xUg93DpvbsX+AXlFMWE3hZ5Y6BfgTl8PJvWelVezNPkmrsixuGoDBTHTCwScRSH0O4wsoeUgLie2w== +jest@27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== dependencies: - "@jest/core" "^27.2.3" + "@jest/core" "^27.5.1" import-local "^3.0.2" - jest-cli "^27.2.3" + jest-cli "^27.5.1" js-string-escape@^1.0.1: version "1.0.1" @@ -12749,7 +12865,7 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@2.x, json5@^2.1.0, json5@^2.1.2, json5@^2.1.3: +json5@2.x, json5@^2.1.2, json5@^2.1.3: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== @@ -12763,6 +12879,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + jsonc-parser@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" @@ -13001,17 +13122,10 @@ libphonenumber-js@^1.9.7: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.26.tgz#888aca88a4019d0b108e45db8e0509d55785ae45" integrity sha512-j0brmDsZLSKJCrcli1HjzHYP29VOf5P/Yz+xxe7WmKTe+0XVnKa0J+UEAaT96D21w1mfLPhlm0ZXBQ13EVF+XQ== -license-webpack-plugin@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.0.tgz#11cf6ac96559f4a987cf55d3d2a33f295ae8f13b" - integrity sha512-b9iMrROrw2fTOJBZ57h0xJfT5/1Cxg4ucYbtpWoukv4Awb2TFPfDDFVHNM8w6SYQpVfB13a5tQJxgGamqwrsyw== - dependencies: - webpack-sources "^3.0.0" - -license-webpack-plugin@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.1.tgz#957930fa595f5b65aa0b21bfd2c19644486f3d9f" - integrity sha512-SQum9mg3BgnY5BK+2KYl4W7pk9b26Q8tW2lTsO6tidD0/Ds9ksdXvp3ip2s9LqDjj5gtBMyWRfOPZptWj4PfCg== +license-webpack-plugin@4.0.2, license-webpack-plugin@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" + integrity sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw== dependencies: webpack-sources "^3.0.0" @@ -13187,6 +13301,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -13207,7 +13326,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -13282,13 +13401,20 @@ luxon@^1.28.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== -magic-string@0.25.7, magic-string@^0.25.0: +magic-string@0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== dependencies: sourcemap-codec "^1.4.4" +magic-string@^0.26.0: + version "0.26.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.1.tgz#ba9b651354fa9512474199acecf9c6dbe93f97fd" + integrity sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -13577,7 +13703,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.30, mime-types@^2.1.31, dependencies: mime-db "1.49.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0, mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -13633,6 +13759,13 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" + integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -14091,6 +14224,11 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" +node-machine-id@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/node-machine-id/-/node-machine-id-1.1.12.tgz#37904eee1e59b320bb9c5d6c0a59f3b469cb6267" + integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ== + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -14106,6 +14244,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" + integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== + node-rsa@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d" @@ -14275,12 +14418,44 @@ 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.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== +nx@14.1.4: + version "14.1.4" + resolved "https://registry.yarnpkg.com/nx/-/nx-14.1.4.tgz#83bfb7564604eb26a78b3f5718cb2f90faeaaf4f" + integrity sha512-cYULObHKIbDbvxUnxn0tVbBlM8Vzg2u9sZYpmL6TEVF/Xbj3PQi8oyazc2/dU5Qw319lqMJcZonG2ihv9yEGKg== dependencies: - "@nrwl/cli" "13.8.5" + "@nrwl/cli" "14.1.4" + "@nrwl/tao" "14.1.4" + "@parcel/watcher" "2.0.4" + "@swc-node/register" "^1.4.2" + "@swc/core" "^1.2.173" + chalk "4.1.0" + chokidar "^3.5.1" + cli-cursor "3.1.0" + cli-spinners "2.6.1" + cliui "^7.0.2" + dotenv "~10.0.0" + enquirer "~2.3.6" + fast-glob "3.2.7" + figures "3.2.0" + flat "^5.0.2" + fs-extra "^9.1.0" + glob "7.1.4" + ignore "^5.0.4" + jsonc-parser "3.0.0" + minimatch "3.0.4" + npm-run-path "^4.0.1" + open "^8.4.0" + rxjs "^6.5.4" + rxjs-for-await "0.0.2" + semver "7.3.4" + string-width "^4.2.3" + tar-stream "~2.2.0" + tmp "~0.2.1" + tsconfig-paths "^3.9.0" + tslib "^2.3.0" + v8-compile-cache "2.3.0" + yargs "^17.4.0" + yargs-parser "21.0.1" oauth@0.9.x: version "0.9.15" @@ -14454,6 +14629,11 @@ open@^7.0.3: is-docker "^2.0.0" is-wsl "^2.1.1" +opener@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optional@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/optional/-/optional-0.1.4.tgz#cdb1a9bedc737d2025f690ceeb50e049444fd5b3" @@ -14746,7 +14926,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -15486,12 +15666,11 @@ pretty-format@^27.0.0, pretty-format@^27.1.0: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^27.2.2, pretty-format@^27.3.1: - version "27.3.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" - integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== +pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: - "@jest/types" "^27.2.5" ansi-regex "^5.0.1" ansi-styles "^5.0.0" react-is "^17.0.1" @@ -15685,6 +15864,13 @@ qs@^6.10.0, qs@^6.6.0: dependencies: side-channel "^1.0.4" +qs@^6.4.0: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -15953,7 +16139,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -16259,11 +16445,6 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -16518,10 +16699,10 @@ sass-loader@^10.1.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@1.49.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078" - integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw== +sass@1.49.9: + version "1.49.9" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9" + integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -16593,6 +16774,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +secure-compare@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" + integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM= + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -16998,7 +17184,7 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -sourcemap-codec@1.4.8, sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: +sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -17531,7 +17717,18 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== -tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: +tar-stream@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@6.1.11, tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -17618,11 +17815,12 @@ terser-webpack-plugin@^5.3.0: source-map "^0.6.1" terser "^5.7.2" -terser@5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" - integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== +terser@5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.11.0.tgz#2da5506c02e12cd8799947f30ce9c5b760be000f" + integrity sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A== dependencies: + acorn "^8.5.0" commander "^2.20.0" source-map "~0.7.2" source-map-support "~0.5.20" @@ -17824,7 +18022,21 @@ ts-essentials@^2.0.3: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745" integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w== -ts-jest@27.0.5, ts-jest@^27.0.0: +ts-jest@27.1.4: + version "27.1.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.4.tgz#84d42cf0f4e7157a52e7c64b1492c46330943e00" + integrity sha512-qjkZlVPWVctAezwsOD1OPzbZ+k7zA5z3oxII4dGdZo5ggX/PL7kvwTM0pXTr10fAtbiVpJaL3bWd502zAhpgSQ== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^27.0.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "20.x" + +ts-jest@^27.0.0: version "27.0.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.0.5.tgz#0b0604e2271167ec43c12a69770f0bb65ad1b750" integrity sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w== @@ -18027,16 +18239,21 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.5.5, typescript@^4.5.3: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== typescript@^3.2.4: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== +typescript@^4.5.3: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + uglify-js@^3.1.4: version "3.14.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.3.tgz#c0f25dfea1e8e5323eccf59610be08b6043c15cf" @@ -18138,6 +18355,13 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +union@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" + integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== + dependencies: + qs "^6.4.0" + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -18255,6 +18479,11 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + url-loader@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" @@ -18353,15 +18582,6 @@ v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -v8-to-istanbul@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.0.0.tgz#4229f2a99e367f3f018fa1d5c2b8ec684667c69c" - integrity sha512-LkmXi8UUNxnCC+JlH7/fsfsKr5AU110l+SYGJimWNkWhxbN5EyeOtm1MJ0hhvqMMOhGwBj1Fp70Yv9i+hX0QAg== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" - v8-to-istanbul@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz#0aeb763894f1a0a1676adf8a8b7612a38902446c" @@ -18719,13 +18939,13 @@ webpack@4: watchpack "^1.7.4" webpack-sources "^1.4.1" -webpack@5.67.0: - version "5.67.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.67.0.tgz#cb43ca2aad5f7cc81c4cd36b626e6b819805dbfd" - integrity sha512-LjFbfMh89xBDpUMgA1W9Ur6Rn/gnr2Cq1jjHFPo4v6a79/ypznSYbAyPgGhwsxBtMIaEmDD1oJoA7BEYw/Fbrw== +webpack@5.70.0: + version "5.70.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" + integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" "@webassemblyjs/ast" "1.11.1" "@webassemblyjs/wasm-edit" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" @@ -18733,7 +18953,7 @@ webpack@5.67.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" + enhanced-resolve "^5.9.2" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" @@ -18830,6 +19050,13 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" @@ -18933,15 +19160,6 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -19032,16 +19250,16 @@ yaml@^1.10.0, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@20.0.0: - version "20.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.0.0.tgz#c65a1daaa977ad63cebdd52159147b789a4e19a9" - integrity sha512-8eblPHTL7ZWRkyjIZJjnGf+TijiKJSwA24svzLRVvtgoi/RZiKa9fFQTrlx0OKLnyHSdt/enrdadji6WFfESVA== - yargs-parser@20.x, yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@21.0.1, yargs-parser@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" @@ -19050,31 +19268,6 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yargs@^12.0.5: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" @@ -19119,6 +19312,19 @@ yargs@^17.2.1: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.4.0: + version "17.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284" + integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From b331f5f04d60044f926db73bbc6f2335dde511c9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 15:52:21 +0200 Subject: [PATCH 310/337] Feature/setup nx cloud (#889) * Upgrade angular, Nx and storybook * Setup Nx Cloud * Update changelog --- CHANGELOG.md | 1 + nx.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a31f3d6e4..502d2286b 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 ### Added - Set up a queue for the data gathering jobs +- Set up _Nx Cloud_ ### Changed diff --git a/nx.json b/nx.json index 3ae447028..62c77eaa6 100644 --- a/nx.json +++ b/nx.json @@ -15,8 +15,9 @@ "npmScope": "ghostfolio", "tasksRunnerOptions": { "default": { - "runner": "@nrwl/workspace/tasks-runners/default", + "runner": "@nrwl/nx-cloud", "options": { + "accessToken": "Mjg0ZGQ2YjAtNGI4NS00NmYwLThhOWEtMWZmNmQzODM4YzU4fHJlYWQ=", "cacheableOperations": [ "build", "lint", From eb9cece4e4f7155b2f5492b9eb7436251d6ad636 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 15:56:39 +0200 Subject: [PATCH 311/337] Update browserslist database (#890) --- yarn.lock | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/yarn.lock b/yarn.lock index cb8212c4b..64d095983 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7291,29 +7291,9 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001254: - version "1.0.30001254" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001254.tgz#974d45e8b7f6e3b63d4b1435e97752717612d4b9" - integrity sha512-GxeHOvR0LFMYPmFGA+NiTOt9uwYDxB3h154tW2yBYwfz2EMX3i1IBgr6gmJGfU0K8KQsqPa5XqLD8zVdP5lUzA== - -caniuse-lite@^1.0.30001280: - version "1.0.30001282" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd" - integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg== - -caniuse-lite@^1.0.30001286: - version "1.0.30001294" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz#4849f27b101fd59ddee3751598c663801032533d" - integrity sha512-LiMlrs1nSKZ8qkNhpUf5KD0Al1KCBE3zaT7OLOwEkagXMEDij98SiOovn9wxVGQpklk9vVC/pUSqgYmkmKOS8g== - -caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001299: - version "1.0.30001311" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz#682ef3f4e617f1a177ad943de59775ed3032e511" - integrity sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A== - -caniuse-lite@^1.0.30001332: +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001254, caniuse-lite@^1.0.30001280, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001299, caniuse-lite@^1.0.30001332: version "1.0.30001338" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz#b5dd7a7941a51a16480bdf6ff82bded1628eec0d" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz" integrity sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ== capture-exit@^2.0.0: From b3e07c8446c19f2bcbb60781bba118c8b4997d81 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 15:59:19 +0200 Subject: [PATCH 312/337] Feature/support permissions in fire calculator (#891) * Support hasPermissionToUpdateUserSettings * Update changelog --- CHANGELOG.md | 1 + .../portfolio/fire/fire-page.component.ts | 7 +++++ .../app/pages/portfolio/fire/fire-page.html | 1 + .../fire-calculator.component.ts | 30 +++++++++++++++---- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 502d2286b..aa30f781a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Migrated the asset profile data gathering to the queue design pattern - Improved the allocations page with no filtering - Harmonized the _No data available_ label in the portfolio proportion chart component +- Improved the _FIRE_ calculator for the _Live Demo_ - Upgraded `angular` from version `13.2.2` to `13.3.6` - Upgraded `Nx` from version `13.8.5` to `14.1.4` - Upgraded `storybook` from version `6.4.18` to `6.4.22` diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index bd4175f93..68df48df4 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } 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 { hasPermission, permissions } from '@ghostfolio/common/permissions'; import Big from 'big.js'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; @@ -16,6 +17,7 @@ import { takeUntil } from 'rxjs/operators'; export class FirePageComponent implements OnDestroy, OnInit { public deviceType: string; public fireWealth: Big; + public hasPermissionToUpdateUserSettings: boolean; public isLoading = false; public user: User; public withdrawalRatePerMonth: Big; @@ -63,6 +65,11 @@ export class FirePageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.hasPermissionToUpdateUserSettings = hasPermission( + this.user.permissions, + permissions.updateUserSettings + ); + this.changeDetectorRef.markForCheck(); } }); diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index 0aa0962e4..d62eeab30 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -59,6 +59,7 @@ [currency]="user?.settings?.baseCurrency" [deviceType]="deviceType" [fireWealth]="fireWealth?.toNumber()" + [hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings" [locale]="user?.settings?.locale" [savingsRate]="user?.settings?.savingsRate" (savingsRateChanged)="onSavingsRateChange($event)" diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index ed3810961..933d1899a 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -41,6 +41,7 @@ export class FireCalculatorComponent @Input() currency: string; @Input() deviceType: string; @Input() fireWealth: number; + @Input() hasPermissionToUpdateUserSettings: boolean; @Input() locale: string; @Input() savingsRate = 0; @@ -76,12 +77,17 @@ export class FireCalculatorComponent Tooltip ); - this.calculatorForm.setValue({ - annualInterestRate: 5, - paymentPerPeriod: this.savingsRate, - principalInvestmentAmount: 0, - time: 10 - }); + this.calculatorForm.setValue( + { + annualInterestRate: 5, + paymentPerPeriod: this.savingsRate, + principalInvestmentAmount: 0, + time: 10 + }, + { + emitEvent: false + } + ); this.calculatorForm.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) @@ -115,6 +121,12 @@ export class FireCalculatorComponent this.changeDetectorRef.markForCheck(); }); } + + if (this.hasPermissionToUpdateUserSettings === true) { + this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); + } else { + this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); + } } public ngOnChanges() { @@ -135,6 +147,12 @@ export class FireCalculatorComponent this.changeDetectorRef.markForCheck(); }); } + + if (this.hasPermissionToUpdateUserSettings === true) { + this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); + } else { + this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); + } } public ngOnDestroy() { From 5be95b7b634a7e05c9b135923d92fb1eca2bf39f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 16:02:44 +0200 Subject: [PATCH 313/337] Feature/simplify about page (#892) * Simplify about page * Update changelog --- CHANGELOG.md | 1 + .../app/pages/about/about-page.component.ts | 1 - .../src/app/pages/about/about-page.html | 190 +++++++++--------- .../src/app/pages/about/about-page.scss | 16 +- 4 files changed, 102 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa30f781a..fa31907b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the allocations page with no filtering - Harmonized the _No data available_ label in the portfolio proportion chart component - Improved the _FIRE_ calculator for the _Live Demo_ +- Simplified the about page - Upgraded `angular` from version `13.2.2` to `13.3.6` - Upgraded `Nx` from version `13.8.5` to `14.1.4` - Upgraded `storybook` from version `6.4.18` to `6.4.22` diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index ab4fdb57e..765e9ba92 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -22,7 +22,6 @@ export class AboutPageComponent implements OnDestroy, OnInit { public hasPermissionForStatistics: boolean; public hasPermissionForSubscription: boolean; public isLoggedIn: boolean; - public lastPublish = environment.lastPublish; public statistics: Statistics; public user: User; public version = environment.version; diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index eb9da1593..9f02c2355 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -2,103 +2,101 @@

About Ghostfolio

- - -

- Ghostfolio is a lightweight wealth management - application for individuals to keep track of their wealth like - stocks, ETFs or cryptocurrencies and make solid, data-driven - investment decisions. The source code is fully available as open - source software (OSS). The project has been initiated by - Thomas Kaul - and is driven by the efforts of its - contributors. - - This instance is running Ghostfolio {{ version }} and has been - last published on {{ lastPublish }}. - - Check the system status at - status.ghostfol.io. -

-

- If you encounter a bug or would like to suggest an improvement or a - new feature, please join the - Ghostfolio - Slack community, tweet to - @ghostfolio_, send an e-mail to - hi@ghostfol.io - or open an issue at - GitHub. -

-

- - - - - - - - - - - - -

-
+

+ Ghostfolio is a lightweight wealth management + application for individuals to keep track of stocks, ETFs or + cryptocurrencies and make solid, data-driven investment decisions. The + source code is fully available as open source software (OSS). The + project has been initiated by + Thomas Kaul -

-
-
-
+ and is driven by the efforts of its + contributors. + + This instance is running Ghostfolio {{ version }}. + + Check the system status at + status.ghostfol.io. +

+

+ If you encounter a bug or would like to suggest an improvement or a + new + feature, please join the + Ghostfolio + Slack community, tweet to + @ghostfolio_, send an e-mail to + hi@ghostfol.io + or open an issue at + GitHub. +

+

+ + + + + + + + + + + + +

+
+ +
+
diff --git a/apps/client/src/app/pages/about/about-page.scss b/apps/client/src/app/pages/about/about-page.scss index 498665eff..df6759442 100644 --- a/apps/client/src/app/pages/about/about-page.scss +++ b/apps/client/src/app/pages/about/about-page.scss @@ -2,15 +2,13 @@ color: rgb(var(--dark-primary-text)); display: block; - .mat-card { - &.about-container { - a { - color: rgba(var(--palette-primary-500), 1); - font-weight: 500; + .about-container { + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; - &:hover { - color: rgba(var(--palette-primary-300), 1); - } + &:hover { + color: rgba(var(--palette-primary-300), 1); } } @@ -29,7 +27,7 @@ :host-context(.is-dark-theme) { color: rgb(var(--light-primary-text)); - .mat-card { + .about-container { .independent-and-bootstrapped-logo { background-image: url('/assets/bootstrapped-light.svg'); opacity: 1; From ff9b6bb4dfa0281fdf5fc9486ad5cf227dcc401b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 16:04:43 +0200 Subject: [PATCH 314/337] Release 1.146.0 (#893) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa31907b3..1148d6dcd 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.146.0 - 08.05.2022 ### Added diff --git a/package.json b/package.json index 8bf08dae0..a803a8bb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.145.0", + "version": "1.146.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 32dd76be5f5ecab9b7fa9b856fe28d2084704124 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 16:55:14 +0200 Subject: [PATCH 315/337] Fix path to jest files (#894) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 075b43396..c02d42d01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,8 @@ RUN node decorate-angular-cli.js COPY ./angular.json angular.json COPY ./nx.json nx.json COPY ./replace.build.js replace.build.js -COPY ./jest.preset.js jest.preset.js -COPY ./jest.config.js jest.config.js +COPY ./jest.preset.ts jest.preset.ts +COPY ./jest.config.ts jest.config.ts COPY ./tsconfig.base.json tsconfig.base.json COPY ./libs libs COPY ./apps apps From e55b05fe3daa647427b65d8350bf1d569c5d429e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 16:57:23 +0200 Subject: [PATCH 316/337] Release 1.146.1 (#895) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1148d6dcd..eac3bba9c 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). -## 1.146.0 - 08.05.2022 +## 1.146.1 - 08.05.2022 ### Added diff --git a/package.json b/package.json index a803a8bb9..02bc6012a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.146.0", + "version": "1.146.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From ea3a9d3b799aed8a87d648850f2289c3901493cd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 22:16:47 +0200 Subject: [PATCH 317/337] Feature/eliminate circular dependencies in common library (#896) * Eliminate circular dependencies * Update changelog --- CHANGELOG.md | 4 ++++ .../interfaces/portfolio-position-detail.interface.ts | 8 +------- apps/api/src/app/portfolio/portfolio.service.ts | 8 +++----- .../app/symbol/interfaces/symbol-item.interface.ts | 2 +- apps/api/src/app/symbol/symbol.service.ts | 2 +- apps/api/src/app/user/interfaces/access.interface.ts | 4 ---- .../ghostfolio-scraper-api.service.ts | 5 ++--- .../google-sheets/google-sheets.service.ts | 5 ++--- .../rakuten-rapid-api/rakuten-rapid-api.service.ts | 5 ++--- .../yahoo-finance/yahoo-finance.service.ts | 7 +++---- apps/api/src/services/interfaces/interfaces.ts | 11 +---------- .../components/home-market/home-market.component.ts | 7 +++++-- .../app/components/positions/positions.component.ts | 6 +----- .../lib/interfaces/historical-data-item.interface.ts | 6 ++++++ libs/common/src/lib/interfaces/index.ts | 2 ++ .../src/lib/interfaces/portfolio-chart.interface.ts | 2 +- .../lib/interfaces/portfolio-position.interface.ts | 3 +-- libs/common/src/lib/interfaces/position.interface.ts | 2 +- libs/common/src/lib/interfaces/user.interface.ts | 6 ++++-- libs/common/src/lib/types/index.ts | 2 ++ libs/common/src/lib/types/market-state-type.ts | 1 + .../lib/trend-indicator/trend-indicator.component.ts | 3 +-- 22 files changed, 45 insertions(+), 56 deletions(-) delete mode 100644 apps/api/src/app/user/interfaces/access.interface.ts create mode 100644 libs/common/src/lib/interfaces/historical-data-item.interface.ts create mode 100644 libs/common/src/lib/types/market-state-type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index eac3bba9c..98e60c756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `Nx` from version `13.8.5` to `14.1.4` - Upgraded `storybook` from version `6.4.18` to `6.4.22` +### Fixed + +- Eliminated the circular dependencies in the `@ghostfolio/common` library + ## 1.145.0 - 07.05.2022 ### Added diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 5f2425d7a..f400923e8 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -1,4 +1,5 @@ import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Tag } from '@prisma/client'; @@ -27,10 +28,3 @@ export interface HistoricalDataContainer { isAllTimeLow: boolean; items: HistoricalDataItem[]; } - -export interface HistoricalDataItem { - averagePrice?: number; - date: string; - grossPerformancePercent?: number; - value: number; -} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 795f516ee..6ffdc94e1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -18,7 +18,6 @@ import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { @@ -30,6 +29,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, Filter, + HistoricalDataItem, PortfolioDetails, PortfolioPerformanceResponse, PortfolioReport, @@ -72,7 +72,6 @@ import { isEmpty, sortBy, uniqBy } from 'lodash'; import { HistoricalDataContainer, - HistoricalDataItem, PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioCalculator } from './portfolio-calculator'; @@ -777,8 +776,7 @@ export class PortfolioService { position.grossPerformancePercentage?.toNumber() ?? null, investment: new Big(position.investment).toNumber(), marketState: - dataProviderResponses[position.symbol]?.marketState ?? - MarketState.delayed, + dataProviderResponses[position.symbol]?.marketState ?? 'delayed', name: symbolProfileMap[position.symbol].name, netPerformance: position.netPerformance?.toNumber() ?? null, netPerformancePercentage: @@ -1062,7 +1060,7 @@ export class PortfolioService { grossPerformancePercent: 0, investment: convertedBalance, marketPrice: 0, - marketState: MarketState.open, + marketState: 'open', name: account.currency, netPerformance: 0, netPerformancePercent: 0, diff --git a/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts b/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts index 787547901..51ed38d4d 100644 --- a/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts +++ b/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts @@ -1,4 +1,4 @@ -import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { DataSource } from '@prisma/client'; export interface SymbolItem { diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index c45f45cd1..6cfcbc209 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,4 +1,3 @@ -import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { IDataGatheringItem, @@ -6,6 +5,7 @@ import { } from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { format, subDays } from 'date-fns'; diff --git a/apps/api/src/app/user/interfaces/access.interface.ts b/apps/api/src/app/user/interfaces/access.interface.ts deleted file mode 100644 index 33682b0cc..000000000 --- a/apps/api/src/app/user/interfaces/access.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Access { - alias?: string; - id: string; -} 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 58ac5de3c..f0ee84237 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 @@ -2,8 +2,7 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState + IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; @@ -133,7 +132,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface { marketPrice: marketData.find((marketDataItem) => { return marketDataItem.symbol === symbolProfile.symbol; }).marketPrice, - marketState: MarketState.delayed + marketState: 'delayed' }; } 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 16e18f529..97022706f 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 @@ -3,8 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState + IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; @@ -114,7 +113,7 @@ export class GoogleSheetsService implements DataProviderInterface { return symbolProfile.symbol === symbol; })?.currency, dataSource: this.getName(), - marketState: MarketState.delayed + marketState: 'delayed' }; } } 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 3cb04dfa3..baa6591f4 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 @@ -3,8 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState + IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; @@ -118,7 +117,7 @@ export class RakutenRapidApiService implements DataProviderInterface { currency: undefined, dataSource: this.getName(), marketPrice: fgi.now.value, - marketState: MarketState.open + marketState: 'open' } }; } 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 0f0683ac8..28c9e8549 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 @@ -3,8 +3,7 @@ import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/c import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState + IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; @@ -216,8 +215,8 @@ export class YahooFinanceService implements DataProviderInterface { marketState: quote.marketState === 'REGULAR' || this.cryptocurrencyService.isCryptocurrency(symbol) - ? MarketState.open - : MarketState.closed, + ? 'open' + : 'closed', marketPrice: quote.regularMarketPrice || 0 }; diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 50fd6009f..dbe3dfa4f 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -1,18 +1,11 @@ +import { MarketState } from '@ghostfolio/common/types'; import { Account, - AssetClass, - AssetSubClass, DataSource, SymbolProfile, Type as TypeOfOrder } from '@prisma/client'; -export const MarketState = { - closed: 'closed', - delayed: 'delayed', - open: 'open' -}; - export interface IOrder { account: Account; currency: string; @@ -44,5 +37,3 @@ export interface IDataGatheringItem { date?: Date; symbol: string; } - -export type MarketState = typeof MarketState[keyof typeof MarketState]; diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index 12ef7b765..0ea2f5944 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -1,10 +1,13 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { resetHours } from '@ghostfolio/common/helper'; -import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { + HistoricalDataItem, + InfoItem, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; diff --git a/apps/client/src/app/components/positions/positions.component.ts b/apps/client/src/app/components/positions/positions.component.ts index bbd7dcf37..b3d6bd3ca 100644 --- a/apps/client/src/app/components/positions/positions.component.ts +++ b/apps/client/src/app/components/positions/positions.component.ts @@ -5,7 +5,6 @@ import { OnChanges, OnInit } from '@angular/core'; -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { Position } from '@ghostfolio/common/interfaces'; @Component({ @@ -42,10 +41,7 @@ export class PositionsComponent implements OnChanges, OnInit { this.positionsWithPriority = []; for (const portfolioPosition of this.positions) { - if ( - portfolioPosition.marketState === MarketState.open || - this.range !== '1d' - ) { + if (portfolioPosition.marketState === 'open' || this.range !== '1d') { // Only show positions where the market is open in today's view this.positionsWithPriority.push(portfolioPosition); } else { diff --git a/libs/common/src/lib/interfaces/historical-data-item.interface.ts b/libs/common/src/lib/interfaces/historical-data-item.interface.ts new file mode 100644 index 000000000..3bb98fdbe --- /dev/null +++ b/libs/common/src/lib/interfaces/historical-data-item.interface.ts @@ -0,0 +1,6 @@ +export interface HistoricalDataItem { + averagePrice?: number; + date: string; + grossPerformancePercent?: number; + value: number; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 0b20b8f23..0f483c100 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -9,6 +9,7 @@ import { import { Coupon } from './coupon.interface'; import { Export } from './export.interface'; import { Filter } from './filter.interface'; +import { HistoricalDataItem } from './historical-data-item.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioDetails } from './portfolio-details.interface'; @@ -40,6 +41,7 @@ export { Coupon, Export, Filter, + HistoricalDataItem, InfoItem, PortfolioChart, PortfolioDetails, diff --git a/libs/common/src/lib/interfaces/portfolio-chart.interface.ts b/libs/common/src/lib/interfaces/portfolio-chart.interface.ts index a696fe632..0ed4a8bb9 100644 --- a/libs/common/src/lib/interfaces/portfolio-chart.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-chart.interface.ts @@ -1,4 +1,4 @@ -import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; +import { HistoricalDataItem } from './historical-data-item.interface'; export interface PortfolioChart { hasError: boolean; diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 1b5714a9e..703d6a6a4 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,7 +1,6 @@ -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; -import { Market } from '../types'; +import { Market, MarketState } from '../types'; import { Country } from './country.interface'; import { Sector } from './sector.interface'; diff --git a/libs/common/src/lib/interfaces/position.interface.ts b/libs/common/src/lib/interfaces/position.interface.ts index a5e3eb93c..72b99c37b 100644 --- a/libs/common/src/lib/interfaces/position.interface.ts +++ b/libs/common/src/lib/interfaces/position.interface.ts @@ -1,5 +1,5 @@ -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { AssetClass, DataSource } from '@prisma/client'; +import { MarketState } from '../types'; export interface Position { assetClass: AssetClass; diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index a6bd3dab0..b40259544 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -1,10 +1,12 @@ -import { Access } from '@ghostfolio/api/app/user/interfaces/access.interface'; import { Account, Tag } from '@prisma/client'; import { UserSettings } from './user-settings.interface'; export interface User { - access: Access[]; + access: { + alias?: string; + id: string; + }[]; accounts: Account[]; alias?: string; id: string; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 40ba51272..7d7050ada 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -2,6 +2,7 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import { AccountWithValue } from './account-with-value.type'; import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; +import { MarketState } from './market-state-type'; import { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; import type { RequestWithUser } from './request-with-user.type'; @@ -13,6 +14,7 @@ export type { DateRange, Granularity, Market, + MarketState, OrderWithAccount, RequestWithUser, ToggleOption diff --git a/libs/common/src/lib/types/market-state-type.ts b/libs/common/src/lib/types/market-state-type.ts new file mode 100644 index 000000000..3311507d7 --- /dev/null +++ b/libs/common/src/lib/types/market-state-type.ts @@ -0,0 +1 @@ +export type MarketState = 'closed' | 'delayed' | 'open'; diff --git a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts index 708b27dc9..4da6d6c8e 100644 --- a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts +++ b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; -import { DateRange } from '@ghostfolio/common/types'; +import { DateRange, MarketState } from '@ghostfolio/common/types'; @Component({ selector: 'gf-trend-indicator', From 10f13eec48d64ceda788e5065810f118eb21e312 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 22:18:00 +0200 Subject: [PATCH 318/337] Release 1.146.2 (#897) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e60c756..2bd7687ba 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). -## 1.146.1 - 08.05.2022 +## 1.146.2 - 08.05.2022 ### Added diff --git a/package.json b/package.json index 02bc6012a..a38eb5265 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.146.1", + "version": "1.146.2", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 904d4db2191e80150507a029a0dd789678891628 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 22:52:46 +0200 Subject: [PATCH 319/337] Refactor build:all and build:dev scripts (#898) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a38eb5265..94751a0a8 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "affected:lint": "nx affected:lint", "affected:test": "nx affected:test", "angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng", - "build:all": "ng build --configuration production api && ng build --configuration production client && yarn replace-placeholders-in-build", - "build:dev": "nx build api && nx build client && yarn replace-placeholders-in-build", + "build:all": "nx run api:build:production && nx run client:build:production && yarn replace-placeholders-in-build", + "build:dev": "nx run api:build && nx run client:build && yarn replace-placeholders-in-build", "build:storybook": "nx run ui:build-storybook", "clean": "rimraf dist", "database:format-schema": "prisma format", From 80862e5c2a0e322a29e43f6a4156d9be94ebe323 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 May 2022 22:54:07 +0200 Subject: [PATCH 320/337] Release 1.146.3 (#899) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd7687ba..be70a9d5f 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). -## 1.146.2 - 08.05.2022 +## 1.146.3 - 08.05.2022 ### Added diff --git a/package.json b/package.json index 94751a0a8..509054789 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.146.2", + "version": "1.146.3", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From dfa67b275c691a67fe8000d428ad50c00107faaf Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 10 May 2022 19:22:57 +0200 Subject: [PATCH 321/337] Feature/improve filtering on allocations page (#900) * Include cash positions on allocations page (with no filtering) * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/portfolio/portfolio.service.ts | 4 ++-- .../positions-table/positions-table.component.ts | 5 ----- .../allocations/allocations-page.component.ts | 11 +++++++++-- .../pages/portfolio/allocations/allocations-page.html | 2 +- apps/client/src/app/pages/public/public-page.html | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be70a9d5f..ebe293e86 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 + +- Improved the allocations page with no filtering (include cash positions) + ## 1.146.3 - 08.05.2022 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6ffdc94e1..314cedf60 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -318,8 +318,8 @@ export class PortfolioService { (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); const userCurrency = - this.request.user?.Settings?.currency ?? user.Settings?.currency ?? + this.request.user?.Settings?.currency ?? baseCurrency; const { orders, portfolioOrders, transactionPoints } = @@ -448,7 +448,7 @@ export class PortfolioService { value: totalValue }); - if (aFilters === undefined) { + if (aFilters?.length === 0) { for (const symbol of Object.keys(cashPositions)) { holdings[symbol] = cashPositions[symbol]; } 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 6c5224db2..390ba5e02 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 @@ -73,11 +73,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { } } - /*public applyFilter(event: Event) { - const filterValue = (event.target as HTMLInputElement).value; - this.dataSource.filter = filterValue.trim().toLowerCase(); - }*/ - 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 1c20dc518..01e9a7cef 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 @@ -33,6 +33,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; + public activeFilters: Filter[] = []; public allFilters: Filter[]; public continents: { [code: string]: { name: string; value: number }; @@ -130,8 +131,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { distinctUntilChanged(), switchMap((filters) => { this.isLoading = true; + this.activeFilters = filters; - return this.dataService.fetchPortfolioDetails({ filters }); + return this.dataService.fetchPortfolioDetails({ + filters: this.activeFilters + }); }), takeUntil(this.unsubscribeSubject) ) @@ -343,7 +347,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } } - if (position.dataSource) { + if ( + this.activeFilters?.length === 0 || + position.assetSubClass !== AssetClass.CASH + ) { this.symbols[prettifySymbol(symbol)] = { dataSource: position.dataSource, name: position.name, 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 07418a4af..9a71dbe27 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -95,7 +95,7 @@ By SymbolBy Position - Symbols + Positions Date: Tue, 10 May 2022 21:24:36 +0200 Subject: [PATCH 322/337] Improve filtering (#901) --- apps/api/src/app/account/account.service.ts | 34 ++++++-- .../src/app/portfolio/portfolio.controller.ts | 2 +- .../src/app/portfolio/portfolio.service.ts | 81 ++++++++++++------- .../allocations/allocations-page.component.ts | 29 +++---- .../activities-filter.component.ts | 4 +- 5 files changed, 92 insertions(+), 58 deletions(-) diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index ec1678131..34c2f4a15 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -1,5 +1,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { Filter } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { Account, Order, Platform, Prisma } from '@prisma/client'; import Big from 'big.js'; @@ -102,22 +103,39 @@ export class AccountService { }); } - public async getCashDetails( - aUserId: string, - aCurrency: string - ): Promise { + public async getCashDetails({ + currency, + filters = [], + userId + }: { + currency: string; + filters?: Filter[]; + userId: string; + }): Promise { let totalCashBalanceInBaseCurrency = new Big(0); - const accounts = await this.accounts({ - where: { userId: aUserId } - }); + const where: Prisma.AccountWhereInput = { userId }; + + if (filters?.length > 0) { + where.id = { + in: filters + .filter(({ type }) => { + return type === 'account'; + }) + .map(({ id }) => { + return id; + }) + }; + } + + const accounts = await this.accounts({ where }); for (const account of accounts) { totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus( this.exchangeRateDataService.toCurrency( account.balance, account.currency, - aCurrency + currency ) ); } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 25021492e..68cd03c0a 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -182,8 +182,8 @@ export class PortfolioController { this.request.user.subscription.type === 'Basic'; return { + accounts, hasError, - accounts: filters.length === 0 ? accounts : {}, holdings: isBasicUser ? {} : holdings }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 314cedf60..3ecb60d41 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -68,7 +68,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty, sortBy, uniqBy } from 'lodash'; +import { isEmpty, sortBy, uniq, uniqBy } from 'lodash'; import { HistoricalDataContainer, @@ -344,10 +344,11 @@ export class PortfolioService { startDate ); - const cashDetails = await this.accountService.getCashDetails( + const cashDetails = await this.accountService.getCashDetails({ userId, - userCurrency - ); + currency: userCurrency, + filters: aFilters + }); const holdings: PortfolioDetails['holdings'] = {}; const totalInvestment = currentPositions.totalInvestment.plus( @@ -440,26 +441,26 @@ export class PortfolioService { }; } - const cashPositions = await this.getCashPositions({ - cashDetails, - emergencyFund, - userCurrency, - investment: totalInvestment, - value: totalValue - }); - if (aFilters?.length === 0) { + const cashPositions = await this.getCashPositions({ + cashDetails, + emergencyFund, + userCurrency, + investment: totalInvestment, + value: totalValue + }); + for (const symbol of Object.keys(cashPositions)) { holdings[symbol] = cashPositions[symbol]; } } - const accounts = await this.getValueOfAccounts( + const accounts = await this.getValueOfAccounts({ orders, + userId, portfolioItemsNow, - userCurrency, - userId - ); + filters: aFilters + }); return { accounts, holdings, hasErrors: currentPositions.hasErrors }; } @@ -890,12 +891,11 @@ export class PortfolioService { for (const position of currentPositions.positions) { portfolioItemsNow[position.symbol] = position; } - const accounts = await this.getValueOfAccounts( + const accounts = await this.getValueOfAccounts({ orders, portfolioItemsNow, - currency, userId - ); + }); return { rules: { accountClusterRisk: await this.rulesService.evaluate( @@ -957,10 +957,10 @@ export class PortfolioService { const performanceInformation = await this.getPerformance(aImpersonationId); - const { balanceInBaseCurrency } = await this.accountService.getCashDetails( + const { balanceInBaseCurrency } = await this.accountService.getCashDetails({ userId, - userCurrency - ); + currency: userCurrency + }); const orders = await this.orderService.getOrders({ userCurrency, userId @@ -1253,21 +1253,40 @@ export class PortfolioService { portfolioCalculator.computeTransactionPoints(); return { - transactionPoints: portfolioCalculator.getTransactionPoints(), orders, - portfolioOrders + portfolioOrders, + transactionPoints: portfolioCalculator.getTransactionPoints() }; } - private async getValueOfAccounts( - orders: OrderWithAccount[], - portfolioItemsNow: { [p: string]: TimelinePosition }, - userCurrency: string, - userId: string - ) { + private async getValueOfAccounts({ + filters = [], + orders, + portfolioItemsNow, + userId + }: { + filters?: Filter[]; + orders: OrderWithAccount[]; + portfolioItemsNow: { [p: string]: TimelinePosition }; + userId: string; + }) { const accounts: PortfolioDetails['accounts'] = {}; - const currentAccounts = await this.accountService.getAccounts(userId); + let currentAccounts = []; + + if (filters.length === 0) { + currentAccounts = await this.accountService.getAccounts(userId); + } else { + const accountIds = uniq( + orders.map(({ accountId }) => { + return accountId; + }) + ); + + currentAccounts = await this.accountService.accounts({ + where: { id: { in: accountIds } } + }); + } for (const account of currentAccounts) { const ordersByAccount = orders.filter(({ accountId }) => { 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 01e9a7cef..b2c63664f 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 @@ -155,15 +155,17 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - const accountFilters: Filter[] = this.user.accounts.map( - ({ id, name }) => { + const accountFilters: Filter[] = this.user.accounts + .filter(({ accountType }) => { + return accountType === 'SECURITIES'; + }) + .map(({ id, name }) => { return { - id: id, + id, label: name, type: 'account' }; - } - ); + }); const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { return { @@ -347,17 +349,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { } } - if ( - this.activeFilters?.length === 0 || - position.assetSubClass !== AssetClass.CASH - ) { - this.symbols[prettifySymbol(symbol)] = { - dataSource: position.dataSource, - name: position.name, - symbol: prettifySymbol(symbol), - value: aPeriod === 'original' ? position.investment : position.value - }; - } + this.symbols[prettifySymbol(symbol)] = { + dataSource: position.dataSource, + name: position.name, + symbol: prettifySymbol(symbol), + value: aPeriod === 'original' ? position.investment : position.value + }; } const marketsTotal = diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index b114317e5..bf8c5878b 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -48,7 +48,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { public constructor() { this.searchControl.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((currentFilter: string) => { + .subscribe((currentFilter: Filter) => { if (currentFilter) { this.filters$.next( this.allFilters @@ -61,7 +61,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { .filter((filter) => { return filter.label .toLowerCase() - .startsWith(currentFilter?.toLowerCase()); + .startsWith(currentFilter.label.toLowerCase()); }) .sort((a, b) => a.label.localeCompare(b.label)) ); From 57bf10e7e76983a4358f20df308bbbd80ba37ff2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 10 May 2022 21:25:25 +0200 Subject: [PATCH 323/337] Release 1.147.0 (#904) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe293e86..722269aad 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.147.0 - 10.05.2022 ### Changed diff --git a/package.json b/package.json index 509054789..dda094a57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.146.3", + "version": "1.147.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From d094bae7def359a411152424f1426b01ae9288cc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 12 May 2022 07:48:12 +0200 Subject: [PATCH 324/337] Bugfix/fix issue in activities filter component with typing (#910) * Handle filter (selecting) or search term (typing) * Update changelog --- CHANGELOG.md | 6 ++++++ .../activities-filter/activities-filter.component.ts | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722269aad..d511e59f0 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 + +- Fixed an issue in the activities filter component (typing a search term) + ## 1.147.0 - 10.05.2022 ### Changed diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index bf8c5878b..f466999bb 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -48,8 +48,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { public constructor() { this.searchControl.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((currentFilter: Filter) => { - if (currentFilter) { + .subscribe((filterOrSearchTerm: Filter | string) => { + if (filterOrSearchTerm) { this.filters$.next( this.allFilters .filter((filter) => { @@ -59,9 +59,15 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { }); }) .filter((filter) => { + if (typeof filterOrSearchTerm === 'string') { + return filter.label + .toLowerCase() + .startsWith(filterOrSearchTerm.toLowerCase()); + } + return filter.label .toLowerCase() - .startsWith(currentFilter.label.toLowerCase()); + .startsWith(filterOrSearchTerm?.label?.toLowerCase()); }) .sort((a, b) => a.label.localeCompare(b.label)) ); From 1a4dc51825ac4c5339913543b0d3a348e26bfc18 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 13 May 2022 06:48:40 +0200 Subject: [PATCH 325/337] Bugfix/fix state of delete account button (#911) * Fix disable state * Update changelog --- CHANGELOG.md | 1 + .../app/components/accounts-table/accounts-table.component.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d511e59f0..7e260a013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed the state of the account delete button (disable if account contains activities) - Fixed an issue in the activities filter component (typing a search term) ## 1.147.0 - 10.05.2022 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 cf8446e42..08c6c3de9 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 @@ -200,7 +200,7 @@
-
+
Tags
{{ tag.name }}
+ +
+ +
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 b2c63664f..7b4f04e0a 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 @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; +import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; @@ -14,6 +15,7 @@ import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Market, ToggleOption } from '@ghostfolio/common/types'; import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -404,12 +406,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, - data: { + data: { dataSource, symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, hasImpersonationId: this.hasImpersonationId, + hasPermissionToReportDataGlitch: hasPermission( + this.user?.permissions, + permissions.reportDataGlitch + ), locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 1717dbbda..65f5da641 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; +import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; @@ -406,12 +407,16 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { const dialogRef = this.dialog.open(PositionDetailDialog, { autoFocus: false, - data: { + data: { dataSource, symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, hasImpersonationId: this.hasImpersonationId, + hasPermissionToReportDataGlitch: hasPermission( + this.user?.permissions, + permissions.reportDataGlitch + ), locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index dc574f7ef..5023369f7 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -20,6 +20,7 @@ export const permissions = { enableStatistics: 'enableStatistics', enableSubscription: 'enableSubscription', enableSystemMessage: 'enableSystemMessage', + reportDataGlitch: 'reportDataGlitch', toggleReadOnlyMode: 'toggleReadOnlyMode', updateAccount: 'updateAccount', updateAuthDevice: 'updateAuthDevice', From f1483569a2429e584e8d8ab8f889468367e27a49 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 14 May 2022 13:55:25 +0200 Subject: [PATCH 329/337] Release 1.148.0 (#921) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bcc8cb9..d95052f78 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.148.0 - 14.05.2022 ### Added diff --git a/package.json b/package.json index dda094a57..d62edcf6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.147.0", + "version": "1.148.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 160335302a5a5455296b43b0a8b6350ce0a76feb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 15 May 2022 21:51:31 +0200 Subject: [PATCH 330/337] Feature/group filters by type (#922) * Add groups to activities filter component * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/account/account.service.ts | 2 +- apps/api/src/app/order/order.service.ts | 2 +- .../src/app/portfolio/portfolio.controller.ts | 4 +- .../allocations/allocations-page.component.ts | 4 +- apps/client/src/app/services/data.service.ts | 2 +- .../lib/interfaces/filter-group.interface.ts | 6 ++ .../src/lib/interfaces/filter.interface.ts | 2 +- libs/common/src/lib/interfaces/index.ts | 2 + .../activities-filter.component.html | 14 ++- .../activities-filter.component.ts | 88 ++++++++++++------- .../activities-table.component.ts | 78 ++++++++-------- 12 files changed, 131 insertions(+), 79 deletions(-) create mode 100644 libs/common/src/lib/interfaces/filter-group.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d95052f78..baf1b8564 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 groups to the activities filter component + ## 1.148.0 - 14.05.2022 ### Added diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 34c2f4a15..240045e8e 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -120,7 +120,7 @@ export class AccountService { where.id = { in: filters .filter(({ type }) => { - return type === 'account'; + return type === 'ACCOUNT'; }) .map(({ id }) => { return id; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index aceb8de06..3e87c5f5e 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -188,7 +188,7 @@ export class OrderService { }): Promise { const where: Prisma.OrderWhereInput = { userId }; - const { account: filtersByAccount, tag: filtersByTag } = groupBy( + const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( filters, (filter) => { return filter.type; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 68cd03c0a..5c648378d 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -119,13 +119,13 @@ export class PortfolioController { ...accountIds.map((accountId) => { return { id: accountId, - type: 'account' + type: 'ACCOUNT' }; }), ...tagIds.map((tagId) => { return { id: tagId, - type: 'tag' + type: 'TAG' }; }) ]; 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 7b4f04e0a..a072319f0 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 @@ -165,7 +165,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { return { id, label: name, - type: 'account' + type: 'ACCOUNT' }; }); @@ -173,7 +173,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { return { id, label: name, - type: 'tag' + type: 'TAG' }; }); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 7679c154f..3edde74e4 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -187,7 +187,7 @@ export class DataService { let params = new HttpParams(); if (filters?.length > 0) { - const { account: filtersByAccount, tag: filtersByTag } = groupBy( + const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( filters, (filter) => { return filter.type; diff --git a/libs/common/src/lib/interfaces/filter-group.interface.ts b/libs/common/src/lib/interfaces/filter-group.interface.ts new file mode 100644 index 000000000..7087b99fa --- /dev/null +++ b/libs/common/src/lib/interfaces/filter-group.interface.ts @@ -0,0 +1,6 @@ +import { Filter } from './filter.interface'; + +export interface FilterGroup { + filters: Filter[]; + name: Filter['type']; +} diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index e6d5bb108..a4280f4f4 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,5 @@ export interface Filter { id: string; label?: string; - type: 'account' | 'tag'; + type: 'ACCOUNT' | 'SYMBOL' | 'TAG'; } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 0f483c100..96275e4d3 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { } from './admin-market-data.interface'; import { Coupon } from './coupon.interface'; import { Export } from './export.interface'; +import { FilterGroup } from './filter-group.interface'; import { Filter } from './filter.interface'; import { HistoricalDataItem } from './historical-data-item.interface'; import { InfoItem } from './info-item.interface'; @@ -41,6 +42,7 @@ export { Coupon, Export, Filter, + FilterGroup, HistoricalDataItem, InfoItem, PortfolioChart, diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html index e0eb5da87..1ade36db5 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -26,9 +26,17 @@ #autocomplete="matAutocomplete" (optionSelected)="onSelectFilter($event)" > - - {{ filter.label | gfSymbol }} - + + + {{ filter.label | gfSymbol }} + + ; + public filterGroups$: Subject = new BehaviorSubject([]); public filters$: Subject = new BehaviorSubject([]); public filters: Observable = this.filters$.asObservable(); public searchControl = new FormControl(); @@ -50,40 +52,27 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((filterOrSearchTerm: Filter | string) => { if (filterOrSearchTerm) { - this.filters$.next( - this.allFilters - .filter((filter) => { - // Filter selected filters - return !this.selectedFilters.some((selectedFilter) => { - return selectedFilter.id === filter.id; - }); - }) - .filter((filter) => { - if (typeof filterOrSearchTerm === 'string') { - return filter.label - .toLowerCase() - .startsWith(filterOrSearchTerm.toLowerCase()); - } - - return filter.label - .toLowerCase() - .startsWith(filterOrSearchTerm?.label?.toLowerCase()); - }) - .sort((a, b) => a.label.localeCompare(b.label)) - ); + const searchTerm = + typeof filterOrSearchTerm === 'string' + ? filterOrSearchTerm + : filterOrSearchTerm?.label; + + this.filterGroups$.next(this.getGroupedFilters(searchTerm)); + } else { + this.filterGroups$.next(this.getGroupedFilters()); } }); } public ngOnChanges(changes: SimpleChanges) { if (changes.allFilters?.currentValue) { - this.updateFilter(); + this.updateFilters(); } } public onAddFilter({ input, value }: MatChipInputEvent): void { if (value?.trim()) { - this.updateFilter(); + this.updateFilters(); } // Reset the input value @@ -99,12 +88,16 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { return filter.id !== aFilter.id; }); - this.updateFilter(); + this.updateFilters(); } public onSelectFilter(event: MatAutocompleteSelectedEvent): void { - this.selectedFilters.push(event.option.value); - this.updateFilter(); + this.selectedFilters.push( + this.allFilters.find((filter) => { + return filter.id === event.option.value; + }) + ); + this.updateFilters(); this.searchInput.nativeElement.value = ''; this.searchControl.setValue(null); } @@ -114,8 +107,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { this.unsubscribeSubject.complete(); } - private updateFilter() { - this.filters$.next( + private getGroupedFilters(searchTerm?: string) { + const filterGroupsMap = groupBy( this.allFilters .filter((filter) => { // Filter selected filters @@ -123,9 +116,44 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { return selectedFilter.id === filter.id; }); }) - .sort((a, b) => a.label.localeCompare(b.label)) + .filter((filter) => { + if (searchTerm) { + // Filter by search term + return filter.label + .toLowerCase() + .includes(searchTerm.toLowerCase()); + } + + return filter; + }) + .sort((a, b) => a.label.localeCompare(b.label)), + (filter) => { + return filter.type; + } ); + const filterGroups: FilterGroup[] = []; + + for (const type of Object.keys(filterGroupsMap)) { + filterGroups.push({ + name: type, + filters: filterGroupsMap[type] + }); + } + + return filterGroups + .sort((a, b) => a.name.localeCompare(b.name)) + .map((filterGroup) => { + return { + ...filterGroup, + filters: filterGroup.filters + }; + }); + } + + private updateFilters() { + this.filterGroups$.next(this.getGroupedFilters()); + // Emit an array with a new reference this.valueChanged.emit([...this.selectedFilters]); } 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 959a4e867..6f73b0b48 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -105,17 +105,17 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.defaultDateFormat = getDateFormatString(this.locale); if (this.activities) { - this.allFilters = this.getSearchableFieldValues(this.activities).map( - (label) => { - return { label, id: label, type: 'tag' }; - } - ); + this.allFilters = this.getSearchableFieldValues(this.activities); this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { const dataString = this.getFilterableValues(data) + .map((currentFilter) => { + return currentFilter.label; + }) .join(' ') .toLowerCase(); + let contains = true; for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) { contains = @@ -190,50 +190,51 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { private getFilterableValues( activity: OrderWithAccount, - fieldValues: Set = new Set() - ): string[] { - fieldValues.add(activity.Account?.name); - fieldValues.add(activity.Account?.Platform?.name); - fieldValues.add(activity.SymbolProfile.currency); + fieldValueMap: { [id: string]: Filter } = {} + ): Filter[] { + fieldValueMap[activity.Account?.id] = { + id: activity.Account?.id, + label: activity.Account?.name, + type: 'ACCOUNT' + }; + + fieldValueMap[activity.SymbolProfile.currency] = { + id: activity.SymbolProfile.currency, + label: activity.SymbolProfile.currency, + type: 'TAG' + }; if (!isUUID(activity.SymbolProfile.symbol)) { - fieldValues.add(activity.SymbolProfile.symbol); + fieldValueMap[activity.SymbolProfile.symbol] = { + id: activity.SymbolProfile.symbol, + label: activity.SymbolProfile.symbol, + type: 'SYMBOL' + }; } - fieldValues.add(activity.type); - fieldValues.add(format(activity.date, 'yyyy')); + fieldValueMap[activity.type] = { + id: activity.type, + label: activity.type, + type: 'TAG' + }; - return [...fieldValues].filter((item) => { - return item !== undefined; - }); + fieldValueMap[format(activity.date, 'yyyy')] = { + id: format(activity.date, 'yyyy'), + label: format(activity.date, 'yyyy'), + type: 'TAG' + }; + + return Object.values(fieldValueMap); } - private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { - const fieldValues = new Set(); + private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] { + const fieldValueMap: { [id: string]: Filter } = {}; for (const activity of activities) { - this.getFilterableValues(activity, fieldValues); + this.getFilterableValues(activity, fieldValueMap); } - return [...fieldValues] - .filter((item) => { - return item !== undefined; - }) - .sort((a, b) => { - const aFirstChar = a.charAt(0); - const bFirstChar = b.charAt(0); - const isANumber = aFirstChar >= '0' && aFirstChar <= '9'; - const isBNumber = bFirstChar >= '0' && bFirstChar <= '9'; - - // Sort priority: text, followed by numbers - if (isANumber && !isBNumber) { - return 1; - } else if (!isANumber && isBNumber) { - return -1; - } else { - return a.toLowerCase() < b.toLowerCase() ? -1 : 1; - } - }); + return Object.values(fieldValueMap); } private getTotalFees() { @@ -276,6 +277,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return filter.label; }) .join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((filter) => { return filter.label.trim().toLowerCase(); }); From 4a123c38f2aec1e920bf8a7d0959e01b31a25343 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 16 May 2022 21:17:58 +0200 Subject: [PATCH 331/337] Refactor placeholder (#925) --- .../allocations/allocations-page.component.ts | 3 +++ .../activities-table/activities-table.component.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) 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 a072319f0..279d2a499 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 @@ -84,6 +84,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { public user: User; + private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...'; private unsubscribeSubject = new Subject(); /** @@ -134,6 +135,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { switchMap((filters) => { this.isLoading = true; this.activeFilters = filters; + this.placeholder = + this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : ''; return this.dataService.fetchPortfolioDetails({ filters: this.activeFilters 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 6f73b0b48..055c82d86 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -22,9 +22,6 @@ import { endOfToday, format, isAfter } from 'date-fns'; import { isNumber } from 'lodash'; import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs'; -const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; -const SEARCH_STRING_SEPARATOR = ','; - @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'gf-activities-table', @@ -70,6 +67,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public totalFees: number; public totalValue: number; + private readonly SEARCH_PLACEHOLDER = + 'Filter by account, currency, symbol or type...'; + private readonly SEARCH_STRING_SEPARATOR = ','; private unsubscribeSubject = new Subject(); public constructor(private router: Router) { @@ -117,7 +117,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { .toLowerCase(); let contains = true; - for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) { + for (const singleFilter of filter.split(this.SEARCH_STRING_SEPARATOR)) { contains = contains && dataString.includes(singleFilter.trim().toLowerCase()); } @@ -276,14 +276,14 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { .map((filter) => { return filter.label; }) - .join(SEARCH_STRING_SEPARATOR); + .join(this.SEARCH_STRING_SEPARATOR); const lowercaseSearchKeywords = filters.map((filter) => { return filter.label.trim().toLowerCase(); }); this.placeholder = - lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; + lowercaseSearchKeywords.length <= 0 ? this.SEARCH_PLACEHOLDER : ''; this.searchKeywords = filters.map((filter) => { return filter.label; From 5cb6e5dec6b324118d52c928fce2958ee6ec84f9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 16 May 2022 21:49:22 +0200 Subject: [PATCH 332/337] Feature/support filtering by asset class on the allocations page (#926) * Support filtering by asset class * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/account/account.service.ts | 21 ++++++---- apps/api/src/app/order/order.service.ts | 41 ++++++++++++++++--- .../src/app/portfolio/portfolio.controller.ts | 8 ++++ .../src/app/portfolio/portfolio.service.ts | 7 +++- .../allocations/allocations-page.component.ts | 15 ++++++- apps/client/src/app/services/data.service.ts | 24 ++++++++--- .../src/lib/interfaces/filter.interface.ts | 2 +- 8 files changed, 96 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf1b8564..e73dcff8a 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 ### Added - Added groups to the activities filter component +- Added support for filtering by asset class on the allocations page ## 1.148.0 - 14.05.2022 diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 240045e8e..b9b65716a 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -4,6 +4,7 @@ import { Filter } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { Account, Order, Platform, Prisma } from '@prisma/client'; import Big from 'big.js'; +import { groupBy } from 'lodash'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -116,15 +117,19 @@ export class AccountService { const where: Prisma.AccountWhereInput = { userId }; - if (filters?.length > 0) { + const { + ACCOUNT: filtersByAccount, + ASSET_CLASS: filtersByAssetClass, + TAG: filtersByTag + } = groupBy(filters, (filter) => { + return filter.type; + }); + + if (filtersByAccount?.length > 0) { where.id = { - in: filters - .filter(({ type }) => { - return type === 'ACCOUNT'; - }) - .map(({ id }) => { - return id; - }) + in: filtersByAccount.map(({ id }) => { + return id; + }) }; } diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 3e87c5f5e..7b09ec559 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -188,12 +188,13 @@ export class OrderService { }): Promise { const where: Prisma.OrderWhereInput = { userId }; - const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( - filters, - (filter) => { - return filter.type; - } - ); + const { + ACCOUNT: filtersByAccount, + ASSET_CLASS: filtersByAssetClass, + TAG: filtersByTag + } = groupBy(filters, (filter) => { + return filter.type; + }); if (filtersByAccount?.length > 0) { where.accountId = { @@ -207,6 +208,34 @@ export class OrderService { where.isDraft = false; } + if (filtersByAssetClass?.length > 0) { + where.SymbolProfile = { + OR: [ + { + AND: [ + { + OR: filtersByAssetClass.map(({ id }) => { + return { assetClass: AssetClass[id] }; + }) + }, + { + SymbolProfileOverrides: { + is: null + } + } + ] + }, + { + SymbolProfileOverrides: { + OR: filtersByAssetClass.map(({ id }) => { + return { assetClass: AssetClass[id] }; + }) + } + } + ] + }; + } + if (filtersByTag?.length > 0) { where.tags = { some: { diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 5c648378d..5d3c683f1 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -107,12 +107,14 @@ export class PortfolioController { public async getDetails( @Headers('impersonation-id') impersonationId: string, @Query('accounts') filterByAccounts?: string, + @Query('assetClasses') filterByAssetClasses?: string, @Query('range') range?: DateRange, @Query('tags') filterByTags?: string ): Promise { let hasError = false; const accountIds = filterByAccounts?.split(',') ?? []; + const assetClasses = filterByAssetClasses?.split(',') ?? []; const tagIds = filterByTags?.split(',') ?? []; const filters: Filter[] = [ @@ -122,6 +124,12 @@ export class PortfolioController { type: 'ACCOUNT' }; }), + ...assetClasses.map((assetClass) => { + return { + id: assetClass, + type: 'ASSET_CLASS' + }; + }), ...tagIds.map((tagId) => { return { id: tagId, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 3ecb60d41..409a40017 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -441,7 +441,12 @@ export class PortfolioService { }; } - if (aFilters?.length === 0) { + if ( + aFilters?.length === 0 || + (aFilters?.length === 1 && + aFilters[0].type === 'ASSET_CLASS' && + aFilters[0].id === 'CASH') + ) { const cashPositions = await this.getCashPositions({ cashDetails, emergencyFund, 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 279d2a499..b8cc651f6 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 @@ -172,6 +172,15 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; }); + const assetClassFilters: Filter[] = []; + for (const assetClass of Object.keys(AssetClass)) { + assetClassFilters.push({ + id: assetClass, + label: assetClass, + type: 'ASSET_CLASS' + }); + } + const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { return { id, @@ -180,7 +189,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; }); - this.allFilters = [...accountFilters, ...tagFilters]; + this.allFilters = [ + ...accountFilters, + ...assetClassFilters, + ...tagFilters + ]; this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 3edde74e4..58a900a72 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -187,12 +187,13 @@ export class DataService { let params = new HttpParams(); if (filters?.length > 0) { - const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( - filters, - (filter) => { - return filter.type; - } - ); + const { + ACCOUNT: filtersByAccount, + ASSET_CLASS: filtersByAssetClass, + TAG: filtersByTag + } = groupBy(filters, (filter) => { + return filter.type; + }); if (filtersByAccount) { params = params.append( @@ -205,6 +206,17 @@ export class DataService { ); } + if (filtersByAssetClass) { + params = params.append( + 'assetClasses', + filtersByAssetClass + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + if (filtersByTag) { params = params.append( 'tags', diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index a4280f4f4..a3e5163e7 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,5 @@ export interface Filter { id: string; label?: string; - type: 'ACCOUNT' | 'SYMBOL' | 'TAG'; + type: 'ACCOUNT' | 'ASSET_CLASS' | 'SYMBOL' | 'TAG'; } From 38f9d54705760647c60cba23d39ae9a835cb8043 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 16 May 2022 21:50:43 +0200 Subject: [PATCH 333/337] Release 1.149.0 (#927) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73dcff8a..a3596cc81 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.149.0 - 16.05.2022 ### Added diff --git a/package.json b/package.json index d62edcf6b..7db114e1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.148.0", + "version": "1.149.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 379977008da5ec9ea2be177a7a66f775b0cec9d5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 19 May 2022 21:05:14 +0200 Subject: [PATCH 334/337] Simplify intro text (#933) --- README.md | 2 +- apps/client/src/app/pages/landing/landing-page.html | 6 +++--- apps/client/src/index.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3c8d98e53..fd737530f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@

-**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of their wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. +**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.
diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index 816da73ca..5e6ac8ec1 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -47,9 +47,9 @@ personal investment strategy.

- Ghostfolio empowers busy people to keep track of their - wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven - investment decisions. + Ghostfolio empowers busy people to keep track of + stocks, ETFs or cryptocurrencies and make solid, data-driven investment + decisions.

diff --git a/apps/client/src/index.html b/apps/client/src/index.html index a7c598972..f0b80253d 100644 --- a/apps/client/src/index.html +++ b/apps/client/src/index.html @@ -17,7 +17,7 @@ Date: Fri, 20 May 2022 20:14:33 +0200 Subject: [PATCH 335/337] Move dependencies to devDependencies (#934) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7db114e1b..a26addca5 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", "@stripe/stripe-js": "1.22.0", - "@types/papaparse": "5.2.6", "alphavantage": "2.2.0", "angular-material-css-vars": "3.0.0", "bent": "7.3.12", @@ -117,7 +116,6 @@ "rxjs": "7.4.0", "stripe": "8.199.0", "svgmap": "2.6.0", - "tslib": "2.0.0", "twitter-api-v2": "1.10.3", "uuid": "8.3.2", "yahoo-finance2": "2.3.2", @@ -156,6 +154,7 @@ "@types/jest": "27.4.1", "@types/lodash": "4.14.174", "@types/node": "14.14.33", + "@types/papaparse": "5.2.6", "@types/passport-google-oauth20": "2.0.11", "@typescript-eslint/eslint-plugin": "5.4.0", "@typescript-eslint/parser": "5.4.0", @@ -175,6 +174,7 @@ "prettier": "2.5.1", "replace-in-file": "6.2.0", "rimraf": "3.0.2", + "tslib": "2.0.0", "ts-jest": "27.1.4", "ts-node": "9.1.1", "typescript": "4.6.4" From 977c5a954456a4ba0112cfcc9be73d0a3a9f212f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 20 May 2022 20:15:19 +0200 Subject: [PATCH 336/337] Feature/skip data enhancement if data is inaccurate (#935) * Skip data enhancer if data is inaccurate * Update changelog --- CHANGELOG.md | 6 ++++++ .../trackinsight/trackinsight.service.ts | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3596cc81..0723c83a9 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 + +- Skipped data enhancer (_Trackinsight_) if data is inaccurate + ## 1.149.0 - 16.05.2022 ### Added 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 f61297368..8ebdb1dba 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 @@ -32,7 +32,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { return response; } - const holdings = await getJSON( + const result = await getJSON( `${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json` ).catch(() => { return getJSON( @@ -42,12 +42,17 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { ); }); + if (result.weight < 0.95) { + // Skip if data is inaccurate + return response; + } + if ( !response.countries || (response.countries as unknown as Country[]).length === 0 ) { response.countries = []; - for (const [name, value] of Object.entries(holdings.countries)) { + for (const [name, value] of Object.entries(result.countries)) { let countryCode: string; for (const [key, country] of Object.entries( @@ -75,7 +80,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { (response.sectors as unknown as Sector[]).length === 0 ) { response.sectors = []; - for (const [name, value] of Object.entries(holdings.sectors)) { + for (const [name, value] of Object.entries(result.sectors)) { response.sectors.push({ name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, weight: value.weight From f5819cc39999f323a902ec5c2b05334eb321692e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 20 May 2022 20:16:23 +0200 Subject: [PATCH 337/337] Bugfix/fix countries in symbol profile overrides (#936) * Fix countries * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/services/symbol-profile.service.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0723c83a9..1c03e17d6 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 - Skipped data enhancer (_Trackinsight_) if data is inaccurate +### Fixed + +- Fixed an issue with countries in the symbol profile overrides + ## 1.149.0 - 16.05.2022 ### Added diff --git a/apps/api/src/services/symbol-profile.service.ts b/apps/api/src/services/symbol-profile.service.ts index 8b119de78..5c8839ebf 100644 --- a/apps/api/src/services/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile.service.ts @@ -71,7 +71,7 @@ export class SymbolProfileService { item.assetSubClass = item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass; item.countries = - (item.SymbolProfileOverrides.sectors as unknown as Country[]) ?? + (item.SymbolProfileOverrides.countries as unknown as Country[]) ?? item.countries; item.name = item.SymbolProfileOverrides?.name ?? item.name; item.sectors =