Skip to content

Commit a74a4cb

Browse files
Merge pull request #71 from beda-software/clean-null-in-arrays
Update logic to clean FHIR resource according to spec
2 parents 8f9fd88 + 6d781e7 commit a74a4cb

4 files changed

Lines changed: 68 additions & 145 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aidbox-react",
3-
"version": "1.11.1",
3+
"version": "1.11.2-0",
44
"scripts": {
55
"build": "tsc & rollup -c",
66
"prebuild": "rimraf lib/* & rimraf dist/*",

src/services/fhir.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AxiosRequestConfig } from 'axios';
22
import { AidboxReference, AidboxResource, ValueSet, Bundle, BundleEntry, id } from 'shared/src/contrib/aidbox';
33

44
import { isFailure, RemoteDataResult, success, failure } from '../libs/remoteData';
5-
import { cleanEmptyValues, removeNullsFromDicts } from '../utils/fhir';
5+
import { cleanObject } from '../utils/fhir';
66
import { buildQueryParams } from './instance';
77
import { SearchParams } from './search';
88
import { service } from './service';
@@ -95,21 +95,20 @@ function getInactiveSearchParam(resourceType: string) {
9595
export async function createFHIRResource<R extends AidboxResource>(
9696
resource: R,
9797
searchParams?: SearchParams,
98-
dropNullsFromDicts = true
98+
needToCleanResource = true
9999
): Promise<RemoteDataResult<WithId<R>>> {
100-
return service(create(resource, searchParams, dropNullsFromDicts));
100+
return service(create(resource, searchParams, needToCleanResource));
101101
}
102102

103103
export function create<R extends AidboxResource>(
104104
resource: R,
105105
searchParams?: SearchParams,
106-
dropNullsFromDicts = true
106+
needToCleanResource = true
107107
): AxiosRequestConfig {
108108
let cleanedResource = resource;
109-
if (dropNullsFromDicts) {
110-
cleanedResource = removeNullsFromDicts(cleanedResource);
109+
if (needToCleanResource) {
110+
cleanedResource = cleanObject(cleanedResource);
111111
}
112-
cleanedResource = cleanEmptyValues(cleanedResource);
113112

114113
return {
115114
method: 'POST',
@@ -122,21 +121,20 @@ export function create<R extends AidboxResource>(
122121
export async function updateFHIRResource<R extends AidboxResource>(
123122
resource: R,
124123
searchParams?: SearchParams,
125-
dropNullsFromDicts = true
124+
needToCleanResource = true
126125
): Promise<RemoteDataResult<WithId<R>>> {
127-
return service(update(resource, searchParams, dropNullsFromDicts));
126+
return service(update(resource, searchParams, needToCleanResource));
128127
}
129128

130129
export function update<R extends AidboxResource>(
131130
resource: R,
132131
searchParams?: SearchParams,
133-
dropNullsFromDicts = true
132+
needToCleanResource = true
134133
): AxiosRequestConfig {
135134
let cleanedResource = resource;
136-
if (dropNullsFromDicts) {
137-
cleanedResource = removeNullsFromDicts(cleanedResource);
135+
if (needToCleanResource) {
136+
cleanedResource = cleanObject(cleanedResource);
138137
}
139-
cleanedResource = cleanEmptyValues(cleanedResource);
140138

141139
if (searchParams) {
142140
return {
@@ -261,18 +259,17 @@ export async function findFHIRResource<R extends AidboxResource>(
261259

262260
export async function saveFHIRResource<R extends AidboxResource>(
263261
resource: R,
264-
dropNullsFromDicts: boolean = true
262+
needToCleanResource = true
265263
): Promise<RemoteDataResult<WithId<R>>> {
266-
return service(save(resource, dropNullsFromDicts));
264+
return service(save(resource, needToCleanResource));
267265
}
268266

269-
export function save<R extends AidboxResource>(resource: R, dropNullsFromDicts: boolean = true): AxiosRequestConfig {
270-
const versionId = resource.meta && resource.meta.versionId;
267+
export function save<R extends AidboxResource>(resource: R, needToCleanResource = true): AxiosRequestConfig {
271268
let cleanedResource = resource;
272-
if (dropNullsFromDicts) {
273-
cleanedResource = removeNullsFromDicts(cleanedResource);
269+
if (needToCleanResource) {
270+
cleanedResource = cleanObject(cleanedResource);
274271
}
275-
cleanedResource = cleanEmptyValues(cleanedResource);
272+
const versionId = cleanedResource.meta && cleanedResource.meta.versionId;
276273

277274
return {
278275
method: resource.id ? 'PUT' : 'POST',
@@ -285,7 +282,7 @@ export function save<R extends AidboxResource>(resource: R, dropNullsFromDicts:
285282
export async function saveFHIRResources<R extends AidboxResource>(
286283
resources: R[],
287284
bundleType: 'transaction' | 'batch',
288-
dropNullsFromDicts: boolean = true
285+
needToCleanResource = true
289286
): Promise<RemoteDataResult<Bundle<WithId<R>>>> {
290287
return service({
291288
method: 'POST',
@@ -294,10 +291,9 @@ export async function saveFHIRResources<R extends AidboxResource>(
294291
type: bundleType,
295292
entry: resources.map((resource) => {
296293
let cleanedResource = resource;
297-
if (dropNullsFromDicts) {
298-
cleanedResource = removeNullsFromDicts(cleanedResource);
294+
if (needToCleanResource) {
295+
cleanedResource = cleanObject(cleanedResource);
299296
}
300-
cleanedResource = cleanEmptyValues(cleanedResource);
301297
const versionId = cleanedResource.meta && cleanedResource.meta.versionId;
302298

303299
return {

src/utils/fhir.ts

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,41 @@
1-
function isEmpty(data: any): boolean {
2-
if (Array.isArray(data)) {
3-
return data.length === 0;
4-
}
5-
6-
if (typeof data === 'object' && data !== null) {
7-
return Object.keys(data).length === 0;
8-
}
9-
10-
return false;
1+
function isEmptyObject(data: any) {
2+
return typeof data === 'object' && !Array.isArray(data) && data !== null && Object.keys(data).length === 0;
113
}
124

13-
export function cleanEmptyValues(data: any): any {
5+
export function cleanObject(data: any, topLevel = true): any {
146
if (Array.isArray(data)) {
15-
return data.map((item) => {
16-
return isEmpty(item) ? null : cleanEmptyValues(item);
7+
const cleanedArray = data.map((item) => {
8+
const cleaned = cleanObject(item, false);
9+
//NOTE: convert undefined → null
10+
return cleaned === undefined ? null : cleaned;
1711
});
18-
}
1912

20-
if (typeof data === 'object' && data !== null) {
21-
const cleaned: Record<string, any> = {};
22-
for (const [key, value] of Object.entries(data)) {
23-
const cleanedValue = cleanEmptyValues(value);
24-
if (!isEmpty(cleanedValue)) {
25-
cleaned[key] = cleanedValue;
26-
}
13+
//NOTE: Trim trailing nulls
14+
while (cleanedArray.length > 0 && cleanedArray[cleanedArray.length - 1] === null) {
15+
cleanedArray.pop();
2716
}
28-
return cleaned;
29-
}
3017

31-
if (typeof data === 'undefined') {
32-
return null;
33-
}
34-
35-
return data;
36-
}
37-
38-
function isNull(value: any): boolean {
39-
return value === null || value === undefined;
40-
}
41-
42-
export function removeNullsFromDicts(data: any): any {
43-
if (Array.isArray(data)) {
44-
return data.map(removeNullsFromDicts);
18+
return cleanedArray.length > 0 ? cleanedArray : undefined;
4519
}
4620

4721
if (typeof data === 'object' && data !== null) {
4822
const result: Record<string, any> = {};
23+
4924
for (const [key, value] of Object.entries(data)) {
50-
if (!isNull(value)) {
51-
result[key] = removeNullsFromDicts(value);
25+
const cleanedValue = cleanObject(value, false);
26+
27+
if (cleanedValue !== undefined && cleanedValue !== null && !isEmptyObject(cleanedValue)) {
28+
result[key] = cleanedValue;
5229
}
5330
}
54-
return result;
55-
}
5631

57-
if (typeof data === 'undefined') {
58-
return null;
32+
const isEmptyResult = Object.keys(result).length === 0;
33+
34+
if (topLevel && isEmptyResult) {
35+
return {};
36+
}
37+
38+
return isEmptyResult ? undefined : result;
5939
}
6040

6141
return data;

tests/utils/fhir.spec.ts

Lines changed: 24 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,25 @@
1-
import { cleanEmptyValues, removeNullsFromDicts } from '../../src/utils/fhir';
2-
3-
describe('cleanEmptyValues', () => {
4-
it('cleans null values from dictionaries and arrays recursively', () => {
5-
expect(cleanEmptyValues({})).toEqual({});
6-
expect(cleanEmptyValues({ str: '' })).toEqual({ str: '' });
7-
8-
expect(cleanEmptyValues({ nested: { nested2: [{}] } })).toEqual({
9-
nested: { nested2: [null] },
10-
});
11-
12-
expect(cleanEmptyValues({ nested: { nested2: {} } })).toEqual({});
13-
14-
expect(cleanEmptyValues({ item: [] })).toEqual({});
15-
expect(cleanEmptyValues({ item: [null] })).toEqual({ item: [null] });
16-
17-
expect(cleanEmptyValues({ item: [null, { item: null }] })).toEqual({
18-
item: [null, { item: null }],
19-
});
20-
21-
expect(cleanEmptyValues({ item: [null, { item: null }, {}] })).toEqual({
22-
item: [null, { item: null }, null],
23-
});
24-
});
25-
26-
it('cleans undefined values from dictionaries and arrays recursively', () => {
27-
expect(cleanEmptyValues({})).toEqual({});
28-
expect(cleanEmptyValues({ str: '' })).toEqual({ str: '' });
29-
30-
expect(cleanEmptyValues({ nested: { nested2: [{}] } })).toEqual({
31-
nested: { nested2: [null] },
32-
});
33-
34-
expect(cleanEmptyValues({ nested: { nested2: {} } })).toEqual({});
35-
36-
expect(cleanEmptyValues({ item: [] })).toEqual({});
37-
expect(cleanEmptyValues({ item: [undefined] })).toEqual({ item: [null] });
38-
39-
expect(cleanEmptyValues({ item: [undefined, { item: undefined }] })).toEqual({
40-
item: [null, { item: null }],
41-
});
42-
43-
expect(cleanEmptyValues({ item: [undefined, { item: undefined }, {}] })).toEqual({
44-
item: [null, { item: null }, null],
45-
});
46-
});
47-
});
48-
49-
describe('removeNullsFromDicts', () => {
50-
it('removes nulls from nested dictionaries but not from arrays', () => {
51-
expect(removeNullsFromDicts({})).toEqual({});
52-
expect(removeNullsFromDicts({ item: [] })).toEqual({ item: [] });
53-
expect(removeNullsFromDicts({ item: [null] })).toEqual({ item: [null] });
54-
expect(removeNullsFromDicts({ item: [null, { item: null }] })).toEqual({
55-
item: [null, {}],
56-
});
57-
expect(removeNullsFromDicts({ item: [null, { item: null }, {}] })).toEqual({
58-
item: [null, {}, {}],
59-
});
60-
});
61-
62-
it('removes undefined from nested dictionaries but not from arrays', () => {
63-
expect(removeNullsFromDicts({})).toEqual({});
64-
expect(removeNullsFromDicts({ item: [] })).toEqual({ item: [] });
65-
expect(removeNullsFromDicts({ item: [undefined] })).toEqual({ item: [null] });
66-
expect(removeNullsFromDicts({ item: [undefined, { item: undefined }] })).toEqual({
67-
item: [null, {}],
68-
});
69-
expect(removeNullsFromDicts({ item: [null, { item: null }, {}] })).toEqual({
70-
item: [null, {}, {}],
71-
});
72-
});
73-
});
74-
75-
describe('combine two cleaning functions', () => {
76-
const data = { item: [undefined, { item: undefined }, {}] };
77-
expect(cleanEmptyValues(removeNullsFromDicts(data))).toEqual({ item: [null, null, null] });
1+
import { cleanObject } from '../../src/utils/fhir';
2+
3+
test.each([
4+
{ data: {}, expected: {} },
5+
{ data: { str: '' }, expected: { str: '' } },
6+
{ data: { item: null }, expected: {} },
7+
{ data: { item: undefined }, expected: {} },
8+
{ data: { nested: { nested2: [null, {}] } }, expected: {} },
9+
{ data: { nested: { nested2: [undefined, {}] } }, expected: {} },
10+
{ data: { item: [null, { item: null }, {}] }, expected: {} },
11+
{ data: { item: [undefined, { item: undefined }, {}] }, expected: {} },
12+
{ data: { item: [null, { item: null }] }, expected: {} },
13+
{ data: { item: [undefined, { item: undefined }] }, expected: {} },
14+
{ data: { item: [] }, expected: {} },
15+
{
16+
data: { nested: { nested2: [null, { nested3: 'some value' }, null] } },
17+
expected: { nested: { nested2: [null, { nested3: 'some value' }] } },
18+
},
19+
{
20+
data: { nested: { nested2: [undefined, { nested3: 'some value' }, undefined] } },
21+
expected: { nested: { nested2: [null, { nested3: 'some value' }] } },
22+
},
23+
])('cleanObnject(). Test case: %o', ({ data, expected }) => {
24+
expect(cleanObject(data)).toEqual(expected);
7825
});

0 commit comments

Comments
 (0)