From 961774ce9ff85db38e6283a6af200a72c54b7403 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 3 Dec 2021 21:24:05 +0100 Subject: [PATCH 1/7] Feature/improve market data detail (#511) * Improve historical data view (hide invalid and future dates) * Update changelog --- CHANGELOG.md | 4 ++++ .../admin-market-data-detail.component.html | 7 +++++-- .../admin-market-data-detail.component.scss | 5 ++++- .../admin-market-data-detail.component.ts | 9 ++++++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a69ce88a..6a39613f2 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 historical data view in the admin control panel (hide invalid and future dates) + ### Fixed - Improved the allocations by currency in combination with cash balances 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 66c3388dd..2c67da932 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 @@ -6,12 +6,15 @@ *ngFor="let dayItem of days; let i = index" class="day" [title]=" - (marketDataByMonth[itemByMonth.key][i + 1]?.date + (itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) | date: defaultDateFormat) ?? '' " [ngClass]="{ + valid: isDateOfInterest( + itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) + ), 'available cursor-pointer': - marketDataByMonth[itemByMonth.key][i + 1]?.day == i + 1 + marketDataByMonth[itemByMonth.key][i + 1]?.day === i + 1 }" (click)=" marketDataByMonth[itemByMonth.key][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 d16aec1d9..b5dabd463 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 @@ -10,11 +10,14 @@ } .day { - background-color: var(--danger); height: 0.5rem; margin-right: 0.25rem; width: 0.5rem; + &.valid { + background-color: var(--danger); + } + &.available { background-color: var(--success); } 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 8c297a36f..83f4d65a3 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 @@ -7,8 +7,9 @@ import { } 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 { MarketData } from '@prisma/client'; -import { format } from 'date-fns'; +import { format, isBefore, isValid, parse } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -59,6 +60,12 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { } } + public isDateOfInterest(aDateString: string) { + // Date is valid and in the past + const date = parse(aDateString, DATE_FORMAT, new Date()); + return isValid(date) && isBefore(date, new Date()); + } + public onOpenMarketDataDetail({ date, marketPrice, symbol }: MarketData) { const dialogRef = this.dialog.open(MarketDataDetailDialog, { data: { From e96e6c717cb2ef60649e3fe20c554358258f25b7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Dec 2021 08:57:22 +0100 Subject: [PATCH 2/7] Feature/enable import by default (#513) * Enable import by default * Update changelog --- CHANGELOG.md | 1 + apps/api/src/services/configuration.service.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a39613f2..e59a63c31 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 historical data view in the admin control panel (hide invalid and future dates) +- Enabled the import functionality for transactions by default ### Fixed diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 677e97c80..d671f6f40 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { bool, cleanEnv, host, json, num, port, str } from 'envalid'; -import { environment } from '../environments/environment'; import { Environment } from './interfaces/environment.interface'; @Injectable() @@ -18,7 +17,7 @@ export class ConfigurationService { ENABLE_FEATURE_BLOG: bool({ default: false }), ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), - ENABLE_FEATURE_IMPORT: bool({ default: !environment.production }), + ENABLE_FEATURE_IMPORT: bool({ default: true }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), ENABLE_FEATURE_STATISTICS: bool({ default: false }), ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }), From 563f354e7ec7cda4ead979a0550aec2baadbc3ac Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Dec 2021 11:40:12 +0100 Subject: [PATCH 3/7] Feature/symbol to uppercase to avoid duplicates (#514) * Convert the symbol to uppercase to avoid case-sensitive duplicates * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/order/order.service.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59a63c31..b9cbe5b8b 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 - Improved the historical data view in the admin control panel (hide invalid and future dates) - Enabled the import functionality for transactions by default +- Converted the symbols to uppercase to avoid case-sensitive duplicates in the symbol profile model ### Fixed diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index e5bfaa1a5..ace5756da 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -45,19 +45,22 @@ export class OrderService { public async createOrder(data: Prisma.OrderCreateInput): Promise { const isDraft = isAfter(data.date as Date, endOfToday()); + // Convert the symbol to uppercase to avoid case-sensitive duplicates + const symbol = data.symbol.toUpperCase(); + if (!isDraft) { // Gather symbol data of order in the background, if not draft this.dataGatheringService.gatherSymbols([ { + symbol, dataSource: data.dataSource, - date: data.date, - symbol: data.symbol + date: data.date } ]); } this.dataGatheringService.gatherProfileData([ - { dataSource: data.dataSource, symbol: data.symbol } + { symbol, dataSource: data.dataSource } ]); await this.cacheService.flush(); @@ -65,7 +68,8 @@ export class OrderService { return this.prismaService.order.create({ data: { ...data, - isDraft + isDraft, + symbol } }); } From 3e82de6b21517374c49ba483cb7d237502df5982 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Dec 2021 11:49:00 +0100 Subject: [PATCH 4/7] Feature/add historical data chart of fear and greed index (#515) * Add historical data chart of market mood * Update changelog --- CHANGELOG.md | 4 +++ .../interfaces/symbol-item.interface.ts | 2 ++ apps/api/src/app/symbol/symbol.controller.ts | 11 +++++-- apps/api/src/app/symbol/symbol.module.ts | 8 ++++- apps/api/src/app/symbol/symbol.service.ts | 31 ++++++++++++++++++- .../fear-and-greed-index.component.html | 6 ++-- .../home-market/home-market.component.ts | 13 +++++++- .../components/home-market/home-market.html | 29 +++++++++++------ .../home-market/home-market.module.ts | 4 +-- .../components/home-market/home-market.scss | 4 +++ apps/client/src/app/services/data.service.ts | 6 +++- .../lib/line-chart/line-chart.component.ts | 17 +++++++++- 12 files changed, 113 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9cbe5b8b..98c910aea 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 historical data chart of the _Fear & Greed Index_ (market mood) + ### Changed - Improved the historical data view in the admin control panel (hide invalid and future dates) 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 03554d649..787547901 100644 --- a/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts +++ b/apps/api/src/app/symbol/interfaces/symbol-item.interface.ts @@ -1,7 +1,9 @@ +import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { DataSource } from '@prisma/client'; export interface SymbolItem { currency: string; dataSource: DataSource; + historicalData: HistoricalDataItem[]; marketPrice: number; } diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index 845645574..a364de6bc 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,10 +1,12 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, + DefaultValuePipe, Get, HttpException, Inject, Param, + ParseBoolPipe, Query, UseGuards } from '@nestjs/common'; @@ -51,7 +53,9 @@ export class SymbolController { @UseGuards(AuthGuard('jwt')) public async getSymbolData( @Param('dataSource') dataSource: DataSource, - @Param('symbol') symbol: string + @Param('symbol') symbol: string, + @Query('includeHistoricalData', new DefaultValuePipe(false), ParseBoolPipe) + includeHistoricalData: boolean ): Promise { if (!DataSource[dataSource]) { throw new HttpException( @@ -60,7 +64,10 @@ export class SymbolController { ); } - const result = await this.symbolService.get({ dataSource, symbol }); + const result = await this.symbolService.get({ + includeHistoricalData, + dataGatheringItem: { dataSource, symbol } + }); if (!result || isEmpty(result)) { throw new HttpException( diff --git a/apps/api/src/app/symbol/symbol.module.ts b/apps/api/src/app/symbol/symbol.module.ts index 7b0da89a2..c5143632b 100644 --- a/apps/api/src/app/symbol/symbol.module.ts +++ b/apps/api/src/app/symbol/symbol.module.ts @@ -1,5 +1,6 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { Module } from '@nestjs/common'; @@ -7,7 +8,12 @@ import { SymbolController } from './symbol.controller'; import { SymbolService } from './symbol.service'; @Module({ - imports: [ConfigurationModule, DataProviderModule, PrismaModule], + imports: [ + ConfigurationModule, + DataProviderModule, + MarketDataModule, + PrismaModule + ], controllers: [SymbolController], providers: [SymbolService] }) diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 60a5e481c..3f377c551 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,8 +1,11 @@ +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 { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; +import { subDays } from 'date-fns'; import { LookupItem } from './interfaces/lookup-item.interface'; import { SymbolItem } from './interfaces/symbol-item.interface'; @@ -11,16 +14,42 @@ import { SymbolItem } from './interfaces/symbol-item.interface'; export class SymbolService { public constructor( private readonly dataProviderService: DataProviderService, + private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService ) {} - public async get(dataGatheringItem: IDataGatheringItem): Promise { + public async get({ + dataGatheringItem, + includeHistoricalData = false + }: { + dataGatheringItem: IDataGatheringItem; + includeHistoricalData?: boolean; + }): Promise { const response = await this.dataProviderService.get([dataGatheringItem]); const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {}; if (dataGatheringItem.dataSource && marketPrice) { + let historicalData: HistoricalDataItem[]; + + if (includeHistoricalData) { + const days = 7; + + const marketData = await this.marketDataService.getRange({ + dateQuery: { gte: subDays(new Date(), days) }, + symbols: [dataGatheringItem.symbol] + }); + + historicalData = marketData.map(({ date, marketPrice }) => { + return { + date: date.toISOString(), + value: marketPrice + }; + }); + } + return { currency, + historicalData, marketPrice, dataSource: dataGatheringItem.dataSource }; diff --git a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html index 0de6ac211..16b2ea602 100644 --- a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html +++ b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html @@ -1,13 +1,13 @@
-
{{ fearAndGreedIndexEmoji }}
+
{{ fearAndGreedIndexEmoji }}
-
+
{{ fearAndGreedIndexText }} {{ fearAndGreedIndex }}/100
- Market Mood + Current Market Mood
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 380ac9c8d..87a86814d 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,7 +1,9 @@ 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 { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataSource } from '@prisma/client'; @@ -16,6 +18,7 @@ import { takeUntil } from 'rxjs/operators'; export class HomeMarketComponent implements OnDestroy, OnInit { public fearAndGreedIndex: number; public hasPermissionToAccessFearAndGreedIndex: boolean; + public historicalData: HistoricalDataItem[]; public isLoading = true; public user: User; @@ -46,11 +49,19 @@ export class HomeMarketComponent implements OnDestroy, OnInit { this.dataService .fetchSymbolItem({ dataSource: DataSource.RAKUTEN, + includeHistoricalData: true, symbol: ghostfolioFearAndGreedIndexSymbol }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ marketPrice }) => { + .subscribe(({ historicalData, marketPrice }) => { this.fearAndGreedIndex = marketPrice; + this.historicalData = [ + ...historicalData, + { + date: resetHours(new Date()).toISOString(), + value: marketPrice + } + ]; this.isLoading = false; this.changeDetectorRef.markForCheck(); 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 c92d29950..2705530bc 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -9,17 +9,26 @@ w-100 " > -
+
- - - - - +
+ Last 7 Days +
+ +
diff --git a/apps/client/src/app/components/home-market/home-market.module.ts b/apps/client/src/app/components/home-market/home-market.module.ts index 436552e61..01267b426 100644 --- a/apps/client/src/app/components/home-market/home-market.module.ts +++ b/apps/client/src/app/components/home-market/home-market.module.ts @@ -1,14 +1,14 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { MatCardModule } from '@angular/material/card'; import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module'; +import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { HomeMarketComponent } from './home-market.component'; @NgModule({ declarations: [HomeMarketComponent], exports: [], - imports: [CommonModule, GfFearAndGreedIndexModule, MatCardModule], + imports: [CommonModule, GfFearAndGreedIndexModule, GfLineChartModule], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/apps/client/src/app/components/home-market/home-market.scss b/apps/client/src/app/components/home-market/home-market.scss index b97d286cc..2d7ffa0dd 100644 --- a/apps/client/src/app/components/home-market/home-market.scss +++ b/apps/client/src/app/components/home-market/home-market.scss @@ -2,4 +2,8 @@ :host { display: block; + + gf-line-chart { + aspect-ratio: 16 / 9; + } } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index a14dc3486..0af1990ac 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -127,12 +127,16 @@ export class DataService { public fetchSymbolItem({ dataSource, + includeHistoricalData = false, symbol }: { dataSource: DataSource; + includeHistoricalData?: boolean; symbol: string; }) { - return this.http.get(`/api/symbol/${dataSource}/${symbol}`); + return this.http.get(`/api/symbol/${dataSource}/${symbol}`, { + params: { includeHistoricalData } + }); } public fetchPositions({ 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 4265aad14..601990715 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -43,6 +43,10 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() showXAxis = false; @Input() showYAxis = false; @Input() symbol: string; + @Input() yMax: number; + @Input() yMaxLabel: string; + @Input() yMin: number; + @Input() yMinLabel: string; @ViewChild('chartCanvas') chartCanvas; @@ -170,11 +174,22 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { grid: { display: false }, + max: this.yMax, + min: this.yMin, ticks: { display: this.showYAxis, - callback: function (tickValue, index, ticks) { + callback: (tickValue, index, ticks) => { if (index === 0 || index === ticks.length - 1) { // Only print last and first legend entry + + if (index === 0 && this.yMinLabel) { + return this.yMinLabel; + } + + if (index === ticks.length - 1 && this.yMaxLabel) { + return this.yMaxLabel; + } + if (typeof tickValue === 'number') { return tickValue.toFixed(2); } From 9bc3505ded7fa25e3ff3f8c3c64abf9c2381afe7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Dec 2021 11:51:06 +0100 Subject: [PATCH 5/7] Release 1.86.0 (#516) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98c910aea..c87b59cfe 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.86.0 - 04.12.2021 ### Added diff --git a/package.json b/package.json index ecac9ba10..6d0910d8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.85.0", + "version": "1.86.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1beb4de62f6ea070c20ec6c581778665e423fe87 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Dec 2021 21:05:11 +0100 Subject: [PATCH 6/7] Feature/support additional currencies (#517) * Support additional currencies * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/admin/admin.controller.ts | 27 ++++++ apps/api/src/app/admin/admin.module.ts | 2 + apps/api/src/app/admin/admin.service.ts | 16 +++- apps/api/src/app/info/info.service.ts | 3 +- .../portfolio/current-rate.service.spec.ts | 2 +- .../src/services/data-gathering.service.ts | 45 +++++---- .../src/services/exchange-rate-data.module.ts | 3 +- .../services/exchange-rate-data.service.ts | 16 +++- .../api/src/services/property/property.dto.ts | 6 ++ .../src/services/property/property.module.ts | 11 +++ .../src/services/property/property.service.ts | 43 +++++++++ .../admin-overview.component.ts | 42 ++++++++- .../admin-overview/admin-overview.html | 94 +++++++++++-------- .../admin-overview/admin-overview.scss | 6 ++ apps/client/src/app/services/data.service.ts | 6 ++ libs/common/src/lib/config.ts | 5 + .../lib/interfaces/admin-data.interface.ts | 3 + 18 files changed, 271 insertions(+), 65 deletions(-) create mode 100644 apps/api/src/services/property/property.dto.ts create mode 100644 apps/api/src/services/property/property.module.ts create mode 100644 apps/api/src/services/property/property.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c87b59cfe..61be1dac1 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 + +- Supported the management of additional currencies in the admin control panel + ## 1.86.0 - 04.12.2021 ### Added diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 4788a02c2..054371458 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,4 +1,7 @@ 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, @@ -11,12 +14,14 @@ import { } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { + Body, Controller, Get, HttpException, Inject, Param, Post, + Put, UseGuards } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; @@ -31,6 +36,7 @@ export class AdminController { public constructor( private readonly adminService: AdminService, private readonly dataGatheringService: DataGatheringService, + private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -153,4 +159,25 @@ export class AdminController { return this.adminService.getMarketDataBySymbol(symbol); } + + @Put('settings/:key') + @UseGuards(AuthGuard('jwt')) + public async updateProperty( + @Param('key') key: string, + @Body() data: PropertyDto + ) { + if ( + !hasPermission( + getPermissions(this.request.user.role), + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return await this.adminService.putSetting(key, data.value); + } } diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index 4184ff2fd..b8e96fa04 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data- import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; 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 { Module } from '@nestjs/common'; import { AdminController } from './admin.controller'; @@ -18,6 +19,7 @@ import { AdminService } from './admin.service'; ExchangeRateDataModule, MarketDataModule, PrismaModule, + PropertyModule, SubscriptionModule ], controllers: [AdminController], diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index d4c21a29e..4b9aaad4c 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -4,7 +4,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; -import { baseCurrency } from '@ghostfolio/common/config'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, @@ -21,6 +22,7 @@ export class AdminService { private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, + private readonly propertyService: PropertyService, private readonly subscriptionService: SubscriptionService ) {} @@ -45,6 +47,7 @@ export class AdminService { }; }), lastDataGathering: await this.getLastDataGathering(), + settings: await this.propertyService.get(), transactionCount: await this.prismaService.order.count(), userCount: await this.prismaService.user.count(), users: await this.getUsersWithAnalytics() @@ -76,6 +79,17 @@ export class AdminService { }; } + public async putSetting(key: string, value: string) { + const response = await this.propertyService.put({ key, value }); + + if (key === PROPERTY_CURRENCIES) { + await this.exchangeRateDataService.initialize(); + await this.dataGatheringService.reset(); + } + + return response; + } + private async getLastDataGathering() { const lastDataGathering = await this.dataGatheringService.getLastDataGathering(); diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 2d9c9a27b..25c5c9906 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 { 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 { PROPERTY_STRIPE_CONFIG } from '@ghostfolio/common/config'; import { InfoItem } from '@ghostfolio/common/interfaces'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface'; @@ -222,7 +223,7 @@ export class InfoService { } const stripeConfig = await this.prismaService.property.findUnique({ - where: { key: 'STRIPE_CONFIG' } + where: { key: PROPERTY_STRIPE_CONFIG } }); if (stripeConfig) { 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 f474a10f0..bb9aa78b6 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -73,7 +73,7 @@ describe('CurrentRateService', () => { beforeAll(async () => { dataProviderService = new DataProviderService(null, [], null); - exchangeRateDataService = new ExchangeRateDataService(null, null); + exchangeRateDataService = new ExchangeRateDataService(null, null, null); marketDataService = new MarketDataService(null); await exchangeRateDataService.initialize(); diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index fb2f34222..48ce87e02 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -1,5 +1,9 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; -import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; +import { + PROPERTY_LAST_DATA_GATHERING, + PROPERTY_LOCKED_DATA_GATHERING, + ghostfolioFearAndGreedIndexSymbol +} from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; @@ -43,7 +47,7 @@ export class DataGatheringService { await this.prismaService.property.create({ data: { - key: 'LOCKED_DATA_GATHERING', + key: PROPERTY_LOCKED_DATA_GATHERING, value: new Date().toISOString() } }); @@ -55,11 +59,11 @@ export class DataGatheringService { await this.prismaService.property.upsert({ create: { - key: 'LAST_DATA_GATHERING', + key: PROPERTY_LAST_DATA_GATHERING, value: new Date().toISOString() }, update: { value: new Date().toISOString() }, - where: { key: 'LAST_DATA_GATHERING' } + where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { Logger.error(error); @@ -67,7 +71,7 @@ export class DataGatheringService { await this.prismaService.property.delete({ where: { - key: 'LOCKED_DATA_GATHERING' + key: PROPERTY_LOCKED_DATA_GATHERING } }); @@ -78,7 +82,7 @@ export class DataGatheringService { public async gatherMax() { const isDataGatheringLocked = await this.prismaService.property.findUnique({ - where: { key: 'LOCKED_DATA_GATHERING' } + where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); if (!isDataGatheringLocked) { @@ -87,7 +91,7 @@ export class DataGatheringService { await this.prismaService.property.create({ data: { - key: 'LOCKED_DATA_GATHERING', + key: PROPERTY_LOCKED_DATA_GATHERING, value: new Date().toISOString() } }); @@ -99,11 +103,11 @@ export class DataGatheringService { await this.prismaService.property.upsert({ create: { - key: 'LAST_DATA_GATHERING', + key: PROPERTY_LAST_DATA_GATHERING, value: new Date().toISOString() }, update: { value: new Date().toISOString() }, - where: { key: 'LAST_DATA_GATHERING' } + where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { Logger.error(error); @@ -111,7 +115,7 @@ export class DataGatheringService { await this.prismaService.property.delete({ where: { - key: 'LOCKED_DATA_GATHERING' + key: PROPERTY_LOCKED_DATA_GATHERING } }); @@ -128,7 +132,7 @@ export class DataGatheringService { symbol: string; }) { const isDataGatheringLocked = await this.prismaService.property.findUnique({ - where: { key: 'LOCKED_DATA_GATHERING' } + where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); if (!isDataGatheringLocked) { @@ -137,7 +141,7 @@ export class DataGatheringService { await this.prismaService.property.create({ data: { - key: 'LOCKED_DATA_GATHERING', + key: PROPERTY_LOCKED_DATA_GATHERING, value: new Date().toISOString() } }); @@ -156,11 +160,11 @@ export class DataGatheringService { await this.prismaService.property.upsert({ create: { - key: 'LAST_DATA_GATHERING', + key: PROPERTY_LAST_DATA_GATHERING, value: new Date().toISOString() }, update: { value: new Date().toISOString() }, - where: { key: 'LAST_DATA_GATHERING' } + where: { key: PROPERTY_LAST_DATA_GATHERING } }); } catch (error) { Logger.error(error); @@ -168,7 +172,7 @@ export class DataGatheringService { await this.prismaService.property.delete({ where: { - key: 'LOCKED_DATA_GATHERING' + key: PROPERTY_LOCKED_DATA_GATHERING } }); @@ -351,13 +355,13 @@ export class DataGatheringService { public async getIsInProgress() { return await this.prismaService.property.findUnique({ - where: { key: 'LOCKED_DATA_GATHERING' } + where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); } public async getLastDataGathering() { const lastDataGathering = await this.prismaService.property.findUnique({ - where: { key: 'LAST_DATA_GATHERING' } + where: { key: PROPERTY_LAST_DATA_GATHERING } }); if (lastDataGathering?.value) { @@ -418,7 +422,10 @@ export class DataGatheringService { await this.prismaService.property.deleteMany({ where: { - OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }] + OR: [ + { key: PROPERTY_LAST_DATA_GATHERING }, + { key: PROPERTY_LOCKED_DATA_GATHERING } + ] } }); } @@ -496,7 +503,7 @@ export class DataGatheringService { const lastDataGathering = await this.getLastDataGathering(); const isDataGatheringLocked = await this.prismaService.property.findUnique({ - where: { key: 'LOCKED_DATA_GATHERING' } + where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); const diffInHours = differenceInHours(new Date(), lastDataGathering); diff --git a/apps/api/src/services/exchange-rate-data.module.ts b/apps/api/src/services/exchange-rate-data.module.ts index 4843b5c77..9c886b06a 100644 --- a/apps/api/src/services/exchange-rate-data.module.ts +++ b/apps/api/src/services/exchange-rate-data.module.ts @@ -3,9 +3,10 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { Module } from '@nestjs/common'; import { PrismaModule } from './prisma.module'; +import { PropertyModule } from './property/property.module'; @Module({ - imports: [DataProviderModule, PrismaModule], + imports: [DataProviderModule, PrismaModule, PropertyModule], providers: [ExchangeRateDataService], exports: [ExchangeRateDataService] }) diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index d0d6bf74e..e0e0e614f 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -1,4 +1,4 @@ -import { baseCurrency } from '@ghostfolio/common/config'; +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'; @@ -7,6 +7,7 @@ import { isEmpty, isNumber, uniq } from 'lodash'; import { DataProviderService } from './data-provider/data-provider.service'; import { IDataGatheringItem } from './interfaces/interfaces'; import { PrismaService } from './prisma.service'; +import { PropertyService } from './property/property.service'; @Injectable() export class ExchangeRateDataService { @@ -16,7 +17,8 @@ export class ExchangeRateDataService { public constructor( private readonly dataProviderService: DataProviderService, - private readonly prismaService: PrismaService + private readonly prismaService: PrismaService, + private readonly propertyService: PropertyService ) { this.initialize(); } @@ -149,7 +151,7 @@ export class ExchangeRateDataService { } private async prepareCurrencies(): Promise { - const currencies: string[] = []; + let currencies: string[] = []; ( await this.prismaService.account.findMany({ @@ -181,6 +183,14 @@ export class ExchangeRateDataService { currencies.push(symbolProfile.currency); }); + const customCurrencies = (await this.propertyService.getByKey( + PROPERTY_CURRENCIES + )) as string[]; + + if (customCurrencies?.length > 0) { + currencies = currencies.concat(customCurrencies); + } + return uniq(currencies).sort(); } diff --git a/apps/api/src/services/property/property.dto.ts b/apps/api/src/services/property/property.dto.ts new file mode 100644 index 000000000..62f90875a --- /dev/null +++ b/apps/api/src/services/property/property.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class PropertyDto { + @IsString() + value: string; +} diff --git a/apps/api/src/services/property/property.module.ts b/apps/api/src/services/property/property.module.ts new file mode 100644 index 000000000..fcd89de40 --- /dev/null +++ b/apps/api/src/services/property/property.module.ts @@ -0,0 +1,11 @@ +import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; +import { Module } from '@nestjs/common'; + +import { PropertyService } from './property.service'; + +@Module({ + exports: [PropertyService], + imports: [PrismaModule], + providers: [PropertyService] +}) +export class PropertyModule {} diff --git a/apps/api/src/services/property/property.service.ts b/apps/api/src/services/property/property.service.ts new file mode 100644 index 000000000..44c4f7e27 --- /dev/null +++ b/apps/api/src/services/property/property.service.ts @@ -0,0 +1,43 @@ +import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class PropertyService { + public constructor(private readonly prismaService: PrismaService) {} + + public async get() { + const response: { + [key: string]: object | string | string[]; + } = { + [PROPERTY_CURRENCIES]: [] + }; + + const properties = await this.prismaService.property.findMany(); + + for (const property of properties) { + let value = property.value; + + try { + value = JSON.parse(property.value); + } catch {} + + response[property.key] = value; + } + + return response; + } + + public async getByKey(aKey: string) { + const properties = await this.get(); + return properties?.[aKey]; + } + + public async put({ key, value }: { key: string; value: string }) { + return this.prismaService.property.upsert({ + create: { key, value }, + update: { value }, + where: { key } + }); + } +} 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 5dd86f23d..ce4a9cd53 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 @@ -3,7 +3,10 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { + DEFAULT_DATE_FORMAT, + PROPERTY_CURRENCIES +} from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { differenceInSeconds, @@ -11,6 +14,7 @@ import { isValid, parseISO } from 'date-fns'; +import { uniq } from 'lodash'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -20,6 +24,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-overview.html' }) export class AdminOverviewComponent implements OnDestroy, OnInit { + public customCurrencies: string[]; public dataGatheringInProgress: boolean; public dataGatheringProgress: number; public defaultDateFormat = DEFAULT_DATE_FORMAT; @@ -57,6 +62,26 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { }); } + public onAddCurrency() { + const currency = prompt('Please add a currency:'); + + if (currency) { + const currencies = uniq([...this.customCurrencies, currency]); + this.putCurrencies(currencies); + } + } + + public onDeleteCurrency(aCurrency: string) { + const confirmation = confirm('Do you really want to delete this currency?'); + + if (confirmation) { + const currencies = this.customCurrencies.filter((currency) => { + return currency !== aCurrency; + }); + this.putCurrencies(currencies); + } + } + public onFlushCache() { this.cacheService .flush() @@ -121,9 +146,11 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { dataGatheringProgress, exchangeRates, lastDataGathering, + settings, transactionCount, userCount }) => { + this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; this.dataGatheringProgress = dataGatheringProgress; this.exchangeRates = exchangeRates; @@ -147,4 +174,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } ); } + + private putCurrencies(aCurrencies: string[]) { + this.dataService + .putAdminSetting(PROPERTY_CURRENCIES, { + value: JSON.stringify(aCurrencies) + }) + .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 2f6c0aa4d..3d702ac90 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -3,32 +3,17 @@
-
-
Exchange Rates
+
+
User Count
+
{{ userCount }}
+
+
+
Transaction Count
- - - - - - - - -
- - {{ exchangeRate.label1 }}= - - {{ exchangeRate.label2 }}
+ + {{ transactionCount }} ({{ transactionCount / userCount | number + : '1.2-2' }} per User) +
@@ -46,7 +31,6 @@
-
-
User Count
-
{{ userCount }}
-
-
-
Transaction Count
+
+
Exchange Rates
- - {{ transactionCount }} ({{ transactionCount / userCount | number - : '1.2-2' }} per User) - + + + + + + + + + +
+ + {{ exchangeRate.label1 }}= + + {{ exchangeRate.label2 }} + +
+
+ +
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 ed7f589cd..46cadd6d7 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.scss +++ b/apps/client/src/app/components/admin-overview/admin-overview.scss @@ -3,6 +3,12 @@ :host { display: block; + .mat-button { + &.mini-icon { + line-height: 1.5; + } + } + .mat-flat-button { ::ng-deep { .mat-button-wrapper { diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 0af1990ac..6a21db0da 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -12,6 +12,7 @@ import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.in import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto'; +import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { Access, Accounts, @@ -239,6 +240,11 @@ export class DataService { return this.http.put(`/api/account/${aAccount.id}`, aAccount); } + public putAdminSetting(key: string, aData: PropertyDto) { + console.log(key, aData); + return this.http.put(`/api/admin/settings/${key}`, aData); + } + public putOrder(aOrder: UpdateOrderDto) { return this.http.put(`/api/order/${aOrder.id}`, aOrder); } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 91267cf90..5781fc65c 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -30,4 +30,9 @@ export const warnColorRgb = { 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_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING'; +export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING'; +export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; + export const UNKNOWN_KEY = 'UNKNOWN'; diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 71df57229..ce4ea8c0b 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,7 +1,10 @@ +import { Property } from '@prisma/client'; + export interface AdminData { dataGatheringProgress?: number; exchangeRates: { label1: string; label2: string; value: number }[]; lastDataGathering?: Date | 'IN_PROGRESS'; + settings: { [key: string]: object | string | string[] }; transactionCount: number; userCount: number; users: { From aed8f5cf0480e8307f4aba1029e4eaf33b776134 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 5 Dec 2021 16:52:24 +0100 Subject: [PATCH 7/7] Feature/upgrade prisma to version 3.6.0 (#518) * Upgrade prisma from version 2.30.2 to 3.6.0 * Update changelog --- CHANGELOG.md | 4 +++ apps/api/src/app/access/access.service.ts | 2 +- apps/api/src/app/account/account.service.ts | 2 +- .../app/auth-device/auth-device.service.ts | 2 +- apps/api/src/app/order/order.service.ts | 2 +- apps/api/src/app/user/user.service.ts | 2 +- .../data-provider/data-provider.service.ts | 6 ++-- apps/api/src/services/market-data.service.ts | 2 +- package.json | 4 +-- yarn.lock | 36 +++++++++---------- 10 files changed, 33 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61be1dac1..3e1edf988 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 - Supported the management of additional currencies in the admin control panel +### Changed + +- Upgraded `prisma` from version `2.30.2` to `3.6.0` + ## 1.86.0 - 04.12.2021 ### Added diff --git a/apps/api/src/app/access/access.service.ts b/apps/api/src/app/access/access.service.ts index f2f98eb23..f1dc4c6ec 100644 --- a/apps/api/src/app/access/access.service.ts +++ b/apps/api/src/app/access/access.service.ts @@ -24,7 +24,7 @@ export class AccessService { take?: number; cursor?: Prisma.AccessWhereUniqueInput; where?: Prisma.AccessWhereInput; - orderBy?: Prisma.AccessOrderByInput; + orderBy?: Prisma.AccessOrderByWithRelationInput; }): Promise { const { include, skip, take, cursor, where, orderBy } = params; diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 159499dbb..9f5aab1b2 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -40,7 +40,7 @@ export class AccountService { take?: number; cursor?: Prisma.AccountWhereUniqueInput; where?: Prisma.AccountWhereInput; - orderBy?: Prisma.AccountOrderByInput; + orderBy?: Prisma.AccountOrderByWithRelationInput; }): Promise< (Account & { Order?: Order[]; diff --git a/apps/api/src/app/auth-device/auth-device.service.ts b/apps/api/src/app/auth-device/auth-device.service.ts index 80968f025..048aca758 100644 --- a/apps/api/src/app/auth-device/auth-device.service.ts +++ b/apps/api/src/app/auth-device/auth-device.service.ts @@ -23,7 +23,7 @@ export class AuthDeviceService { take?: number; cursor?: Prisma.AuthDeviceWhereUniqueInput; where?: Prisma.AuthDeviceWhereInput; - orderBy?: Prisma.AuthDeviceOrderByInput; + orderBy?: Prisma.AuthDeviceOrderByWithRelationInput; }): Promise { const { skip, take, cursor, where, orderBy } = params; return this.prismaService.authDevice.findMany({ diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index ace5756da..4fabb98f1 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -28,7 +28,7 @@ export class OrderService { take?: number; cursor?: Prisma.OrderWhereUniqueInput; where?: Prisma.OrderWhereInput; - orderBy?: Prisma.OrderOrderByInput; + orderBy?: Prisma.OrderOrderByWithRelationInput; }): Promise { const { include, skip, take, cursor, where, orderBy } = params; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index d828c6988..a513cbba6 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -119,7 +119,7 @@ export class UserService { take?: number; cursor?: Prisma.UserWhereUniqueInput; where?: Prisma.UserWhereInput; - orderBy?: Prisma.UserOrderByInput; + orderBy?: Prisma.UserOrderByWithRelationInput; }): Promise { const { skip, take, cursor, where, orderBy } = params; return this.prismaService.user.findMany({ 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 8ce146e4b..15a8a6efb 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -11,7 +11,7 @@ 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 { format } from 'date-fns'; +import { format, isValid } from 'date-fns'; import { isEmpty } from 'lodash'; @Injectable() @@ -62,7 +62,7 @@ export class DataProviderService { [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; } = {}; - if (isEmpty(aItems)) { + if (isEmpty(aItems) || !isValid(from) || !isValid(to)) { return response; } @@ -96,7 +96,7 @@ export class DataProviderService { ORDER BY date;`; const marketDataByGranularity: MarketData[] = - await this.prismaService.$queryRaw(queryRaw); + await this.prismaService.$queryRawUnsafe(queryRaw); response = marketDataByGranularity.reduce((r, marketData) => { const { date, marketPrice, symbol } = marketData; diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index 4ba6c966f..f9c7fc003 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/apps/api/src/services/market-data.service.ts @@ -53,7 +53,7 @@ export class MarketDataService { take?: number; cursor?: Prisma.MarketDataWhereUniqueInput; where?: Prisma.MarketDataWhereInput; - orderBy?: Prisma.MarketDataOrderByInput; + orderBy?: Prisma.MarketDataOrderByWithRelationInput; }): Promise { const { skip, take, cursor, where, orderBy } = params; diff --git a/package.json b/package.json index 6d0910d8a..08c4c9e2b 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.2.2", - "@prisma/client": "2.30.2", + "@prisma/client": "3.6.0", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -105,7 +105,6 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "2.30.2", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", @@ -162,6 +161,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index b79552d7c..a5f1c3de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2904,22 +2904,22 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@prisma/client@2.30.2": - version "2.30.2" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.2.tgz#4e7f90f27a176d95769609e65932a4b95aa8da3f" - integrity sha512-cfS/1UWBRE4sq0z7spGLYqOE0oN2YJJvKtm+63o7h/BI6ji10VWla79aXBTXj3AImtfykNtC6OHtFVawEl/49w== +"@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== dependencies: - "@prisma/engines-version" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + "@prisma/engines-version" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" -"@prisma/engines-version@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": - version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#d5ef55c92beeba56e52bba12b703af0bfd30530d" - integrity sha512-/iDRgaoSQC77WN2oDsOM8dn61fykm6tnZUAClY+6p+XJbOEgZ9gy4CKuKTBgrjSGDVjtQ/S2KGcYd3Ring8xaw== +"@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@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20": - version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#2df768aa7c9f84acaa1f35c970417822233a9fb1" - integrity sha512-WPnA/IUrxDihrRhdP6+8KAVSwsc0zsh8ioPYsLJjOhzVhwpRbuFH2tJDRIAbc+qFh+BbTIZbeyBYt8fpNXaYQQ== +"@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== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -14431,12 +14431,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@2.30.2: - version "2.30.2" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.30.2.tgz#f65ea0a04ff642e1e393d5b42c804bdcb099a465" - integrity sha512-AoLz3CfD7XM+vfE7IWzxQq1O2+ZZ31+6yfabWeMr32wxdLLxSsGagW80LFgKIsZgWW6Ypwleu5ujyKNvO91WHA== +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== dependencies: - "@prisma/engines" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20" + "@prisma/engines" "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" prismjs@^1.21.0, prismjs@^1.23.0, prismjs@~1.24.0: version "1.24.1"