Skip to content

Commit bed8636

Browse files
Merge pull request #70 from beda-software/clean-resource-empty-fields
Clean FHIR resources before saving
2 parents a88c184 + f43498e commit bed8636

5 files changed

Lines changed: 201 additions & 24 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.10.1",
3+
"version": "1.11.0",
44
"scripts": {
55
"build": "tsc & rollup -c",
66
"prebuild": "rimraf lib/* & rimraf dist/*",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './utils/date';
1010
export * from './utils/error';
1111
export * from './utils/tests';
1212
export * from './utils/uuid';
13+
export * from './utils/fhir';
1314

1415
export * from './hooks/bus';
1516
export * from './hooks/service';

src/services/fhir.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +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';
56
import { buildQueryParams } from './instance';
67
import { SearchParams } from './search';
78
import { service } from './service';
@@ -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+
resource: 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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
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);
45+
}
46+
47+
if (typeof data === 'object' && data !== null) {
48+
const result: Record<string, any> = {};
49+
for (const [key, value] of Object.entries(data)) {
50+
if (!isNull(value)) {
51+
result[key] = removeNullsFromDicts(value);
52+
}
53+
}
54+
return result;
55+
}
56+
57+
if (typeof data === 'undefined') {
58+
return null;
59+
}
60+
61+
return data;
62+
}

tests/utils/fhir.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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] });
78+
});

0 commit comments

Comments
 (0)