Skip to content

Commit 28d1dd9

Browse files
committed
refactor: starting integration of processInstrumentCSV
1 parent 83f3bac commit 28d1dd9

1 file changed

Lines changed: 186 additions & 1 deletion

File tree

apps/web/src/utils/upload2.ts

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { isObjectLike, isPlainObject, isZodType } from '@douglasneuroinformatics/libjs';
33
import type { Language } from '@douglasneuroinformatics/libui/i18n';
44
import type { AnyUnilingualFormInstrument, FormTypes } from '@opendatacapture/runtime-core';
5-
import { unparse } from 'papaparse';
5+
import { parse, unparse } from 'papaparse';
66
import { z as z3 } from 'zod/v3';
77
import { z as z4 } from 'zod/v4';
88

@@ -30,6 +30,18 @@ type CSVUploadTemplate = {
3030
filename: string;
3131
};
3232

33+
type UploadOperationResult<T> =
34+
| {
35+
message: { en: string; fr: string };
36+
success: false;
37+
}
38+
| {
39+
success: true;
40+
value: T;
41+
};
42+
43+
const SUBJECT_ID_REGEX = /^[^$\s]+$/;
44+
3345
class UploadError extends Error {
3446
description: {
3547
[L in Language]?: string;
@@ -103,6 +115,10 @@ namespace Zod3 {
103115
return isZodArrayDef(def) && isZodObject(def.type);
104116
}
105117

118+
function isZodEffectsDef(def: AnyZodTypeDef): def is z3.ZodEffectsDef {
119+
return def.typeName === z3.ZodFirstPartyTypeKind.ZodEffects;
120+
}
121+
106122
function interpretZodArray(def: ZodObjectArrayDef, isOptional?: boolean): ZodTypeNameResult {
107123
const listOfZodElements: ZodTypeNameResult[] = [];
108124
const listOfZodKeys: string[] = [];
@@ -283,6 +299,163 @@ namespace Zod3 {
283299
filename: getTemplateFilename(instrumentInternal)
284300
};
285301
}
302+
303+
//new processInstrumentCSV code
304+
export function processInstrumentCSV(input: File, instrument: AnyUnilingualFormInstrument) {
305+
const instrumentSchema = instrument.validationSchema as z3.AnyZodObject;
306+
let shape: { [key: string]: z3.ZodTypeAny } = {};
307+
308+
let instrumentSchemaWithInternal: z3.AnyZodObject;
309+
const instrumentSchemaDef: unknown = instrumentSchema._def;
310+
if (isZodTypeDef(instrumentSchemaDef) && isZodEffectsDef(instrumentSchemaDef)) {
311+
if (!isZodObject(instrumentSchemaDef.schema)) {
312+
return { message: { en: 'Invalid instrument schema', fr: "Schéma d'instrument invalide" }, success: false };
313+
}
314+
instrumentSchemaWithInternal = instrumentSchemaDef.schema.extend({
315+
date: z3.coerce.date(),
316+
subjectID: z3.string().regex(SUBJECT_ID_REGEX)
317+
});
318+
shape = (instrumentSchemaWithInternal._def as z3.ZodObjectDef).shape() as { [key: string]: z3.ZodTypeAny };
319+
} else {
320+
instrumentSchemaWithInternal = instrumentSchema.extend({
321+
date: z3.coerce.date(),
322+
subjectID: z3.string().regex(SUBJECT_ID_REGEX)
323+
});
324+
shape = instrumentSchemaWithInternal.shape as { [key: string]: z3.ZodTypeAny };
325+
}
326+
return new Promise<UploadOperationResult<FormTypes.Data[]>>((resolve) => {
327+
const reader = new FileReader();
328+
reader.onload = () => {
329+
const text = reader.result as string;
330+
const parseResultCsv = parse<string[]>(text, {
331+
header: false,
332+
skipEmptyLines: true
333+
});
334+
335+
const [headers, ...dataLines] = parseResultCsv.data;
336+
337+
//remove sample data if included
338+
if (dataLines[0]?.[0]?.startsWith(MONGOLIAN_VOWEL_SEPARATOR)) {
339+
dataLines.shift();
340+
}
341+
342+
if (dataLines.length === 0) {
343+
return resolve({
344+
message: { en: 'data lines is empty array', fr: 'les lignes de données sont un tableau vide' },
345+
success: false
346+
});
347+
}
348+
349+
const result: FormTypes.Data[] = [];
350+
351+
if (!headers?.length) {
352+
return resolve({
353+
message: {
354+
en: 'headers is undefined or empty array',
355+
fr: 'les en-têtes ne sont pas définis ou constituent un tableau vide'
356+
},
357+
success: false
358+
});
359+
}
360+
let rowNumber = 1;
361+
for (const elements of dataLines) {
362+
const jsonLine: { [key: string]: unknown } = {};
363+
for (let j = 0; j < headers.length; j++) {
364+
const key = headers[j]!.trim();
365+
const rawValue = elements[j]!.trim();
366+
367+
if (rawValue === '\n') {
368+
continue;
369+
}
370+
if (shape[key] === undefined) {
371+
return resolve({
372+
message: {
373+
en: `Schema value at column ${j} is not defined! Please check if Column has been edited/deleted from original template`,
374+
fr: `La valeur du schéma à la colonne ${j} n'est pas définie ! Veuillez vérifier si la colonne a été modifiée/supprimée du modèle original`
375+
},
376+
success: false
377+
});
378+
}
379+
try {
380+
const typeNameResult = getZodTypeName(shape[key]);
381+
382+
let interpreterResult: UploadOperationResult<FormTypes.FieldValue> = {
383+
message: {
384+
en: 'Could not interpret a correct value',
385+
fr: "Impossible d'interpréter une valeur correcte"
386+
},
387+
success: false
388+
};
389+
390+
if (typeNameResult.typeName === 'ZodArray' || typeNameResult.typeName === 'ZodObject') {
391+
// eslint-disable-next-line max-depth
392+
if (typeNameResult.multiKeys && typeNameResult.multiValues) {
393+
interpreterResult = interpretZodObjectValue(
394+
rawValue,
395+
typeNameResult.isOptional,
396+
typeNameResult.multiValues,
397+
typeNameResult.multiKeys
398+
);
399+
// TODO - what if this is not the case? Once generics are handled correctly should not be a problem
400+
// Dealt with via else statement for now
401+
} else {
402+
interpreterResult.message = {
403+
en: 'Record Array keys do not exist',
404+
fr: "Les clés du tableau d'enregistrements n'existent pas"
405+
};
406+
}
407+
} else {
408+
interpreterResult = interpretZodValue(rawValue, typeNameResult.typeName, typeNameResult.isOptional);
409+
}
410+
if (!interpreterResult.success) {
411+
return resolve({
412+
message: {
413+
en: `${interpreterResult.message.en} at column name: '${key}' and row number ${rowNumber}`,
414+
fr: `${interpreterResult.message.fr} au nom de colonne : '${key}' et numéro de ligne ${rowNumber}`
415+
},
416+
success: false
417+
});
418+
}
419+
jsonLine[headers[j]!] = interpreterResult.value;
420+
} catch (error: unknown) {
421+
if (error instanceof UploadError) {
422+
return resolve({
423+
message: {
424+
en: `${UploadError.description.en} at column name: '${key}' and row number`,
425+
fr: `${UploadError.description.fr} au nom de colonne : '${key}'`
426+
},
427+
success: false
428+
});
429+
} else {
430+
return resolve({
431+
message: {
432+
en: `Error parsing CSV`,
433+
fr: `Erreur avec la CSV`
434+
},
435+
success: false
436+
});
437+
}
438+
}
439+
}
440+
441+
const zodCheck = instrumentSchemaWithInternal.safeParse(jsonLine);
442+
443+
if (!zodCheck.success) {
444+
console.error(zodCheck.error.issues);
445+
const zodIssues = zodCheck.error.issues.map((issue) => {
446+
return `issue message: \n ${issue.message} \n path: ${issue.path.toString()}`;
447+
});
448+
console.error(`Failed to parse data: ${JSON.stringify(jsonLine)}`);
449+
return resolve({ message: { en: zodIssues.join(), fr: zodIssues.join() }, success: false });
450+
}
451+
result.push(zodCheck.data);
452+
rowNumber++;
453+
}
454+
resolve({ success: true, value: result });
455+
};
456+
reader.readAsText(input);
457+
});
458+
}
286459
}
287460

288461
namespace Zod4 {
@@ -421,6 +594,10 @@ namespace Zod4 {
421594
filename: getTemplateFilename(instrumentInternal)
422595
};
423596
}
597+
//to be filled
598+
export function processInstrumentCSV(input: File, instrument: AnyUnilingualFormInstrument) {
599+
return undefined;
600+
}
424601
}
425602

426603
export function createUploadTemplateCSV(instrument: AnyUnilingualFormInstrument) {
@@ -430,3 +607,11 @@ export function createUploadTemplateCSV(instrument: AnyUnilingualFormInstrument)
430607
}
431608
return Zod3.createUploadTemplateCSV(instrumentSchema, instrument.internal);
432609
}
610+
611+
//new process instrument csv methods
612+
export function processInstrumentCSV(input: File, instrument: AnyUnilingualFormInstrument) {
613+
if (isZodType(instrument, { version: 4 })) {
614+
return Zod4.processInstrumentCSV(input, instrument);
615+
}
616+
return Zod3.processInstrumentCSV(input, instrument);
617+
}

0 commit comments

Comments
 (0)