11/* eslint-disable @typescript-eslint/no-namespace */
22import { isNumberLike , isObjectLike , isPlainObject , isZodType , parseNumber } from '@douglasneuroinformatics/libjs' ;
33import type { Language } from '@douglasneuroinformatics/libui/i18n' ;
4- import type { AnyUnilingualFormInstrument , FormTypes } from '@opendatacapture/runtime-core' ;
4+ import type { AnyUnilingualFormInstrument , FormTypes , Json } from '@opendatacapture/runtime-core' ;
5+ import type { Group } from '@opendatacapture/schemas/group' ;
6+ import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument' ;
7+ import type { UploadInstrumentRecordsData } from '@opendatacapture/schemas/instrument-records' ;
8+ import { encodeScopedSubjectId } from '@opendatacapture/subject-utils' ;
59import { parse , unparse } from 'papaparse' ;
610import { z as z3 } from 'zod/v3' ;
711import { z as z4 } from 'zod/v4' ;
@@ -84,7 +88,7 @@ function extractRecordArrayEntry(entry: string) {
8488}
8589
8690namespace Zod3 {
87- type ZodTypeName = Extract < `${z3 . ZodFirstPartyTypeKind } `, ( typeof ZOD_TYPE_NAMES ) [ number ] > ;
91+ export type ZodTypeName = Extract < `${z3 . ZodFirstPartyTypeKind } `, ( typeof ZOD_TYPE_NAMES ) [ number ] > ;
8892
8993 export type RequiredZodTypeName = Exclude < ZodTypeName , 'ZodEffects' | 'ZodOptional' > ;
9094
@@ -174,7 +178,7 @@ namespace Zod3 {
174178 } ;
175179 }
176180
177- function getZodTypeName ( schema : z3 . ZodTypeAny , isOptional ?: boolean ) : ZodTypeNameResult {
181+ export function getZodTypeName ( schema : z3 . ZodTypeAny , isOptional ?: boolean ) : ZodTypeNameResult {
178182 const def : unknown = schema . _def ;
179183 if ( ! isZodTypeDef ( def ) ) {
180184 console . error ( `Cannot parse ZodType from schema: ${ JSON . stringify ( schema ) } ` ) ;
@@ -322,15 +326,23 @@ namespace Zod3 {
322326 }
323327
324328 //new processInstrumentCSV code
325- export function processInstrumentCSV ( input : File , instrument : AnyUnilingualFormInstrument ) {
329+ export function processInstrumentCSV (
330+ input : File ,
331+ instrument : AnyUnilingualFormInstrument
332+ ) : Promise < UploadOperationResult < FormTypes . Data [ ] > > {
326333 const instrumentSchema = instrument . validationSchema as z3 . AnyZodObject ;
327334 let shape : { [ key : string ] : z3 . ZodTypeAny } = { } ;
328335
329336 let instrumentSchemaWithInternal : z3 . AnyZodObject ;
330337 const instrumentSchemaDef : unknown = instrumentSchema . _def ;
331338 if ( isZodTypeDef ( instrumentSchemaDef ) && isZodEffectsDef ( instrumentSchemaDef ) ) {
332339 if ( ! isZodObject ( instrumentSchemaDef . schema ) ) {
333- return { message : { en : 'Invalid instrument schema' , fr : "Schéma d'instrument invalide" } , success : false } ;
340+ return new Promise < UploadOperationResult < FormTypes . Data [ ] > > ( ( resolve ) => {
341+ return resolve ( {
342+ message : { en : 'Invalid instrument schema' , fr : "Schéma d'instrument invalide" } ,
343+ success : false
344+ } ) ;
345+ } ) ;
334346 }
335347 instrumentSchemaWithInternal = instrumentSchemaDef . schema . extend ( {
336348 date : z3 . coerce . date ( ) ,
@@ -799,8 +811,157 @@ namespace Zod4 {
799811 } ;
800812 }
801813 //to be filled
802- export function processInstrumentCSV ( input : File , instrument : AnyUnilingualFormInstrument ) {
803- return undefined ;
814+ export async function processInstrumentCSV (
815+ input : File ,
816+ instrument : AnyUnilingualFormInstrument
817+ ) : Promise < UploadOperationResult < FormTypes . Data [ ] > > {
818+ const instrumentSchema = instrument . validationSchema as z4 . ZodObject ;
819+ let shape : { [ key : string ] : z3 . ZodTypeAny } = { } ;
820+ let instrumentSchemaWithInternal : z4 . ZodObject ;
821+
822+ if ( instrumentSchema instanceof z4 . ZodObject ) {
823+ instrumentSchemaWithInternal = instrumentSchema . extend ( {
824+ date : z4 . coerce . date ( ) ,
825+ subjectID : z4 . string ( ) . regex ( SUBJECT_ID_REGEX )
826+ } ) ;
827+ shape = instrumentSchemaWithInternal . shape ;
828+ }
829+
830+ return new Promise < UploadOperationResult < FormTypes . Data [ ] > > ( ( resolve ) => {
831+ const reader = new FileReader ( ) ;
832+ reader . onload = ( ) => {
833+ const text = reader . result as string ;
834+ const parseResultCsv = parse < string [ ] > ( text , {
835+ header : false ,
836+ skipEmptyLines : true
837+ } ) ;
838+
839+ const [ headers , ...dataLines ] = parseResultCsv . data ;
840+
841+ //remove sample data if included
842+ if ( dataLines [ 0 ] ?. [ 0 ] ?. startsWith ( MONGOLIAN_VOWEL_SEPARATOR ) ) {
843+ dataLines . shift ( ) ;
844+ }
845+
846+ if ( dataLines . length === 0 ) {
847+ return resolve ( {
848+ message : { en : 'data lines is empty array' , fr : 'les lignes de données sont un tableau vide' } ,
849+ success : false
850+ } ) ;
851+ }
852+
853+ const result : FormTypes . Data [ ] = [ ] ;
854+
855+ if ( ! headers ?. length ) {
856+ return resolve ( {
857+ message : {
858+ en : 'headers is undefined or empty array' ,
859+ fr : 'les en-têtes ne sont pas définis ou constituent un tableau vide'
860+ } ,
861+ success : false
862+ } ) ;
863+ }
864+ let rowNumber = 1 ;
865+ for ( const elements of dataLines ) {
866+ const jsonLine : { [ key : string ] : unknown } = { } ;
867+ for ( let j = 0 ; j < headers . length ; j ++ ) {
868+ const key = headers [ j ] ! . trim ( ) ;
869+ const rawValue = elements [ j ] ! . trim ( ) ;
870+
871+ if ( rawValue === '\n' ) {
872+ continue ;
873+ }
874+ if ( shape [ key ] === undefined ) {
875+ return resolve ( {
876+ message : {
877+ en : `Schema value at column ${ j } is not defined! Please check if Column has been edited/deleted from original template` ,
878+ 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`
879+ } ,
880+ success : false
881+ } ) ;
882+ }
883+ try {
884+ const typeNameResult = Zod3 . getZodTypeName ( shape [ key ] ) ;
885+
886+ let interpreterResult : UploadOperationResult < FormTypes . FieldValue > = {
887+ message : {
888+ en : 'Could not interpret a correct value' ,
889+ fr : "Impossible d'interpréter une valeur correcte"
890+ } ,
891+ success : false
892+ } ;
893+
894+ if ( typeNameResult . typeName === 'ZodArray' || typeNameResult . typeName === 'ZodObject' ) {
895+ // eslint-disable-next-line max-depth
896+ if ( typeNameResult . multiKeys && typeNameResult . multiValues ) {
897+ interpreterResult = Zod3 . interpretZodObjectValue (
898+ rawValue ,
899+ typeNameResult . isOptional ,
900+ typeNameResult . multiValues ,
901+ typeNameResult . multiKeys
902+ ) ;
903+ // TODO - what if this is not the case? Once generics are handled correctly should not be a problem
904+ // Dealt with via else statement for now
905+ } else {
906+ interpreterResult . message = {
907+ en : 'Record Array keys do not exist' ,
908+ fr : "Les clés du tableau d'enregistrements n'existent pas"
909+ } ;
910+ }
911+ } else {
912+ interpreterResult = Zod3 . interpretZodValue (
913+ rawValue ,
914+ typeNameResult . typeName ,
915+ typeNameResult . isOptional
916+ ) ;
917+ }
918+ // if (!interpreterResult.success) {
919+ // return resolve({
920+ // message: {
921+ // en: `${interpreterResult.message.en} at column name: '${key}' and row number ${rowNumber}`,
922+ // fr: `${interpreterResult.message.fr} au nom de colonne : '${key}' et numéro de ligne ${rowNumber}`
923+ // },
924+ // success: false
925+ // });
926+ // }
927+ if ( interpreterResult . success ) jsonLine [ headers [ j ] ! ] = interpreterResult . value ;
928+ } catch ( error : unknown ) {
929+ if ( error instanceof UploadError ) {
930+ return resolve ( {
931+ message : {
932+ en : `${ error . description . en } at column name: '${ key } ' and row number '${ rowNumber } '` ,
933+ fr : `${ error . description . fr } au nom de colonne : '${ key } ' et numéro de ligne '${ rowNumber } `
934+ } ,
935+ success : false
936+ } ) ;
937+ } else {
938+ return resolve ( {
939+ message : {
940+ en : `Error parsing CSV` ,
941+ fr : `Erreur avec la CSV`
942+ } ,
943+ success : false
944+ } ) ;
945+ }
946+ }
947+ }
948+
949+ const zodCheck = instrumentSchemaWithInternal . safeParse ( jsonLine ) ;
950+ if ( ! zodCheck . success ) {
951+ console . error ( zodCheck . error . issues ) ;
952+ const zodIssues = zodCheck . error . issues . map ( ( issue ) => {
953+ return `issue message: \n ${ issue . message } \n path: ${ issue . path . toString ( ) } ` ;
954+ } ) ;
955+ console . error ( `Failed to parse data: ${ JSON . stringify ( jsonLine ) } ` ) ;
956+ return resolve ( { message : { en : zodIssues . join ( ) , fr : zodIssues . join ( ) } , success : false } ) ;
957+ }
958+ result . push ( zodCheck . data as FormTypes . Data ) ;
959+ rowNumber ++ ;
960+ }
961+ resolve ( { success : true , value : result } ) ;
962+ } ;
963+ reader . readAsText ( input ) ;
964+ } ) ;
804965 }
805966}
806967
@@ -819,3 +980,32 @@ export function processInstrumentCSV(input: File, instrument: AnyUnilingualFormI
819980 }
820981 return Zod3 . processInstrumentCSV ( input , instrument ) ;
821982}
983+
984+ export function reformatInstrumentData ( {
985+ currentGroup,
986+ data,
987+ instrument
988+ } : {
989+ currentGroup : Group | null ;
990+ data : FormTypes . Data [ ] ;
991+ instrument : UnilingualInstrumentInfo ;
992+ } ) : UploadInstrumentRecordsData {
993+ const recordsList : { data : Json ; date : Date ; subjectId : string } [ ] = [ ] ;
994+ for ( const dataInfo of data ) {
995+ const { date : dataDate , subjectID : dataSubjectId , ...restOfData } = dataInfo ; // Destructure and extract the rest of the data
996+ const createdRecord = {
997+ data : restOfData as Json ,
998+ date : dataDate as Date ,
999+ subjectId : encodeScopedSubjectId ( dataSubjectId as string , {
1000+ groupName : currentGroup ?. name ?? 'root'
1001+ } )
1002+ } ;
1003+ recordsList . push ( createdRecord ) ;
1004+ }
1005+ const reformatForSending : UploadInstrumentRecordsData = {
1006+ groupId : currentGroup ?. id ,
1007+ instrumentId : instrument . id ,
1008+ records : recordsList
1009+ } ;
1010+ return reformatForSending ;
1011+ }
0 commit comments