Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 1 week ago
parent
commit
bd22dcbfd1
  1. 10
      CHANGELOG.md
  2. 1
      apps/api/src/app/app.module.ts
  3. 4
      apps/api/src/app/asset/asset.controller.ts
  4. 6
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 4
      apps/api/src/app/exchange-rate/exchange-rate.controller.ts
  6. 4
      apps/api/src/app/export/export.controller.ts
  7. 4
      apps/api/src/app/export/export.service.ts
  8. 4
      apps/api/src/app/info/info.controller.ts
  9. 5
      apps/api/src/app/order/order.controller.ts
  10. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  11. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  12. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  13. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  14. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  15. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  16. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  17. 4
      apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts
  18. 4
      apps/api/src/app/symbol/symbol.controller.ts
  19. 10
      apps/api/src/app/symbol/symbol.service.ts
  20. 1
      apps/api/src/models/interfaces/rule-settings.interface.ts
  21. 7
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  22. 3
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  23. 7
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  24. 7
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  25. 7
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  26. 7
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  27. 7
      apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
  28. 7
      apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
  29. 7
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  30. 7
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  31. 7
      apps/api/src/models/rules/liquidity/buying-power.ts
  32. 7
      apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts
  33. 7
      apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts
  34. 7
      apps/api/src/models/rules/regional-market-cluster-risk/europe.ts
  35. 7
      apps/api/src/models/rules/regional-market-cluster-risk/japan.ts
  36. 7
      apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
  37. 14
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  38. 2
      apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts
  39. 12
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  40. 24
      apps/api/src/services/data-provider/data-provider.service.ts
  41. 14
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  42. 14
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  43. 14
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  44. 12
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  45. 10
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  46. 12
      apps/api/src/services/data-provider/manual/manual.service.ts
  47. 2
      apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts
  48. 8
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  49. 14
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  50. 4
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  51. 6
      apps/api/src/services/interfaces/interfaces.ts
  52. 4
      apps/api/src/services/market-data/market-data.service.ts
  53. 4
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  54. 12
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  55. 2
      apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts
  56. 6
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  57. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts
  58. 4
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts
  59. 57
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  60. 2
      apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts
  61. 4
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  62. 4
      apps/client/src/app/components/rule/rule.component.ts
  63. 4
      apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts
  64. 2
      apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts
  65. 4
      apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts
  66. 2
      apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts
  67. 6
      apps/client/src/app/core/notification/interfaces/interfaces.ts
  68. 12
      apps/client/src/app/core/notification/notification.service.ts
  69. 4
      apps/client/src/app/pages/resources/glossary/resources-glossary.component.html
  70. 4
      apps/client/src/app/services/admin.service.ts
  71. 19
      apps/client/src/app/services/data.service.ts
  72. 4
      apps/client/src/app/services/ics/ics.service.ts
  73. 4
      apps/client/src/main.ts
  74. 10
      libs/common/src/lib/interfaces/index.ts
  75. 3
      libs/common/src/lib/interfaces/responses/activity-response.interface.ts
  76. 3
      libs/common/src/lib/interfaces/responses/asset-response.interface.ts
  77. 4
      libs/common/src/lib/interfaces/responses/dividends-response.interface.ts
  78. 6
      libs/common/src/lib/interfaces/responses/export-response.interface.ts
  79. 4
      libs/common/src/lib/interfaces/responses/historical-response.interface.ts
  80. 3
      libs/common/src/lib/interfaces/responses/info-response.interface.ts
  81. 4
      libs/common/src/lib/interfaces/responses/quotes-response.interface.ts
  82. 9
      libs/common/src/lib/interfaces/user.interface.ts
  83. 8
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  84. 163
      libs/ui/src/lib/assistant/assistant.component.ts
  85. 170
      libs/ui/src/lib/assistant/assistant.html
  86. 26
      libs/ui/src/lib/assistant/interfaces/interfaces.ts
  87. 2
      libs/ui/src/lib/portfolio-filter-form/index.ts
  88. 1
      libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts
  89. 8
      libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts
  90. 75
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html
  91. 3
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss
  92. 79
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts
  93. 177
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts
  94. 80
      package-lock.json
  95. 2
      package.json
  96. 5
      prisma.config.ts

10
CHANGELOG.md

@ -14,9 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extracted the portfolio filter form of the assistant to a reusable component
- Formatted the holdings table in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)
- Formatted the holdings table in the _Copy portfolio data to clipboard for AI prompt_ action of the analysis page (experimental)
- Formatted the holdings table in the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental)
- Reverted the explicit configuration of the _Redis_ address family in the job queue module
- Improved the language localization for German (`de`)
- Upgraded `ioredis` from version `5.6.1` to `5.8.2`
### Fixed
- Fixed the enter key press to submit the form of the login with access token dialog
- Fixed an issue in the database seeding process caused by unresolved environment variables in `DATABASE_URL`
## 2.209.0 - 2025-10-18

1
apps/api/src/app/app.module.ts

@ -71,7 +71,6 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
family: 0,
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD,
port: parseInt(process.env.REDIS_PORT ?? '6379', 10)

4
apps/api/src/app/asset/asset.controller.ts

