Browse Source

Merge branch 'main' into feature/move-subscription-offer-from-info-to-user-service

pull/4533/head
Thomas Kaul 4 months ago
committed by GitHub
parent
commit
32542800af
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 32
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  3. 69
      apps/api/src/app/portfolio/current-rate.service.ts
  4. 40
      apps/api/src/services/market-data/market-data.service.ts
  5. 5
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  6. 1366
      package-lock.json
  7. 34
      package.json

3
CHANGELOG.md

@ -10,8 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Deactivated asset profiles automatically on delisting in the _Yahoo Finance_ service - Deactivated asset profiles automatically on delisting in the _Yahoo Finance_ service
- Optimized the query of the data range functionality (`getRange()`) in the market data service
- Moved the subscription offer from the info to the user service - Moved the subscription offer from the info to the user service
- Upgraded `Nx` from version `20.7.1` to `20.8.0`
- Upgraded `prisma` from version `6.5.0` to `6.6.0` - Upgraded `prisma` from version `6.5.0` to `6.6.0`
- Upgraded `storybook` from version `8.4.7` to `8.6.12`
## 2.151.0 - 2025-04-11 ## 2.151.0 - 2025-04-11

32
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -6,6 +6,7 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { DateQuery } from './interfaces/date-query.interface';
import { GetValuesObject } from './interfaces/get-values-object.interface'; import { GetValuesObject } from './interfaces/get-values-object.interface';
jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => { jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
@ -25,33 +26,40 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
}, },
getRange: ({ getRange: ({
assetProfileIdentifiers, assetProfileIdentifiers,
dateRangeEnd, dateQuery
dateRangeStart
}: { }: {
assetProfileIdentifiers: AssetProfileIdentifier[]; assetProfileIdentifiers: AssetProfileIdentifier[];
dateRangeEnd: Date; dateQuery: DateQuery;
dateRangeStart: Date; skip?: number;
take?: number;
}) => { }) => {
return Promise.resolve<MarketData[]>([ return Promise.resolve<MarketData[]>([
{ {
createdAt: dateRangeStart, createdAt: dateQuery.gte,
dataSource: assetProfileIdentifiers[0].dataSource, dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeStart, date: dateQuery.gte,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d', id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902, marketPrice: 1841.823902,
state: 'CLOSE', state: 'CLOSE',
symbol: assetProfileIdentifiers[0].symbol symbol: assetProfileIdentifiers[0].symbol
}, },
{ {
createdAt: dateRangeEnd, createdAt: dateQuery.lt,
dataSource: assetProfileIdentifiers[0].dataSource, dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeEnd, date: dateQuery.lt,
id: '082d6893-df27-4c91-8a5d-092e84315b56', id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966, marketPrice: 1847.839966,
state: 'CLOSE', state: 'CLOSE',
symbol: assetProfileIdentifiers[0].symbol symbol: assetProfileIdentifiers[0].symbol
} }
]); ]);
},
getRangeCount: ({}: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateRangeEnd: Date;
dateRangeStart: Date;
}) => {
return Promise.resolve<number>(2);
} }
}; };
}) })
@ -128,9 +136,15 @@ describe('CurrentRateService', () => {
values: [ values: [
{ {
dataSource: 'YAHOO', dataSource: 'YAHOO',
date: undefined, date: new Date('2020-01-01T00:00:00.000Z'),
marketPrice: 1841.823902, marketPrice: 1841.823902,
symbol: 'AMZN' symbol: 'AMZN'
},
{
dataSource: 'YAHOO',
date: new Date('2020-01-02T00:00:00.000Z'),
marketPrice: 1847.839966,
symbol: 'AMZN'
} }
] ]
}); });

69
apps/api/src/app/portfolio/current-rate.service.ts

