Browse Source

Merge branch 'ghostfolio:main' into main

pull/985/head
Leon Stoldt 4 years ago
committed by GitHub
parent
commit
1a8667ee0b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      CHANGELOG.md
  2. 2
      apps/api/src/app/access/access.service.ts
  3. 2
      apps/api/src/app/account/account.service.ts
  4. 27
      apps/api/src/app/admin/admin.controller.ts
  5. 2
      apps/api/src/app/admin/admin.module.ts
  6. 16
      apps/api/src/app/admin/admin.service.ts
  7. 2
      apps/api/src/app/auth-device/auth-device.service.ts
  8. 3
      apps/api/src/app/info/info.service.ts
  9. 14
      apps/api/src/app/order/order.service.ts
  10. 2
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  11. 2
      apps/api/src/app/symbol/interfaces/symbol-item.interface.ts
  12. 11
      apps/api/src/app/symbol/symbol.controller.ts
  13. 8
      apps/api/src/app/symbol/symbol.module.ts
  14. 31
      apps/api/src/app/symbol/symbol.service.ts
  15. 2
      apps/api/src/app/user/user.service.ts
  16. 3
      apps/api/src/services/configuration.service.ts
  17. 45
      apps/api/src/services/data-gathering.service.ts
  18. 6
      apps/api/src/services/data-provider/data-provider.service.ts
  19. 3
      apps/api/src/services/exchange-rate-data.module.ts
  20. 16
      apps/api/src/services/exchange-rate-data.service.ts
  21. 2
      apps/api/src/services/market-data.service.ts
  22. 6
      apps/api/src/services/property/property.dto.ts
  23. 11
      apps/api/src/services/property/property.module.ts
  24. 43
      apps/api/src/services/property/property.service.ts
  25. 7
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html
  26. 5
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss
  27. 9
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
  28. 42
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  29. 94
      apps/client/src/app/components/admin-overview/admin-overview.html
  30. 6
      apps/client/src/app/components/admin-overview/admin-overview.scss
  31. 6
      apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html
  32. 13
      apps/client/src/app/components/home-market/home-market.component.ts
  33. 29
      apps/client/src/app/components/home-market/home-market.html
  34. 4
      apps/client/src/app/components/home-market/home-market.module.ts
  35. 4
      apps/client/src/app/components/home-market/home-market.scss
  36. 12
      apps/client/src/app/services/data.service.ts
  37. 5
      libs/common/src/lib/config.ts
  38. 3
      libs/common/src/lib/interfaces/admin-data.interface.ts
  39. 17
      libs/ui/src/lib/line-chart/line-chart.component.ts
  40. 6
      package.json
  41. 36
      yarn.lock

20
CHANGELOG.md

@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- 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
- 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)
- Enabled the import functionality for transactions by default
- Converted the symbols to uppercase to avoid case-sensitive duplicates in the symbol profile model
### Fixed
- Improved the allocations by currency in combination with cash balances

2
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<AccessWithGranteeUser[]> {
const { include, skip, take, cursor, where, orderBy } = params;

2
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[];

27
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);
}
}

2
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],

16
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();

2
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<AuthDevice[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prismaService.authDevice.findMany({

3
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) {

14
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<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
@ -45,19 +45,22 @@ export class OrderService {
public async createOrder(data: Prisma.OrderCreateInput): Promise<Order> {
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: <Date>data.date,
symbol: data.symbol
date: <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
}
});
}

2
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();

2
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;
}

11
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<SymbolItem> {
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(

8
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]
})

31
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<SymbolItem> {
public async get({
dataGatheringItem,
includeHistoricalData = false
}: {
dataGatheringItem: IDataGatheringItem;
includeHistoricalData?: boolean;
}): Promise<SymbolItem> {
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
};

2
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<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prismaService.user.findMany({

3
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 }),

45
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);

6
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;

3
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]
})

16
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<string[]> {
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();
}

2
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<MarketData[]> {
const { skip, take, cursor, where, orderBy } = params;

6
apps/api/src/services/property/property.dto.ts

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class PropertyDto {
@IsString()
value: string;
}

11
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 {}

43
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 }
});
}
}

7
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] &&

5
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);
}

9
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: {

42
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);
});
}
}

94
apps/client/src/app/components/admin-overview/admin-overview.html

