Skip to content

Commit 6d50160

Browse files
committed
Clean FHIR resources before saving according to specification
1 parent a88c184 commit 6d50160

File tree

3 files changed

+152
-23
lines changed

3 files changed

+152
-23
lines changed

src/services/fhir.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AxiosRequestConfig } from 'axios';
22
import { AidboxReference, AidboxResource, ValueSet, Bundle, BundleEntry, id } from 'shared/src/contrib/aidbox';
3+
import { cleanEmptyValues, removeNullsFromDicts } from 'utils/fhir';
34

45
import { isFailure, RemoteDataResult, success, failure } from '../libs/remoteData';
56
import { buildQueryParams } from './instance';
@@ -93,17 +94,28 @@ function getInactiveSearchParam(resourceType: string) {
9394

9495
export async function createFHIRResource<R extends AidboxResource>(
9596
resource: R,
96-
searchParams?: SearchParams
97+
searchParams?: SearchParams,
98+
dropNullsFromDicts = true
9799
): Promise<RemoteDataResult<WithId<R>>> {
98-
return service(create(resource, searchParams));
100+
return service(create(resource, searchParams, dropNullsFromDicts));
99101
}
100102

101-
export function create<R extends AidboxResource>(resource: R, searchParams?: SearchParams): AxiosRequestConfig {
103+
export function create<R extends AidboxResource>(
104+
resource: R,
105+
searchParams?: SearchParams,
106+
dropNullsFromDicts = true
107+
): AxiosRequestConfig {
108+
let cleanedResource = resource;
109+
if (dropNullsFromDicts) {
110+
cleanedResource = removeNullsFromDicts(cleanedResource);
111+
}
112+
cleanedResource = cleanEmptyValues(cleanedResource);
113+
102114
return {
103115
method: 'POST',
104-
url: `/${resource.resourceType}`,
116+
url: `/${cleanedResource.resourceType}`,
105117
params: searchParams,
106-
data: resource,
118+
data: cleanedResource,
107119
};
108120
}
109121

@@ -114,23 +126,33 @@ export async function updateFHIRResource<R extends AidboxResource>(
114126
return service(update(resource, searchParams));
115127
}
116128

117-
export function update<R extends AidboxResource>(resource: R, searchParams?: SearchParams): AxiosRequestConfig {
129+
export function update<R extends AidboxResource>(
130+
resource: R,
131+
searchParams?: SearchParams,
132+
dropNullsFromDicts = true
133+
): AxiosRequestConfig {
134+
let cleanedResource = resource;
135+
if (dropNullsFromDicts) {
136+
cleanedResource = removeNullsFromDicts(cleanedResource);
137+
}
138+
cleanedResource = cleanEmptyValues(cleanedResource);
139+
118140
if (searchParams) {
119141
return {
120142
method: 'PUT',
121-
url: `/${resource.resourceType}`,
122-
data: resource,
143+
url: `/${cleanedResource.resourceType}`,
144+
data: cleanedResource,
123145
params: searchParams,
124146
};
125147
}
126148

127-
if (resource.id) {
128-
const versionId = resource.meta && resource.meta.versionId;
149+
if (cleanedResource.id) {
150+
const versionId = cleanedResource.meta && cleanedResource.meta.versionId;
129151

130152
return {
131153
method: 'PUT',
132-
url: `/${resource.resourceType}/${resource.id}`,
133-
data: resource,
154+
url: `/${cleanedResource.resourceType}/${cleanedResource.id}`,
155+
data: cleanedResource,
134156
...(versionId ? { headers: { 'If-Match': versionId } } : {}),
135157
};
136158
}
@@ -236,39 +258,53 @@ export async function findFHIRResource<R extends AidboxResource>(
236258
}
237259
}
238260

239-
export async function saveFHIRResource<R extends AidboxResource>(resource: R): Promise<RemoteDataResult<WithId<R>>> {
240-
return service(save(resource));
261+
export async function saveFHIRResource<R extends AidboxResource>(
262+
resource: R,
263+
dropNullsFromDicts: boolean = true
264+
): Promise<RemoteDataResult<WithId<R>>> {
265+
return service(save(resource, dropNullsFromDicts));
241266
}
242267

243-
export function save<R extends AidboxResource>(resource: R): AxiosRequestConfig {
268+
export function save<R extends AidboxResource>(resource: R, dropNullsFromDicts: boolean = true): AxiosRequestConfig {
244269
const versionId = resource.meta && resource.meta.versionId;
270+
let cleanedResource = resource;
271+
if (dropNullsFromDicts) {
272+
cleanedResource = removeNullsFromDicts(cleanedResource);
273+
}
274+
cleanedResource = cleanEmptyValues(cleanedResource);
245275

246276
return {
247277
method: resource.id ? 'PUT' : 'POST',
248-
data: resource,
278+
data: cleanedResource,
249279
url: `/${resource.resourceType}${resource.id ? '/' + resource.id : ''}`,
250280
...(resource.id && versionId ? { headers: { 'If-Match': versionId } } : {}),
251281
};
252282
}
253283

254284
export async function saveFHIRResources<R extends AidboxResource>(
255285
resources: R[],
256-
bundleType: 'transaction' | 'batch'
286+
bundleType: 'transaction' | 'batch',
287+
dropNullsFromDicts: boolean = true
257288
): Promise<RemoteDataResult<Bundle<WithId<R>>>> {
258289
return service({
259290
method: 'POST',
260291
url: '/',
261292
data: {
262293
type: bundleType,
263294
entry: resources.map((resource) => {
264-
const versionId = resource.meta && resource.meta.versionId;
295+
let cleanedResource = resource;
296+
if (dropNullsFromDicts) {
297+
cleanedResource = removeNullsFromDicts(cleanedResource);
298+
}
299+
cleanedResource = cleanEmptyValues(cleanedResource);
300+
const versionId = cleanedResource.meta && cleanedResource.meta.versionId;
265301

266302
return {
267-
resource,
303+
cleanedResource,
268304
request: {
269-
method: resource.id ? 'PUT' : 'POST',
270-
url: `/${resource.resourceType}${resource.id ? '/' + resource.id : ''}`,
271-
...(resource.id && versionId ? { ifMatch: versionId } : {}),
305+
method: cleanedResource.id ? 'PUT' : 'POST',
306+
url: `/${cleanedResource.resourceType}${cleanedResource.id ? '/' + cleanedResource.id : ''}`,
307+
...(cleanedResource.id && versionId ? { ifMatch: versionId } : {}),
272308
},
273309
};
274310
}),
@@ -395,7 +431,7 @@ export type ResourcesMap<T extends AidboxResource> = {
395431
export function extractBundleResources<T extends AidboxResource>(bundle: Bundle<T>): ResourcesMap<T> {
396432
const entriesByResourceType = {} as ResourcesMap<T>;
397433
const entries = bundle.entry || [];
398-
entries.forEach(function(entry) {
434+
entries.forEach(function (entry) {
399435
const type = entry.resource!.resourceType;
400436
if (!entriesByResourceType[type]) {
401437
entriesByResourceType[type] = [];

src/utils/fhir.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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;
11+
}
12+
13+
export function cleanEmptyValues(data: any): any {
14+
if (Array.isArray(data)) {
15+
return data.map((item) => {
16+
return isEmpty(item) ? null : cleanEmptyValues(item);
17+
});
18+
}
19+
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+
}
27+
}
28+
return cleaned;
29+
}
30+
31+
return data;
32+
}
33+
34+
function isNull(value: any): boolean {
35+
return value === null;
36+
}
37+
38+
export function removeNullsFromDicts(data: any): any {
39+
if (Array.isArray(data)) {
40+
return data.map(removeNullsFromDicts);
41+
}
42+
43+
if (typeof data === 'object' && data !== null) {
44+
const result: Record<string, any> = {};
45+
for (const [key, value] of Object.entries(data)) {
46+
if (!isNull(value)) {
47+
result[key] = removeNullsFromDicts(value);
48+
}
49+
}
50+
return result;
51+
}
52+
53+
return data;
54+
}

tests/utils/fhir.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { cleanEmptyValues, removeNullsFromDicts } from '../../src/utils/fhir';
2+
3+
describe('cleanEmptyValues', () => {
4+
it('cleans empty 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+
27+
describe('removeNullsFromDicts', () => {
28+
it('removes nulls from nested dictionaries but not from arrays', () => {
29+
expect(removeNullsFromDicts({})).toEqual({});
30+
expect(removeNullsFromDicts({ item: [] })).toEqual({ item: [] });
31+
expect(removeNullsFromDicts({ item: [null] })).toEqual({ item: [null] });
32+
expect(removeNullsFromDicts({ item: [null, { item: null }] })).toEqual({
33+
item: [null, {}],
34+
});
35+
expect(removeNullsFromDicts({ item: [null, { item: null }, {}] })).toEqual({
36+
item: [null, {}, {}],
37+
});
38+
});
39+
});

0 commit comments

Comments
 (0)