Browse Source

Merge remote-tracking branch 'upstream/main' into feature/oidc-auth

pull/5981/head
Germán Martín 2 weeks ago
parent
commit
288e454aa4
  1. 8
      CHANGELOG.md
  2. 6
      apps/api/src/app/import/import.service.ts
  3. 4
      apps/api/src/app/order/order.service.ts
  4. 4
      apps/api/src/services/demo/demo.service.ts
  5. 204
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  6. 2
      package-lock.json
  7. 1
      package.json

8
CHANGELOG.md

@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added OIDC (OpenID Connect) as a login auth provider - Added OIDC (OpenID Connect) as a login auth provider
### Changed
- Eliminated `uuid` in favor of using `randomUUID` from `node:crypto`
### Fixed
- Fixed an issue with the exchange rate calculation when converting between derived currencies and their root currencies
## 2.219.0 - 2025-11-23 ## 2.219.0 - 2025-11-23
### Added ### Added

6
apps/api/src/app/import/import.service.ts

@ -35,7 +35,7 @@ import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns';
import { omit, uniqBy } from 'lodash'; import { omit, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'node:crypto';
import { ImportDataDto } from './import-data.dto'; import { ImportDataDto } from './import-data.dto';
@ -277,7 +277,7 @@ export class ImportService {
// Asset profile belongs to a different user // Asset profile belongs to a different user
if (existingAssetProfile) { if (existingAssetProfile) {
const symbol = uuidv4(); const symbol = randomUUID();
assetProfileSymbolMapping[assetProfile.symbol] = symbol; assetProfileSymbolMapping[assetProfile.symbol] = symbol;
assetProfile.symbol = symbol; assetProfile.symbol = symbol;
} }
@ -496,7 +496,7 @@ export class ImportService {
accountId: validatedAccount?.id, accountId: validatedAccount?.id,
accountUserId: undefined, accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
id: uuidv4(), id: randomUUID(),
isDraft: isAfter(date, endOfToday()), isDraft: isAfter(date, endOfToday()),
SymbolProfile: { SymbolProfile: {
assetClass, assetClass,

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

@ -37,7 +37,7 @@ import { Big } from 'big.js';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { groupBy, uniqBy } from 'lodash'; import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class OrderService { export class OrderService {
@ -143,7 +143,7 @@ export class OrderService {
} else { } else {
// Create custom asset profile // Create custom asset profile
name = name ?? data.SymbolProfile.connectOrCreate.create.symbol; name = name ?? data.SymbolProfile.connectOrCreate.create.symbol;
symbol = uuidv4(); symbol = randomUUID();
} }
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;

4
apps/api/src/services/demo/demo.service.ts

@ -7,7 +7,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class DemoService { export class DemoService {
@ -41,7 +41,7 @@ export class DemoService {
accountId: demoAccountId, accountId: demoAccountId,
accountUserId: demoUserId, accountUserId: demoUserId,
comment: null, comment: null,
id: uuidv4(), id: randomUUID(),
userId: demoUserId userId: demoUserId
}; };
}); });

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

@ -30,6 +30,7 @@ import ms from 'ms';
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencies: string[] = []; private currencies: string[] = [];
private currencyPairs: DataGatheringItem[] = []; private currencyPairs: DataGatheringItem[] = [];
private derivedCurrencyFactors: { [currencyPair: string]: number } = {};
private exchangeRates: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor( public constructor(
@ -135,8 +136,14 @@ export class ExchangeRateDataService {
public async initialize() { public async initialize() {
this.currencies = await this.prepareCurrencies(); this.currencies = await this.prepareCurrencies();
this.currencyPairs = []; this.currencyPairs = [];
this.derivedCurrencyFactors = {};
this.exchangeRates = {}; this.exchangeRates = {};
for (const { currency, factor, rootCurrency } of DERIVED_CURRENCIES) {
this.derivedCurrencyFactors[`${currency}${rootCurrency}`] = 1 / factor;
this.derivedCurrencyFactors[`${rootCurrency}${currency}`] = factor;
}
for (const { for (const {
currency1, currency1,
currency2, currency2,
@ -266,10 +273,14 @@ export class ExchangeRateDataService {
return this.toCurrency(aValue, aFromCurrency, aToCurrency); return this.toCurrency(aValue, aFromCurrency, aToCurrency);
} }
const derivedCurrencyFactor =
this.derivedCurrencyFactors[`${aFromCurrency}${aToCurrency}`];
let factor: number; let factor: number;
if (aFromCurrency === aToCurrency) { if (aFromCurrency === aToCurrency) {
factor = 1; factor = 1;
} else if (derivedCurrencyFactor) {
factor = derivedCurrencyFactor;
} else { } else {
const dataSource = const dataSource =
this.dataProviderService.getDataSourceForExchangeRates(); this.dataProviderService.getDataSourceForExchangeRates();
@ -357,111 +368,120 @@ export class ExchangeRateDataService {
for (const date of dates) { for (const date of dates) {
factors[format(date, DATE_FORMAT)] = 1; factors[format(date, DATE_FORMAT)] = 1;
} }
} else {
const dataSource =
this.dataProviderService.getDataSourceForExchangeRates();
const symbol = `${currencyFrom}${currencyTo}`;
const marketData = await this.marketDataService.getRange({ return factors;
assetProfileIdentifiers: [ }
{
dataSource, const derivedCurrencyFactor =
symbol this.derivedCurrencyFactors[`${currencyFrom}${currencyTo}`];
}
], if (derivedCurrencyFactor) {
dateQuery: { gte: startDate, lt: endDate } for (const date of dates) {
}); factors[format(date, DATE_FORMAT)] = derivedCurrencyFactor;
}
if (marketData?.length > 0) { return factors;
for (const { date, marketPrice } of marketData) { }
factors[format(date, DATE_FORMAT)] = marketPrice;
const dataSource = this.dataProviderService.getDataSourceForExchangeRates();
const symbol = `${currencyFrom}${currencyTo}`;
const marketData = await this.marketDataService.getRange({
assetProfileIdentifiers: [
{
dataSource,
symbol
} }
} else { ],
// Calculate indirectly via base currency dateQuery: { gte: startDate, lt: endDate }
});
const marketPriceBaseCurrencyFromCurrency: { if (marketData?.length > 0) {
[dateString: string]: number; for (const { date, marketPrice } of marketData) {
} = {}; factors[format(date, DATE_FORMAT)] = marketPrice;
const marketPriceBaseCurrencyToCurrency: { }
[dateString: string]: number; } else {
} = {}; // Calculate indirectly via base currency
const marketPriceBaseCurrencyFromCurrency: {
[dateString: string]: number;
} = {};
const marketPriceBaseCurrencyToCurrency: {
[dateString: string]: number;
} = {};
try {
if (currencyFrom === DEFAULT_CURRENCY) {
for (const date of dates) {
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = 1;
}
} else {
const marketData = await this.marketDataService.getRange({
assetProfileIdentifiers: [
{
dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
}
],
dateQuery: { gte: startDate, lt: endDate }
});
try { for (const { date, marketPrice } of marketData) {
if (currencyFrom === DEFAULT_CURRENCY) { marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
for (const date of dates) { marketPrice;
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
1;
}
} else {
const marketData = await this.marketDataService.getRange({
assetProfileIdentifiers: [
{
dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
}
],
dateQuery: { gte: startDate, lt: endDate }
});
for (const { date, marketPrice } of marketData) {
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] =
marketPrice;
}
} }
} catch {} }
} catch {}
try { try {
if (currencyTo === DEFAULT_CURRENCY) { if (currencyTo === DEFAULT_CURRENCY) {
for (const date of dates) { for (const date of dates) {
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1;
} }
} else { } else {
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
assetProfileIdentifiers: [ assetProfileIdentifiers: [
{ {
dataSource, dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyTo}` symbol: `${DEFAULT_CURRENCY}${currencyTo}`
}
],
dateQuery: {
gte: startDate,
lt: endDate
} }
}); ],
dateQuery: {
for (const { date, marketPrice } of marketData) { gte: startDate,
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = lt: endDate
marketPrice;
} }
});
for (const { date, marketPrice } of marketData) {
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] =
marketPrice;
} }
} catch {} }
} catch {}
for (const date of dates) { for (const date of dates) {
try { try {
const factor = const factor =
(1 / (1 /
marketPriceBaseCurrencyFromCurrency[ marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)]) *
format(date, DATE_FORMAT) marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
]) *
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
if (isNaN(factor)) {
throw new Error('Exchange rate is not a number');
} else {
factors[format(date, DATE_FORMAT)] = factor;
}
} catch {
let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
date,
DATE_FORMAT
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`;
if (DEFAULT_CURRENCY !== currencyTo) {
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`;
}
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); if (isNaN(factor)) {
throw new Error('Exchange rate is not a number');
} else {
factors[format(date, DATE_FORMAT)] = factor;
} }
} catch {
let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
date,
DATE_FORMAT
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`;
if (DEFAULT_CURRENCY !== currencyTo) {
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`;
}
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService');
} }
} }
} }

2
package-lock.json

@ -90,7 +90,6 @@
"svgmap": "2.14.0", "svgmap": "2.14.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.27.0", "twitter-api-v2": "1.27.0",
"uuid": "11.1.0",
"yahoo-finance2": "3.10.2", "yahoo-finance2": "3.10.2",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
@ -40639,6 +40638,7 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/esm/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }

1
package.json

@ -136,7 +136,6 @@
"svgmap": "2.14.0", "svgmap": "2.14.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.27.0", "twitter-api-v2": "1.27.0",
"uuid": "11.1.0",
"yahoo-finance2": "3.10.2", "yahoo-finance2": "3.10.2",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },

Loading…
Cancel
Save