Feature/refactor filters (#1299)

* Refactor filters

Co-Authored-By: Zakaria YAHI <9142557+ZakYahi@users.noreply.github.com>
pull/1331/head
Thomas Kaul 2 years ago committed by GitHub
parent e8afbcad9c
commit c2df99072d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -95,11 +95,10 @@ export class AccountController {
); );
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations({
impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
undefined, withExcludedAccounts: true
true });
);
if ( if (
impersonationUserId || impersonationUserId ||
@ -139,11 +138,11 @@ export class AccountController {
); );
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations({
impersonationUserId || this.request.user.id, filters: [{ id, type: 'ACCOUNT' }],
[{ id, type: 'ACCOUNT' }], userId: impersonationUserId || this.request.user.id,
true withExcludedAccounts: true
); });
if ( if (
impersonationUserId || impersonationUserId ||

@ -3,8 +3,8 @@ import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Filter } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -36,6 +36,7 @@ import { UpdateOrderDto } from './update-order.dto';
@Controller('order') @Controller('order')
export class OrderController { export class OrderController {
public constructor( public constructor(
private readonly apiService: ApiService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
@ -73,30 +74,11 @@ export class OrderController {
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<Activities> { ): Promise<Activities> {
const accountIds = filterByAccounts?.split(',') ?? []; const filters = this.apiService.buildFiltersFromQueryParams({
const assetClasses = filterByAssetClasses?.split(',') ?? []; filterByAccounts,
const tagIds = filterByTags?.split(',') ?? []; filterByAssetClasses,
filterByTags
const filters: Filter[] = [ });
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(

@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module'; import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -18,6 +19,7 @@ import { OrderService } from './order.service';
controllers: [OrderController], controllers: [OrderController],
exports: [OrderService], exports: [OrderService],
imports: [ imports: [
ApiModule,
CacheModule, CacheModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,

@ -7,18 +7,17 @@ import {
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { import {
Filter,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport
PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { import type {
@ -52,6 +51,7 @@ export class PortfolioController {
public constructor( public constructor(
private readonly accessService: AccessService, private readonly accessService: AccessService,
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@ -123,32 +123,11 @@ export class PortfolioController {
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasError = false; let hasError = false;
const accountIds = filterByAccounts?.split(',') ?? []; const filters = this.apiService.buildFiltersFromQueryParams({
const assetClasses = filterByAssetClasses?.split(',') ?? []; filterByAccounts,
const tagIds = filterByTags?.split(',') ?? []; filterByAssetClasses,
filterByTags
const filters: Filter[] = [ });
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
let portfolioSummary: PortfolioSummary;
const { const {
accounts, accounts,
@ -158,18 +137,18 @@ export class PortfolioController {
holdings, holdings,
summary, summary,
totalValueInBaseCurrency totalValueInBaseCurrency
} = await this.portfolioService.getDetails( } = await this.portfolioService.getDetails({
filters,
impersonationId, impersonationId,
this.request.user.id, dateRange: range,
range, userId: this.request.user.id
filters });
);
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
hasError = true; hasError = true;
} }
portfolioSummary = summary; let portfolioSummary = summary;
if ( if (
impersonationId || impersonationId ||
@ -400,12 +379,12 @@ export class PortfolioController {
hasDetails = user.subscription.type === 'Premium'; hasDetails = user.subscription.type === 'Premium';
} }
const { holdings } = await this.portfolioService.getDetails( const { holdings } = await this.portfolioService.getDetails({
access.userId, dateRange: 'max',
access.userId, filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
'max', impersonationId: access.userId,
[{ id: 'EQUITY', type: 'ASSET_CLASS' }] userId: access.userId
); });
const portfolioPublicDetails: PortfolioPublicDetails = { const portfolioPublicDetails: PortfolioPublicDetails = {
hasDetails, hasDetails,

@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -22,6 +23,7 @@ import { RulesService } from './rules.service';
exports: [PortfolioService], exports: [PortfolioService],
imports: [ imports: [
AccessModule, AccessModule,
ApiModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,

@ -107,15 +107,19 @@ export class PortfolioService {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
} }
public async getAccounts( public async getAccounts({
aUserId: string, filters,
aFilters?: Filter[], userId,
withExcludedAccounts = false withExcludedAccounts = false
): Promise<AccountWithValue[]> { }: {
const where: Prisma.AccountWhereInput = { userId: aUserId }; filters?: Filter[];
userId: string;
withExcludedAccounts?: boolean;
}): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: userId };
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') { if (filters?.[0].id && filters?.[0].type === 'ACCOUNT') {
where.id = aFilters[0].id; where.id = filters[0].id;
} }
const [accounts, details] = await Promise.all([ const [accounts, details] = await Promise.all([
@ -124,13 +128,12 @@ export class PortfolioService {
include: { Order: true, Platform: true }, include: { Order: true, Platform: true },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getDetails( this.getDetails({
aUserId, filters,
aUserId, userId,
undefined, withExcludedAccounts,
aFilters, impersonationId: userId
withExcludedAccounts })
)
]); ]);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
@ -168,16 +171,20 @@ export class PortfolioService {
}); });
} }
public async getAccountsWithAggregations( public async getAccountsWithAggregations({
aUserId: string, filters,
aFilters?: Filter[], userId,
withExcludedAccounts = false withExcludedAccounts = false
): Promise<Accounts> { }: {
const accounts = await this.getAccounts( filters?: Filter[];
aUserId, userId: string;
aFilters, withExcludedAccounts?: boolean;
}): Promise<Accounts> {
const accounts = await this.getAccounts({
filters,
userId,
withExcludedAccounts withExcludedAccounts
); });
let totalBalanceInBaseCurrency = new Big(0); let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
@ -421,14 +428,21 @@ export class PortfolioService {
}; };
} }
public async getDetails( public async getDetails({
aImpersonationId: string, impersonationId,
aUserId: string, userId,
aDateRange: DateRange = 'max', dateRange = 'max',
aFilters?: Filter[], filters,
withExcludedAccounts = false withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> { }: {
const userId = await this.getUserId(aImpersonationId, aUserId); impersonationId: string;
userId: string;
dateRange?: DateRange;
filters?: Filter[];
withExcludedAccounts?: boolean;
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
// TODO:
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const emergencyFund = new Big( const emergencyFund = new Big(
@ -441,9 +455,9 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
filters,
userId, userId,
withExcludedAccounts, withExcludedAccounts
filters: aFilters
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
@ -457,15 +471,15 @@ export class PortfolioService {
const portfolioStart = parseDate( const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
); );
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate startDate
); );
const cashDetails = await this.accountService.getCashDetails({ const cashDetails = await this.accountService.getCashDetails({
filters,
userId, userId,
currency: userCurrency, currency: userCurrency
filters: aFilters
}); });
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
@ -475,10 +489,10 @@ export class PortfolioService {
let filteredValueInBaseCurrency = currentPositions.currentValue; let filteredValueInBaseCurrency = currentPositions.currentValue;
if ( if (
aFilters?.length === 0 || filters?.length === 0 ||
(aFilters?.length === 1 && (filters?.length === 1 &&
aFilters[0].type === 'ASSET_CLASS' && filters[0].type === 'ASSET_CLASS' &&
aFilters[0].id === 'CASH') filters[0].id === 'CASH')
) { ) {
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus( filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency cashDetails.balanceInBaseCurrency
@ -574,10 +588,10 @@ export class PortfolioService {
} }
if ( if (
aFilters?.length === 0 || filters?.length === 0 ||
(aFilters?.length === 1 && (filters?.length === 1 &&
aFilters[0].type === 'ASSET_CLASS' && filters[0].type === 'ASSET_CLASS' &&
aFilters[0].id === 'CASH') filters[0].id === 'CASH')
) { ) {
const cashPositions = await this.getCashPositions({ const cashPositions = await this.getCashPositions({
cashDetails, cashDetails,
@ -593,15 +607,15 @@ export class PortfolioService {
} }
const accounts = await this.getValueOfAccounts({ const accounts = await this.getValueOfAccounts({
filters,
orders, orders,
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts, withExcludedAccounts
filters: aFilters
}); });
const summary = await this.getSummary(aImpersonationId); const summary = await this.getSummary(impersonationId);
return { return {
accounts, accounts,

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ApiService } from './api.service';
@Module({
exports: [ApiService],
providers: [ApiService]
})
export class ApiModule {}

@ -0,0 +1,42 @@
import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ApiService {
public constructor() {}
public buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
}: {
filterByAccounts?: string;
filterByAssetClasses?: string;
filterByTags?: string;
}): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const tagIds = filterByTags?.split(',') ?? [];
return [
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
}
}

@ -74,60 +74,19 @@ export class DataService {
}: { }: {
filters?: Filter[]; filters?: Filter[];
}): Observable<Activities> { }): Observable<Activities> {
let params = new HttpParams(); return this.http
.get<any>('/api/v1/order', {
if (filters?.length > 0) { params: this.buildFiltersAsQueryParams({ filters })
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
});
if (filtersByAccount) {
params = params.append(
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetClass) {
params = params.append(
'assetClasses',
filtersByAssetClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return this.http.get<any>('/api/v1/order', { params }).pipe(
map(({ activities }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities };
}) })
); .pipe(
map(({ activities }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities };
})
);
} }
public fetchAdminData() { public fetchAdminData() {
@ -135,30 +94,8 @@ export class DataService {
} }
public fetchAdminMarketData({ filters }: { filters?: Filter[] }) { public fetchAdminMarketData({ filters }: { filters?: Filter[] }) {
let params = new HttpParams();
if (filters?.length > 0) {
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
if (filtersByAssetSubClass) {
params = params.append(
'assetSubClasses',
filtersByAssetSubClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', { return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
params params: this.buildFiltersAsQueryParams({ filters })
}); });
} }
@ -306,54 +243,9 @@ export class DataService {
}: { }: {
filters?: Filter[]; filters?: Filter[];
}): Observable<PortfolioDetails> { }): Observable<PortfolioDetails> {
let params = new HttpParams();
if (filters?.length > 0) {
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
});
if (filtersByAccount) {
params = params.append(
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetClass) {
params = params.append(
'assetClasses',
filtersByAssetClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return this.http return this.http
.get<any>('/api/v1/portfolio/details', { .get<any>('/api/v1/portfolio/details', {
params params: this.buildFiltersAsQueryParams({ filters })
}) })
.pipe( .pipe(
map((response) => { map((response) => {
@ -458,4 +350,53 @@ export class DataService {
couponCode couponCode
}); });
} }
private buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) {
let params = new HttpParams();
if (filters?.length > 0) {
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
});
if (filtersByAccount) {
params = params.append(
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetClass) {
params = params.append(
'assetClasses',
filtersByAssetClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return params;
}
} }

Loading…
Cancel
Save