Browse Source

Merge branch 'main' into bug/csv_import_of_custom_asset_profile_fails

pull/5749/head
Sven Günther 3 weeks ago
committed by GitHub
parent
commit
8287e5b258
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 10
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 20
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  4. 21
      apps/api/src/services/data-provider/data-provider.service.ts
  5. 7
      apps/api/src/services/data-provider/errors/asset-profile-invalid.error.ts
  6. 77
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 10
      libs/common/src/lib/helper.ts
  8. 7
      package-lock.json
  9. 1
      package.json

4
CHANGELOG.md

@ -14,12 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Disabled the zoom functionality in the _Progressive Web App_ (PWA)
- Improved the currency validation in the get asset profiles functionality of the data provider service
- Improved the currency validation in the search functionality of the data provider service
- Optimized the get quotes functionality by utilizing the asset profile resolutions in the _Financial Modeling Prep_ service
- Extracted the footer to a component
### Fixed
- Fixed an issue in the `csv` file import where custom asset profiles failed due to validation errors
- Respected the include indices flag in the search functionality of the _Financial Modeling Prep_ service
## 2.208.0 - 2025-10-11

10
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AssetProfileInvalidError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-invalid.error';
import { parseDate } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
@ -66,7 +67,14 @@ export class GhostfolioController {
});
return assetProfile;
} catch {
} catch (error) {
if (error instanceof AssetProfileInvalidError) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR

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

@ -40,10 +40,7 @@ export class GhostfolioService {
private readonly propertyService: PropertyService
) {}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: GetAssetProfileParams) {
public async getAssetProfile({ symbol }: GetAssetProfileParams) {
let result: DataProviderGhostfolioAssetProfileResponse = {};
try {
@ -51,12 +48,15 @@ export class GhostfolioService {
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService
.getAssetProfile({
requestTimeout,
symbol
})
.then(async (assetProfile) => {
this.dataProviderService
.getAssetProfiles([
{
symbol,
dataSource: dataProviderService.getName()
}
])
.then(async (assetProfiles) => {
const assetProfile = assetProfiles[symbol];
const dataSourceOrigin = DataSource.GHOSTFOLIO;
if (assetProfile) {

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

@ -35,6 +35,8 @@ import { eachDayOfInterval, format, isValid } from 'date-fns';
import { groupBy, isEmpty, isNumber, uniqWith } from 'lodash';
import ms from 'ms';
import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@Injectable()
export class DataProviderService implements OnModuleInit {
private dataProviderMapping: { [dataProviderName: string]: string };
@ -106,9 +108,9 @@ export class DataProviderService implements OnModuleInit {
);
promises.push(
promise.then((symbolProfile) => {
if (symbolProfile) {
response[symbol] = symbolProfile;
promise.then((assetProfile) => {
if (isCurrency(assetProfile?.currency)) {
response[symbol] = assetProfile;
}
})
);
@ -117,6 +119,12 @@ export class DataProviderService implements OnModuleInit {
try {
await Promise.all(promises);
if (isEmpty(response)) {
throw new AssetProfileInvalidError(
'No valid asset profiles have been found'
);
}
} catch (error) {
Logger.error(error, 'DataProviderService');
@ -645,8 +653,11 @@ export class DataProviderService implements OnModuleInit {
const filteredItems = lookupItems
.filter(({ currency }) => {
// Only allow symbols with supported currency
return currency ? true : false;
if (includeIndices) {
return true;
}
return currency ? isCurrency(currency) : false;
})
.map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {

7
apps/api/src/services/data-provider/errors/asset-profile-invalid.error.ts

@ -0,0 +1,7 @@
export class AssetProfileInvalidError extends Error {
public constructor(message: string) {
super(message);
this.name = 'AssetProfileInvalidError';
}
}

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

@ -102,6 +102,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
).then((res) => res.json());
if (!assetProfile) {
throw new Error(`${symbol} not found`);
}
const { assetClass, assetSubClass } =
this.parseAssetClass(assetProfile);
@ -373,26 +377,42 @@ export class FinancialModelingPrepService implements DataProviderInterface {
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json())
).then(
(res) => res.json() as unknown as { price: number; symbol: string }[]
)
]);
if (assetProfileResolutions.length === symbols.length) {
for (const { currency, symbolTarget } of assetProfileResolutions) {
currencyBySymbolMap[symbolTarget] = { currency };
for (const { currency, symbolTarget } of assetProfileResolutions) {
currencyBySymbolMap[symbolTarget] = { currency };
}
const resolvedSymbols = assetProfileResolutions.map(
({ symbolTarget }) => {
return symbolTarget;
}
} else {
);
const symbolsToFetch = quotes
.map(({ symbol }) => {
return symbol;
})
.filter((symbol) => {
return !resolvedSymbols.includes(symbol);
});
if (symbolsToFetch.length > 0) {
await Promise.all(
quotes.map(({ symbol }) => {
return this.getAssetProfile({
symbolsToFetch.map(async (symbol) => {
const assetProfile = await this.getAssetProfile({
requestTimeout,
symbol
}).then((assetProfile) => {
if (assetProfile?.currency) {
currencyBySymbolMap[symbol] = {
currency: assetProfile.currency
};
}
});
if (assetProfile?.currency) {
currencyBySymbolMap[symbol] = {
currency: assetProfile.currency
};
}
})
);
}
@ -438,6 +458,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
public async search({
includeIndices = false,
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
@ -484,17 +505,25 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
).then((res) => res.json());
items = result.map(({ currency, name, symbol }) => {
return {
currency,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: this.formatName({ name })
};
});
items = result
.filter(({ symbol }) => {
if (includeIndices === false && symbol.startsWith('^')) {
return false;
}
return true;
})
.map(({ currency, name, symbol }) => {
return {
currency,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: this.formatName({ name })
};
});
}
} catch (error) {
let message = error;

10
libs/common/src/lib/helper.ts

@ -1,7 +1,7 @@
import * as currencies from '@dinero.js/currencies';
import { NumberParser } from '@internationalized/number';
import { Type as ActivityType, DataSource, MarketData } from '@prisma/client';
import { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator';
import {
getDate,
getMonth,
@ -340,8 +340,12 @@ export function interpolate(template: string, context: any) {
});
}
export function isCurrency(aCurrency = '') {
return currencies[aCurrency] || isDerivedCurrency(aCurrency);
export function isCurrency(aCurrency: string) {
if (!aCurrency) {
return false;
}
return isISO4217CurrencyCode(aCurrency) || isDerivedCurrency(aCurrency);
}
export function isDerivedCurrency(aCurrency: string) {

7
package-lock.json

@ -28,7 +28,6 @@
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0",
@ -5018,12 +5017,6 @@
"ts-node": "^10.8.2"
}
},
"node_modules/@dinero.js/currencies": {
"version": "2.0.0-alpha.8",
"resolved": "https://registry.npmjs.org/@dinero.js/currencies/-/currencies-2.0.0-alpha.8.tgz",
"integrity": "sha512-zApiqtuuPwjiM9LJA5/kNcT48VSHRiz2/mktkXjIpfxrJKzthXybUAgEenExIH6dYhLDgVmsLQZtZFOsdYl0Ag==",
"license": "MIT"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",

1
package.json

@ -74,7 +74,6 @@
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0",

Loading…
Cancel
Save