Skip to content

Commit e49155f

Browse files
authored
Merge pull request #1057 from david-roper/large-upload-fix
2 parents 72375fc + 15f3617 commit e49155f

File tree

3 files changed

+129
-98
lines changed

3 files changed

+129
-98
lines changed

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

Lines changed: 54 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
LinearRegressionResults,
1212
UploadInstrumentRecordsData
1313
} from '@opendatacapture/schemas/instrument-records';
14-
import type { InstrumentRecordModel, Prisma, SessionModel } from '@prisma/generated-client';
14+
import { type InstrumentRecordModel, Prisma, type SessionModel } from '@prisma/generated-client';
1515
import { isNumber, pickBy } from 'lodash-es';
1616

1717
import { accessibleQuery } from '@/ability/ability.utils';
@@ -274,91 +274,84 @@ export class InstrumentRecordsService {
274274
);
275275
}
276276

277-
const createdRecordsArray: InstrumentRecordModel[] = [];
278277
const createdSessionsArray: SessionModel[] = [];
279278

280279
try {
281-
for (let i = 0; i < records.length; i++) {
282-
const { data: rawData, date, subjectId } = records[i]!;
283-
await this.createSubjectIfNotFound(subjectId);
284-
285-
const session = await this.sessionsService.create({
286-
date: date,
287-
groupId: groupId ? groupId : null,
288-
subjectData: {
289-
id: subjectId
290-
},
291-
type: 'RETROSPECTIVE'
292-
});
280+
const preProcessedRecords = await Promise.all(
281+
records.map(async (record) => {
282+
const { data: rawData, date, subjectId } = record;
283+
284+
// Validate data
285+
const parseResult = instrument.validationSchema.safeParse(this.parseJson(rawData));
286+
if (!parseResult.success) {
287+
console.error(parseResult.error.issues);
288+
throw new UnprocessableEntityException(
289+
`Data received for record does not pass validation schema of instrument '${instrument.id}'`
290+
);
291+
}
293292

294-
createdSessionsArray.push(session);
293+
// Ensure subject exists
294+
await this.createSubjectIfNotFound(subjectId);
295295

296-
const sessionId = session.id;
296+
const session = await this.sessionsService.create({
297+
date: date,
298+
groupId: groupId ?? null,
299+
subjectData: { id: subjectId },
300+
type: 'RETROSPECTIVE'
301+
});
297302

298-
const parseResult = instrument.validationSchema.safeParse(this.parseJson(rawData));
299-
if (!parseResult.success) {
300-
console.error(parseResult.error.issues);
301-
throw new UnprocessableEntityException(
302-
`Data received for record at index '${i}' does not pass validation schema of instrument '${instrument.id}'`
303-
);
304-
}
303+
createdSessionsArray.push(session);
305304

306-
const createdRecord = await this.instrumentRecordModel.create({
307-
data: {
308-
computedMeasures: instrument.measures
309-
? this.instrumentMeasuresService.computeMeasures(instrument.measures, parseResult.data)
310-
: null,
305+
const computedMeasures = instrument.measures
306+
? this.instrumentMeasuresService.computeMeasures(instrument.measures, parseResult.data)
307+
: null;
308+
309+
return {
310+
computedMeasures,
311311
data: this.serializeData(parseResult.data),
312312
date,
313-
group: groupId
314-
? {
315-
connect: { id: groupId }
316-
}
317-
: undefined,
318-
instrument: {
319-
connect: {
320-
id: instrumentId
321-
}
322-
},
323-
session: {
324-
connect: {
325-
id: sessionId
326-
}
327-
},
328-
subject: {
329-
connect: {
330-
id: subjectId
331-
}
332-
}
333-
}
334-
});
313+
groupId,
314+
instrumentId,
315+
sessionId: session.id,
316+
subjectId
317+
};
318+
})
319+
);
335320

336-
createdRecordsArray.push(createdRecord);
337-
}
338-
} catch (err) {
339-
await this.instrumentRecordModel.deleteMany({
321+
await this.instrumentRecordModel.createMany({
322+
data: preProcessedRecords
323+
});
324+
325+
return this.instrumentRecordModel.findMany({
340326
where: {
341-
id: {
342-
in: createdRecordsArray.map((record) => record.id)
343-
}
327+
groupId,
328+
instrumentId
344329
}
345330
});
331+
} catch (err) {
346332
await this.sessionsService.deleteByIds(createdSessionsArray.map((session) => session.id));
347333
throw err;
348334
}
349-
350-
return createdRecordsArray;
351335
}
352336

353337
private async createSubjectIfNotFound(subjectId: string) {
354338
try {
355-
await this.subjectsService.findById(subjectId);
339+
return await this.subjectsService.findById(subjectId);
356340
} catch (exception) {
357341
if (exception instanceof NotFoundException) {
358342
const addedSubject: CreateSubjectDto = {
359343
id: subjectId
360344
};
361-
await this.subjectsService.create(addedSubject);
345+
try {
346+
return await this.subjectsService.create(addedSubject);
347+
} catch (prismaError) {
348+
if (prismaError instanceof Prisma.PrismaClientKnownRequestError && prismaError.code === 'P2002') {
349+
console.error(prismaError);
350+
return await this.subjectsService.findById(subjectId);
351+
} else {
352+
throw prismaError;
353+
}
354+
}
362355
} else {
363356
throw exception;
364357
}
Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react';
22

3-
import { Button, FileDropzone, Heading } from '@douglasneuroinformatics/libui/components';
3+
import { Button, FileDropzone, Heading, Spinner } from '@douglasneuroinformatics/libui/components';
44
import { useDownload, useTranslation } from '@douglasneuroinformatics/libui/hooks';
55
import { useNotificationsStore } from '@douglasneuroinformatics/libui/hooks';
66
import type { AnyUnilingualFormInstrument } from '@opendatacapture/runtime-core';
@@ -16,6 +16,7 @@ import { createUploadTemplateCSV, processInstrumentCSV, reformatInstrumentData }
1616

1717
export const UploadPage = () => {
1818
const [file, setFile] = useState<File | null>(null);
19+
const [isLoading, setIsLoading] = useState(false);
1920
const download = useDownload();
2021
const addNotification = useNotificationsStore((store) => store.addNotification);
2122
const currentGroup = useAppStore((store) => store.currentGroup);
@@ -31,21 +32,44 @@ export const UploadPage = () => {
3132
};
3233

3334
const handleInstrumentCSV = async () => {
34-
const processedDataResult = await processInstrumentCSV(file!, instrument!);
35-
if (processedDataResult.success) {
36-
const reformattedData = reformatInstrumentData({
37-
currentGroup,
38-
data: processedDataResult.value,
39-
instrument: instrument!
40-
});
41-
uploadInstrumentRecordsMutation.mutate(reformattedData);
42-
} else {
43-
addNotification({
44-
message: processedDataResult.message,
45-
type: 'error'
46-
});
35+
try {
36+
setIsLoading(true);
37+
const processedDataResult = await processInstrumentCSV(file!, instrument!);
38+
if (processedDataResult.success) {
39+
const reformattedData = reformatInstrumentData({
40+
currentGroup,
41+
data: processedDataResult.value,
42+
instrument: instrument!
43+
});
44+
if (reformattedData.records.length > 1000) {
45+
addNotification({
46+
message: t({
47+
en: 'Lots of entries loading, please wait...',
48+
fr: 'Beaucoup de données, veuillez patienter...'
49+
}),
50+
type: 'info'
51+
});
52+
}
53+
await uploadInstrumentRecordsMutation.mutateAsync(reformattedData);
54+
} else {
55+
addNotification({
56+
message: processedDataResult.message,
57+
type: 'error'
58+
});
59+
}
60+
setFile(null);
61+
} catch (error) {
62+
if (error instanceof Error)
63+
addNotification({
64+
message: t({
65+
en: `An error has happended within the request \n '${error.message}'`,
66+
fr: `Une erreur s'est produite lors du téléversement \n '${error.message}'.`
67+
}),
68+
type: 'error'
69+
});
70+
} finally {
71+
setIsLoading(false);
4772
}
48-
setFile(null);
4973
};
5074

5175
if (!instrument) {
@@ -62,28 +86,42 @@ export const UploadPage = () => {
6286
})}
6387
</Heading>
6488
</PageHeader>
65-
<div className="mx-auto flex w-full max-w-3xl flex-grow flex-col justify-center">
66-
<FileDropzone
67-
acceptedFileTypes={{
68-
'text/csv': ['.csv']
69-
}}
70-
className="flex h-80 w-full flex-col"
71-
file={file}
72-
setFile={setFile}
73-
/>
74-
<div className="mt-4 flex justify-between space-x-2">
75-
<Button disabled={!(file && instrument)} variant={'primary'} onClick={handleInstrumentCSV}>
76-
{t('core.submit')}
77-
</Button>
78-
<Button className="gap-1.5" disabled={!instrument} variant={'primary'} onClick={handleTemplateDownload}>
79-
<DownloadIcon />
80-
{t({
81-
en: 'Download Template',
82-
fr: 'Télécharger le modèle'
83-
})}
84-
</Button>
89+
{!isLoading ? (
90+
<div className="mx-auto flex w-full max-w-3xl flex-grow flex-col justify-center">
91+
<FileDropzone
92+
acceptedFileTypes={{
93+
'text/csv': ['.csv']
94+
}}
95+
className="flex h-80 w-full flex-col"
96+
file={file}
97+
setFile={setFile}
98+
/>
99+
<div className="mt-4 flex justify-between space-x-2">
100+
<Button disabled={!(file && instrument)} variant={'primary'} onClick={handleInstrumentCSV}>
101+
{t('core.submit')}
102+
</Button>
103+
<Button className="gap-1.5" disabled={!instrument} variant={'primary'} onClick={handleTemplateDownload}>
104+
<DownloadIcon />
105+
{t({
106+
en: 'Download Template',
107+
fr: 'Télécharger le modèle'
108+
})}
109+
</Button>
110+
</div>
85111
</div>
86-
</div>
112+
) : (
113+
<>
114+
<div className="mx-auto flex w-full max-w-3xl flex-grow flex-col justify-center">
115+
<Spinner className="mx-auto size-1/2"></Spinner>
116+
<Heading className="text-center" variant="h3">
117+
{t({
118+
en: 'Data currently uploading...',
119+
fr: 'Données en cours de téléchargement...'
120+
})}
121+
</Heading>
122+
</div>
123+
</>
124+
)}
87125
</React.Fragment>
88126
);
89127
};

apps/web/src/services/axios.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ axios.interceptors.request.use((config) => {
1313
config.headers.setAccept('application/json');
1414

1515
// Do not set timeout for setup (can be CPU intensive, especially on slow server)
16-
if (config.url !== '/v1/setup') {
16+
if (config.url !== '/v1/setup' && config.url !== '/v1/instrument-records/upload') {
1717
config.timeout = 10000; // abort request after 10 seconds
1818
config.timeoutErrorMessage = i18n.t({
1919
en: 'Network Error',

0 commit comments

Comments
 (0)