Skip to content

Commit 272a173

Browse files
authored
Merge pull request #1146 from david-roper/expand-array-export
Expand array export
2 parents 0197f7a + ca6a6ad commit 272a173

File tree

4 files changed

+99
-19
lines changed

4 files changed

+99
-19
lines changed

apps/api/src/instrument-records/instrument-records.service.ts

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { linearRegression } from '@douglasneuroinformatics/libstats';
55
import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
66
import type { Json, ScalarInstrument } from '@opendatacapture/runtime-core';
77
import { DEFAULT_GROUP_NAME } from '@opendatacapture/schemas/core';
8+
import { $RecordArrayFieldValue } from '@opendatacapture/schemas/instrument';
89
import type {
910
CreateInstrumentRecordData,
1011
InstrumentRecord,
@@ -26,6 +27,17 @@ import { SubjectsService } from '@/subjects/subjects.service';
2627

2728
import { InstrumentMeasuresService } from './instrument-measures.service';
2829

30+
type ExpandDataType =
31+
| {
32+
measure: string;
33+
measureValue: boolean | Date | number | string | undefined;
34+
success: true;
35+
}
36+
| {
37+
message: string;
38+
success: false;
39+
};
40+
2941
@Injectable()
3042
export class InstrumentRecordsService {
3143
constructor(
@@ -153,26 +165,53 @@ export class InstrumentRecordsService {
153165
instrument = (await this.instrumentsService.findById(record.instrumentId)) as ScalarInstrument;
154166
instruments.set(record.instrumentId, instrument);
155167
}
156-
157168
for (const [measureKey, measureValue] of Object.entries(record.computedMeasures)) {
158-
data.push({
159-
instrumentEdition: instrument.internal.edition,
160-
instrumentName: instrument.internal.name,
161-
measure: measureKey,
162-
sessionDate: record.session.date.toISOString(),
163-
sessionId: record.session.id,
164-
sessionType: record.session.type,
165-
subjectAge: record.subject.dateOfBirth ? yearsPassed(record.subject.dateOfBirth) : null,
166-
// eslint-disable-next-line perfectionist/sort-objects
167-
groupId: record.subject.groupIds[0] ?? DEFAULT_GROUP_NAME,
168-
subjectId: record.subject.id,
169-
subjectSex: record.subject.sex,
170-
timestamp: record.date.toISOString(),
171-
value: measureValue
172-
});
169+
if (measureValue == null) {
170+
continue;
171+
}
172+
173+
if (!Array.isArray(measureValue)) {
174+
data.push({
175+
groupId: record.subject.groupIds[0] ?? DEFAULT_GROUP_NAME,
176+
instrumentEdition: instrument.internal.edition,
177+
instrumentName: instrument.internal.name,
178+
measure: measureKey,
179+
sessionDate: record.session.date.toISOString(),
180+
sessionId: record.session.id,
181+
sessionType: record.session.type,
182+
subjectAge: record.subject.dateOfBirth ? yearsPassed(record.subject.dateOfBirth) : null,
183+
subjectId: record.subject.id,
184+
subjectSex: record.subject.sex,
185+
timestamp: record.date.toISOString(),
186+
value: measureValue
187+
});
188+
}
189+
190+
if (Array.isArray(measureValue) && measureValue.length < 1) continue;
191+
192+
if (Array.isArray(measureValue) && measureValue.length >= 1) {
193+
const arrayResult = this.expandData(measureValue);
194+
arrayResult.forEach((arrayEntry: ExpandDataType) => {
195+
if (!arrayEntry.success)
196+
throw new Error(`exportRecords: ${instrument.internal.name}.${measureKey}${arrayEntry.message}`);
197+
data.push({
198+
groupId: record.subject.groupIds[0] ?? DEFAULT_GROUP_NAME,
199+
instrumentEdition: instrument.internal.edition,
200+
instrumentName: instrument.internal.name,
201+
measure: `${measureKey} - ${arrayEntry.measure}`,
202+
sessionDate: record.session.date.toISOString(),
203+
sessionId: record.session.id,
204+
sessionType: record.session.type,
205+
subjectAge: record.subject.dateOfBirth ? yearsPassed(record.subject.dateOfBirth) : null,
206+
subjectId: record.subject.id,
207+
subjectSex: record.subject.sex,
208+
timestamp: record.date.toISOString(),
209+
value: arrayEntry.measureValue
210+
});
211+
});
212+
}
173213
}
174214
}
175-
176215
return data;
177216
}
178217

@@ -378,6 +417,30 @@ export class InstrumentRecordsService {
378417
}
379418
}
380419