@ -3,32 +3,17 @@
<div class="col">
<mat-card class="mb-3">
<mat-card-content>
<div
*ngIf="exchangeRates?.length > 0"
class="align-items-start d-flex my-3"
>
<div class="w-50" i18n>Exchange Rates</div>
<div class="d-flex my-3">
<div class="w-50" i18n>User Count</div>
<div class="w-50">{{ userCount }}</div>
</div>
<div class="d-flex my-3">
<div class="w-50" i18n>Transaction Count</div>
<div class="w-50">
<table>
<tr *ngFor="let exchangeRate of exchangeRates">
<td class="d-flex">
<gf-value
[locale]="user?.settings?.locale"
[value]="1"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td>
<td class="d-flex justify-content-end">
<gf-value
[locale]="user?.settings?.locale"
[precision]="4"
[value]="exchangeRate.value"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label2 }}</td>
</tr>
</table>
<ng-container *ngIf="transactionCount">
{{ transactionCount }} ({{ transactionCount / userCount | number
: '1.2-2' }} <span i18n>per User</span>)
</ng-container>
</div>
</div>
<div class="d-flex my-3">
@ -46,7 +31,6 @@
<div class="mt-2 overflow-hidden">
<div class="mb-2">
<button
class="mw-100"
color="accent"
mat-flat-button
(click)="onFlushCache()"
@ -60,7 +44,6 @@
</div>
<div class="mb-2">
<button
class="mw-100"
color="warn"
mat-flat-button
[disabled]="dataGatheringInProgress"
@ -72,9 +55,10 @@
</div>
<div>
<button
class="mb-2 mr-2 mw-100"
class="mb-2 mr-2"
color="accent"
mat-flat-button
[disabled]="dataGatheringInProgress"
(click)="onGatherProfileData()"
>
<ion-icon
@ -87,17 +71,51 @@
</div>
</div>
</div>
<div class="d-flex my-3">
<div class="w-50" i18n>User Count</div>
<div class="w-50">{{ userCount }}</div>
</div>
<div class="d-flex my-3">
<div class="w-50" i18n>Transaction Count</div>
<div class="align-items-start d-flex my-3">
<div class="w-50" i18n>Exchange Rates</div>
<div class="w-50">
<ng-container *ngIf="transactionCount">
{{ transactionCount }} ({{ transactionCount / userCount | number
: '1.2-2' }} <span i18n>per User</span>)
</ng-container>
<table>
<tr *ngFor="let exchangeRate of exchangeRates">
<td class="d-flex">
<gf-value
[locale]="user?.settings?.locale"
[value]="1"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td>
<td class="d-flex justify-content-end">
<gf-value
[locale]="user?.settings?.locale"
[precision]="4"
[value]="exchangeRate.value"
></gf-value>
</td>
<td class="pl-1">{{ exchangeRate.label2 }}</td>
<td>
<button
*ngIf="customCurrencies.includes(exchangeRate.label2)"
class="mini-icon mx-1 no-min-width px-2"
mat-button
[disabled]="dataGatheringInProgress"
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<ion-icon name="trash-outline"></ion-icon>
</button>
</td>
</tr>
</table>
<div class="mt-2">
<button
color="primary"
mat-flat-button
[disabled]="dataGatheringInProgress"
(click)="onAddCurrency()"
>
<ion-icon class="mr-1" name="add-outline"></ion-icon>
<span i18n>Add Currency</span>
</button>
</div>
</div>
</div>
</mat-card-content>

6
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 {

6
apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html

@ -1,13 +1,13 @@
<div class="align-items-center d-flex flex-row">
<div class="h3 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div>
<div class="h3 mb-0">
<div class="h4 mb-0">
<span class="mr-2">{{ fearAndGreedIndexText }}</span>
<small class="text-muted"
><strong>{{ fearAndGreedIndex }}</strong
>/100</small
>
</div>
<small class="d-block" i18n>Market Mood</small>
<small class="d-block" i18n>Current Market Mood</small>
</div>
</div>

13
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();

29
apps/client/src/app/components/home-market/home-market.html

@ -9,17 +9,26 @@
w-100
"
>
<div class="row w-100">
<div class="no-gutters row w-100">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-content>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</mat-card-content>
</mat-card>
<div class="mb-2 text-center text-muted">
<small i18n>Last 7 Days</small>
</div>
<gf-line-chart
class="mb-5"
yMax="100"
yMaxLabel="Greed"
yMin="0"
yMinLabel="Fear"
[historicalDataItems]="historicalData"
[showXAxis]="true"
[showYAxis]="true"
></gf-line-chart>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</div>
</div>
</div>

4
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]
})

4
apps/client/src/app/components/home-market/home-market.scss

@ -2,4 +2,8 @@
:host {
display: block;
gf-line-chart {
aspect-ratio: 16 / 9;
}
}

12
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,
@ -127,12 +128,16 @@ export class DataService {
public fetchSymbolItem({
dataSource,
includeHistoricalData = false,
symbol
}: {
dataSource: DataSource;
includeHistoricalData?: boolean;
symbol: string;
}) {
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`);
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`, {
params: { includeHistoricalData }
});
}
public fetchPositions({
@ -235,6 +240,11 @@ export class DataService {
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
}
public putAdminSetting(key: string, aData: PropertyDto) {
console.log(key, aData);
return this.http.put<void>(`/api/admin/settings/${key}`, aData);
}
public putOrder(aOrder: UpdateOrderDto) {
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
}

5
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';

3
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: {

17
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);
}

6
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": {
@ -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",

36
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"

Loading…
Cancel
Save