@ -21,6 +21,8 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
@Injectable() @Injectable()
export class CurrentRateService { export class CurrentRateService {
private static readonly MARKET_DATA_PAGE_SIZE = 50000;
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@ -41,30 +43,29 @@ export class CurrentRateService {
(!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) &&
(!dateQuery.in || this.containsToday(dateQuery.in)); (!dateQuery.in || this.containsToday(dateQuery.in));
const promises: Promise<GetValueObject[]>[] = [];
const quoteErrors: ResponseError['errors'] = []; const quoteErrors: ResponseError['errors'] = [];
const today = resetHours(new Date()); const today = resetHours(new Date());
const values: GetValueObject[] = [];
if (includesToday) { if (includesToday) {
promises.push( const quotesBySymbol = await this.dataProviderService.getQuotes({
this.dataProviderService items: dataGatheringItems,
.getQuotes({ items: dataGatheringItems, user: this.request?.user }) user: this.request?.user
.then((dataResultProvider) => { });
const result: GetValueObject[] = [];
for (const { dataSource, symbol } of dataGatheringItems) { for (const { dataSource, symbol } of dataGatheringItems) {
if (dataResultProvider?.[symbol]?.dataProviderInfo) { const quote = quotesBySymbol[symbol];
dataProviderInfos.push(
dataResultProvider[symbol].dataProviderInfo if (quote?.dataProviderInfo) {
); dataProviderInfos.push(quote.dataProviderInfo);
} }
if (dataResultProvider?.[symbol]?.marketPrice) { if (quote?.marketPrice) {
result.push({ values.push({
dataSource, dataSource,
symbol, symbol,
date: today, date: today,
marketPrice: dataResultProvider?.[symbol]?.marketPrice marketPrice: quote.marketPrice
}); });
} else { } else {
quoteErrors.push({ quoteErrors.push({
@ -73,10 +74,6 @@ export class CurrentRateService {
}); });
} }
} }
return result;
})
);
} }
const assetProfileIdentifiers: AssetProfileIdentifier[] = const assetProfileIdentifiers: AssetProfileIdentifier[] =
@ -84,34 +81,42 @@ export class CurrentRateService {
return { dataSource, symbol }; return { dataSource, symbol };
}); });
promises.push( const marketDataCount = await this.marketDataService.getRangeCount({
this.marketDataService
.getRange({
assetProfileIdentifiers, assetProfileIdentifiers,
dateQuery dateQuery
}) });
.then((data) => {
return data.map(({ dataSource, date, marketPrice, symbol }) => { for (
return { let i = 0;
i < marketDataCount;
i += CurrentRateService.MARKET_DATA_PAGE_SIZE
) {
// Use page size to limit the number of records fetched at once
const data = await this.marketDataService.getRange({
assetProfileIdentifiers,
dateQuery,
skip: i,
take: CurrentRateService.MARKET_DATA_PAGE_SIZE
});
values.push(
...data.map(({ dataSource, date, marketPrice, symbol }) => ({
dataSource, dataSource,
date, date,
marketPrice, marketPrice,
symbol symbol
}; }))
});
})
); );
}
const values = await Promise.all(promises).then((array) => {
return array.flat();
});
const response: GetValuesObject = { const response: GetValuesObject = {
dataProviderInfos, dataProviderInfos,
errors: quoteErrors.map(({ dataSource, symbol }) => { errors: quoteErrors.map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };
}), }),
values: uniqBy(values, ({ date, symbol }) => `${date}-${symbol}`) values: uniqBy(values, ({ date, symbol }) => {
return `${date}-${symbol}`;
})
}; };
if (!isEmpty(quoteErrors)) { if (!isEmpty(quoteErrors)) {

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

@ -60,12 +60,18 @@ export class MarketDataService {
public async getRange({ public async getRange({
assetProfileIdentifiers, assetProfileIdentifiers,
dateQuery dateQuery,
skip,
take
}: { }: {
assetProfileIdentifiers: AssetProfileIdentifier[]; assetProfileIdentifiers: AssetProfileIdentifier[];
dateQuery: DateQuery; dateQuery: DateQuery;
skip?: number;
take?: number;
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
return this.prismaService.marketData.findMany({ return this.prismaService.marketData.findMany({
skip,
take,
orderBy: [ orderBy: [
{ {
date: 'asc' date: 'asc'
@ -75,17 +81,33 @@ export class MarketDataService {
} }
], ],
where: { where: {
dataSource: {
in: assetProfileIdentifiers.map(({ dataSource }) => {
return dataSource;
})
},
date: dateQuery, date: dateQuery,
symbol: { OR: assetProfileIdentifiers.map(({ dataSource, symbol }) => {
in: assetProfileIdentifiers.map(({ symbol }) => { return {
return symbol; dataSource,
symbol
};
}) })
} }
});
}
public async getRangeCount({
assetProfileIdentifiers,
dateQuery
}: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateQuery: DateQuery;
}): Promise<number> {
return this.prismaService.marketData.count({
where: {
date: dateQuery,
OR: assetProfileIdentifiers.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
})
} }
}); });
} }

5
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -4,10 +4,7 @@
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<gf-membership-card <gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat" [expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[hasPermissionToCreateApiKey]=" [hasPermissionToCreateApiKey]="hasPermissionToCreateApiKey"
hasPermissionToCreateApiKey &&
user?.settings?.isExperimentalFeatures
"
[name]="user?.subscription?.type" [name]="user?.subscription?.type"
(generateApiKeyClicked)="onGenerateApiKey()" (generateApiKeyClicked)="onGenerateApiKey()"
/> />

1366
package-lock.json

File diff suppressed because it is too large

34
package.json

@ -155,22 +155,22 @@
"@eslint/js": "9.24.0", "@eslint/js": "9.24.0",
"@nestjs/schematics": "10.2.3", "@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.15", "@nestjs/testing": "10.4.15",
"@nx/angular": "20.7.1", "@nx/angular": "20.8.0",
"@nx/cypress": "20.7.1", "@nx/cypress": "20.8.0",
"@nx/eslint-plugin": "20.7.1", "@nx/eslint-plugin": "20.8.0",
"@nx/jest": "20.7.1", "@nx/jest": "20.8.0",
"@nx/js": "20.7.1", "@nx/js": "20.8.0",
"@nx/module-federation": "20.7.1", "@nx/module-federation": "20.8.0",
"@nx/nest": "20.7.1", "@nx/nest": "20.8.0",
"@nx/node": "20.7.1", "@nx/node": "20.8.0",
"@nx/storybook": "20.7.1", "@nx/storybook": "20.8.0",
"@nx/web": "20.7.1", "@nx/web": "20.8.0",
"@nx/workspace": "20.7.1", "@nx/workspace": "20.8.0",
"@schematics/angular": "19.2.1", "@schematics/angular": "19.2.1",
"@storybook/addon-essentials": "8.4.7", "@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.4.7", "@storybook/addon-interactions": "8.6.12",
"@storybook/angular": "8.4.7", "@storybook/angular": "8.6.12",
"@storybook/core-server": "8.4.7", "@storybook/core-server": "8.6.12",
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2", "@types/big.js": "6.2.2",
"@types/cache-manager": "4.0.6", "@types/cache-manager": "4.0.6",
@ -193,7 +193,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.4.2", "jest-preset-angular": "14.4.2",
"nx": "20.7.1", "nx": "20.8.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.6.0", "prisma": "6.6.0",
@ -201,7 +201,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.3.0", "replace-in-file": "8.3.0",
"shx": "0.3.4", "shx": "0.3.4",
"storybook": "8.4.7", "storybook": "8.6.12",
"ts-jest": "29.1.0", "ts-jest": "29.1.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"tslib": "2.8.1", "tslib": "2.8.1",

Loading…
Cancel
Save