@ -1,7 +1,7 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import type { AssetResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class AssetController {
public async getAsset(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
): Promise<AssetResponse> {
const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol });

6
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -8,7 +8,7 @@ import {
GetQuotesParams,
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
@ -114,7 +114,7 @@ export class GhostfolioService {
try {
const promises: Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}>[] = [];
for (const dataProviderService of this.getDataProviderServices()) {
@ -156,7 +156,7 @@ export class GhostfolioService {
try {
const promises: Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>[] = [];
for (const dataProviderService of this.getDataProviderServices()) {

4
apps/api/src/app/exchange-rate/exchange-rate.controller.ts

@ -1,5 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import {
Controller,
@ -25,7 +25,7 @@ export class ExchangeRateController {
public async getExchangeRate(
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({

4
apps/api/src/app/export/export.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -35,7 +35,7 @@ export class ExportController {
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise<Export> {
): Promise<ExportResponse> {
const activityIds = filterByActivityIds?.split(',') ?? [];
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,

4
apps/api/src/app/export/export.service.ts

@ -3,7 +3,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { Filter, Export } from '@ghostfolio/common/interfaces';
import { ExportResponse, Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { Platform, Prisma } from '@prisma/client';
@ -28,7 +28,7 @@ export class ExportService {
filters?: Filter[];
userCurrency: string;
userId: string;
}): Promise<Export> {
}): Promise<ExportResponse> {
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
return type;
});

4
apps/api/src/app/info/info.controller.ts

@ -1,5 +1,5 @@
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { InfoResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
@ -11,7 +11,7 @@ export class InfoController {
@Get()
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getInfo(): Promise<InfoItem> {
public async getInfo(): Promise<InfoResponse> {
return this.infoService.get();
}
}

5
apps/api/src/app/order/order.controller.ts

@ -11,6 +11,7 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import { ActivityResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
@ -36,7 +37,7 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto';
import { Activities, Activity } from './interfaces/activities.interface';
import { Activities } from './interfaces/activities.interface';
import { OrderService } from './order.service';
import { UpdateOrderDto } from './update-order.dto';
@ -157,7 +158,7 @@ export class OrderController {
public async getOrderById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string
): Promise<Activity> {
): Promise<ActivityResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.settings.settings.baseCurrency;

4
apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts

@ -1,4 +1,4 @@
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { readFileSync } from 'node:fs';
@ -39,6 +39,6 @@ export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
};
export function loadExportFile(filePath: string): Export {
export function loadExportFile(filePath: string): ExportResponse {
return JSON.parse(readFileSync(filePath, 'utf8'));
}

4
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -9,7 +9,7 @@ import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
@ -193,7 +193,7 @@ export abstract class PortfolioCalculator {
}
const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null;
let totalInterestWithCurrencyEffect = new Big(0);

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -15,7 +15,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
@ -52,7 +52,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
});
describe('PortfolioCalculator', () => {
let exportResponse: Export;
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;

4
apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

@ -1,8 +1,8 @@
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DateQuery } from './date-query.interface';
export interface GetValuesParams {
dataGatheringItems: IDataGatheringItem[];
dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery;
}

4
apps/api/src/app/symbol/symbol.controller.ts

@ -1,7 +1,7 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { LookupResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -97,7 +97,7 @@ export class SymbolController {
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
): Promise<DataProviderHistoricalResponse> {
const date = parseISO(dateString);
if (!isDate(date)) {

10
apps/api/src/app/symbol/symbol.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse
DataGatheringItem,
DataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -27,7 +27,7 @@ export class SymbolService {
dataGatheringItem,
includeHistoricalData
}: {
dataGatheringItem: IDataGatheringItem;
dataGatheringItem: DataGatheringItem;
includeHistoricalData?: number;
}): Promise<SymbolItem> {
const quotes = await this.dataProviderService.getQuotes({
@ -75,10 +75,10 @@ export class SymbolService {
dataSource,
date = new Date(),
symbol
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
}: DataGatheringItem): Promise<DataProviderHistoricalResponse> {
let historicalData: {
[symbol: string]: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
} = {
[symbol]: {}

1
apps/api/src/models/interfaces/rule-settings.interface.ts

@ -1,3 +1,4 @@
export interface RuleSettings {
isActive: boolean;
locale: string;
}

7
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -121,9 +121,14 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
};

3
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -72,8 +72,9 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
});
}
public getSettings({ xRayRules }: UserSettings): RuleSettings {
public getSettings({ locale, xRayRules }: UserSettings): RuleSettings {
return {
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true
};
}

7
apps/api/src/models/rules/asset-class-cluster-risk/equity.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.82,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.78

7
apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts

@ -109,9 +109,14 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.22,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.18

7
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -97,9 +97,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true
};
}

7
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -98,9 +98,14 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5
};

7
apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.72,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68

7
apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts

@ -104,9 +104,14 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28

7
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -59,9 +59,14 @@ export class EmergencyFundSetup extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true
};
}

7
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -82,9 +82,14 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01
};

7
apps/api/src/models/rules/liquidity/buying-power.ts

@ -86,9 +86,14 @@ export class BuyingPower extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0
};

7
apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02

7
apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts

@ -96,9 +96,14 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08

7
apps/api/src/models/rules/regional-market-cluster-risk/europe.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11

7
apps/api/src/models/rules/regional-market-cluster-risk/japan.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04

7
apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts

@ -94,9 +94,14 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
public getSettings({
baseCurrency,
locale,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true,
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65

14
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -23,7 +23,7 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage';
import { format, isAfter, isBefore, parse } from 'date-fns';
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
import { AlphaVantageHistoricalResponse } from './interfaces/interfaces';
@Injectable()
export class AlphaVantageService implements DataProviderInterface {
@ -68,11 +68,11 @@ export class AlphaVantageService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const historicalData: {
[symbol: string]: IAlphaVantageHistoricalResponse[];
[symbol: string]: AlphaVantageHistoricalResponse[];
} = await this.alphaVantage.crypto.daily(
symbol
.substring(0, symbol.length - DEFAULT_CURRENCY.length)
@ -81,7 +81,7 @@ export class AlphaVantageService implements DataProviderInterface {
);
const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
response[symbol] = {};
@ -115,7 +115,7 @@ export class AlphaVantageService implements DataProviderInterface {
}
public async getQuotes({}: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
}> {
return {};
}

2
apps/api/src/services/data-provider/alpha-vantage/interfaces/interfaces.ts

@ -1 +1 @@
export interface IAlphaVantageHistoricalResponse {}
export interface AlphaVantageHistoricalResponse {}

12
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -109,7 +109,7 @@ export class CoinGeckoService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const { error, prices, status } = await fetch(
@ -133,7 +133,7 @@ export class CoinGeckoService implements DataProviderInterface {
}
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {
[symbol]: {}
};
@ -166,8 +166,8 @@ export class CoinGeckoService implements DataProviderInterface {
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

24
apps/api/src/services/data-provider/data-provider.service.ts

@ -2,8 +2,8 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -215,10 +215,10 @@ export class DataProviderService implements OnModuleInit {
from: Date,
to: Date
): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
let response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
if (isEmpty(aItems) || !isValid(from) || !isValid(to)) {
@ -284,7 +284,7 @@ export class DataProviderService implements OnModuleInit {
from: Date;
to: Date;
}): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if (
@ -317,11 +317,11 @@ export class DataProviderService implements OnModuleInit {
);
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
const promises: Promise<{
data: { [date: string]: IDataProviderHistoricalResponse };
data: { [date: string]: DataProviderHistoricalResponse };
symbol: string;
}>[] = [];
for (const { dataSource, symbol } of assetProfileIdentifiers) {
@ -329,7 +329,7 @@ export class DataProviderService implements OnModuleInit {
if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) {
const data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
for (const date of eachDayOfInterval({ end: to, start: from })) {
@ -399,10 +399,10 @@ export class DataProviderService implements OnModuleInit {
useCache?: boolean;
user?: UserWithSettings;
}): Promise<{
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
}> {
const response: {
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
} = {};
const startTimeTotal = performance.now();
@ -716,7 +716,7 @@ export class DataProviderService implements OnModuleInit {
}: {
allData: {
data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
symbol: string;
}[];
@ -728,7 +728,7 @@ export class DataProviderService implements OnModuleInit {
})?.data;
const data: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
for (const date in rootData) {

14
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -89,7 +89,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}> {
symbol = this.convertToEodSymbol(symbol);
@ -99,7 +99,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
try {
const response: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
const historicalResult = await fetch(
@ -141,7 +141,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
symbol = this.convertToEodSymbol(symbol);
@ -198,8 +198,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

14
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import {
@ -245,7 +245,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
try {
const response: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
const dividends = await fetch(
@ -289,11 +289,11 @@ export class FinancialModelingPrepService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
const MAX_YEARS_PER_REQUEST = 5;
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {
[symbol]: {}
};
@ -353,8 +353,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

14
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -9,8 +9,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
@ -111,10 +111,10 @@ export class GhostfolioService implements DataProviderInterface {
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}> {
let dividends: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
try {
@ -164,7 +164,7 @@ export class GhostfolioService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const response = await fetch(
@ -228,9 +228,9 @@ export class GhostfolioService implements DataProviderInterface {
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{
[symbol: string]: IDataProviderResponse;
[symbol: string]: DataProviderResponse;
}> {
let quotes: { [symbol: string]: IDataProviderResponse } = {};
let quotes: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return quotes;

12
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -60,7 +60,7 @@ export class GoogleSheetsService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const sheet = await this.getSheet({
@ -71,7 +71,7 @@ export class GoogleSheetsService implements DataProviderInterface {
const rows = await sheet.getRows();
const historicalData: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
rows
@ -104,8 +104,8 @@ export class GoogleSheetsService implements DataProviderInterface {
public async getQuotes({
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

10
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -1,6 +1,6 @@
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import {
DataProviderInfo,
@ -26,7 +26,7 @@ export interface DataProviderInterface {
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
}>;
getHistorical({
@ -36,7 +36,7 @@ export interface DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>; // TODO: Return only one symbol
getMaxNumberOfSymbolsPerRequest?(): number;
@ -46,7 +46,7 @@ export interface DataProviderInterface {
getQuotes({
requestTimeout,
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }>;
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }>;
getTestSymbol(): string;

12
apps/api/src/services/data-provider/manual/manual.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -77,7 +77,7 @@ export class ManualService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
@ -88,7 +88,7 @@ export class ManualService implements DataProviderInterface {
if (defaultMarketPrice) {
const historical: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {
[symbol]: {}
};
@ -132,8 +132,8 @@ export class ManualService implements DataProviderInterface {
public async getQuotes({
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

2
apps/api/src/services/data-provider/rapid-api/interfaces/interfaces.ts

@ -1 +1 @@
export interface IRapidApiResponse {}
export interface RapidApiResponse {}

8
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -8,8 +8,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import {
ghostfolioFearAndGreedIndexSymbol,
@ -59,7 +59,7 @@ export class RapidApiService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
try {
if (
@ -96,7 +96,7 @@ export class RapidApiService implements DataProviderInterface {
public async getQuotes({
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
if (symbols.length <= 0) {
return {};
}

14
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -10,8 +10,8 @@ import {
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
DataProviderHistoricalResponse,
DataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
@ -96,7 +96,7 @@ export class YahooFinanceService implements DataProviderInterface {
)
);
const response: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
} = {};
for (const historicalItem of historicalResult) {
@ -124,7 +124,7 @@ export class YahooFinanceService implements DataProviderInterface {
symbol,
to
}: GetHistoricalParams): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}> {
if (isSameDay(from, to)) {
to = addDays(to, 1);
@ -145,7 +145,7 @@ export class YahooFinanceService implements DataProviderInterface {
);
const response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
} = {};
response[symbol] = {};
@ -183,8 +183,8 @@ export class YahooFinanceService implements DataProviderInterface {
public async getQuotes({
symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return response;

4
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -1,6 +1,6 @@
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -31,7 +31,7 @@ import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interfa
@Injectable()
export class ExchangeRateDataService {
private currencies: string[] = [];
private currencyPairs: IDataGatheringItem[] = [];
private currencyPairs: DataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {};
public constructor(

6
apps/api/src/services/interfaces/interfaces.ts

@ -6,11 +6,11 @@ import { MarketState } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
export interface IDataProviderHistoricalResponse {
export interface DataProviderHistoricalResponse {
marketPrice: number;
}
export interface IDataProviderResponse {
export interface DataProviderResponse {
currency: string;
dataProviderInfo?: DataProviderInfo;
dataSource: DataSource;
@ -18,6 +18,6 @@ export interface IDataProviderResponse {
marketState: MarketState;
}
export interface IDataGatheringItem extends AssetProfileIdentifier {
export interface DataGatheringItem extends AssetProfileIdentifier {
date?: Date;
}

4
apps/api/src/services/market-data/market-data.service.ts

@ -1,6 +1,6 @@
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -30,7 +30,7 @@ export class MarketDataService {
dataSource,
date = new Date(),
symbol
}: IDataGatheringItem): Promise<MarketData> {
}: DataGatheringItem): Promise<MarketData> {
return await this.prismaService.marketData.findFirst({
where: {
dataSource,

4
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -1,6 +1,6 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -99,7 +99,7 @@ export class DataGatheringProcessor {
),
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME
})
public async gatherHistoricalMarketData(job: Job<IDataGatheringItem>) {
public async gatherHistoricalMarketData(job: Job<DataGatheringItem>) {
const { dataSource, date, symbol } = job.data;
try {

12
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -94,7 +94,7 @@ export class DataGatheringService {
});
}
public async gatherSymbol({ dataSource, date, symbol }: IDataGatheringItem) {
public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) {
await this.marketDataService.deleteMany({ dataSource, symbol });
const dataGatheringItems = (await this.getSymbolsMax())
@ -276,7 +276,7 @@ export class DataGatheringService {
dataGatheringItems,
priority
}: {
dataGatheringItems: IDataGatheringItem[];
dataGatheringItems: DataGatheringItem[];
priority: number;
}) {
await this.addJobsToQueue(
@ -348,7 +348,7 @@ export class DataGatheringService {
});
}
private async getCurrencies7D(): Promise<IDataGatheringItem[]> {
private async getCurrencies7D(): Promise<DataGatheringItem[]> {
const assetProfileIdentifiersWithCompleteMarketData =
await this.getAssetProfileIdentifiersWithCompleteMarketData();
@ -376,7 +376,7 @@ export class DataGatheringService {
withUserSubscription = false
}: {
withUserSubscription?: boolean;
}): Promise<IDataGatheringItem[]> {
}): Promise<DataGatheringItem[]> {
const symbolProfiles =
await this.symbolProfileService.getActiveSymbolProfilesByUserSubscription(
{
@ -407,7 +407,7 @@ export class DataGatheringService {
});
}
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
private async getSymbolsMax(): Promise<DataGatheringItem[]> {
const benchmarkAssetProfileIdMap: { [key: string]: boolean } = {};
(
(await this.propertyService.getByKey<BenchmarkProperty[]>(

2
apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts

@ -1,7 +1,7 @@
import { Filter } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export interface IPortfolioSnapshotQueueJob {
export interface PortfolioSnapshotQueueJob {
calculationType: PerformanceCalculationType;
filters: Filter[];
userCurrency: string;

6
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -16,7 +16,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { Job } from 'bull';
import { addMilliseconds } from 'date-fns';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
@ -37,9 +37,7 @@ export class PortfolioSnapshotProcessor {
),
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME
})
public async calculatePortfolioSnapshot(
job: Job<IPortfolioSnapshotQueueJob>
) {
public async calculatePortfolioSnapshot(job: Job<PortfolioSnapshotQueueJob>) {
try {
const startTime = performance.now();

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts

@ -1,13 +1,13 @@
import { Job, JobOptions } from 'bull';
import { setTimeout } from 'timers/promises';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
export const PortfolioSnapshotServiceMock = {
addJobToQueue({
opts
}: {
data: IPortfolioSnapshotQueueJob;
data: PortfolioSnapshotQueueJob;
name: string;
opts?: JobOptions;
}): Promise<Job<any>> {

4
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts

@ -4,7 +4,7 @@ import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { JobOptions, Queue } from 'bull';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable()
export class PortfolioSnapshotService {
@ -18,7 +18,7 @@ export class PortfolioSnapshotService {
name,
opts
}: {
data: IPortfolioSnapshotQueueJob;
data: PortfolioSnapshotQueueJob;
name: string;
opts?: JobOptions;
}) {

57
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -2,13 +2,14 @@
<div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column">
<form class="w-100" (ngSubmit)="onLoginWithAccessToken()">
<form class="w-100">
<mat-form-field appearance="outline" class="without-hint w-100">
<mat-label i18n>Security Token</mat-label>
<input
matInput
[formControl]="accessTokenFormControl"
[type]="isAccessTokenHidden ? 'password' : 'text'"
(keydown.enter)="onLoginWithAccessToken(); $event.preventDefault()"
/>
<button
mat-button
@ -21,35 +22,37 @@
/>
</button>
</mat-form-field>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</form>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</div>
</div>
<div mat-dialog-actions>
<div class="flex-grow-1">
<mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"

2
apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts

@ -3,7 +3,7 @@ import {
XRayRulesSettings
} from '@ghostfolio/common/interfaces';
export interface IRuleSettingsDialogParams {
export interface RuleSettingsDialogParams {
categoryName: string;
locale: string;
rule: PortfolioReportRule;

4
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -12,7 +12,7 @@ import {
} from '@angular/material/dialog';
import { MatSliderModule } from '@angular/material/slider';
import { IRuleSettingsDialogParams } from './interfaces/interfaces';
import { RuleSettingsDialogParams } from './interfaces/interfaces';
@Component({
imports: [
@ -31,7 +31,7 @@ export class GfRuleSettingsDialogComponent {
public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
public constructor(
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams,
@Inject(MAT_DIALOG_DATA) public data: RuleSettingsDialogParams,
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
) {}
}

4
apps/client/src/app/components/rule/rule.component.ts

@ -31,7 +31,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
import { IRuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { RuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component';
@Component({
@ -83,7 +83,7 @@ export class GfRuleComponent implements OnInit {
rule,
categoryName: this.categoryName,
settings: this.settings
} as IRuleSettingsDialogParams,
} as RuleSettingsDialogParams,
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});

4
apps/client/src/app/core/notification/alert-dialog/alert-dialog.component.ts

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces';
import { AlertDialogParams } from './interfaces/interfaces';
@Component({
imports: [MatButtonModule, MatDialogModule],
@ -17,7 +17,7 @@ export class GfAlertDialogComponent {
public constructor(public dialogRef: MatDialogRef<GfAlertDialogComponent>) {}
public initialize(aParams: IAlertDialogParams) {
public initialize(aParams: AlertDialogParams) {
this.discardLabel = aParams.discardLabel;
this.message = aParams.message;
this.title = aParams.title;

2
apps/client/src/app/core/notification/alert-dialog/interfaces/interfaces.ts

@ -1,4 +1,4 @@
export interface IAlertDialogParams {
export interface AlertDialogParams {
confirmLabel?: string;
discardLabel?: string;
message?: string;

4
apps/client/src/app/core/notification/confirmation-dialog/confirmation-dialog.component.ts

@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces';
import { ConfirmDialogParams } from './interfaces/interfaces';
@Component({
imports: [MatButtonModule, MatDialogModule],
@ -29,7 +29,7 @@ export class GfConfirmationDialogComponent {
}
}
public initialize(aParams: IConfirmDialogParams) {
public initialize(aParams: ConfirmDialogParams) {
this.confirmLabel = aParams.confirmLabel;
this.confirmType = aParams.confirmType;
this.discardLabel = aParams.discardLabel;

2
apps/client/src/app/core/notification/confirmation-dialog/interfaces/interfaces.ts

@ -1,6 +1,6 @@
import { ConfirmationDialogType } from '../confirmation-dialog.type';
export interface IConfirmDialogParams {
export interface ConfirmDialogParams {
confirmLabel?: string;
confirmType: ConfirmationDialogType;
discardLabel?: string;

6
apps/client/src/app/core/notification/interfaces/interfaces.ts

@ -1,13 +1,13 @@
import { ConfirmationDialogType } from '../confirmation-dialog/confirmation-dialog.type';
export interface IAlertParams {
export interface AlertParams {
discardFn?: () => void;
discardLabel?: string;
message?: string;
title: string;
}
export interface IConfirmParams {
export interface ConfirmParams {
confirmFn: () => void;
confirmLabel?: string;
confirmType?: ConfirmationDialogType;
@ -18,7 +18,7 @@ export interface IConfirmParams {
title: string;
}
export interface IPromptParams {
export interface PromptParams {
confirmFn: (value: string) => void;
confirmLabel?: string;
defaultValue?: string;

12
apps/client/src/app/core/notification/notification.service.ts

@ -8,9 +8,9 @@ import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component';
import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { ConfirmationDialogType } from './confirmation-dialog/confirmation-dialog.type';
import {
IAlertParams,
IConfirmParams,
IPromptParams
AlertParams,
ConfirmParams,
PromptParams
} from './interfaces/interfaces';
import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component';
@ -21,7 +21,7 @@ export class NotificationService {
public constructor(private matDialog: MatDialog) {}
public alert(aParams: IAlertParams) {
public alert(aParams: AlertParams) {
if (!aParams.discardLabel) {
aParams.discardLabel = translate('CLOSE');
}
@ -45,7 +45,7 @@ export class NotificationService {
});
}
public confirm(aParams: IConfirmParams) {
public confirm(aParams: ConfirmParams) {
if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('YES');
}
@ -78,7 +78,7 @@ export class NotificationService {
});
}
public prompt(aParams: IPromptParams) {
public prompt(aParams: PromptParams) {
if (!aParams.confirmLabel) {
aParams.confirmLabel = translate('OK');
}

4
apps/client/src/app/pages/resources/glossary/resources-glossary.component.html

@ -105,8 +105,8 @@
<h3 class="h5 mt-0">Personal Finance Tools</h3>
<div class="mb-1">
Personal finance tools are software applications that help
individuals manage their money, track expenses, set budgets,
monitor investments, and make informed financial decisions.
manage your money, track expenses, set budgets, monitor
investments, and make informed financial decisions.
</div>
<div>
<a [routerLink]="routerLinkResourcesPersonalFinanceTools"

4
apps/client/src/app/services/admin.service.ts

@ -1,7 +1,7 @@
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import {
HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN
@ -208,7 +208,7 @@ export class AdminService {
}) {
const url = `/api/v1/symbol/${dataSource}/${symbol}/${dateString}`;
return this.http.get<IDataProviderHistoricalResponse>(url);
return this.http.get<DataProviderHistoricalResponse>(url);
}
public patchAssetProfile(

19
apps/client/src/app/services/data.service.ts

@ -9,17 +9,14 @@ import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import {
Activities,
Activity
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateOwnAccessTokenDto } from '@ghostfolio/api/app/user/update-own-access-token.dto';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
@ -27,13 +24,15 @@ import {
AccessTokenResponse,
AccountBalancesResponse,
AccountsResponse,
ActivityResponse,
AiPromptResponse,
ApiKeyResponse,
AssetProfileIdentifier,
AssetResponse,
BenchmarkMarketDataDetailsResponse,
BenchmarkResponse,
DataProviderHealthResponse,
Export,
ExportResponse,
Filter,
ImportResponse,
InfoItem,
@ -247,7 +246,7 @@ export class DataService {
}
public fetchActivity(aActivityId: string) {
return this.http.get<Activity>(`/api/v1/order/${aActivityId}`).pipe(
return this.http.get<ActivityResponse>(`/api/v1/order/${aActivityId}`).pipe(
map((activity) => {
activity.createdAt = parseISO(activity.createdAt as unknown as string);
activity.date = parseISO(activity.date as unknown as string);
@ -291,7 +290,7 @@ export class DataService {
date: Date;
symbol: string;
}) {
return this.http.get<IDataProviderHistoricalResponse>(
return this.http.get<DataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
);
}
@ -345,7 +344,7 @@ export class DataService {
public fetchAsset({
dataSource,
symbol
}: AssetProfileIdentifier): Observable<MarketDataDetailsResponse> {
}: AssetProfileIdentifier): Observable<AssetResponse> {
return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
map((data) => {
for (const item of data.marketData) {
@ -406,7 +405,7 @@ export class DataService {
params = params.append('activityIds', activityIds.join(','));
}
return this.http.get<Export>('/api/v1/export', {
return this.http.get<ExportResponse>('/api/v1/export', {
params
});
}

4
apps/client/src/app/services/ics/ics.service.ts

@ -1,5 +1,5 @@
import { capitalize } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core';
import { Type } from '@prisma/client';
@ -13,7 +13,7 @@ export class IcsService {
private readonly ICS_LINE_BREAK = '\r\n';
public transformActivitiesToIcsContent(
aActivities: Export['activities']
aActivities: ExportResponse['activities']
): string {
const header = [
'BEGIN:VCALENDAR',

4
apps/client/src/main.ts

@ -1,5 +1,5 @@
import { locale } from '@ghostfolio/common/config';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { InfoResponse } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { enableProdMode } from '@angular/core';
@ -11,7 +11,7 @@ import { environment } from './environments/environment';
(async () => {
const response = await fetch('/api/v1/info');
const info: InfoItem = await response.json();
const info: InfoResponse = await response.json();
const utmSource = window.localStorage.getItem('utm_source') as
| 'ios'
| 'trusted-web-activity';

10
libs/common/src/lib/interfaces/index.ts

@ -15,7 +15,6 @@ import type { Benchmark } from './benchmark.interface';
import type { Coupon } from './coupon.interface';
import type { DataProviderInfo } from './data-provider-info.interface';
import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
import type { Export } from './export.interface';
import type { FilterGroup } from './filter-group.interface';
import type { Filter } from './filter.interface';
import type { FireWealth } from './fire-wealth.interface';
@ -38,8 +37,10 @@ import type { Product } from './product';
import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { AccountsResponse } from './responses/accounts-response.interface';
import type { ActivityResponse } from './responses/activity-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface';
@ -48,8 +49,10 @@ import type { DataProviderGhostfolioStatusResponse } from './responses/data-prov
import type { DataProviderHealthResponse } from './responses/data-provider-health-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface';
import type { ResponseError } from './responses/errors.interface';
import type { ExportResponse } from './responses/export-response.interface';
import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface';
import type { InfoResponse } from './responses/info-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
@ -80,6 +83,7 @@ export {
AccountBalance,
AccountBalancesResponse,
AccountsResponse,
ActivityResponse,
AdminData,
AdminJobs,
AdminMarketData,
@ -90,6 +94,7 @@ export {
ApiKeyResponse,
AssetClassSelectorOption,
AssetProfileIdentifier,
AssetResponse,
Benchmark,
BenchmarkMarketDataDetailsResponse,
BenchmarkProperty,
@ -102,7 +107,7 @@ export {
DataProviderInfo,
DividendsResponse,
EnhancedSymbolProfile,
Export,
ExportResponse,
Filter,
FilterGroup,
FireWealth,
@ -112,6 +117,7 @@ export {
HoldingWithParents,
ImportResponse,
InfoItem,
InfoResponse,
InvestmentItem,
LineChartItem,
LookupItem,

3
libs/common/src/lib/interfaces/responses/activity-response.interface.ts

@ -0,0 +1,3 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
export interface ActivityResponse extends Activity {}

3
libs/common/src/lib/interfaces/responses/asset-response.interface.ts

@ -0,0 +1,3 @@
import type { AdminMarketDataDetails } from '../admin-market-data-details.interface';
export interface AssetResponse extends AdminMarketDataDetails {}

4
libs/common/src/lib/interfaces/responses/dividends-response.interface.ts

@ -1,7 +1,7 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface DividendsResponse {
dividends: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
}

6
libs/common/src/lib/interfaces/export.interface.ts → libs/common/src/lib/interfaces/responses/export-response.interface.ts

@ -7,10 +7,10 @@ import {
Tag
} from '@prisma/client';
import { AccountBalance } from './account-balance.interface';
import { MarketData } from './market-data.interface';
import { AccountBalance } from '../account-balance.interface';
import { MarketData } from '../market-data.interface';
export interface Export {
export interface ExportResponse {
accounts: (Omit<Account, 'createdAt' | 'updatedAt' | 'userId'> & {
balances: AccountBalance[];
})[];

4
libs/common/src/lib/interfaces/responses/historical-response.interface.ts

@ -1,7 +1,7 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface HistoricalResponse {
historicalData: {
[date: string]: IDataProviderHistoricalResponse;
[date: string]: DataProviderHistoricalResponse;
};
}

3
libs/common/src/lib/interfaces/responses/info-response.interface.ts

@ -0,0 +1,3 @@
import { InfoItem } from '../index';
export interface InfoResponse extends InfoItem {}

4
libs/common/src/lib/interfaces/responses/quotes-response.interface.ts

@ -1,5 +1,5 @@
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface QuotesResponse {
quotes: { [symbol: string]: IDataProviderResponse };
quotes: { [symbol: string]: DataProviderResponse };
}

9
libs/common/src/lib/interfaces/user.interface.ts

@ -1,6 +1,9 @@
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import {
AccountWithPlatform,
SubscriptionType
} from '@ghostfolio/common/types';
import { Access, Account, Tag } from '@prisma/client';
import { Access, Tag } from '@prisma/client';
import { SubscriptionOffer } from './subscription-offer.interface';
import { SystemMessage } from './system-message.interface';
@ -9,7 +12,7 @@ import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings
export interface User {
access: Pick<Access, 'alias' | 'id' | 'permissions'>[];
accounts: Account[];
accounts: AccountWithPlatform[];
activitiesCount: number;
dateOfFirstActivity: Date;
id: string;

8
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -18,8 +18,8 @@ import { Params, RouterModule } from '@angular/router';
import { SearchMode } from '../enums/search-mode';
import {
IAssetSearchResultItem,
ISearchResultItem
AssetSearchResultItem,
SearchResultItem
} from '../interfaces/interfaces';
@Component({
@ -37,7 +37,7 @@ export class GfAssistantListItemComponent
return this.hasFocus;
}
@Input() item: ISearchResultItem;
@Input() item: SearchResultItem;
@Output() clicked = new EventEmitter<void>();
@ -86,7 +86,7 @@ export class GfAssistantListItemComponent
this.changeDetectorRef.markForCheck();
}
public isAsset(item: ISearchResultItem): item is IAssetSearchResultItem {
public isAsset(item: SearchResultItem): item is AssetSearchResultItem {
return (
(item.mode === SearchMode.ASSET_PROFILE ||
item.mode === SearchMode.HOLDING) &&

163
libs/ui/src/lib/assistant/assistant.component.ts

@ -1,11 +1,10 @@
import { GfSymbolPipe } from '@ghostfolio/client/pipes/symbol/symbol.pipe';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { DateRange } from '@ghostfolio/common/types';
import { AccountWithPlatform, DateRange } from '@ghostfolio/common/types';
import { FocusKeyManager } from '@angular/cdk/a11y';
import {
@ -25,19 +24,14 @@ import {
ViewChild,
ViewChildren
} from '@angular/core';
import {
FormBuilder,
FormControl,
FormsModule,
ReactiveFormsModule
} from '@angular/forms';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { Account, AssetClass, DataSource } from '@prisma/client';
import { AssetClass, DataSource } from '@prisma/client';
import { differenceInYears } from 'date-fns';
import Fuse from 'fuse.js';
import { addIcons } from 'ionicons';
@ -60,14 +54,17 @@ import {
tap
} from 'rxjs/operators';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { translate } from '../i18n';
import {
GfPortfolioFilterFormComponent,
PortfolioFilterFormValue
} from '../portfolio-filter-form';
import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
import { SearchMode } from './enums/search-mode';
import {
IDateRangeOption,
ISearchResultItem,
ISearchResults
DateRangeOption,
SearchResultItem,
SearchResults
} from './interfaces/interfaces';
@Component({
@ -75,8 +72,7 @@ import {
imports: [
FormsModule,
GfAssistantListItemComponent,
GfEntityLogoComponent,
GfSymbolPipe,
GfPortfolioFilterFormComponent,
IonIcon,
MatButtonModule,
MatFormFieldModule,
@ -141,16 +137,10 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
public accounts: Account[] = [];
public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = [];
public dateRangeFormControl = new FormControl<string>(undefined);
public dateRangeOptions: IDateRangeOption[] = [];
public filterForm = this.formBuilder.group({
account: new FormControl<string>(undefined),
assetClass: new FormControl<string>(undefined),
holding: new FormControl<PortfolioPosition>(undefined),
tag: new FormControl<string>(undefined)
});
public dateRangeOptions: DateRangeOption[] = [];
public holdings: PortfolioPosition[] = [];
public isLoading = {
accounts: false,
@ -160,8 +150,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
};
public isOpen = false;
public placeholder = $localize`Find account, holding or page...`;
public portfolioFilterFormControl = new FormControl<PortfolioFilterFormValue>(
{
account: null,
assetClass: null,
holding: null,
tag: null
}
);
public searchFormControl = new FormControl('');
public searchResults: ISearchResults = {
public searchResults: SearchResults = {
accounts: [],
assetProfiles: [],
holdings: [],
@ -186,8 +184,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private formBuilder: FormBuilder
private dataService: DataService
) {
addIcons({ closeCircleOutline, closeOutline, searchOutline });
}
@ -229,7 +226,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
assetProfiles: [],
holdings: [],
quickLinks: []
} as ISearchResults;
} as SearchResults;
if (!searchTerm) {
return of(results).pipe(
@ -244,8 +241,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
);
}
// Accounts
const accounts$: Observable<Partial<ISearchResults>> =
const accounts$: Observable<Partial<SearchResults>> =
this.searchAccounts(searchTerm).pipe(
map((accounts) => ({
accounts: accounts.slice(
@ -255,7 +251,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})),
catchError((error) => {
console.error('Error fetching accounts for assistant:', error);
return of({ accounts: [] as ISearchResultItem[] });
return of({ accounts: [] as SearchResultItem[] });
}),
tap(() => {
this.isLoading.accounts = false;
@ -263,8 +259,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})
);
// Asset profiles
const assetProfiles$: Observable<Partial<ISearchResults>> = this
const assetProfiles$: Observable<Partial<SearchResults>> = this
.hasPermissionToAccessAdminControl
? this.searchAssetProfiles(searchTerm).pipe(
map((assetProfiles) => ({
@ -278,22 +273,21 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
'Error fetching asset profiles for assistant:',
error
);
return of({ assetProfiles: [] as ISearchResultItem[] });
return of({ assetProfiles: [] as SearchResultItem[] });
}),
tap(() => {
this.isLoading.assetProfiles = false;
this.changeDetectorRef.markForCheck();
})
)
: of({ assetProfiles: [] as ISearchResultItem[] }).pipe(
: of({ assetProfiles: [] as SearchResultItem[] }).pipe(
tap(() => {
this.isLoading.assetProfiles = false;
this.changeDetectorRef.markForCheck();
})
);
// Holdings
const holdings$: Observable<Partial<ISearchResults>> =
const holdings$: Observable<Partial<SearchResults>> =
this.searchHoldings(searchTerm).pipe(
map((holdings) => ({
holdings: holdings.slice(
@ -303,7 +297,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})),
catchError((error) => {
console.error('Error fetching holdings for assistant:', error);
return of({ holdings: [] as ISearchResultItem[] });
return of({ holdings: [] as SearchResultItem[] });
}),
tap(() => {
this.isLoading.holdings = false;
@ -311,8 +305,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})
);
// Quick links
const quickLinks$: Observable<Partial<ISearchResults>> = of(
const quickLinks$: Observable<Partial<SearchResults>> = of(
this.searchQuickLinks(searchTerm)
).pipe(
map((quickLinks) => ({
@ -327,10 +320,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
})
);
// Merge all results
return merge(accounts$, assetProfiles$, holdings$, quickLinks$).pipe(
scan(
(acc: ISearchResults, curr: Partial<ISearchResults>) => ({
(acc: SearchResults, curr: Partial<SearchResults>) => ({
...acc,
...curr
}),
@ -339,7 +331,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
assetProfiles: [],
holdings: [],
quickLinks: []
} as ISearchResults
} as SearchResults
)
);
}),
@ -362,22 +354,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
quickLinks: []
};
this.changeDetectorRef.markForCheck();
},
complete: () => {
this.isLoading = {
accounts: false,
assetProfiles: false,
holdings: false,
quickLinks: false
};
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnChanges() {
this.accounts = this.user?.accounts ?? [];
this.dateRangeOptions = [
{
label: $localize`Today`,
@ -445,7 +426,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null);
this.filterForm.disable({ emitEvent: false });
if (this.hasPermissionToChangeFilters) {
this.portfolioFilterFormControl.enable({ emitEvent: false });
} else {
this.portfolioFilterFormControl.disable({ emitEvent: false });
}
this.tags =
this.user?.tags
@ -459,29 +444,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
type: 'TAG'
};
}) ?? [];
if (this.tags.length === 0) {
this.filterForm.get('tag').disable({ emitEvent: false });
}
}
public hasFilter(aFormValue: { [key: string]: string }) {
return Object.values(aFormValue).some((value) => {
return !!value;
});
}
public holdingComparisonFunction(
option: PortfolioPosition,
value: PortfolioPosition
): boolean {
if (value === null) {
return false;
}
return (
getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
);
}
public initialize() {
@ -527,36 +489,35 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.sort((a, b) => {
return a.name?.localeCompare(b.name);
});
this.setFilterFormValues();
if (this.hasPermissionToChangeFilters) {
this.filterForm.enable({ emitEvent: false });
}
this.setPortfolioFilterFormValues();
this.changeDetectorRef.markForCheck();
});
}
public onApplyFilters() {
const filterValue = this.portfolioFilterFormControl.value;
this.filtersChanged.emit([
{
id: this.filterForm.get('account').value,
id: filterValue?.account,
type: 'ACCOUNT'
},
{
id: this.filterForm.get('assetClass').value,
id: filterValue?.assetClass,
type: 'ASSET_CLASS'
},
{
id: this.filterForm.get('holding').value?.dataSource,
id: filterValue?.holding?.dataSource,
type: 'DATA_SOURCE'
},
{
id: this.filterForm.get('holding').value?.symbol,
id: filterValue?.holding?.symbol,
type: 'SYMBOL'
},
{
id: this.filterForm.get('tag').value,
id: filterValue?.tag,
type: 'TAG'
}
]);
@ -569,12 +530,15 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}
public onCloseAssistant() {
this.portfolioFilterFormControl.reset();
this.setIsOpen(false);
this.closed.emit();
}
public onResetFilters() {
this.portfolioFilterFormControl.reset();
this.filtersChanged.emit(
this.filterTypes.map((type) => {
return {
@ -658,7 +622,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}, this.PRESELECTION_DELAY);
}
private searchAccounts(aSearchTerm: string): Observable<ISearchResultItem[]> {
private searchAccounts(aSearchTerm: string): Observable<SearchResultItem[]> {
return this.dataService
.fetchAccounts({
filters: [
@ -688,7 +652,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
private searchAssetProfiles(
aSearchTerm: string
): Observable<ISearchResultItem[]> {
): Observable<SearchResultItem[]> {
return this.adminService
.fetchAdminMarketData({
filters: [
@ -721,7 +685,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
);
}
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> {
private searchHoldings(aSearchTerm: string): Observable<SearchResultItem[]> {
return this.dataService
.fetchPortfolioHoldings({
filters: [
@ -753,7 +717,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
);
}
private searchQuickLinks(aSearchTerm: string): ISearchResultItem[] {
private searchQuickLinks(aSearchTerm: string): SearchResultItem[] {
const searchTerm = aSearchTerm.toLowerCase();
const allRoutes = Object.values(internalRoutes)
@ -786,7 +750,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
});
}
private setFilterFormValues() {
private setPortfolioFilterFormValues() {
const dataSource = this.user?.settings?.[
'filters.dataSource'
] as DataSource;
@ -800,16 +764,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
);
});
this.filterForm.setValue(
{
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
holding: selectedHolding ?? null,
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
},
{
emitEvent: false
}
);
this.portfolioFilterFormControl.setValue({
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
holding: selectedHolding ?? null,
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
});
}
}

170
libs/ui/src/lib/assistant/assistant.html

@ -164,119 +164,61 @@
</div>
}
</div>
<form [formGroup]="filterForm">
@if (!searchFormControl.value) {
<div class="date-range-selector-container p-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label>
<mat-select
[formControl]="dateRangeFormControl"
(selectionChange)="onChangeDateRange($event.value)"
>
@for (range of dateRangeOptions; track range) {
<mat-option [value]="range.value">{{ range.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="p-3">
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Account</mat-label>
<mat-select formControlName="account">
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
<div class="d-flex">
@if (account.platform?.url) {
<gf-entity-logo
class="mr-1"
[tooltip]="account.platform?.name"
[url]="account.platform?.url"
/>
}
<span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Holding</mat-label>
<mat-select
formControlName="holding"
[compareWith]="holdingComparisonFunction"
>
<mat-select-trigger>{{
filterForm.get('holding')?.value?.name
}}</mat-select-trigger>
<mat-option [value]="null" />
@for (holding of holdings; track holding.name) {
<mat-option [value]="holding">
<div class="line-height-1 text-truncate">
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tag</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="d-flex w-100">
<button
i18n
mat-button
[disabled]="
!hasFilter(filterForm.value) || !hasPermissionToChangeFilters
"
(click)="onResetFilters()"
>
Reset Filters
</button>
<span class="gf-spacer"></span>
<button
color="primary"
i18n
mat-flat-button
[disabled]="!filterForm.dirty || !hasPermissionToChangeFilters"
(click)="onApplyFilters()"
>
Apply Filters
</button>
</div>
@if (!searchFormControl.value) {
<div class="date-range-selector-container p-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label>
<mat-select
[formControl]="dateRangeFormControl"
(selectionChange)="onChangeDateRange($event.value)"
>
@for (
dateRangeOption of dateRangeOptions;
track dateRangeOption.value
) {
<mat-option [value]="dateRangeOption.value">{{
dateRangeOption.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="p-3">
<gf-portfolio-filter-form
#portfolioFilterForm
[accounts]="user?.accounts"
[assetClasses]="assetClasses"
[formControl]="portfolioFilterFormControl"
[holdings]="holdings"
[tags]="tags"
/>
<div class="d-flex w-100">
<button
i18n
mat-button
type="button"
[disabled]="
!portfolioFilterForm.hasFilters() || portfolioFilterForm.disabled
"
(click)="onResetFilters()"
>
Reset Filters
</button>
<span class="gf-spacer"></span>
<button
color="primary"
i18n
mat-flat-button
type="button"
[disabled]="
!portfolioFilterForm.filterForm.dirty ||
portfolioFilterForm.disabled
"
(click)="onApplyFilters()"
>
Apply Filters
</button>
</div>
}
</form>
</div>
}
</div>

26
libs/ui/src/lib/assistant/interfaces/interfaces.ts

@ -3,38 +3,38 @@ import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { SearchMode } from '../enums/search-mode';
export interface IAccountSearchResultItem
export interface AccountSearchResultItem
extends Pick<AccountWithValue, 'id' | 'name'> {
mode: SearchMode.ACCOUNT;
routerLink: string[];
}
export interface IAssetSearchResultItem extends AssetProfileIdentifier {
export interface AssetSearchResultItem extends AssetProfileIdentifier {
assetSubClassString: string;
currency: string;
mode: SearchMode.ASSET_PROFILE | SearchMode.HOLDING;
name: string;
}
export interface IDateRangeOption {
export interface DateRangeOption {
label: string;
value: DateRange;
}
export interface IQuickLinkSearchResultItem {
export interface QuickLinkSearchResultItem {
mode: SearchMode.QUICK_LINK;
name: string;
routerLink: string[];
}
export type ISearchResultItem =
| IAccountSearchResultItem
| IAssetSearchResultItem
| IQuickLinkSearchResultItem;
export type SearchResultItem =
| AccountSearchResultItem
| AssetSearchResultItem
| QuickLinkSearchResultItem;
export interface ISearchResults {
accounts: ISearchResultItem[];
assetProfiles: ISearchResultItem[];
holdings: ISearchResultItem[];
quickLinks: ISearchResultItem[];
export interface SearchResults {
accounts: SearchResultItem[];
assetProfiles: SearchResultItem[];
holdings: SearchResultItem[];
quickLinks: SearchResultItem[];
}

2
libs/ui/src/lib/portfolio-filter-form/index.ts

@ -0,0 +1,2 @@
export * from './interfaces';
export * from './portfolio-filter-form.component';

1
libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts

@ -0,0 +1 @@
export * from './portfolio-filter-form-value.interface';

8
libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts

@ -0,0 +1,8 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioFilterFormValue {
account: string;
assetClass: string;
holding: PortfolioPosition;
tag: string;
}

75
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html

@ -0,0 +1,75 @@
<form [formGroup]="filterForm">
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Account</mat-label>
<mat-select formControlName="account">
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
<div class="d-flex">
@if (account.platform?.url) {
<gf-entity-logo
class="mr-1"
[tooltip]="account.platform?.name"
[url]="account.platform?.url"
/>
}
<span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Holding</mat-label>
<mat-select
formControlName="holding"
[compareWith]="holdingComparisonFunction"
>
<mat-select-trigger>{{
filterForm.get('holding')?.value?.name
}}</mat-select-trigger>
<mat-option [value]="null" />
@for (holding of holdings; track holding.name) {
<mat-option [value]="holding">
<div class="line-height-1 text-truncate">
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency }}</small
>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tag</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</form>

3
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

79
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts

@ -0,0 +1,79 @@
import '@angular/localize/init';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { GfPortfolioFilterFormComponent } from './portfolio-filter-form.component';
const meta: Meta<GfPortfolioFilterFormComponent> = {
title: 'Portfolio Filter Form',
component: GfPortfolioFilterFormComponent,
decorators: [
moduleMetadata({
imports: [GfPortfolioFilterFormComponent]
})
]
};
export default meta;
type Story = StoryObj<GfPortfolioFilterFormComponent>;
export const Default: Story = {
args: {
accounts: [
{
id: '733110b6-7c55-44eb-8cc5-c4c3e9d48a79',
name: 'Trading Account',
platform: {
name: 'Interactive Brokers',
url: 'https://interactivebrokers.com'
}
},
{
id: '24ba27d6-e04b-4fb4-b856-b24c2ef0422a',
name: 'Investment Account',
platform: {
name: 'Fidelity',
url: 'https://fidelity.com'
}
}
] as any,
assetClasses: [
{ id: 'COMMODITY', label: 'Commodity', type: 'ASSET_CLASS' },
{ id: 'EQUITY', label: 'Equity', type: 'ASSET_CLASS' },
{ id: 'FIXED_INCOME', label: 'Fixed Income', type: 'ASSET_CLASS' }
] as any,
holdings: [
{
currency: 'USD',
dataSource: 'YAHOO',
name: 'Apple Inc.',
symbol: 'AAPL'
},
{
currency: 'USD',
dataSource: 'YAHOO',
name: 'Microsoft Corporation',
symbol: 'MSFT'
}
] as any,
tags: [
{
id: 'EMERGENCY_FUND',
label: 'Emergency Fund',
type: 'TAG'
},
{
id: 'RETIREMENT_FUND',
label: 'Retirement Fund',
type: 'TAG'
}
] as any,
disabled: false
}
};
export const Disabled: Story = {
args: {
...Default.args,
disabled: true
}
};

177
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts

@ -0,0 +1,177 @@
import { GfSymbolPipe } from '@ghostfolio/client/pipes/symbol/symbol.pipe';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
forwardRef
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule
} from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { Subject, takeUntil } from 'rxjs';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { PortfolioFilterFormValue } from './interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FormsModule,
GfEntityLogoComponent,
GfSymbolPipe,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule
],
providers: [
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GfPortfolioFilterFormComponent)
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-portfolio-filter-form',
styleUrls: ['./portfolio-filter-form.component.scss'],
templateUrl: './portfolio-filter-form.component.html'
})
export class GfPortfolioFilterFormComponent
implements ControlValueAccessor, OnInit, OnChanges, OnDestroy
{
@Input() accounts: AccountWithPlatform[] = [];
@Input() assetClasses: Filter[] = [];
@Input() holdings: PortfolioPosition[] = [];
@Input() tags: Filter[] = [];
@Input() disabled = false;
public filterForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private formBuilder: FormBuilder
) {
this.filterForm = this.formBuilder.group({
account: new FormControl<string>(null),
assetClass: new FormControl<string>(null),
holding: new FormControl<PortfolioPosition>(null),
tag: new FormControl<string>(null)
});
}
public ngOnInit() {
this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((value) => {
this.onChange(value as PortfolioFilterFormValue);
this.onTouched();
});
}
public hasFilters() {
const formValue = this.filterForm.value;
return Object.values(formValue).some((value) => {
return !!value;
});
}
public holdingComparisonFunction(
option: PortfolioPosition,
value: PortfolioPosition
) {
if (value === null) {
return false;
}
return (
getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
);
}
public ngOnChanges() {
if (this.disabled) {
this.filterForm.disable({ emitEvent: false });
} else {
this.filterForm.enable({ emitEvent: false });
}
const tagControl = this.filterForm.get('tag');
if (this.tags.length === 0) {
tagControl?.disable({ emitEvent: false });
} else if (!this.disabled) {
tagControl?.enable({ emitEvent: false });
}
this.changeDetectorRef.markForCheck();
}
public registerOnChange(fn: (value: PortfolioFilterFormValue) => void) {
this.onChange = fn;
}
public registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.filterForm.disable({ emitEvent: false });
} else {
this.filterForm.enable({ emitEvent: false });
}
this.changeDetectorRef.markForCheck();
}
public writeValue(value: PortfolioFilterFormValue | null) {
if (value) {
this.filterForm.setValue(
{
account: value.account ?? null,
assetClass: value.assetClass ?? null,
holding: value.holding ?? null,
tag: value.tag ?? null
},
{ emitEvent: false }
);
} else {
this.filterForm.reset({}, { emitEvent: false });
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onChange = (_value: PortfolioFilterFormValue): void => {
// ControlValueAccessor onChange callback
};
private onTouched = (): void => {
// ControlValueAccessor onTouched callback
};
}

80
package-lock.json

@ -65,6 +65,8 @@
"countries-list": "3.1.1",
"countup.js": "2.9.0",
"date-fns": "4.1.0",
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
@ -6113,9 +6115,9 @@
}
},
"node_modules/@ioredis/commands": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz",
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
"integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
"license": "MIT"
},
"node_modules/@isaacs/balanced-match": {
@ -9081,6 +9083,33 @@
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/dotenv-expand": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/core": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz",
@ -21037,9 +21066,9 @@
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@ -21049,9 +21078,9 @@
}
},
"node_modules/dotenv-expand": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
@ -21063,6 +21092,18 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -25067,12 +25108,12 @@
}
},
"node_modules/ioredis": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
"integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"@ioredis/commands": "1.4.0",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
@ -33459,6 +33500,19 @@
"node": ">=8"
}
},
"node_modules/nx/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/nx/node_modules/dotenv-expand": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",

2
package.json

@ -111,6 +111,8 @@
"countries-list": "3.1.1",
"countup.js": "2.9.0",
"date-fns": "4.1.0",
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",

5
prisma.config.ts

@ -1,7 +1,10 @@
import 'dotenv/config';
import { config } from 'dotenv';
import { expand } from 'dotenv-expand';
import { join } from 'node:path';
import { defineConfig } from 'prisma/config';
expand(config({ quiet: true }));
export default defineConfig({
migrations: {
path: join('prisma', 'migrations'),

Loading…
Cancel
Save