Merge branch 'main' of gitea.suda.codes:giteauser/ghostfolio-mirror

This commit is contained in:
ksyasuda 2024-10-06 23:31:49 -07:00
commit 2725b94169
51 changed files with 253 additions and 294 deletions

View File

@ -34,9 +34,11 @@
{ {
"files": ["*.ts"], "files": ["*.ts"],
"plugins": ["eslint-plugin-import", "@typescript-eslint"], "plugins": ["eslint-plugin-import", "@typescript-eslint"],
"extends": ["plugin:@typescript-eslint/recommended-type-checked"], "extends": [
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": { "rules": {
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/dot-notation": "off", "@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [ "@typescript-eslint/explicit-member-accessibility": [
"off", "off",
@ -45,8 +47,33 @@
} }
], ],
"@typescript-eslint/member-ordering": "warn", "@typescript-eslint/member-ordering": "warn",
"@typescript-eslint/naming-convention": "off", "@typescript-eslint/naming-convention": [
"@typescript-eslint/no-empty-function": "off", "off",
{
"selector": "default",
"format": ["camelCase"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": ["variable", "classProperty", "typeProperty"],
"format": ["camelCase", "UPPER_CASE"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": "objectLiteralProperty",
"format": null
},
{
"selector": "enumMember",
"format": ["camelCase", "UPPER_CASE", "PascalCase"]
},
{
"selector": "typeLike",
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-empty-interface": "warn", "@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-inferrable-types": [ "@typescript-eslint/no-inferrable-types": [
"warn", "warn",
@ -61,7 +88,6 @@
"hoist": "all" "hoist": "all"
} }
], ],
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/unified-signatures": "error", "@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/no-loss-of-precision": "warn", "@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-var-requires": "warn", "@typescript-eslint/no-var-requires": "warn",
@ -109,12 +135,22 @@
"@typescript-eslint/no-unsafe-enum-comparison": "warn", "@typescript-eslint/no-unsafe-enum-comparison": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn", "@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/require-await": "warn", "@typescript-eslint/require-await": "warn",
"@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/unbound-method": "warn", "@typescript-eslint/unbound-method": "warn",
"prefer-const": "warn" "prefer-const": "warn",
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/no-empty-function": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "warn", // TODO: Requires strictNullChecks: true
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/consistent-indexed-object-style": "warn",
"@typescript-eslint/consistent-generic-constructors": "warn"
} }
} }
], ],

View File

@ -29,6 +29,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Check code style
run: npm run lint
- name: Check formatting - name: Check formatting
run: npm run format:check run: npm run format:check

View File

@ -5,16 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.113.0 - 2024-10-06
### Added ### Added
- Set up a git-hook via `husky` to lint and format the changes before a commit - Set up a git-hook via `husky` to lint and format the changes before a commit
- Added the `typescript-eslint/recommended-type-checked` rule to the `eslint` configuration - Added the `typescript-eslint/recommended-type-checked` rule to the `eslint` configuration
- Added the `typescript-eslint/stylistic-type-checked` rule to the `eslint` configuration
### Changed ### Changed
- Optimized the portfolio calculations by reusing date intervals - Optimized the portfolio calculations by reusing date intervals
- Refactored the calculation of the allocations by market on the allocations page
- Refactored the calculation of the allocations by market on the public page
### Fixed ### Fixed

View File

@ -74,15 +74,12 @@ export class AccountController {
); );
} }
return this.accountService.deleteAccount( return this.accountService.deleteAccount({
{ id_userId: {
id_userId: { id,
id, userId: this.request.user.id
userId: this.request.user.id }
} });
},
this.request.user.id
);
} }
@Get() @Get()

View File

