@@ -8,7 +8,9 @@ import type { Group } from '@opendatacapture/schemas/group';
88import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument' ;
99import type { UploadInstrumentRecordsData } from '@opendatacapture/schemas/instrument-records' ;
1010import { encodeScopedSubjectId } from '@opendatacapture/subject-utils' ;
11+ import { mapValues } from 'lodash-es' ;
1112import { parse , unparse } from 'papaparse' ;
13+ import type { Merge } from 'type-fest' ;
1214import { z as z3 } from 'zod/v3' ;
1315import z , { z as z4 } from 'zod/v4' ;
1416
@@ -82,88 +84,46 @@ function extractSetEntry(entry: string) {
8284 ) ;
8385}
8486
85- function extractRecordArrayEntry ( entry : string ) {
87+ function extractRecordArrayEntry ( entry : string ) : { [ key : string ] : string } [ ] {
8688 const result = / R E C O R D _ A R R A Y \( \s * ( .* ?) [ \s ; ] * \) / . exec ( entry ) ;
8789 if ( ! result ?. [ 1 ] ) {
8890 throw new Error (
8991 `Failed to extract record array value from entry: '${ entry } ' / Échec de l'extraction de la valeur du tableau d'enregistrements de l'entrée : '${ entry } '`
9092 ) ;
9193 }
94+
9295 const recordArrayDataList = result [ 1 ] . split ( ';' ) ;
9396
9497 if ( recordArrayDataList . at ( - 1 ) === '' ) {
9598 recordArrayDataList . pop ( ) ;
9699 }
97100
98- for ( const listData of recordArrayDataList ) {
99- const recordArrayObject : { [ key : string ] : any } = { } ;
100-
101- const record = listData . split ( ',' ) ;
102-
103- if ( ! record ) {
104- return {
105- message : {
106- en : `Record in the record array was left undefined` ,
107- fr : `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
108- } ,
109- success : false
110- } ;
111- }
112- if ( record . some ( ( str ) => str === '' ) ) {
113- return {
114- message : {
115- en : `One or more of the record array fields was left empty` ,
116- fr : `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
117- } ,
118- success : false
119- } ;
101+ return recordArrayDataList . map ( ( listData ) => {
102+ const recordsList = listData . split ( ',' ) ;
103+ if ( ! recordsList ) {
104+ throw new UploadError ( {
105+ en : `Record in the record array was left undefined` ,
106+ fr : `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
107+ } ) ;
120108 }
121- if ( ! ( zList . length === zKeys . length && zList . length === record . length ) ) {
122- return {
123- message : {
124- en : `Incorrect number of entries for record array` ,
125- fr : `Nombre incorrect d'entrées pour le tableau d'enregistrements`
126- } ,
127- success : false
128- } ;
109+ if ( recordsList . some ( ( str ) => str === '' ) ) {
110+ throw new UploadError ( {
111+ en : `One or more of the record array fields was left empty` ,
112+ fr : `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
113+ } ) ;
129114 }
130- for ( let i = 0 ; i < record . length ; i ++ ) {
131- if ( ! record [ i ] ) {
132- return {
133- message : { en : `Failed to interpret field '${ i } '` , fr : `Échec de l'interprétation du champ '${ i } '` } ,
134- success : false
135- } ;
136- }
137-
138- const recordValue = record [ i ] ! . split ( ':' ) [ 1 ] ! . trim ( ) ;
139-
140- const zListResult = zList [ i ] ! ;
141- if ( ! ( zListResult && zListResult . typeName !== 'ZodArray' && zListResult . typeName !== 'ZodObject' ) ) {
142- return {
143- message : { en : `Failed to interpret field '${ i } '` , fr : `Échec de l'interprétation du champ '${ i } '` } ,
144- success : false
145- } ;
146- }
147- const interpretZodValueResult : UploadOperationResult < FormTypes . FieldValue > = interpretZodValue (
148- recordValue ,
149- zListResult . typeName ,
150- zListResult . isOptional
151- ) ;
152- if ( ! interpretZodValueResult . success ) {
153- return {
154- message : {
155- en : `failed to interpret value at entry ${ i } in record array row ${ listData } ` ,
156- fr : `échec de l'interprétation de la valeur à l'entrée ${ i } dans la ligne de tableau d'enregistrements ${ listData } `
157- } ,
158- success : false
159- } ;
115+ const records : { [ key : string ] : string } = { } ;
116+ recordsList . forEach ( ( rawRecord , i ) => {
117+ const [ recordKey , recordValue ] = rawRecord . split ( ':' ) . map ( ( s ) => s . trim ( ) ) ;
118+ if ( ! ( recordKey && recordValue ) ) {
119+ throw new UploadError ( {
120+ en : `Malformed record at index ${ i } `
121+ } ) ;
160122 }
161-
162- recordArrayObject [ zKeys [ i ] ! ] = interpretZodValueResult . value ;
163- }
164- recordArray . push ( recordArrayObject ) ;
165- }
166- return result [ 1 ] ;
123+ records [ recordKey ] = recordValue ;
124+ } ) ;
125+ return records ;
126+ } ) ;
167127}
168128
169129namespace Zod3 {
@@ -755,25 +715,55 @@ namespace Zod3 {
755715}
756716
757717namespace Zod4 {
758- function parseZodSchema ( schema : unknown , isOptional = false ) : Zod3 . ZodTypeNameResult {
718+ type BaseZodConvertResult = {
719+ isOptional : boolean ;
720+ typeName : Exclude < Zod3 . RequiredZodTypeName , 'ZodArray' | 'ZodEnum' | 'ZodObject' > ;
721+ } ;
722+
723+ type ArrayZodConvertResult = Merge <
724+ BaseZodConvertResult ,
725+ {
726+ innerType : ZodConvertResult ;
727+ typeName : 'ZodArray' ;
728+ }
729+ > ;
730+
731+ type EnumZodConvertResult = Merge <
732+ BaseZodConvertResult ,
733+ {
734+ enumValues ?: readonly string [ ] ;
735+ typeName : 'ZodEnum' ;
736+ }
737+ > ;
738+
739+ type ObjectZodConvertResult = Merge <
740+ BaseZodConvertResult ,
741+ {
742+ dimensions : {
743+ [ key : string ] : ZodConvertResult ;
744+ } ;
745+ typeName : 'ZodObject' ;
746+ }
747+ > ;
748+
749+ type ZodConvertResult = ArrayZodConvertResult | BaseZodConvertResult | EnumZodConvertResult | ObjectZodConvertResult ;
750+
751+ function parseZodSchema ( schema : unknown , isOptional = false ) : ZodConvertResult {
759752 switch ( true ) {
760753 case schema instanceof z4 . ZodArray :
761754 return {
755+ innerType : parseZodSchema ( schema . element ) ,
762756 isOptional,
763- multiValues : [ parseZodSchema ( schema . element ) ] ,
764757 typeName : 'ZodArray'
765758 } ;
766759 case schema instanceof z4 . ZodObject :
767- const multiValues : Zod3 . ZodTypeNameResult [ ] = [ ] ;
768- const multiKeys : string [ ] = [ ] ;
760+ const dimensions : { [ key : string ] : ZodConvertResult } = { } ;
769761 Object . entries ( schema . shape ) . forEach ( ( [ key , value ] ) => {
770- multiKeys . push ( key ) ;
771- multiValues . push ( parseZodSchema ( value ) ) ;
762+ dimensions [ key ] = parseZodSchema ( value ) ;
772763 } ) ;
773764 return {
765+ dimensions,
774766 isOptional,
775- multiKeys,
776- multiValues,
777767 typeName : 'ZodObject'
778768 } ;
779769 case schema instanceof z4 . ZodBoolean :
@@ -815,117 +805,29 @@ namespace Zod4 {
815805 } ) ;
816806 }
817807 }
818-
819- //insert zodObjectValue function
820- export function interpretZodObjectValue (
821- entry : string ,
822- isOptional : boolean ,
823- zList : ZodTypeNameResult [ ] ,
824- zKeys : string [ ]
825- ) : UploadOperationResult < FormTypes . FieldValue > {
826- try {
827- if ( entry === '' && isOptional ) {
828- return { success : true , value : undefined } ;
829- } else if ( ! entry . startsWith ( 'RECORD_ARRAY(' ) ) {
830- return { message : { en : `Invalid ZodType` , fr : `ZodType invalide` } , success : false } ;
831- }
832-
833- const recordArray : { [ key : string ] : any } [ ] = [ ] ;
834- const recordArrayDataEntry = extractRecordArrayEntry ( entry ) ;
835- const recordArrayDataList = recordArrayDataEntry . split ( ';' ) ;
836-
837- if ( recordArrayDataList . at ( - 1 ) === '' ) {
838- recordArrayDataList . pop ( ) ;
839- }
840-
841- for ( const listData of recordArrayDataList ) {
842- const recordArrayObject : { [ key : string ] : any } = { } ;
843-
844- const record = listData . split ( ',' ) ;
845-
846- if ( ! record ) {
847- return {
848- message : {
849- en : `Record in the record array was left undefined` ,
850- fr : `L'enregistrement dans le tableau d'enregistrements n'est pas défini`
851- } ,
852- success : false
853- } ;
854- }
855- if ( record . some ( ( str ) => str === '' ) ) {
808+ function interpetZodConvertResult ( convertResult : ZodConvertResult , entry : string ) {
809+ if ( entry === '' && convertResult . isOptional ) {
810+ return { success : true , value : undefined } ;
811+ }
812+ switch ( convertResult . typeName ) {
813+ case 'ZodArray' :
814+ try {
815+ const parsedRecords = extractRecordArrayEntry ( entry ) . map ( ( parsedRecord ) => {
816+ return mapValues ( parsedRecord , ( entry ) : unknown => interpetZodTypeResult ( convertResult . innerType , entry ) ) ;
817+ } ) ;
856818 return {
857- message : {
858- en : `One or more of the record array fields was left empty` ,
859- fr : `Un ou plusieurs champs du tableau d'enregistrements ont été laissés vides`
860- } ,
861- success : false
819+ success : true ,
820+ value : parsedRecords
862821 } ;
863- }
864- if ( ! ( zList . length === zKeys . length && zList . length === record . length ) ) {
822+ } catch {
865823 return {
866824 message : {
867- en : `Incorrect number of entries for record array` ,
868- fr : `Nombre incorrect d'entrées pour le tableau d'enregistrements`
825+ en : `failed to interpret record array entries ` ,
826+ fr : `échec de l'interprétation des entrées du tableau d'enregistrements`
869827 } ,
870828 success : false
871829 } ;
872830 }
873- for ( let i = 0 ; i < record . length ; i ++ ) {
874- if ( ! record [ i ] ) {
875- return {
876- message : { en : `Failed to interpret field '${ i } '` , fr : `Échec de l'interprétation du champ '${ i } '` } ,
877- success : false
878- } ;
879- }
880-
881- const recordValue = record [ i ] ! . split ( ':' ) [ 1 ] ! . trim ( ) ;
882-
883- const zListResult = zList [ i ] ! ;
884- if ( ! ( zListResult && zListResult . typeName !== 'ZodArray' && zListResult . typeName !== 'ZodObject' ) ) {
885- return {
886- message : { en : `Failed to interpret field '${ i } '` , fr : `Échec de l'interprétation du champ '${ i } '` } ,
887- success : false
888- } ;
889- }
890- const interpretZodValueResult : UploadOperationResult < FormTypes . FieldValue > = interpretZodValue (
891- recordValue ,
892- zListResult . typeName ,
893- zListResult . isOptional
894- ) ;
895- if ( ! interpretZodValueResult . success ) {
896- return {
897- message : {
898- en : `failed to interpret value at entry ${ i } in record array row ${ listData } ` ,
899- fr : `échec de l'interprétation de la valeur à l'entrée ${ i } dans la ligne de tableau d'enregistrements ${ listData } `
900- } ,
901- success : false
902- } ;
903- }
904-
905- recordArrayObject [ zKeys [ i ] ! ] = interpretZodValueResult . value ;
906- }
907- recordArray . push ( recordArrayObject ) ;
908- }
909-
910- return { success : true , value : recordArray } ;
911- } catch {
912- return {
913- message : {
914- en : `failed to interpret record array entries` ,
915- fr : `échec de l'interprétation des entrées du tableau d'enregistrements`
916- } ,
917- success : false
918- } ;
919- }
920- }
921-
922- function interpetZodTypeResult ( zodTypeNameResult : Zod3 . ZodTypeNameResult , entry : string ) {
923- if ( entry === '' && zodTypeNameResult . isOptional ) {
924- return { success : true , value : undefined } ;
925- }
926- switch ( zodTypeNameResult . typeName ) {
927- case 'ZodArray' :
928-
929831 case 'ZodBoolean' :
930832 if ( entry . toLowerCase ( ) === 'true' ) {
931833 return { success : true , value : true } ;
@@ -980,8 +882,8 @@ namespace Zod4 {
980882 default :
981883 return {
982884 message : {
983- en : `Invalid ZodType: ${ zodTypeNameResult . typeName satisfies never } ` ,
984- fr : `ZodType invalide : ${ zodTypeNameResult . typeName satisfies never } `
885+ en : `Invalid ZodType: ${ convertResult . typeName } ` ,
886+ fr : `ZodType invalide : ${ convertResult . typeName } `
985887 } ,
986888 success : false
987889 } ;
@@ -1079,41 +981,8 @@ namespace Zod4 {
1079981 } ) ;
1080982 }
1081983 try {
1082- const typeNameResult = parseZodSchema ( shape [ key ] ) ;
1083-
1084- let interpreterResult : UploadOperationResult < FormTypes . FieldValue > = {
1085- message : {
1086- en : 'Could not interpret a correct value' ,
1087- fr : "Impossible d'interpréter une valeur correcte"
1088- } ,
1089- success : false
1090- } ;
1091-
1092- if ( typeNameResult . typeName === 'ZodArray' || typeNameResult . typeName === 'ZodObject' ) {
1093- // eslint-disable-next-line max-depth
1094- if ( typeNameResult . multiKeys && typeNameResult . multiValues ) {
1095- interpreterResult = Zod3 . interpretZodObjectValue (
1096- rawValue ,
1097- typeNameResult . isOptional ,
1098- typeNameResult . multiValues ,
1099- typeNameResult . multiKeys
1100- ) ;
1101- // TODO - what if this is not the case? Once generics are handled correctly should not be a problem
1102- // Dealt with via else statement for now
1103- } else {
1104- interpreterResult . message = {
1105- en : 'Record Array keys do not exist' ,
1106- fr : "Les clés du tableau d'enregistrements n'existent pas"
1107- } ;
1108- }
1109- } else {
1110- interpreterResult = Zod3 . interpretZodValue (
1111- rawValue ,
1112- typeNameResult . typeName ,
1113- typeNameResult . isOptional
1114- ) ;
1115- }
1116- if ( interpreterResult . success ) jsonLine [ headers [ j ] ! ] = interpreterResult . value ;
984+ const result = interpetZodConvertResult ( parseZodSchema ( shape [ key ] ) , rawValue ) ;
985+ jsonLine [ headers [ i ] ! ] = result ;
1117986 } catch ( error : unknown ) {
1118987 if ( error instanceof UploadError ) {
1119988 return resolve ( {
@@ -1135,7 +1004,7 @@ namespace Zod4 {
11351004 }
11361005 }
11371006
1138- const zodCheck = instrumentSchemaWithInternal . safeParse ( jsonLine ) ;
1007+ const zodCheck = instrumentSchema . safeParse ( jsonLine ) ;
11391008 if ( ! zodCheck . success ) {
11401009 console . error ( zodCheck . error . issues ) ;
11411010 const zodIssues = zodCheck . error . issues . map ( ( issue ) => {
0 commit comments