From 6df2f4e07fed68369b94127a6a7266d7a6fef47a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Jul 2026 14:58:38 +0200 Subject: [PATCH] Task/harmonize filter parsing using groupBy() (#7211) * Harmonize filter parsing using groupBy() * Update changelog --- CHANGELOG.md | 1 + .../account-balance.service.ts | 9 ++-- apps/api/src/app/account/account.service.ts | 4 +- .../src/app/activities/activities.service.ts | 45 +++++++--------- .../asset-profiles/asset-profiles.service.ts | 27 +++++----- apps/api/src/app/export/export.service.ts | 4 +- .../src/app/portfolio/portfolio.service.ts | 51 ++++++++++--------- libs/ui/src/lib/services/data.service.ts | 38 +++++++------- 8 files changed, 86 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d1558a97..f7299820e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the parsing of integer query parameters (`skip` and `take`) in the `GET api/v1/asset-profiles` endpoint - Improved the parsing of the integer query parameter (`includeHistoricalData`) in the `GET api/v1/market-data/markets` endpoint - Improved the parsing of the integer query parameter (`includeHistoricalData`) in the `GET api/v1/symbol/:dataSource/:symbol` endpoint +- Harmonized the filter parsing using `groupBy` across various services - Improved the language localization by translating various tooltips across the application - Improved the language localization for Ukrainian (`uk`) - Upgraded `yahoo-finance2` from version `3.14.3` to `3.15.4` diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts index 321624003..4851a1293 100644 --- a/apps/api/src/app/account-balance/account-balance.service.ts +++ b/apps/api/src/app/account-balance/account-balance.service.ts @@ -15,6 +15,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { AccountBalance, Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { format, parseISO } from 'date-fns'; +import { groupBy } from 'lodash'; @Injectable() export class AccountBalanceService { @@ -144,12 +145,12 @@ export class AccountBalanceService { }): Promise { const where: Prisma.AccountBalanceWhereInput = { userId }; - const accountFilter = filters?.find(({ type }) => { - return type === 'ACCOUNT'; + const { ACCOUNT: [filterByAccount] = [] } = groupBy(filters, ({ type }) => { + return type; }); - if (accountFilter) { - where.accountId = accountFilter.id; + if (filterByAccount) { + where.accountId = filterByAccount.id; } if (withExcludedAccounts === false) { diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index e1b01a6ed..f030b73da 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -187,11 +187,11 @@ export class AccountService { where.isExcluded = false; } - const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => { + const { ACCOUNT: filtersByAccount = [] } = groupBy(filters, ({ type }) => { return type; }); - if (filtersByAccount?.length > 0) { + if (filtersByAccount.length > 0) { where.id = { in: filtersByAccount.map(({ id }) => { return id; diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 44ab67e45..55f76f0c9 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -556,26 +556,17 @@ export class ActivitiesService { } const { - ACCOUNT: filtersByAccount, - ASSET_CLASS: filtersByAssetClass, - TAG: filtersByTag + ACCOUNT: filtersByAccount = [], + ASSET_CLASS: filtersByAssetClass = [], + DATA_SOURCE: [filterByDataSource] = [], + SEARCH_QUERY: [filterBySearchQuery] = [], + SYMBOL: [filterBySymbol] = [], + TAG: filtersByTag = [] } = groupBy(filters, ({ type }) => { return type; }); - const filterByDataSource = filters?.find(({ type }) => { - return type === 'DATA_SOURCE'; - })?.id; - - const filterBySymbol = filters?.find(({ type }) => { - return type === 'SYMBOL'; - })?.id; - - const searchQuery = filters?.find(({ type }) => { - return type === 'SEARCH_QUERY'; - })?.id; - - if (filtersByAccount?.length > 0) { + if (filtersByAccount.length > 0) { where.accountId = { in: filtersByAccount.map(({ id }) => { return id; @@ -587,7 +578,7 @@ export class ActivitiesService { where.isDraft = false; } - if (filtersByAssetClass?.length > 0) { + if (filtersByAssetClass.length > 0) { where.SymbolProfile = { OR: [ { @@ -623,8 +614,8 @@ export class ActivitiesService { where.SymbolProfile, { AND: [ - { dataSource: filterByDataSource as DataSource }, - { symbol: filterBySymbol } + { dataSource: filterByDataSource.id as DataSource }, + { symbol: filterBySymbol.id } ] } ] @@ -632,19 +623,19 @@ export class ActivitiesService { } else { where.SymbolProfile = { AND: [ - { dataSource: filterByDataSource as DataSource }, - { symbol: filterBySymbol } + { dataSource: filterByDataSource.id as DataSource }, + { symbol: filterBySymbol.id } ] }; } } - if (searchQuery) { + if (filterBySearchQuery) { const searchQueryWhereInput: Prisma.SymbolProfileWhereInput[] = [ - { id: { mode: 'insensitive', startsWith: searchQuery } }, - { isin: { mode: 'insensitive', startsWith: searchQuery } }, - { name: { mode: 'insensitive', startsWith: searchQuery } }, - { symbol: { mode: 'insensitive', startsWith: searchQuery } } + { id: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { isin: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { name: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { symbol: { mode: 'insensitive', startsWith: filterBySearchQuery.id } } ]; if (where.SymbolProfile) { @@ -663,7 +654,7 @@ export class ActivitiesService { } } - if (filtersByTag?.length > 0) { + if (filtersByTag.length > 0) { where.tags = { some: { OR: filtersByTag.map(({ id }) => { diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts index 3b0828868..68b8d5627 100644 --- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts @@ -133,13 +133,10 @@ export class AssetProfilesService { }; } - const searchQuery = filters.find(({ type }) => { - return type === 'SEARCH_QUERY'; - })?.id; - const { - ASSET_SUB_CLASS: filtersByAssetSubClass, - DATA_SOURCE: filtersByDataSource + ASSET_SUB_CLASS: [filterByAssetSubClass] = [], + DATA_SOURCE: [filterByDataSource] = [], + SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy(filters, ({ type }) => { return type; }); @@ -149,20 +146,20 @@ export class AssetProfilesService { by: ['dataSource', 'symbol'] }); - if (filtersByAssetSubClass) { - where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; + if (filterByAssetSubClass) { + where.assetSubClass = AssetSubClass[filterByAssetSubClass.id]; } - if (filtersByDataSource) { - where.dataSource = DataSource[filtersByDataSource[0].id]; + if (filterByDataSource) { + where.dataSource = DataSource[filterByDataSource.id]; } - if (searchQuery) { + if (filterBySearchQuery) { where.OR = [ - { id: { mode: 'insensitive', startsWith: searchQuery } }, - { isin: { mode: 'insensitive', startsWith: searchQuery } }, - { name: { mode: 'insensitive', startsWith: searchQuery } }, - { symbol: { mode: 'insensitive', startsWith: searchQuery } } + { id: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { isin: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { name: { mode: 'insensitive', startsWith: filterBySearchQuery.id } }, + { symbol: { mode: 'insensitive', startsWith: filterBySearchQuery.id } } ]; } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 4da942cd7..b94b2aa74 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -35,7 +35,7 @@ export class ExportService { userId: string; userSettings: UserSettings; }): Promise { - const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => { + const { ACCOUNT: filtersByAccount = [] } = groupBy(filters, ({ type }) => { return type; }); const platformsMap: { [platformId: string]: Platform } = {}; @@ -59,7 +59,7 @@ export class ExportService { const where: Prisma.AccountWhereInput = { userId }; - if (filtersByAccount?.length > 0) { + if (filtersByAccount.length > 0) { where.id = { in: filtersByAccount.map(({ id }) => { return id; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b440684da..a09661448 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -95,6 +95,7 @@ import { parseISO, set } from 'date-fns'; +import { groupBy } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; @@ -138,20 +139,16 @@ export class PortfolioService { }): Promise { const where: Prisma.AccountWhereInput = { userId }; - const filterByAccount = filters?.find(({ type }) => { - return type === 'ACCOUNT'; - })?.id; - - const filterByDataSource = filters?.find(({ type }) => { - return type === 'DATA_SOURCE'; - })?.id; - - const filterBySymbol = filters?.find(({ type }) => { - return type === 'SYMBOL'; - })?.id; + const { + ACCOUNT: [filterByAccount] = [], + DATA_SOURCE: [filterByDataSource] = [], + SYMBOL: [filterBySymbol] = [] + } = groupBy(filters, ({ type }) => { + return type; + }); if (filterByAccount) { - where.id = filterByAccount; + where.id = filterByAccount.id; } if (filterByDataSource && filterBySymbol) { @@ -159,8 +156,8 @@ export class PortfolioService { some: { SymbolProfile: { AND: [ - { dataSource: filterByDataSource as DataSource }, - { symbol: filterBySymbol } + { dataSource: filterByDataSource.id as DataSource }, + { symbol: filterBySymbol.id } ] } } @@ -274,17 +271,20 @@ export class PortfolioService { let activitiesCount = 0; - const searchQuery = filters.find(({ type }) => { - return type === 'SEARCH_QUERY'; - })?.id; + const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy( + filters, + ({ type }) => { + return type; + } + ); - if (searchQuery) { + if (filterBySearchQuery) { const fuse = new Fuse(accounts, { keys: ['name', 'platform.name'], threshold: 0.3 }); - accounts = fuse.search(searchQuery).map(({ item }) => { + accounts = fuse.search(filterBySearchQuery.id).map(({ item }) => { return item; }); } @@ -376,17 +376,20 @@ export class PortfolioService { let holdings = Object.values(holdingsMap); - const searchQuery = filters.find(({ type }) => { - return type === 'SEARCH_QUERY'; - })?.id; + const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy( + filters, + ({ type }) => { + return type; + } + ); - if (searchQuery) { + if (filterBySearchQuery) { const fuse = new Fuse(holdings, { keys: ['isin', 'name', 'symbol'], threshold: 0.3 }); - holdings = fuse.search(searchQuery).map(({ item }) => { + holdings = fuse.search(filterBySearchQuery.id).map(({ item }) => { return item; }); } diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index f49beab23..5492794d7 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -96,17 +96,17 @@ export class DataService { if (filters && filters.length > 0) { const { - ACCOUNT: filtersByAccount, - ASSET_CLASS: filtersByAssetClass, - ASSET_SUB_CLASS: filtersByAssetSubClass, + ACCOUNT: filtersByAccount = [], + ASSET_CLASS: filtersByAssetClass = [], + ASSET_SUB_CLASS: filtersByAssetSubClass = [], DATA_SOURCE: [filterByDataSource] = [], - HOLDING_TYPE: filtersByHoldingType, - PRESET_ID: filtersByPresetId, - SEARCH_QUERY: filtersBySearchQuery, + HOLDING_TYPE: [filterByHoldingType] = [], + PRESET_ID: [filterByPresetId] = [], + SEARCH_QUERY: [filterBySearchQuery] = [], SYMBOL: [filterBySymbol] = [], - TAG: filtersByTag - } = groupBy(filters, (filter) => { - return filter.type; + TAG: filtersByTag = [] + } = groupBy(filters, ({ type }) => { + return type; }); if (filterByDataSource) { @@ -117,7 +117,7 @@ export class DataService { params = params.append('symbol', filterBySymbol.id); } - if (filtersByAccount) { + if (filtersByAccount.length > 0) { params = params.append( 'accounts', filtersByAccount @@ -128,7 +128,7 @@ export class DataService { ); } - if (filtersByAssetClass) { + if (filtersByAssetClass.length > 0) { params = params.append( 'assetClasses', filtersByAssetClass @@ -139,7 +139,7 @@ export class DataService { ); } - if (filtersByAssetSubClass) { + if (filtersByAssetSubClass.length > 0) { params = params.append( 'assetSubClasses', filtersByAssetSubClass @@ -150,19 +150,19 @@ export class DataService { ); } - if (filtersByHoldingType) { - params = params.append('holdingType', filtersByHoldingType[0].id); + if (filterByHoldingType) { + params = params.append('holdingType', filterByHoldingType.id); } - if (filtersByPresetId) { - params = params.append('presetId', filtersByPresetId[0].id); + if (filterByPresetId) { + params = params.append('presetId', filterByPresetId.id); } - if (filtersBySearchQuery) { - params = params.append('query', filtersBySearchQuery[0].id); + if (filterBySearchQuery) { + params = params.append('query', filterBySearchQuery.id); } - if (filtersByTag) { + if (filtersByTag.length > 0) { params = params.append( 'tags', filtersByTag