420+
private expandData(listEntry: any[]): ExpandDataType[] {
421+
const validRecordArrayList: ExpandDataType[] = [];
422+
if (listEntry.length < 1) {
423+
throw new Error('Record Array is Empty');
424+
}
425+
for (const objectEntry of Object.values(listEntry)) {
426+
for (const [dataKey, dataValue] of Object.entries(objectEntry as { [key: string]: any })) {
427+
const parseResult = $RecordArrayFieldValue.safeParse(dataValue);
428+
if (!parseResult.success) {
429+
validRecordArrayList.push({
430+
message: `Error interpreting value ${dataValue} and record array key ${dataKey}`,
431+
success: false
432+
});
433+
}
434+
validRecordArrayList.push({
435+
measure: dataKey,
436+
measureValue: parseResult.data,
437+
success: true
438+
});
439+
}
440+
}
441+
return validRecordArrayList;
442+
}
443+
381444
private getInstrumentById(instrumentId: string) {
382445
return this.instrumentsService
383446
.findById(instrumentId)

apps/web/src/routes/_app/datahub/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,19 @@ const RouteComponent = () => {
103103
return download(`${baseFilename}.json`, JSON.stringify(data, null, 2));
104104
}
105105
})
106-
.catch(console.error);
106+
.then(() => {
107+
addNotification({
108+
message: t({ en: 'Export successful', fr: 'Exportation réussie' }),
109+
type: 'success'
110+
});
111+
})
112+
.catch((err) => {
113+
console.error(err);
114+
addNotification({
115+
message: t({ en: 'Export failed', fr: "Échec de l'exportation" }),
116+
type: 'error'
117+
});
118+
});
107119
};
108120

109121
const lookupSubject = async ({ id }: { id: string }) => {

packages/runtime-core/src/types/instrument.base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RecordArrayFieldValue } from '@douglasneuroinformatics/libui-form-types';
12
import type { LicenseIdentifier } from '@opendatacapture/licenses';
23
import type { ConditionalKeys, Merge, SetRequired } from 'type-fest';
34
import type { z as z3 } from 'zod/v3';
@@ -104,7 +105,7 @@ type UnilingualClientInstrumentDetails = ClientInstrumentDetails<Language>;
104105
type MultilingualClientInstrumentDetails = ClientInstrumentDetails<Language[]>;
105106

106107
/** @public */
107-
type InstrumentMeasureValue = boolean | Date | number | string | undefined;
108+
type InstrumentMeasureValue = boolean | Date | number | RecordArrayFieldValue[] | string | undefined;
108109

109110
/** @public */
110111
type InstrumentMeasureVisibility = 'hidden' | 'visible';

packages/schemas/src/instrument/instrument.base.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,14 @@ const $UnilingualInstrumentDetails = $InstrumentDetails.extend({
9292

9393
const $InstrumentMeasureVisibility: z.ZodType<InstrumentMeasureVisibility> = z.enum(['hidden', 'visible']);
9494

95+
const $RecordArrayFieldValue = z.union([z.string(), z.boolean(), z.number(), z.date(), z.undefined()]);
96+
9597
const $InstrumentMeasureValue: z.ZodType<InstrumentMeasureValue> = z.union([
9698
z.string(),
9799
z.boolean(),
98100
z.number(),
99101
z.date(),
102+
z.array($RecordArrayFieldValue),
100103
z.undefined()
101104
]);
102105

@@ -241,6 +244,7 @@ export {
241244
$InstrumentLanguage,
242245
$InstrumentMeasureValue,
243246
$InstrumentValidationSchema,
247+
$RecordArrayFieldValue,
244248
$ScalarInstrument,
245249
$ScalarInstrumentBundleContainer,
246250
$ScalarInstrumentInternal,

0 commit comments

Comments
 (0)