@@ -37,6 +37,16 @@ interface ContentType {
3737 schema : any [ ] ; // Replace `any` with the specific type if known
3838}
3939
40+ const RESERVED_UIDS = new Set ( [ 'locale' , 'publish_details' , 'tags' ] ) ;
41+
42+ function sanitizeUid ( uid ?: string ) {
43+ if ( ! uid ) return uid ;
44+ let out = uid ?. replace ?.( / [ ^ a - z A - Z 0 - 9 _ ] / g, '_' ) . replace ?.( / ^ _ + / , '' ) ;
45+ if ( ! / ^ [ a - z A - Z ] / . test ( out ) ) out = `field_${ out } ` ;
46+ if ( RESERVED_UIDS . has ( out ) ) out = `cm_${ out } ` ; // avoid reserved values
47+ return out . toLowerCase ( ) ;
48+ }
49+
4050function extractFieldName ( input : string ) : string {
4151 // Extract text inside parentheses (e.g., "JSON Editor-App")
4252 const match = input . match ( / \( ( [ ^ ) ] + ) \) / ) ;
@@ -208,6 +218,11 @@ function getLastSegmentNew(str: string, separator: string): string {
208218}
209219
210220export function buildSchemaTree ( fields : any [ ] , parentUid = '' , parentType = '' ) : any [ ] {
221+
222+ if ( ! Array . isArray ( fields ) ) {
223+ console . warn ( 'buildSchemaTree called with invalid fields:' , fields ) ;
224+ return [ ] ;
225+ }
211226 // Build a lookup map for O(1) access
212227 const fieldMap = new Map < string , any > ( ) ;
213228 fields . forEach ( f => {
@@ -315,8 +330,8 @@ const saveAppMapper = async ({ marketPlacePath, data, fileName }: any) => {
315330
316331const convertToSchemaFormate = ( { field, advanced = false , marketPlacePath, keyMapper } : any ) => {
317332 // Clean up field UID by removing ALL leading underscores
318- const cleanedUid = field ?. uid ?. replace ( / ^ _ + / , '' ) || field ?. uid ;
319-
333+ const rawUid = field ?. uid ;
334+ const cleanedUid = sanitizeUid ( rawUid ) ;
320335 switch ( field ?. contentstackFieldType ) {
321336 case 'single_line_text' : {
322337 return {
@@ -416,18 +431,18 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM
416431
417432 case 'dropdown' : {
418433 // 🔧 CONDITIONAL LOGIC: Check if choices have key-value pairs or just values
419- const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
420- ? field ?. advanced ?. options
434+ const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
435+ ? field ?. advanced ?. options
421436 : [ { value : "NF" } ] ;
422-
437+
423438 // Filter out null/undefined choices and ensure they are valid objects
424- const choices = Array . isArray ( rawChoices )
439+ const choices = Array . isArray ( rawChoices )
425440 ? rawChoices . filter ( ( choice : any ) => choice != null && typeof choice === 'object' )
426441 : [ { value : "NF" } ] ;
427-
428- const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
442+
443+ const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
429444 choices . some ( ( choice : any ) => choice != null && typeof choice === 'object' && choice . key !== undefined && choice . key !== null ) ;
430-
445+
431446 const data = {
432447 "data_type" : [ 'dropdownNumber' , 'radioNumber' , 'ratingNumber' ] . includes ( field . otherCmsType ) ? 'number' : "text" ,
433448 "display_name" : field ?. title ,
@@ -456,18 +471,18 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM
456471 }
457472 case 'radio' : {
458473 // 🔧 CONDITIONAL LOGIC: Check if choices have key-value pairs or just values
459- const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
460- ? field ?. advanced ?. options
474+ const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
475+ ? field ?. advanced ?. options
461476 : [ { value : "NF" } ] ;
462-
477+
463478 // Filter out null/undefined choices and ensure they are valid objects
464- const choices = Array . isArray ( rawChoices )
479+ const choices = Array . isArray ( rawChoices )
465480 ? rawChoices . filter ( ( choice : any ) => choice != null && typeof choice === 'object' )
466481 : [ { value : "NF" } ] ;
467-
468- const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
482+
483+ const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
469484 choices . some ( ( choice : any ) => choice != null && typeof choice === 'object' && choice . key !== undefined && choice . key !== null ) ;
470-
485+
471486 const data = {
472487 "data_type" : [ 'dropdownNumber' , 'radioNumber' , 'ratingNumber' ] . includes ( field . otherCmsType ) ? 'number' : "text" ,
473488 "display_name" : field ?. title ,
@@ -495,18 +510,18 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM
495510 }
496511 case 'checkbox' : {
497512 // 🔧 CONDITIONAL LOGIC: Check if choices have key-value pairs or just values
498- const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
499- ? field ?. advanced ?. options
513+ const rawChoices = Array . isArray ( field ?. advanced ?. options ) && field ?. advanced ?. options ?. length > 0
514+ ? field ?. advanced ?. options
500515 : [ { value : "NF" } ] ;
501-
516+
502517 // Filter out null/undefined choices and ensure they are valid objects
503- const choices = Array . isArray ( rawChoices )
518+ const choices = Array . isArray ( rawChoices )
504519 ? rawChoices . filter ( ( choice : any ) => choice != null && typeof choice === 'object' )
505520 : [ { value : "NF" } ] ;
506-
507- const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
521+
522+ const hasKeyValuePairs = Array . isArray ( choices ) && choices . length > 0 &&
508523 choices . some ( ( choice : any ) => choice != null && typeof choice === 'object' && choice . key !== undefined && choice . key !== null ) ;
509-
524+
510525 const data = {
511526 "data_type" : "text" ,
512527 "display_name" : field ?. title ,
@@ -803,7 +818,7 @@ const convertToSchemaFormate = ({ field, advanced = false, marketPlacePath, keyM
803818 "non_localizable" : field . advanced ?. nonLocalizable ?? false ,
804819 }
805820 } else {
806- console . info ( 'Contnet Type Filed ' , field ?. contentstackField )
821+ console . info ( 'Content Type Field ' , field ?. contentstackField )
807822 }
808823 }
809824 }
@@ -866,22 +881,22 @@ const writeGlobalField = async (schema: any, globalSave: string) => {
866881 return ;
867882 }
868883 }
869-
884+
870885 // 🔧 FIX: Check for duplicates before adding
871886 if ( ! schema || typeof schema !== 'object' ) {
872887 console . error ( "🚀 ~ writeGlobalField ~ Invalid schema provided" ) ;
873888 return ;
874889 }
875-
890+
876891 if ( ! schema . uid ) {
877892 console . error ( "🚀 ~ writeGlobalField ~ Schema missing uid" ) ;
878893 return ;
879894 }
880-
895+
881896 if ( ! Array . isArray ( globalfields ) ) {
882897 globalfields = [ ] ;
883898 }
884-
899+
885900 const existingIndex = globalfields . findIndex ( ( gf : any ) => gf != null && gf . uid === schema . uid ) ;
886901 if ( existingIndex !== - 1 && existingIndex < globalfields . length ) {
887902 // Replace existing global field instead of duplicating
@@ -896,7 +911,7 @@ const writeGlobalField = async (schema: any, globalSave: string) => {
896911 console . error ( "🚀 ~ writeGlobalField ~ Cannot push schema: invalid schema or globalfields array" ) ;
897912 }
898913 }
899-
914+
900915 try {
901916 await fs . promises . writeFile ( filePath , JSON . stringify ( globalfields , null , 2 ) ) ;
902917 } catch ( writeErr ) {
@@ -971,90 +986,40 @@ const mergeTwoCts = async (ct: any, mergeCts: any) => {
971986export const contenTypeMaker = async ( { contentType, destinationStackId, projectId, newStack, keyMapper, region, user_id } : any ) => {
972987 const marketPlacePath = path . join ( process . cwd ( ) , MIGRATION_DATA_CONFIG . DATA , destinationStackId ) ;
973988 const srcFunc = 'contenTypeMaker' ;
989+
974990 let ct : ContentType = {
975991 title : contentType ?. contentstackTitle ,
976992 uid : contentType ?. contentstackUid ,
977993 schema : [ ]
978- }
994+ } ;
995+
979996 let currentCt : any = { } ;
980997 if ( Object ?. keys ?.( keyMapper ) ?. length &&
981998 keyMapper ?. [ contentType ?. contentstackUid ] !== "" &&
982999 keyMapper ?. [ contentType ?. contentstackUid ] !== undefined ) {
9831000 currentCt = await existingCtMapper ( { keyMapper, contentTypeUid : contentType ?. contentstackUid , projectId, region, user_id } ) ;
9841001 }
985- const ctData : any = buildSchemaTree ( contentType ?. fieldMapping ) ;
986- ctData ?. forEach ( ( item : any ) => {
987- if ( item ?. contentstackFieldType === 'group' ) {
988- const group : Group = {
989- "data_type" : "group" ,
990- "display_name" : item ?. contentstackField ,
991- "field_metadata" : { } ,
992- "schema" : [ ] ,
993- "uid" : item ?. contentstackFieldUid ,
994- "multiple" : false ,
995- "mandatory" : false ,
996- "unique" : false
997- }
998- // 🔧 FIX: Track processed fields to prevent duplicates within groups
999- const processedFieldUIDs = new Set ( ) ;
1000-
1001- item ?. schema ?. forEach ( ( element : any ) => {
1002- const fieldUID = extractValue ( element ?. contentstackFieldUid , item ?. contentstackFieldUid , '.' ) ;
1003-
1004- // Skip if this field UID was already processed in this group
1005- if ( processedFieldUIDs . has ( fieldUID ) ) {
1006- return ;
1007- }
1008- processedFieldUIDs . add ( fieldUID ) ;
1009-
1010- const field : any = {
1011- ...element ,
1012- uid : fieldUID ,
1013- title : extractValue ( element ?. contentstackField , item ?. contentstackField , ' >' ) ?. trim ( ) ,
1014- }
1015- const schema : any = convertToSchemaFormate ( {
1016- field,
1017- advanced : element ?. advanced ?? false , // 🔧 FIX: Pass advanced from element data
1018- marketPlacePath,
1019- keyMapper
1020- } ) ;
1021- if ( typeof schema === 'object' && Array . isArray ( group ?. schema ) && element ?. isDeleted === false ) {
1022- group . schema . push ( schema ) ;
1023- }
1024- } )
1025-
1026- // 🔧 FIX: Only add group if it has schema and doesn't already exist
1027- if ( group ?. schema && Array . isArray ( group . schema ) && group . schema . length > 0 && group ?. uid ) {
1028- if ( ! ct ?. schema || ! Array . isArray ( ct . schema ) ) {
1029- ct . schema = [ ] ;
1030- }
1031- const existingGroupIndex = ct . schema . findIndex ( ( g : any ) => g != null && g . uid === group . uid ) ;
1032- if ( existingGroupIndex !== - 1 && existingGroupIndex < ct . schema . length ) {
1033- ct . schema [ existingGroupIndex ] = group ;
1034- } else {
1035- ct . schema . push ( group ) ;
1036- }
1037- }
1038- } else {
1039- const dt : any = convertToSchemaFormate ( {
1040- field : {
1041- ...item ,
1042- title : item ?. contentstackField ,
1043- uid : item ?. contentstackFieldUid
1044- } ,
1045- advanced : item ?. advanced ?? false , // 🔧 FIX: Pass advanced from item data
1046- marketPlacePath,
1047- keyMapper
1048- } ) ;
1049- if ( dt && item ?. isDeleted === false ) {
1050- ct ?. schema ?. push ( dt ) ;
1051- }
1002+
1003+ // Safe: ensures we never pass undefined to the builder
1004+ const ctData : any [ ] = buildSchemaTree ( contentType ?. fieldMapping || [ ] ) ;
1005+
1006+ // Use the deep converter that properly handles groups & modular blocks
1007+ for ( const item of ctData ) {
1008+ if ( item ?. isDeleted === true ) continue ;
1009+
1010+ const fieldSchema = buildFieldSchema ( item , marketPlacePath , '' ) ;
1011+ if ( fieldSchema ) {
1012+ ct ?. schema . push ( fieldSchema ) ;
10521013 }
1053- } )
1014+ }
1015+
1016+ // dedupe by uid to avoid dup nodes after merges
1017+ ct . schema = removeDuplicateFields ( ct . schema || [ ] ) ;
1018+
10541019 if ( currentCt ?. uid ) {
10551020 ct = await mergeTwoCts ( ct , currentCt ) ;
10561021 }
1057- if ( ct ?. uid && ct ?. schema ? .length ) {
1022+ if ( ct ?. uid && Array . isArray ( ct ?. schema ) && ct ?. schema . length ) {
10581023 if ( contentType ?. type === 'global_field' ) {
10591024 const globalSave = path . join ( MIGRATION_DATA_CONFIG . DATA , destinationStackId , GLOBAL_FIELDS_DIR_NAME ) ;
10601025 const message = getLogMessage ( srcFunc , `Global Field ${ ct ?. uid } has been successfully Transformed.` , { } ) ;
@@ -1067,6 +1032,6 @@ export const contenTypeMaker = async ({ contentType, destinationStackId, project
10671032 await saveContent ( ct , contentSave ) ;
10681033 }
10691034 } else {
1070- console . info ( contentType ?. contentstackUid , 'missing' )
1035+ console . info ( contentType ?. contentstackUid , 'missing' ) ;
10711036 }
1072- }
1037+ } ;
0 commit comments