From 7ee9653b1fd1a0cd9c2983ea211e1283fb6b9f5c Mon Sep 17 00:00:00 2001 From: Vijay Sai Date: Fri, 19 Jun 2026 11:59:42 +0530 Subject: [PATCH] Fix detection of not defined values in nested objects hasNotDefinedValuesInObject returned the result of the first nested object it encountered, so sibling keys after a fully defined nested object were never checked. A later null or undefined value (or a later nested object holding one) was therefore missed and the function incorrectly returned false. Only return early on a positive hit and keep scanning the remaining keys otherwise, so the function returns true if any key (nested or not) is null or undefined and false only after all keys are clean. This ensures hasError is set correctly in the portfolio details endpoint. --- CHANGELOG.md | 1 + apps/api/src/helper/object.helper.spec.ts | 111 +++++++++++++++++++++- apps/api/src/helper/object.helper.ts | 4 +- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f964c9cae..38cb48bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue with the localization of the country names +- Fixed the detection of not defined values in nested objects of the portfolio details endpoint ## 3.12.0 - 2026-06-17 diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index ba8760c70..0ff3bcbaf 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,6 +1,10 @@ import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config'; -import { query, redactPaths } from './object.helper'; +import { + hasNotDefinedValuesInObject, + query, + redactPaths +} from './object.helper'; describe('query', () => { it('should get market price from stock API response', () => { @@ -22,6 +26,111 @@ describe('query', () => { }); }); +describe('hasNotDefinedValuesInObject', () => { + it('should return false when all values are defined', () => { + const object = { + currency: 'USD', + market: { + previousClose: 273.04, + price: 271.86 + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(false); + }); + + it('should return true when a top-level value is null', () => { + const object = { + currency: 'USD', + marketPrice: null, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when a top-level value is undefined', () => { + const object = { + currency: 'USD', + marketPrice: undefined, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when a value nested in an object is null', () => { + const object = { + currency: 'USD', + market: { + previousClose: 273.04, + price: null + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later sibling key is null', () => { + const object = { + market: { + previousClose: 273.04, + price: 271.86 + }, + marketPrice: null, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later sibling key is undefined', () => { + const object = { + market: { + previousClose: 273.04, + price: 271.86 + }, + marketPrice: undefined, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return true when the first nested object is fully defined but a later nested object holds null', () => { + const object = { + developedMarkets: { + UNKNOWN: 0, + weight: 1 + }, + emergingMarkets: { + UNKNOWN: null, + weight: 0 + } + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(true); + }); + + it('should return false when a deeply nested object is fully defined and is followed by a defined sibling', () => { + const object = { + holding: { + market: { + region: { + UNKNOWN: 0, + developedMarkets: 1 + } + } + }, + symbol: 'AAPL' + }; + + expect(hasNotDefinedValuesInObject(object)).toBe(false); + }); +}); + describe('redactAttributes', () => { it('should redact provided attributes', () => { expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({}); diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 350d5fe04..822ba30ec 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -7,7 +7,9 @@ export function hasNotDefinedValuesInObject(aObject: Object): boolean { if (aObject[key] === null || aObject[key] === undefined) { return true; } else if (isObject(aObject[key])) { - return hasNotDefinedValuesInObject(aObject[key]); + if (hasNotDefinedValuesInObject(aObject[key])) { + return true; + } } }