Compare commits

...

21 Commits

Author SHA1 Message Date
fa44cee781 Release 1.96.0 (#590) 2021-12-27 21:08:33 +01:00
db1d474ddf 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
2021-12-27 12:14:41 +01:00
994275e093 Feature/upgrade angular 3rd party dependencies (#588)
* Upgrade angular 3rd party dependencies
  * ngx-device-detector
  * ngx-markdown
  * ngx-stripe

* Update changelog
2021-12-26 21:58:56 +01:00
ee397c8047 Bugfix/fix file type detection for import (#587)
* Fix file type detection ("application/vnd.ms-excel" instead of "text/csv")

* Update changelog
2021-12-26 20:54:53 +01:00
7203939c42 Feature/upgrade prisma to version 3.7.0 (#586)
* Upgrade prisma from version 3.6.0 to 3.7.0

* Update changelog
2021-12-26 17:30:26 +01:00
9725f16c81 Clean up schema.prisma (#584) 2021-12-26 14:33:18 +01:00
bb8b1e4f43 Release 1.95.0 (#583) 2021-12-26 10:14:13 +01:00
9d3610331a Add guard (#582) 2021-12-26 10:07:51 +01:00
0043b44670 Feature/improve data gathering for currencies (#581)
* Improve data gathering for currencies, add warning if it fails

* Update changelog
2021-12-26 09:15:10 +01:00
bbc4e64cb4 Bugfix/filter currencies with null value (#579)
* Filter currencies with null value

* Update changelog
2021-12-25 17:08:56 +01:00
c7f4825499 Release 1.94.0 (#578) 2021-12-25 14:45:58 +01:00
8f583709ef Feature/add support for cosmos and polkadot (#577)
* Add support for cryptocurrencies ATOM and DOT

* Update changelog
2021-12-25 14:23:07 +01:00
4c30212a72 Feature/improve data gathering (#576)
* Eliminate benchmarks to gather

* Optimize 7d data gathering

* Update changelog
2021-12-25 14:18:46 +01:00
cade2f6a5e Feature/upgrade prettier to version 2.5.1 (#575)
* Upgrade prettier from version 2.3.2 to 2.5.1

* Update changelog
2021-12-25 10:29:56 +01:00
3b9a8fabb5 Clean up (#574) 2021-12-24 18:42:30 +01:00
3435b3a348 Feature/make the csv import more flexible (#573)
* Make the csv import more flexible

* Update changelog
2021-12-24 18:21:27 +01:00
5d39b267ab write portfolio calculator test case for symbol BALN.SW (refs #554) (#572) 2021-12-24 17:52:08 +01:00
ffaaa14dba Feature/increase fear and greed index to 30 days (#571)
* Increase Fear & Greed index to 30 days

* Update changelog
2021-12-24 09:40:24 +01:00
c65746d119 Simplify instructions for development setup (#570) 2021-12-22 18:09:00 +01:00
1a6840f1f6 Fix instruction for database setup (#568) 2021-12-21 20:59:01 +01:00
fb7fb886f6 Fix anchor link (#567) 2021-12-21 20:53:15 +01:00
22 changed files with 412 additions and 226 deletions

View File

@ -5,6 +5,45 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.96.0 - 27.12.2021
### 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`
- 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
### Fixed
- Filtered potential `null` currencies
- Improved the 7d data gathering optimization for currencies
## 1.94.0 - 25.12.2021
### 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
- 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 ## 1.93.0 - 21.12.2021
### Added ### Added

View File

@ -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. 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? ## Why Ghostfolio?
@ -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 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 ### b. Build and run environment
Run the following commands to build and start the Docker images: 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 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: Run the following command to setup the database once Ghostfolio is running:
```bash ```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 ### Fetch Historical Data
@ -145,9 +153,7 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat
### Setup ### Setup
1. Run `yarn install` 1. Run `yarn install`
1. Run `cd docker` 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 `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 `yarn database:setup` to initialize the database schema and populate your database with (example) data 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. Start server and client (see [_Development_](#Development))
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` 1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`

View File

@ -22,6 +22,20 @@ function mockGetValue(symbol: string, date: Date) {
switch (symbol) { switch (symbol) {
case 'AMZN': case 'AMZN':
return { marketPrice: 2021.99 }; 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': case 'MFA':
if (isSameDay(parseDate('2010-12-31'), date)) { if (isSameDay(parseDate('2010-12-31'), date)) {
return { marketPrice: 1 }; return { marketPrice: 1 };
@ -45,10 +59,10 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 }; return { marketPrice: 0 };
case 'TSLA': case 'TSLA':
if (isSameDay(parseDate('2021-07-26'), date)) { if (isSameDay(parseDate('2021-01-02'), date)) {
return { marketPrice: 657.62 };
} else if (isSameDay(parseDate('2021-01-02'), date)) {
return { marketPrice: 666.66 }; return { marketPrice: 666.66 };
} else if (isSameDay(parseDate('2021-07-26'), date)) {
return { marketPrice: 657.62 };
} }
return { marketPrice: 0 }; return { marketPrice: 0 };
@ -62,6 +76,7 @@ function mockGetValue(symbol: string, date: Date) {
) )
.toNumber() .toNumber()
}; };
default: default:
return { marketPrice: 0 }; return { marketPrice: 0 };
} }
@ -72,20 +87,10 @@ jest.mock('./current-rate.service', () => {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return { return {
getValue: ({ getValue: ({ date, symbol }: GetValueParams) => {
currency,
date,
symbol,
userCurrency
}: GetValueParams) => {
return Promise.resolve(mockGetValue(symbol, date)); return Promise.resolve(mockGetValue(symbol, date));
}, },
getValues: ({ getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => {
currencies,
dateQuery,
dataGatheringItems,
userCurrency
}: GetValuesParams) => {
const result = []; const result = [];
if (dateQuery.lt) { if (dateQuery.lt) {
for ( for (
@ -1486,6 +1491,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', () => { describe('calculate timeline', () => {

View File

@ -51,7 +51,7 @@ export class PortfolioController {
@Get('investments') @Get('investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async findAll( public async findAll(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Res() res: Response @Res() res: Response
): Promise<InvestmentItem[]> { ): Promise<InvestmentItem[]> {
if ( if (
@ -87,7 +87,7 @@ export class PortfolioController {
@Get('chart') @Get('chart')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getChart( public async getChart(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioChart> { ): Promise<PortfolioChart> {
@ -98,18 +98,14 @@ export class PortfolioController {
let chartData = historicalDataContainer.items; let chartData = historicalDataContainer.items;
let hasNullValue = false; let hasError = false;
chartData.forEach((chartDataItem) => { chartData.forEach((chartDataItem) => {
if (hasNotDefinedValuesInObject(chartDataItem)) { if (hasNotDefinedValuesInObject(chartDataItem)) {
hasNullValue = true; hasError = true;
} }
}); });
if (hasNullValue) {
res.status(StatusCodes.ACCEPTED);
}
if ( if (
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
@ -131,6 +127,7 @@ export class PortfolioController {
} }
return <any>res.json({ return <any>res.json({
hasError,
chart: chartData, chart: chartData,
isAllTimeHigh: historicalDataContainer.isAllTimeHigh, isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
isAllTimeLow: historicalDataContainer.isAllTimeLow isAllTimeLow: historicalDataContainer.isAllTimeLow
@ -140,7 +137,7 @@ export class PortfolioController {
@Get('details') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getDetails( public async getDetails(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioDetails> { ): Promise<PortfolioDetails> {
@ -152,6 +149,8 @@ export class PortfolioController {
return <any>res.json({ accounts: {}, holdings: {} }); return <any>res.json({ accounts: {}, holdings: {} });
} }
let hasError = false;
const { accounts, holdings, hasErrors } = const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails( await this.portfolioService.getDetails(
impersonationId, impersonationId,
@ -160,7 +159,7 @@ export class PortfolioController {
); );
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
res.status(StatusCodes.ACCEPTED); hasError = true;
} }
if ( if (
@ -198,43 +197,38 @@ export class PortfolioController {
} }
} }
return <any>res.json({ accounts, holdings }); return <any>res.json({ accounts, hasError, holdings });
} }
@Get('performance') @Get('performance')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPerformance( public async getPerformance(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioPerformance> { ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
const performanceInformation = await this.portfolioService.getPerformance( const performanceInformation = await this.portfolioService.getPerformance(
impersonationId, impersonationId,
range range
); );
if (performanceInformation?.hasErrors) {
res.status(StatusCodes.ACCEPTED);
}
let performance = performanceInformation.performance;
if ( if (
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
performance = nullifyValuesInObject(performance, [ performanceInformation.performance = nullifyValuesInObject(
'currentGrossPerformance', performanceInformation.performance,
'currentValue' ['currentGrossPerformance', 'currentValue']
]); );
} }
return <any>res.json(performance); return <any>res.json(performanceInformation);
} }
@Get('positions') @Get('positions')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPositions( public async getPositions(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioPositions> { ): Promise<PortfolioPositions> {
@ -243,10 +237,6 @@ export class PortfolioController {
range range
); );
if (result?.hasErrors) {
res.status(StatusCodes.ACCEPTED);
}
if ( if (
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
@ -353,7 +343,7 @@ export class PortfolioController {
@Get('position/:symbol') @Get('position/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPosition( public async getPosition(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Param('symbol') symbol @Param('symbol') symbol
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
let position = await this.portfolioService.getPosition( let position = await this.portfolioService.getPosition(
@ -387,7 +377,7 @@ export class PortfolioController {
@Get('report') @Get('report')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getReport( public async getReport(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId: string,
@Res() res: Response @Res() res: Response
): Promise<PortfolioReport> { ): Promise<PortfolioReport> {
if ( if (

View File

@ -8,7 +8,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data.service'
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
import { LookupItem } from './interfaces/lookup-item.interface'; import { LookupItem } from './interfaces/lookup-item.interface';
@ -36,17 +36,17 @@ export class SymbolService {
let historicalData: HistoricalDataItem[]; let historicalData: HistoricalDataItem[];
if (includeHistoricalData) { if (includeHistoricalData) {
const days = 10; const days = 30;
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) }, dateQuery: { gte: subDays(new Date(), days) },
symbols: [dataGatheringItem.symbol] symbols: [dataGatheringItem.symbol]
}); });
historicalData = marketData.map(({ date, marketPrice }) => { historicalData = marketData.map(({ date, marketPrice: value }) => {
return { return {
date: date.toISOString(), value,
value: marketPrice date: date.toISOString()
}; };
}); });
} }

View File

@ -1,7 +1,9 @@
{ {
"1INCH": "1inch", "1INCH": "1inch",
"ALGO": "Algorand", "ALGO": "Algorand",
"ATOM": "Cosmos",
"AVAX": "Avalanche", "AVAX": "Avalanche",
"DOT": "Polkadot",
"MATIC": "Polygon", "MATIC": "Polygon",
"SHIB": "Shiba Inu", "SHIB": "Shiba Inu",
"SOL": "Solana", "SOL": "Solana",

View File

@ -1,12 +1,11 @@
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { import {
PROPERTY_LAST_DATA_GATHERING, PROPERTY_LAST_DATA_GATHERING,
PROPERTY_LOCKED_DATA_GATHERING, PROPERTY_LOCKED_DATA_GATHERING
ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { import {
differenceInHours, differenceInHours,
format, format,
@ -17,7 +16,6 @@ import {
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider/data-provider.service'; import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from './exchange-rate-data.service'; import { ExchangeRateDataService } from './exchange-rate-data.service';
@ -29,7 +27,6 @@ export class DataGatheringService {
private dataGatheringProgress: number; private dataGatheringProgress: number;
public constructor( public constructor(
private readonly configurationService: ConfigurationService,
@Inject('DataEnhancers') @Inject('DataEnhancers')
private readonly dataEnhancers: DataEnhancerInterface[], private readonly dataEnhancers: DataEnhancerInterface[],
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
@ -245,7 +242,7 @@ export class DataGatheringService {
try { try {
currentData[symbol] = await dataEnhancer.enhance({ currentData[symbol] = await dataEnhancer.enhance({
response, response,
symbol: symbolMapping[dataEnhancer.getName()] ?? symbol symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
}); });
} catch (error) { } catch (error) {
Logger.error(`Failed to enhance data for symbol ${symbol}`, error); Logger.error(`Failed to enhance data for symbol ${symbol}`, error);
@ -337,16 +334,25 @@ export class DataGatheringService {
?.marketPrice; ?.marketPrice;
} }
try { if (lastMarketPrice) {
await this.prismaService.marketData.create({ try {
data: { await this.prismaService.marketData.create({
dataSource, data: {
symbol, dataSource,
date: currentDate, symbol,
marketPrice: lastMarketPrice date: currentDate,
} marketPrice: lastMarketPrice
}); }
} catch {} });
} catch {}
} else {
Logger.warn(
`Failed to gather data for symbol ${symbol} at ${format(
currentDate,
DATE_FORMAT
)}.`
);
}
// Count month one up for iteration // Count month one up for iteration
currentDate = new Date( currentDate = new Date(
@ -448,11 +454,7 @@ export class DataGatheringService {
}; };
}); });
return [ return [...currencyPairsToGather, ...symbolProfilesToGather];
...this.getBenchmarksToGather(startDate),
...currencyPairsToGather,
...symbolProfilesToGather
];
} }
public async reset() { public async reset() {
@ -468,23 +470,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<IDataGatheringItem[]> { private async getSymbols7D(): Promise<IDataGatheringItem[]> {
const startDate = subDays(resetHours(new Date()), 7); 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 = ( const symbolProfilesToGather = (
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
@ -494,15 +500,22 @@ export class DataGatheringService {
symbol: true symbol: true
} }
}) })
).map((symbolProfile) => { )
return { .filter(({ symbol }) => {
...symbolProfile, return symbolsToGather.includes(symbol);
date: startDate })
}; .map((symbolProfile) => {
}); return {
...symbolProfile,
date: startDate
};
});
const currencyPairsToGather = this.exchangeRateDataService const currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs() .getCurrencyPairs()
.filter(({ symbol }) => {
return symbolsToGather.includes(symbol);
})
.map(({ dataSource, symbol }) => { .map(({ dataSource, symbol }) => {
return { return {
dataSource, dataSource,
@ -511,30 +524,22 @@ export class DataGatheringService {
}; };
}); });
return [ return [...currencyPairsToGather, ...symbolProfilesToGather];
...this.getBenchmarksToGather(startDate),
...currencyPairsToGather,
...symbolProfilesToGather
];
} }
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> { private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
const startDate = subDays(resetHours(new Date()), 7);
const distinctOrders = await this.prismaService.order.findMany({ const distinctOrders = await this.prismaService.order.findMany({
distinct: ['symbol'], distinct: ['symbol'],
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
select: { dataSource: true, symbol: true } select: { dataSource: true, symbol: true }
}); });
return [...this.getBenchmarksToGather(startDate), ...distinctOrders].filter( return distinctOrders.filter((distinctOrder) => {
(distinctOrder) => { return (
return ( distinctOrder.dataSource !== DataSource.GHOSTFOLIO &&
distinctOrder.dataSource !== DataSource.GHOSTFOLIO && distinctOrder.dataSource !== DataSource.RAKUTEN
distinctOrder.dataSource !== DataSource.RAKUTEN );
); });
}
);
} }
private async isDataGatheringNeeded() { private async isDataGatheringNeeded() {

View File

@ -157,7 +157,12 @@ export class ExchangeRateDataService {
await this.prismaService.account.findMany({ await this.prismaService.account.findMany({
distinct: ['currency'], distinct: ['currency'],
orderBy: [{ currency: 'asc' }], orderBy: [{ currency: 'asc' }],
select: { currency: true } select: { currency: true },
where: {
currency: {
not: null
}
}
}) })
).forEach((account) => { ).forEach((account) => {
currencies.push(account.currency); currencies.push(account.currency);
@ -167,7 +172,12 @@ export class ExchangeRateDataService {
await this.prismaService.settings.findMany({ await this.prismaService.settings.findMany({
distinct: ['currency'], distinct: ['currency'],
orderBy: [{ currency: 'asc' }], orderBy: [{ currency: 'asc' }],
select: { currency: true } select: { currency: true },
where: {
currency: {
not: null
}
}
}) })
).forEach((userSettings) => { ).forEach((userSettings) => {
currencies.push(userSettings.currency); currencies.push(userSettings.currency);
@ -177,7 +187,12 @@ export class ExchangeRateDataService {
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
distinct: ['currency'], distinct: ['currency'],
orderBy: [{ currency: 'asc' }], orderBy: [{ currency: 'asc' }],
select: { currency: true } select: { currency: true },
where: {
currency: {
not: null
}
}
}) })
).forEach((symbolProfile) => { ).forEach((symbolProfile) => {
currencies.push(symbolProfile.currency); currencies.push(symbolProfile.currency);

View File

@ -12,7 +12,7 @@
<div class="no-gutters row w-100"> <div class="no-gutters row w-100">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted"> <div class="mb-2 text-center text-muted">
<small i18n>Last 10 Days</small> <small i18n>Last 30 Days</small>
</div> </div>
<gf-line-chart <gf-line-chart
class="mb-5" class="mb-5"

View File

@ -29,6 +29,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
{ label: 'Max', value: 'max' } { label: 'Max', value: 'max' }
]; ];
public deviceType: string; public deviceType: string;
public hasError: boolean;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean; public isAllTimeHigh: boolean;
@ -116,7 +117,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
.fetchPortfolioPerformance({ range: this.dateRange }) .fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.performance = response; this.hasError = response.hasErrors;
this.performance = response.performance;
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

View File

@ -1,15 +1,5 @@
<div <div
class=" class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
align-items-center
container
d-flex
flex-column
h-100
justify-content-center
overview
p-0
position-relative
"
> >
<div class="row w-100"> <div class="row w-100">
<div class="chart-container col"> <div class="chart-container col">
@ -37,6 +27,8 @@
<gf-portfolio-performance <gf-portfolio-performance
class="pb-4" class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasError]="hasError"
[isAllTimeHigh]="isAllTimeHigh" [isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow" [isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"

View File

@ -1,12 +1,15 @@
<div class="container p-0"> <div class="container p-0">
<div <div class="no-gutters row">
class="no-gutters row" <div
[ngClass]="{ class="flex-grow-1 status text-muted text-right"
'text-danger': isAllTimeLow, [title]="
'text-success': isAllTimeHigh hasError
}" ? 'Sorry! Our data provider partner is experiencing the hiccups.'
> : ''
<div class="flex-grow-1"></div> "
>
<ion-icon *ngIf="hasError" name="alert-circle-outline"></ion-icon>
</div>
<div *ngIf="isLoading" class="align-items-center d-flex"> <div *ngIf="isLoading" class="align-items-center d-flex">
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
@ -20,6 +23,10 @@
<div <div
class="display-4 font-weight-bold m-0 text-center value-container" class="display-4 font-weight-bold m-0 text-center value-container"
[hidden]="isLoading" [hidden]="isLoading"
[ngClass]="{
'text-danger': isAllTimeLow,
'text-success': isAllTimeHigh
}"
> >
<span #value id="value"></span> <span #value id="value"></span>
</div> </div>

View File

@ -1,6 +1,10 @@
:host { :host {
display: block; display: block;
.status {
font-size: 1.33rem;
}
.value-container { .value-container {
#value { #value {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;

View File

@ -19,6 +19,8 @@ import { isNumber } from 'lodash';
}) })
export class PortfolioPerformanceComponent implements OnChanges, OnInit { export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasError: boolean;
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean; @Input() isAllTimeLow: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@ -44,7 +46,11 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
this.unit = this.baseCurrency; this.unit = this.baseCurrency;
new CountUp('value', this.performance?.currentValue, { new CountUp('value', this.performance?.currentValue, {
decimalPlaces: 2, decimalPlaces:
this.deviceType === 'mobile' &&
this.performance?.currentValue >= 100000
? 0
: 2,
duration: 1, duration: 1,
separator: `'` separator: `'`
}).start(); }).start();

View File

@ -4,8 +4,7 @@ import {
HttpEvent, HttpEvent,
HttpHandler, HttpHandler,
HttpInterceptor, HttpInterceptor,
HttpRequest, HttpRequest
HttpResponse
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
@ -43,26 +42,6 @@ export class HttpResponseInterceptor implements HttpInterceptor {
): Observable<HttpEvent<any>> { ): Observable<HttpEvent<any>> {
return next.handle(request).pipe( return next.handle(request).pipe(
tap((event: HttpEvent<any>) => { tap((event: HttpEvent<any>) => {
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; return event;
}), }),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {

View File

@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
const fileContent = readerEvent.target.result as string; const fileContent = readerEvent.target.result as string;
try { try {
if (file.type === 'application/json') { if (file.name.endsWith('.json')) {
const content = JSON.parse(fileContent); const content = JSON.parse(fileContent);
if (!isArray(content.orders)) { if (!isArray(content.orders)) {
@ -203,11 +203,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.handleImportSuccess(); this.handleImportSuccess();
} catch (error) { } catch (error) {
console.error(error);
this.handleImportError({ error, orders: content.orders }); this.handleImportError({ error, orders: content.orders });
} }
return; return;
} else if (file.type === 'text/csv') { } else if (file.name.endsWith('.csv')) {
try { try {
await this.importTransactionsService.importCsv({ await this.importTransactionsService.importCsv({
fileContent, fileContent,
@ -217,6 +218,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.handleImportSuccess(); this.handleImportSuccess();
} catch (error) { } catch (error) {
console.error(error);
this.handleImportError({ this.handleImportError({
error: { error: {
error: { message: error?.error?.message ?? [error?.message] } error: { message: error?.error?.message ?? [error?.message] }
@ -230,6 +232,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
throw new Error(); throw new Error();
} catch (error) { } catch (error) {
console.error(error);
this.handleImportError({ this.handleImportError({
error: { error: { message: ['Unexpected format'] } }, error: { error: { message: ['Unexpected format'] } },
orders: [] orders: []

View File

@ -181,7 +181,10 @@ export class DataService {
} }
public fetchPortfolioPerformance(aParams: { [param: string]: any }) { public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
return this.http.get<PortfolioPerformance>('/api/portfolio/performance', { return this.http.get<{
hasErrors: boolean;
performance: PortfolioPerformance;
}>('/api/portfolio/performance', {
params: aParams params: aParams
}); });
} }

View File

@ -15,8 +15,8 @@ export class ImportTransactionsService {
private static CURRENCY_KEYS = ['ccy', 'currency']; private static CURRENCY_KEYS = ['ccy', 'currency'];
private static DATE_KEYS = ['date']; private static DATE_KEYS = ['date'];
private static FEE_KEYS = ['commission', 'fee']; private static FEE_KEYS = ['commission', 'fee'];
private static QUANTITY_KEYS = ['qty', 'quantity', 'shares']; private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units'];
private static SYMBOL_KEYS = ['code', 'symbol']; private static SYMBOL_KEYS = ['code', 'symbol', 'ticker'];
private static TYPE_KEYS = ['action', 'type']; private static TYPE_KEYS = ['action', 'type'];
private static UNIT_PRICE_KEYS = ['price', 'unitprice', 'value']; private static UNIT_PRICE_KEYS = ['price', 'unitprice', 'value'];

View File

@ -1,6 +1,7 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
export interface PortfolioChart { export interface PortfolioChart {
hasError: boolean;
isAllTimeHigh: boolean; isAllTimeHigh: boolean;
isAllTimeLow: boolean; isAllTimeLow: boolean;
chart: HistoricalDataItem[]; chart: HistoricalDataItem[];

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.93.0", "version": "1.96.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -69,7 +69,7 @@
"@nestjs/schedule": "1.0.2", "@nestjs/schedule": "1.0.2",
"@nestjs/serve-static": "2.2.2", "@nestjs/serve-static": "2.2.2",
"@nrwl/angular": "13.3.0", "@nrwl/angular": "13.3.0",
"@prisma/client": "3.6.0", "@prisma/client": "3.7.0",
"@simplewebauthn/browser": "4.1.0", "@simplewebauthn/browser": "4.1.0",
"@simplewebauthn/server": "4.1.0", "@simplewebauthn/server": "4.1.0",
"@simplewebauthn/typescript-types": "4.0.0", "@simplewebauthn/typescript-types": "4.0.0",
@ -94,18 +94,18 @@
"cryptocurrencies": "7.0.0", "cryptocurrencies": "7.0.0",
"date-fns": "2.22.1", "date-fns": "2.22.1",
"envalid": "7.2.1", "envalid": "7.2.1",
"http-status-codes": "2.1.4", "http-status-codes": "2.2.0",
"ionicons": "5.5.1", "ionicons": "5.5.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"ngx-device-detector": "2.1.1", "ngx-device-detector": "3.0.0",
"ngx-markdown": "12.0.1", "ngx-markdown": "13.0.0",
"ngx-skeleton-loader": "2.9.1", "ngx-skeleton-loader": "2.9.1",
"ngx-stripe": "12.0.2", "ngx-stripe": "13.0.0",
"papaparse": "5.3.1", "papaparse": "5.3.1",
"passport": "0.4.1", "passport": "0.4.1",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "3.6.0", "prisma": "3.7.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"round-to": "5.0.0", "round-to": "5.0.0",
"rxjs": "7.4.0", "rxjs": "7.4.0",
@ -161,7 +161,7 @@
"import-sort-style-module": "6.0.0", "import-sort-style-module": "6.0.0",
"jest": "27.2.3", "jest": "27.2.3",
"jest-preset-angular": "11.0.0", "jest-preset-angular": "11.0.0",
"prettier": "2.3.2", "prettier": "2.5.1",
"replace-in-file": "6.2.0", "replace-in-file": "6.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-jest": "27.0.5", "ts-jest": "27.0.5",

View File

@ -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 { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")

112
yarn.lock
View File

@ -2854,22 +2854,22 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
"@prisma/client@3.6.0": "@prisma/client@3.7.0":
version "3.6.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.6.0.tgz#68a60cd4c73a369b11f72e173e86fd6789939293" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a"
integrity sha512-ycSGY9EZGROtje0iCNsgC5Zqi/ttX2sO7BNMYaLsUMiTlf3F69ZPH+08pRo0hrDfkZzyimXYqeXJlaoYDH1w7A== integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw==
dependencies: 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": "@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f":
version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#25aa447776849a774885866b998732b37ec4f4f5" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90"
integrity sha512-vtoO2ys6mSfc8ONTWdcYztKN3GBU1tcKBj0aXObyjzSuGwHFcM/pEA0xF+n1W4/0TAJgfoPX2khNEit6g0jtNA== integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg==
"@prisma/engines@3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727": "@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f":
version "3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727.tgz#c68ede6aeffa9ef7743a32cfa6daf9172a4e15b3" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2"
integrity sha512-dRClHS7DsTVchDKzeG72OaEyeDskCv91pnZ72Fftn0mp4BkUvX2LvWup65hCNzwwQm5IDd6A88APldKDnMiEMA== integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg==
"@samverschueren/stream-to-observable@^0.3.0": "@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1" version "0.3.1"
@ -6688,11 +6688,16 @@ commander@^5.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^6.0.0, commander@^6.2.1: commander@^6.2.1:
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== 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: common-tags@^1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" 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" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-toolkit@^6.5.0: emoji-toolkit@^6.6.0:
version "6.6.0" version "6.6.0"
resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1" resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1"
integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ== integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ==
@ -10027,10 +10032,10 @@ http-signature@~1.2.0:
jsprim "^1.2.2" jsprim "^1.2.2"
sshpk "^1.7.0" sshpk "^1.7.0"
http-status-codes@2.1.4: http-status-codes@2.2.0:
version "2.1.4" version "2.2.0"
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be"
integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==
https-browserify@^1.0.0: https-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
@ -11859,12 +11864,12 @@ karma-source-map-support@1.4.0:
dependencies: dependencies:
source-map-support "^0.5.5" source-map-support "^0.5.5"
katex@^0.13.0: katex@^0.15.1:
version "0.13.18" version "0.15.1"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931" resolved "https://registry.yarnpkg.com/katex/-/katex-0.15.1.tgz#cf4ce2fa1257c3279cc7a7fe0c8d1fab40800893"
integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA== integrity sha512-KIk+gizli0gl1XaJlCYS8/donGMbzXYTka6BbH3AgvDJTOwyDY4hJ+YmzJ1F0y/3XzX5B9ED8AqB2Hmn2AZ0uA==
dependencies: dependencies:
commander "^6.0.0" commander "^8.0.0"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2" 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" resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
ngx-device-detector@2.1.1: ngx-device-detector@3.0.0:
version "2.1.1" version "3.0.0"
resolved "https://registry.yarnpkg.com/ngx-device-detector/-/ngx-device-detector-2.1.1.tgz#a22a9477f382d02edf28786c5609878a57d2834f" resolved "https://registry.yarnpkg.com/ngx-device-detector/-/ngx-device-detector-3.0.0.tgz#9c5b1db66e03837d5de0e93fe4a1de93948c9c81"
integrity sha512-eTuQLAmc2XRRbxDnO9h1QVV0piSyPjstXT5G8fo1rvXy7Ly3MAiniEM2WvTiN7FjtY/VdhEeuBmu/ErSm5cLJg== integrity sha512-mzegvxnNTDkHTxh+UeWnCUgZ91/XDOcN2kj8aCupvA7wNgDc/NZ0L90feKJsc+wES7IWq0/DIIKq2F732WOkfw==
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
ngx-markdown@12.0.1: ngx-markdown@13.0.0:
version "12.0.1" version "13.0.0"
resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-12.0.1.tgz#94345e99176533c17396f93e97ff5dc172d8ebcc" resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-13.0.0.tgz#07c9ef46db6827290fc533c0ee64d3856e964bfd"
integrity sha512-vMp9SyqmVQZCX374MiCV4sRR1SIv5m3xR2HZ39b3+6/BGjAb46mb4wRXKdIxYUoPba7NYZ8GAt5moUCyVZcCyA== integrity sha512-XIFCoqffGUHoc8mpHphVskFBHck6hUBocyGVHNBznk7dzHdy6+Ir08jECDQa6xhsoU4dTDgo9aofjK+yvzGIXw==
dependencies: dependencies:
"@types/marked" "^2.0.0" "@types/marked" "^2.0.0"
emoji-toolkit "^6.5.0" emoji-toolkit "^6.6.0"
katex "^0.13.0" katex "^0.15.1"
marked "^2.0.0" marked "^2.0.0"
prismjs "^1.23.0" prismjs "^1.25.0"
tslib "^2.1.0" tslib "^2.3.0"
ngx-skeleton-loader@2.9.1: ngx-skeleton-loader@2.9.1:
version "2.9.1" version "2.9.1"
@ -12848,12 +12853,12 @@ ngx-skeleton-loader@2.9.1:
perf-marks "^1.13.4" perf-marks "^1.13.4"
tslib "^1.10.0" tslib "^1.10.0"
ngx-stripe@12.0.2: ngx-stripe@13.0.0:
version "12.0.2" version "13.0.0"
resolved "https://registry.yarnpkg.com/ngx-stripe/-/ngx-stripe-12.0.2.tgz#b250acc2a08dc96dac035fc0a67b4a8cbeca3efb" resolved "https://registry.yarnpkg.com/ngx-stripe/-/ngx-stripe-13.0.0.tgz#d5ed50590447aa74012de4e75ac9bcdafc68b1c8"
integrity sha512-/arfIi996yv3EpzqjYsb20TUdQ9t+GVMNVIx1mdsiWcpiNjL36tO3lG45T0hyiBJNAds87Ag40Fm8PfsuHFCUw== integrity sha512-SImKvoC/mZZrtzh2UUmxFdkqMLKX2y+BtcvMAPdHD4D7miXWEjCTZeXt8h85mcfy7y1NKKwIipH4CSr9eBzZ4w==
dependencies: dependencies:
tslib "^2.1.0" tslib "^2.3.0"
nice-napi@^1.0.2: nice-napi@^1.0.2:
version "1.0.2" version "1.0.2"
@ -14380,10 +14385,10 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier@2.3.2: prettier@2.5.1:
version "2.3.2" version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
prettier@^2.2.1: prettier@^2.2.1:
version "2.4.1" version "2.4.1"
@ -14436,18 +14441,23 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
prisma@3.6.0: prisma@3.7.0:
version "3.6.0" version "3.7.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.6.0.tgz#99532abc02e045e58c6133a19771bdeb28cecdbe" resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2"
integrity sha512-6SqgHS/5Rq6HtHjsWsTxlj+ySamGyCLBUQfotc2lStOjPv52IQuDVpp58GieNqc9VnfuFyHUvTZw7aQB+G2fvQ== integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg==
dependencies: 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: prismjs@^1.21.0, prismjs@~1.24.0:
version "1.24.1" version "1.24.1"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== 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: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"