22import { isObjectLike , isPlainObject , isZodType } from '@douglasneuroinformatics/libjs' ;
33import type { Language } from '@douglasneuroinformatics/libui/i18n' ;
44import type { AnyUnilingualFormInstrument , FormTypes } from '@opendatacapture/runtime-core' ;
5- import { unparse } from 'papaparse' ;
5+ import { parse , unparse } from 'papaparse' ;
66import { z as z3 } from 'zod/v3' ;
77import { 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+
3345class 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
288461namespace 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
426603export 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