From ef71a5821aa55e788f72e5f3a401673055c381f6 Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Wed, 21 May 2025 12:11:27 +0500 Subject: [PATCH 1/4] Add function to clean FHIR resource according to spec - remove fields with null/undefined value - remove fields with empty object/array as a value - trim null/undefined elements of array value - keep null/undefined elements in the array value if there is non empty element with higher index --- src/services/fhir.ts | 32 ++++++------- src/utils/fhir.ts | 74 +++++++++++++--------------- tests/utils/fhir.spec.ts | 101 ++++++++++----------------------------- 3 files changed, 71 insertions(+), 136 deletions(-) diff --git a/src/services/fhir.ts b/src/services/fhir.ts index 9611310..6e9e88f 100644 --- a/src/services/fhir.ts +++ b/src/services/fhir.ts @@ -2,7 +2,7 @@ import { AxiosRequestConfig } from 'axios'; import { AidboxReference, AidboxResource, ValueSet, Bundle, BundleEntry, id } from 'shared/src/contrib/aidbox'; import { isFailure, RemoteDataResult, success, failure } from '../libs/remoteData'; -import { cleanEmptyValues, removeNullsFromDicts } from '../utils/fhir'; +import { cleanObject } from '../utils/fhir'; import { buildQueryParams } from './instance'; import { SearchParams } from './search'; import { service } from './service'; @@ -103,13 +103,12 @@ export async function createFHIRResource( export function create( resource: R, searchParams?: SearchParams, - dropNullsFromDicts = true + needToCleanResource = true ): AxiosRequestConfig { let cleanedResource = resource; - if (dropNullsFromDicts) { - cleanedResource = removeNullsFromDicts(cleanedResource); + if (needToCleanResource) { + cleanedResource = cleanObject(cleanedResource); } - cleanedResource = cleanEmptyValues(cleanedResource); return { method: 'POST', @@ -130,13 +129,12 @@ export async function updateFHIRResource( export function update( resource: R, searchParams?: SearchParams, - dropNullsFromDicts = true + needToCleanResource = true ): AxiosRequestConfig { let cleanedResource = resource; - if (dropNullsFromDicts) { - cleanedResource = removeNullsFromDicts(cleanedResource); + if (needToCleanResource) { + cleanedResource = cleanObject(cleanedResource); } - cleanedResource = cleanEmptyValues(cleanedResource); if (searchParams) { return { @@ -266,13 +264,12 @@ export async function saveFHIRResource( return service(save(resource, dropNullsFromDicts)); } -export function save(resource: R, dropNullsFromDicts: boolean = true): AxiosRequestConfig { - const versionId = resource.meta && resource.meta.versionId; +export function save(resource: R, needToCleanResource = true): AxiosRequestConfig { let cleanedResource = resource; - if (dropNullsFromDicts) { - cleanedResource = removeNullsFromDicts(cleanedResource); + if (needToCleanResource) { + cleanedResource = cleanObject(cleanedResource); } - cleanedResource = cleanEmptyValues(cleanedResource); + const versionId = cleanedResource.meta && cleanedResource.meta.versionId; return { method: resource.id ? 'PUT' : 'POST', @@ -285,7 +282,7 @@ export function save(resource: R, dropNullsFromDicts: export async function saveFHIRResources( resources: R[], bundleType: 'transaction' | 'batch', - dropNullsFromDicts: boolean = true + needToCleanResource = true ): Promise>>> { return service({ method: 'POST', @@ -294,10 +291,9 @@ export async function saveFHIRResources( type: bundleType, entry: resources.map((resource) => { let cleanedResource = resource; - if (dropNullsFromDicts) { - cleanedResource = removeNullsFromDicts(cleanedResource); + if (needToCleanResource) { + cleanedResource = cleanObject(cleanedResource); } - cleanedResource = cleanEmptyValues(cleanedResource); const versionId = cleanedResource.meta && cleanedResource.meta.versionId; return { diff --git a/src/utils/fhir.ts b/src/utils/fhir.ts index 1587b7e..dd92294 100644 --- a/src/utils/fhir.ts +++ b/src/utils/fhir.ts @@ -1,61 +1,53 @@ -function isEmpty(data: any): boolean { - if (Array.isArray(data)) { - return data.length === 0; - } +// function isEmpty(data: any): boolean { +// if (Array.isArray(data)) { +// return data.length === 0; +// } - if (typeof data === 'object' && data !== null) { - return Object.keys(data).length === 0; - } +// if (typeof data === 'object' && data !== null) { +// return Object.keys(data).length === 0; +// } + +// return false; +// } - return false; +function isEmptyObject(data: any) { + return typeof data === 'object' && !Array.isArray(data) && data !== null && Object.keys(data).length === 0; } -export function cleanEmptyValues(data: any): any { +export function cleanObject(data: any, topLevel = true): any { if (Array.isArray(data)) { - return data.map((item) => { - return isEmpty(item) ? null : cleanEmptyValues(item); + const cleanedArray = data.map((item) => { + const cleaned = cleanObject(item, false); + //NOTE: convert undefined → null + return cleaned === undefined ? null : cleaned; }); - } - if (typeof data === 'object' && data !== null) { - const cleaned: Record = {}; - for (const [key, value] of Object.entries(data)) { - const cleanedValue = cleanEmptyValues(value); - if (!isEmpty(cleanedValue)) { - cleaned[key] = cleanedValue; - } + //NOTE: Trim trailing nulls + while (cleanedArray.length > 0 && cleanedArray[cleanedArray.length - 1] === null) { + cleanedArray.pop(); } - return cleaned; - } - - if (typeof data === 'undefined') { - return null; - } - return data; -} - -function isNull(value: any): boolean { - return value === null || value === undefined; -} - -export function removeNullsFromDicts(data: any): any { - if (Array.isArray(data)) { - return data.map(removeNullsFromDicts); + return cleanedArray.length > 0 ? cleanedArray : undefined; } if (typeof data === 'object' && data !== null) { const result: Record = {}; + for (const [key, value] of Object.entries(data)) { - if (!isNull(value)) { - result[key] = removeNullsFromDicts(value); + const cleanedValue = cleanObject(value, false); + + if (cleanedValue !== undefined && cleanedValue !== null && !isEmptyObject(cleanedValue)) { + result[key] = cleanedValue; } } - return result; - } - if (typeof data === 'undefined') { - return null; + const isEmptyResult = Object.keys(result).length === 0; + + if (topLevel && isEmptyResult) { + return {}; + } + + return isEmptyResult ? undefined : result; } return data; diff --git a/tests/utils/fhir.spec.ts b/tests/utils/fhir.spec.ts index f923d58..33783f4 100644 --- a/tests/utils/fhir.spec.ts +++ b/tests/utils/fhir.spec.ts @@ -1,78 +1,25 @@ -import { cleanEmptyValues, removeNullsFromDicts } from '../../src/utils/fhir'; - -describe('cleanEmptyValues', () => { - it('cleans null values from dictionaries and arrays recursively', () => { - expect(cleanEmptyValues({})).toEqual({}); - expect(cleanEmptyValues({ str: '' })).toEqual({ str: '' }); - - expect(cleanEmptyValues({ nested: { nested2: [{}] } })).toEqual({ - nested: { nested2: [null] }, - }); - - expect(cleanEmptyValues({ nested: { nested2: {} } })).toEqual({}); - - expect(cleanEmptyValues({ item: [] })).toEqual({}); - expect(cleanEmptyValues({ item: [null] })).toEqual({ item: [null] }); - - expect(cleanEmptyValues({ item: [null, { item: null }] })).toEqual({ - item: [null, { item: null }], - }); - - expect(cleanEmptyValues({ item: [null, { item: null }, {}] })).toEqual({ - item: [null, { item: null }, null], - }); - }); - - it('cleans undefined values from dictionaries and arrays recursively', () => { - expect(cleanEmptyValues({})).toEqual({}); - expect(cleanEmptyValues({ str: '' })).toEqual({ str: '' }); - - expect(cleanEmptyValues({ nested: { nested2: [{}] } })).toEqual({ - nested: { nested2: [null] }, - }); - - expect(cleanEmptyValues({ nested: { nested2: {} } })).toEqual({}); - - expect(cleanEmptyValues({ item: [] })).toEqual({}); - expect(cleanEmptyValues({ item: [undefined] })).toEqual({ item: [null] }); - - expect(cleanEmptyValues({ item: [undefined, { item: undefined }] })).toEqual({ - item: [null, { item: null }], - }); - - expect(cleanEmptyValues({ item: [undefined, { item: undefined }, {}] })).toEqual({ - item: [null, { item: null }, null], - }); - }); -}); - -describe('removeNullsFromDicts', () => { - it('removes nulls from nested dictionaries but not from arrays', () => { - expect(removeNullsFromDicts({})).toEqual({}); - expect(removeNullsFromDicts({ item: [] })).toEqual({ item: [] }); - expect(removeNullsFromDicts({ item: [null] })).toEqual({ item: [null] }); - expect(removeNullsFromDicts({ item: [null, { item: null }] })).toEqual({ - item: [null, {}], - }); - expect(removeNullsFromDicts({ item: [null, { item: null }, {}] })).toEqual({ - item: [null, {}, {}], - }); - }); - - it('removes undefined from nested dictionaries but not from arrays', () => { - expect(removeNullsFromDicts({})).toEqual({}); - expect(removeNullsFromDicts({ item: [] })).toEqual({ item: [] }); - expect(removeNullsFromDicts({ item: [undefined] })).toEqual({ item: [null] }); - expect(removeNullsFromDicts({ item: [undefined, { item: undefined }] })).toEqual({ - item: [null, {}], - }); - expect(removeNullsFromDicts({ item: [null, { item: null }, {}] })).toEqual({ - item: [null, {}, {}], - }); - }); -}); - -describe('combine two cleaning functions', () => { - const data = { item: [undefined, { item: undefined }, {}] }; - expect(cleanEmptyValues(removeNullsFromDicts(data))).toEqual({ item: [null, null, null] }); +import { cleanObject } from '../../src/utils/fhir'; + +test.each([ + { data: {}, expected: {} }, + { data: { str: '' }, expected: { str: '' } }, + { data: { item: null }, expected: {} }, + { data: { item: undefined }, expected: {} }, + { data: { nested: { nested2: [null, {}] } }, expected: {} }, + { data: { nested: { nested2: [undefined, {}] } }, expected: {} }, + { data: { item: [null, { item: null }, {}] }, expected: {} }, + { data: { item: [undefined, { item: undefined }, {}] }, expected: {} }, + { data: { item: [null, { item: null }] }, expected: {} }, + { data: { item: [undefined, { item: undefined }] }, expected: {} }, + { data: { item: [] }, expected: {} }, + { + data: { nested: { nested2: [null, { nested3: 'some value' }, null] } }, + expected: { nested: { nested2: [null, { nested3: 'some value' }] } }, + }, + { + data: { nested: { nested2: [undefined, { nested3: 'some value' }, undefined] } }, + expected: { nested: { nested2: [null, { nested3: 'some value' }] } }, + }, +])('cleanObnject(). Test case: %o', ({ data, expected }) => { + expect(cleanObject(data)).toEqual(expected); }); From f95ec9860b9190e14d312b5965cf6abc5044fc4d Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Wed, 21 May 2025 14:08:12 +0500 Subject: [PATCH 2/4] 1.11.2-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca76f0b..974f430 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aidbox-react", - "version": "1.11.1", + "version": "1.11.2-0", "scripts": { "build": "tsc & rollup -c", "prebuild": "rimraf lib/* & rimraf dist/*", From b3eab73ffa47b145b376971bbf19c6793e2c365d Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Wed, 21 May 2025 14:27:59 +0500 Subject: [PATCH 3/4] Remove commented code --- src/utils/fhir.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/utils/fhir.ts b/src/utils/fhir.ts index dd92294..306c0c7 100644 --- a/src/utils/fhir.ts +++ b/src/utils/fhir.ts @@ -1,15 +1,3 @@ -// function isEmpty(data: any): boolean { -// if (Array.isArray(data)) { -// return data.length === 0; -// } - -// if (typeof data === 'object' && data !== null) { -// return Object.keys(data).length === 0; -// } - -// return false; -// } - function isEmptyObject(data: any) { return typeof data === 'object' && !Array.isArray(data) && data !== null && Object.keys(data).length === 0; } From 6d781e7d734b39d5dc37d0f8af55b04dca31bfc0 Mon Sep 17 00:00:00 2001 From: Dmitry Shutov Date: Wed, 21 May 2025 14:39:14 +0500 Subject: [PATCH 4/4] Rename dropNullsFromDicts to needToCleanResource --- src/services/fhir.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/fhir.ts b/src/services/fhir.ts index 6e9e88f..ab2ced1 100644 --- a/src/services/fhir.ts +++ b/src/services/fhir.ts @@ -95,9 +95,9 @@ function getInactiveSearchParam(resourceType: string) { export async function createFHIRResource( resource: R, searchParams?: SearchParams, - dropNullsFromDicts = true + needToCleanResource = true ): Promise>> { - return service(create(resource, searchParams, dropNullsFromDicts)); + return service(create(resource, searchParams, needToCleanResource)); } export function create( @@ -121,9 +121,9 @@ export function create( export async function updateFHIRResource( resource: R, searchParams?: SearchParams, - dropNullsFromDicts = true + needToCleanResource = true ): Promise>> { - return service(update(resource, searchParams, dropNullsFromDicts)); + return service(update(resource, searchParams, needToCleanResource)); } export function update( @@ -259,9 +259,9 @@ export async function findFHIRResource( export async function saveFHIRResource( resource: R, - dropNullsFromDicts: boolean = true + needToCleanResource = true ): Promise>> { - return service(save(resource, dropNullsFromDicts)); + return service(save(resource, needToCleanResource)); } export function save(resource: R, needToCleanResource = true): AxiosRequestConfig {