@ -108,8 +108,7 @@ export class AccountService {
} }
public async deleteAccount( public async deleteAccount(
where: Prisma.AccountWhereUniqueInput, where: Prisma.AccountWhereUniqueInput
aUserId: string
): Promise<Account> { ): Promise<Account> {
const account = await this.prismaService.account.delete({ const account = await this.prismaService.account.delete({
where where
@ -170,11 +169,7 @@ export class AccountService {
where.isExcluded = false; where.isExcluded = false;
} }
const { const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, ({ type }) => {
return type; return type;
}); });

View File

@ -1,5 +1,5 @@
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator'; import { ArrayNotEmpty, IsArray } from 'class-validator';
import { UpdateMarketDataDto } from './update-market-data.dto'; import { UpdateMarketDataDto } from './update-market-data.dto';

View File

@ -29,8 +29,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
token: string, token: string,
refreshToken: string, refreshToken: string,
profile: Profile, profile: Profile,
done: Function, done: Function
done2: Function
) { ) {
try { try {
const jwt = await this.authService.validateOAuthLogin({ const jwt = await this.authService.validateOAuthLogin({

View File

@ -57,7 +57,7 @@ export class PublicController {
} }
const [ const [
{ holdings }, { holdings, markets },
{ performance: performance1d }, { performance: performance1d },
{ performance: performanceMax }, { performance: performanceMax },
{ performance: performanceYtd } { performance: performanceYtd }
@ -76,8 +76,13 @@ export class PublicController {
}) })
]); ]);
Object.values(markets).forEach((market) => {
delete market.valueInBaseCurrency;
});
const publicPortfolioResponse: PublicPortfolioResponse = { const publicPortfolioResponse: PublicPortfolioResponse = {
hasDetails, hasDetails,
markets,
alias: access.alias, alias: access.alias,
holdings: {}, holdings: {},
performance: { performance: {

View File

@ -3,24 +3,14 @@ import {
AssetProfileIdentifier, AssetProfileIdentifier,
SymbolMetrics SymbolMetrics
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
export class MWRPortfolioCalculator extends PortfolioCalculator { export class MWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance( protected calculateOverallPerformance(): PortfolioSnapshot {
positions: TimelinePosition[]
): PortfolioSnapshot {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
protected getSymbolMetrics({ protected getSymbolMetrics({}: {
dataSource,
end,
exchangeRates,
marketSymbolMap,
start,
step = 1,
symbol
}: {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

View File

@ -155,10 +155,27 @@ describe('PortfolioCalculator', () => {
dividendInBaseCurrency: new Big('0.62'), dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'), fee: new Big('19'),
firstBuyDate: '2021-09-16', firstBuyDate: '2021-09-16',
grossPerformance: new Big('33.25'),
grossPerformancePercentage: new Big('0.11136043941322258691'),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.11136043941322258691'
),
grossPerformanceWithCurrencyEffect: new Big('33.25'),
investment: new Big('298.58'), investment: new Big('298.58'),
investmentWithCurrencyEffect: new Big('298.58'), investmentWithCurrencyEffect: new Big('298.58'),
marketPrice: 331.83, marketPrice: 331.83,
marketPriceInBaseCurrency: 331.83, marketPriceInBaseCurrency: 331.83,
netPerformance: new Big('14.25'),
netPerformancePercentage: new Big('0.04772590260566682296'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('0.04772590260566682296')
},
netPerformanceWithCurrencyEffectMap: {
'1d': new Big('-5.39'),
'5y': new Big('14.25'),
max: new Big('14.25'),
wtd: new Big('-5.39')
},
quantity: new Big('1'), quantity: new Big('1'),
symbol: 'MSFT', symbol: 'MSFT',
tags: [], tags: [],

View File

@ -1,42 +1,3 @@
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
test.skip('Skip empty test', () => 1); test.skip('Skip empty test', () => 1);
}); });

View File

@ -12,13 +12,7 @@ import { DateRange } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns';
addDays,
addMilliseconds,
differenceInDays,
format,
isBefore
} from 'date-fns';
import { cloneDeep, first, last, sortBy } from 'lodash'; import { cloneDeep, first, last, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator { export class TWRPortfolioCalculator extends PortfolioCalculator {

View File

@ -65,6 +65,8 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 89.12 }; return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2021-11-16'), date)) { } else if (isSameDay(parseDate('2021-11-16'), date)) {
return { marketPrice: 339.51 }; return { marketPrice: 339.51 };
} else if (isSameDay(parseDate('2023-07-09'), date)) {
return { marketPrice: 337.22 };
} else if (isSameDay(parseDate('2023-07-10'), date)) { } else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 }; return { marketPrice: 331.83 };
} }

View File

@ -79,7 +79,7 @@ jest.mock('@ghostfolio/api/services/property/property.service', () => {
return { return {
PropertyService: jest.fn().mockImplementation(() => { PropertyService: jest.fn().mockImplementation(() => {
return { return {
getByKey: (key: string) => Promise.resolve({}) getByKey: () => Promise.resolve({})
}; };
}) })
}; };

View File

@ -13,7 +13,10 @@ import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import {
HEADER_KEY_IMPERSONATION,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
@ -95,15 +98,22 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const { accounts, hasErrors, holdings, platforms, summary } = const {
await this.portfolioService.getDetails({ accounts,
dateRange, hasErrors,
filters, holdings,
impersonationId, markets,
withMarkets, marketsAdvanced,
userId: this.request.user.id, platforms,
withSummary: true summary
}); } = await this.portfolioService.getDetails({
dateRange,
filters,
impersonationId,
withMarkets,
userId: this.request.user.id,
withSummary: true
});
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
hasError = true; hasError = true;
@ -162,6 +172,13 @@ export class PortfolioController {
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
Object.values(markets).forEach((market) => {
delete market.valueInBaseCurrency;
});
Object.values(marketsAdvanced).forEach((market) => {
delete market.valueInBaseCurrency;
});
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
'cash', 'cash',
'committedFunds', 'committedFunds',
@ -214,6 +231,58 @@ export class PortfolioController {
hasError, hasError,
holdings, holdings,
platforms, platforms,
markets: hasDetails
? markets
: {
[UNKNOWN_KEY]: {
id: UNKNOWN_KEY,
valueInPercentage: 1
},
developedMarkets: {
id: 'developedMarkets',
valueInPercentage: 0
},
emergingMarkets: {
id: 'emergingMarkets',
valueInPercentage: 0
},
otherMarkets: {
id: 'otherMarkets',
valueInPercentage: 0
}
},
marketsAdvanced: hasDetails
? marketsAdvanced
: {
[UNKNOWN_KEY]: {
id: UNKNOWN_KEY,
valueInPercentage: 0
},
asiaPacific: {
id: 'asiaPacific',
valueInPercentage: 0
},
emergingMarkets: {
id: 'emergingMarkets',
valueInPercentage: 0
},
europe: {
id: 'europe',
valueInPercentage: 0
},
japan: {
id: 'japan',
valueInPercentage: 0
},
northAmerica: {
id: 'northAmerica',
valueInPercentage: 0
},
otherMarkets: {
id: 'otherMarkets',
valueInPercentage: 0
}
},
summary: portfolioSummary summary: portfolioSummary
}; };
} }

View File

@ -1053,8 +1053,7 @@ export class PortfolioService {
dateRange = 'max', dateRange = 'max',
filters, filters,
impersonationId, impersonationId,
userId, userId
withExcludedAccounts = false
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
filters?: Filter[]; filters?: Filter[];
@ -1308,7 +1307,7 @@ export class PortfolioService {
} }
}; };
for (const [symbol, position] of Object.entries(holdings)) { for (const [, position] of Object.entries(holdings)) {
const value = position.valueInBaseCurrency; const value = position.valueInBaseCurrency;
if (position.assetClass !== AssetClass.LIQUIDITY) { if (position.assetClass !== AssetClass.LIQUIDITY) {

View File

@ -1,7 +1,5 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { Milliseconds } from 'cache-manager';
export const RedisCacheServiceMock = { export const RedisCacheServiceMock = {
cache: new Map<string, string>(), cache: new Map<string, string>(),
get: (key: string): Promise<string> => { get: (key: string): Promise<string> => {
@ -20,7 +18,7 @@ export const RedisCacheServiceMock = {
return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`; return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`;
}, },
set: (key: string, value: string, ttl?: Milliseconds): Promise<string> => { set: (key: string, value: string): Promise<string> => {
RedisCacheServiceMock.cache.set(key, value); RedisCacheServiceMock.cache.set(key, value);
return Promise.resolve(value); return Promise.resolve(value);

View File

@ -1,5 +1,4 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
import type { import type {
ColorScheme, ColorScheme,
DateRange, DateRange,

View File

@ -18,7 +18,7 @@ export class EmergencyFundSetup extends Rule<Settings> {
this.emergencyFund = emergencyFund; this.emergencyFund = emergencyFund;
} }
public evaluate(ruleSettings: Settings) { public evaluate() {
if (!this.emergencyFund) { if (!this.emergencyFund) {
return { return {
evaluation: 'No emergency fund has been set up', evaluation: 'No emergency fund has been set up',

View File

@ -33,7 +33,7 @@ export class AlphaVantageService implements DataProviderInterface {
}); });
} }
public canHandle(symbol: string) { public canHandle() {
return !!this.configurationService.get('API_KEY_ALPHA_VANTAGE'); return !!this.configurationService.get('API_KEY_ALPHA_VANTAGE');
} }

View File

@ -48,7 +48,7 @@ export class CoinGeckoService implements DataProviderInterface {
} }
} }
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }

View File

@ -83,7 +83,6 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
} }
public async enhance({ public async enhance({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
response, response,
symbol symbol
}: { }: {

View File

@ -43,7 +43,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA'); this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA');
} }
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }
@ -163,7 +163,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
).json<any>(); ).json<any>();
return response.reduce( return response.reduce(
(result, { close, date }, index, array) => { (result, { close, date }) => {
if (isNumber(close)) { if (isNumber(close)) {
result[this.convertFromEodSymbol(symbol)][date] = { result[this.convertFromEodSymbol(symbol)][date] = {
marketPrice: close marketPrice: close

View File

@ -33,7 +33,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
); );
} }
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }

View File

@ -29,7 +29,7 @@ export class GoogleSheetsService implements DataProviderInterface {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }

View File

@ -39,7 +39,7 @@ export class ManualService implements DataProviderInterface {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }
@ -86,12 +86,8 @@ export class ManualService implements DataProviderInterface {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[{ symbol, dataSource: this.getName() }] [{ symbol, dataSource: this.getName() }]
); );
const { const { defaultMarketPrice, selector, url } =
defaultMarketPrice, symbolProfile?.scraperConfiguration ?? {};
headers = {},
selector,
url
} = symbolProfile?.scraperConfiguration ?? {};
if (defaultMarketPrice) { if (defaultMarketPrice) {
const historical: { const historical: {

View File

@ -26,7 +26,7 @@ export class RapidApiService implements DataProviderInterface {
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return !!this.configurationService.get('API_KEY_RAPID_API'); return !!this.configurationService.get('API_KEY_RAPID_API');
} }

View File

@ -34,7 +34,7 @@ export class YahooFinanceService implements DataProviderInterface {
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }

View File

@ -1,10 +1,5 @@
export const ExchangeRateDataServiceMock = { export const ExchangeRateDataServiceMock = {
getExchangeRatesByCurrency: ({ getExchangeRatesByCurrency: ({ targetCurrency }): Promise<any> => {
currencies,
endDate,
startDate,
targetCurrency
}): Promise<any> => {
if (targetCurrency === 'CHF') { if (targetCurrency === 'CHF') {
return Promise.resolve({ return Promise.resolve({
CHFCHF: { CHFCHF: {

View File

@ -5,8 +5,6 @@ import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queu
export const PortfolioSnapshotServiceMock = { export const PortfolioSnapshotServiceMock = {
addJobToQueue({ addJobToQueue({
data,
name,
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: IPortfolioSnapshotQueueJob;

View File

@ -4,8 +4,7 @@ import {
registerDecorator, registerDecorator,
ValidationOptions, ValidationOptions,
ValidatorConstraint, ValidatorConstraint,
ValidatorConstraintInterface, ValidatorConstraintInterface
ValidationArguments
} from 'class-validator'; } from 'class-validator';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator';
@ -25,7 +24,7 @@ export function IsCurrencyCode(validationOptions?: ValidationOptions) {
export class IsExtendedCurrencyConstraint export class IsExtendedCurrencyConstraint
implements ValidatorConstraintInterface implements ValidatorConstraintInterface
{ {
public defaultMessage(args: ValidationArguments) { public defaultMessage() {
return '$value must be a valid ISO4217 currency code'; return '$value must be a valid ISO4217 currency code';
} }

View File

@ -15,7 +15,7 @@ export class CustomDateAdapter extends NativeDateAdapter {
/** /**
* Formats a date as a string * Formats a date as a string
*/ */
public format(aDate: Date, aParseFormat: string): string { public format(aDate: Date): string {
return format(aDate, getDateFormatString(this.locale)); return format(aDate, getDateFormatString(this.locale));
} }

View File

@ -12,7 +12,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs'; import { EMPTY, catchError, finalize, forkJoin } from 'rxjs';
@Injectable() @Injectable()
export class AdminMarketDataService { export class AdminMarketDataService {

View File

@ -11,7 +11,6 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';

View File

@ -33,7 +33,7 @@ export class NotificationService {
title: aParams.title title: aParams.title
}); });
return dialog.afterClosed().subscribe((result) => { return dialog.afterClosed().subscribe(() => {
if (isFunction(aParams.discardFn)) { if (isFunction(aParams.discardFn)) {
aParams.discardFn(); aParams.discardFn();
} }

View File

@ -1,5 +1,5 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';

View File

@ -47,7 +47,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public isLoading = false; public isLoading = false;
public markets: { public markets: {
[key in Market]: { name: string; value: number }; [key in Market]: { id: Market; valueInPercentage: number };
}; };
public marketsAdvanced: { public marketsAdvanced: {
[key in MarketAdvanced]: { [key in MarketAdvanced]: {
@ -219,24 +219,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: 0 value: 0
} }
}; };
this.markets = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
},
developedMarkets: {
name: 'developedMarkets',
value: 0
},
emergingMarkets: {
name: 'emergingMarkets',
value: 0
},
otherMarkets: {
name: 'otherMarkets',
value: 0
}
};
this.marketsAdvanced = { this.marketsAdvanced = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
id: UNKNOWN_KEY, id: UNKNOWN_KEY,
@ -318,6 +300,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}; };
} }
this.markets = this.portfolioDetails.markets;
Object.values(this.portfolioDetails.marketsAdvanced).forEach(
({ id, valueInBaseCurrency, valueInPercentage }) => {
this.marketsAdvanced[id].value = isNumber(valueInBaseCurrency)
? valueInBaseCurrency
: valueInPercentage;
}
);
for (const [symbol, position] of Object.entries( for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings this.portfolioDetails.holdings
)) { )) {
@ -348,48 +340,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity // Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) { if (position.countries.length > 0) {
this.markets.developedMarkets.value +=
position.markets.developedMarkets *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.markets.emergingMarkets.value +=
position.markets.emergingMarkets *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.markets.otherMarkets.value +=
position.markets.otherMarkets *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.marketsAdvanced.asiaPacific.value +=
position.marketsAdvanced.asiaPacific *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.marketsAdvanced.emergingMarkets.value +=
position.marketsAdvanced.emergingMarkets *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.marketsAdvanced.europe.value +=
position.marketsAdvanced.europe *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.marketsAdvanced.japan.value +=
position.marketsAdvanced.japan *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
this.marketsAdvanced.northAmerica.value +=
position.marketsAdvanced.northAmerica *
(isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: position.valueInPercentage);
for (const country of position.countries) { for (const country of position.countries) {
const { code, continent, name, weight } = country; const { code, continent, name, weight } = country;
@ -439,18 +389,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
) )
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
: this.portfolioDetails.holdings[symbol].valueInPercentage; : this.portfolioDetails.holdings[symbol].valueInPercentage;
this.markets[UNKNOWN_KEY].value += isNumber(
position.valueInBaseCurrency
)
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
: this.portfolioDetails.holdings[symbol].valueInPercentage;
this.marketsAdvanced[UNKNOWN_KEY].value += isNumber(
position.valueInBaseCurrency
)
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
: this.portfolioDetails.holdings[symbol].valueInPercentage;
} }
if (position.holdings.length > 0) { if (position.holdings.length > 0) {
@ -538,21 +476,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}; };
} }
const marketsTotal =
this.markets.developedMarkets.value +
this.markets.emergingMarkets.value +
this.markets.otherMarkets.value +
this.markets[UNKNOWN_KEY].value;
this.markets.developedMarkets.value =
this.markets.developedMarkets.value / marketsTotal;
this.markets.emergingMarkets.value =
this.markets.emergingMarkets.value / marketsTotal;
this.markets.otherMarkets.value =
this.markets.otherMarkets.value / marketsTotal;
this.markets[UNKNOWN_KEY].value =
this.markets[UNKNOWN_KEY].value / marketsTotal;
this.topHoldings = Object.values(this.topHoldingsMap) this.topHoldings = Object.values(this.topHoldingsMap)
.map(({ name, value }) => { .map(({ name, value }) => {
if (this.hasImpersonationId || this.user.settings.isRestrictedView) { if (this.hasImpersonationId || this.user.settings.isRestrictedView) {

View File

@ -218,7 +218,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.developedMarkets?.value" [value]="markets?.developedMarkets?.valueInPercentage"
>Developed Markets</gf-value >Developed Markets</gf-value
> >
</div> </div>
@ -227,7 +227,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.emergingMarkets?.value" [value]="markets?.emergingMarkets?.valueInPercentage"
>Emerging Markets</gf-value >Emerging Markets</gf-value
> >
</div> </div>
@ -236,17 +236,17 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.otherMarkets?.value" [value]="markets?.otherMarkets?.valueInPercentage"
>Other Markets</gf-value >Other Markets</gf-value
> >
</div> </div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) { @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
<div class="col-xs-12 col-md my-2"> <div class="col-xs-12 col-md my-2">
<gf-value <gf-value
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value" [value]="markets?.[UNKNOWN_KEY]?.valueInPercentage"
>No data available</gf-value >No data available</gf-value
> >
</div> </div>

View File

@ -32,7 +32,7 @@ export class PublicPageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public holdings: PublicPortfolioResponse['holdings'][string][]; public holdings: PublicPortfolioResponse['holdings'][string][];
public markets: { public markets: {
[key in Market]: { name: string; value: number }; [key in Market]: { id: Market; valueInPercentage: number };
}; };
public positions: { public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & { [symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
@ -102,24 +102,7 @@ export class PublicPageComponent implements OnInit {
} }
}; };
this.holdings = []; this.holdings = [];
this.markets = { this.markets = this.publicPortfolioDetails.markets;
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
},
developedMarkets: {
name: 'developedMarkets',
value: 0
},
emergingMarkets: {
name: 'emergingMarkets',
value: 0
},
otherMarkets: {
name: 'otherMarkets',
value: 0
}
};
this.positions = {}; this.positions = {};
this.sectors = { this.sectors = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
@ -150,13 +133,6 @@ export class PublicPageComponent implements OnInit {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity // Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) { if (position.countries.length > 0) {
this.markets.developedMarkets.value +=
position.markets.developedMarkets * position.valueInBaseCurrency;
this.markets.emergingMarkets.value +=
position.markets.emergingMarkets * position.valueInBaseCurrency;
this.markets.otherMarkets.value +=
position.markets.otherMarkets * position.valueInBaseCurrency;
for (const country of position.countries) { for (const country of position.countries) {
const { code, continent, name, weight } = country; const { code, continent, name, weight } = country;
@ -192,9 +168,6 @@ export class PublicPageComponent implements OnInit {
this.countries[UNKNOWN_KEY].value += this.countries[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
this.markets[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
} }
if (position.sectors.length > 0) { if (position.sectors.length > 0) {
@ -227,21 +200,6 @@ export class PublicPageComponent implements OnInit {
: position.valueInPercentage : position.valueInPercentage
}; };
} }
const marketsTotal =
this.markets.developedMarkets.value +
this.markets.emergingMarkets.value +
this.markets.otherMarkets.value +
this.markets[UNKNOWN_KEY].value;
this.markets.developedMarkets.value =
this.markets.developedMarkets.value / marketsTotal;
this.markets.emergingMarkets.value =
this.markets.emergingMarkets.value / marketsTotal;
this.markets.otherMarkets.value =
this.markets.otherMarkets.value / marketsTotal;
this.markets[UNKNOWN_KEY].value =
this.markets[UNKNOWN_KEY].value / marketsTotal;
} }
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -156,7 +156,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.developedMarkets?.value" [value]="markets?.developedMarkets?.valueInPercentage"
>Developed Markets</gf-value >Developed Markets</gf-value
> >
</div> </div>
@ -165,7 +165,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.emergingMarkets?.value" [value]="markets?.emergingMarkets?.valueInPercentage"
>Emerging Markets</gf-value >Emerging Markets</gf-value
> >
</div> </div>
@ -174,17 +174,17 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.otherMarkets?.value" [value]="markets?.otherMarkets?.valueInPercentage"
>Other Markets</gf-value >Other Markets</gf-value
> >
</div> </div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) { @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
<div class="col-xs-12 col-md my-2"> <div class="col-xs-12 col-md my-2">
<gf-value <gf-value
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value" [value]="markets?.[UNKNOWN_KEY]?.valueInPercentage"
>No data available</gf-value >No data available</gf-value
> >
</div> </div>

View File

@ -15,6 +15,8 @@
], ],
"angularCompilerOptions": { "angularCompilerOptions": {
"strictInjectionParameters": true, "strictInjectionParameters": true,
// TODO: Enable stricter rules for this project
"strictInputAccessModifiers": false,
"strictTemplates": false "strictTemplates": false
}, },
"compilerOptions": { "compilerOptions": {

View File

@ -18,14 +18,14 @@ export interface PortfolioDetails {
markets?: { markets?: {
[key in Market]: { [key in Market]: {
id: Market; id: Market;
valueInBaseCurrency: number; valueInBaseCurrency?: number;
valueInPercentage: number; valueInPercentage: number;
}; };
}; };
marketsAdvanced?: { marketsAdvanced?: {
[key in MarketAdvanced]: { [key in MarketAdvanced]: {
id: MarketAdvanced; id: MarketAdvanced;
valueInBaseCurrency: number; valueInBaseCurrency?: number;
valueInPercentage: number; valueInPercentage: number;
}; };
}; };

View File

@ -1,4 +1,5 @@
import { PortfolioPosition } from '../portfolio-position.interface'; import { PortfolioDetails, PortfolioPosition } from '..';
import { Market } from '../../types';
export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
alias?: string; alias?: string;
@ -22,6 +23,12 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
| 'valueInPercentage' | 'valueInPercentage'
>; >;
}; };
markets: {
[key in Market]: Pick<
PortfolioDetails['markets'][key],
'id' | 'valueInPercentage'
>;
};
} }
interface PublicPortfolioResponseV1 { interface PublicPortfolioResponseV1 {

View File

@ -36,7 +36,6 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Account, AssetClass } from '@prisma/client'; import { Account, AssetClass } from '@prisma/client';
import { eachYearOfInterval, format } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {

View File

@ -18,7 +18,6 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit,
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';

View File

@ -270,7 +270,7 @@ export class GfPortfolioProportionChartComponent
} }
]; ];
let labels = chartDataSorted.map(([symbol, { name }]) => { let labels = chartDataSorted.map(([, { name }]) => {
return name; return name;
}); });

View File

@ -10,7 +10,6 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';

View File

@ -14,11 +14,11 @@
} }
], ],
"compilerOptions": { "compilerOptions": {
"forceConsistentCasingInFileNames": true, "target": "es2020",
// TODO: Remove once solved in tsconfig.base.json
"strict": false, "strict": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true
"target": "es2020"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"strictInjectionParameters": true, "strictInjectionParameters": true,

8
package-lock.json generated
View File

@ -84,7 +84,6 @@
"passport": "0.7.0", "passport": "0.7.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"prisma": "5.20.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.5.6", "rxjs": "7.5.6",
"stripe": "15.11.0", "stripe": "15.11.0",
@ -150,6 +149,7 @@
"nx": "19.5.6", "nx": "19.5.6",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "5.20.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "7.0.1", "replace-in-file": "7.0.1",
@ -9668,12 +9668,14 @@
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz",
"integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==", "integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==",
"devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz",
"integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==", "integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==",
"devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -9687,12 +9689,14 @@
"version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", "version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz",
"integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==", "integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==",
"devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz",
"integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==", "integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.20.0", "@prisma/debug": "5.20.0",
@ -9704,6 +9708,7 @@
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz",
"integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==", "integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "5.20.0" "@prisma/debug": "5.20.0"
@ -28839,6 +28844,7 @@
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz",
"integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==", "integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==",
"devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.112.0", "version": "2.113.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -130,7 +130,6 @@
"passport": "0.7.0", "passport": "0.7.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"prisma": "5.20.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.5.6", "rxjs": "7.5.6",
"stripe": "15.11.0", "stripe": "15.11.0",
@ -196,6 +195,7 @@
"nx": "19.5.6", "nx": "19.5.6",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "5.20.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "7.0.1", "replace-in-file": "7.0.1",

View File

@ -20,7 +20,20 @@
"@ghostfolio/client/*": ["apps/client/src/app/*"], "@ghostfolio/client/*": ["apps/client/src/app/*"],
"@ghostfolio/common/*": ["libs/common/src/lib/*"], "@ghostfolio/common/*": ["libs/common/src/lib/*"],
"@ghostfolio/ui/*": ["libs/ui/src/lib/*"] "@ghostfolio/ui/*": ["libs/ui/src/lib/*"]
} },
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"noImplicitReturns": false,
"noImplicitAny": false,
"noImplicitThis": false,
"noImplicitOverride": false,
"noPropertyAccessFromIndexSignature": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"allowUnreachableCode": true
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]
} }