@@ -50,66 +50,91 @@ class AirtableSyncWorker {
5050 console . log ( `Deleted ${ deleteResult . count } records no longer in Airtable` ) ;
5151 }
5252
53- for ( const record of records ) {
54- const slug = record . get ( 'slug' ) as string ;
55- const websiteJson = record . get ( 'website_json' ) as string ;
56- const websiteActive = record . get ( 'website_active' ) === true ;
57-
58- if ( ! slug ) {
59- console . warn ( `Skipping record ${ record . id } : missing slug` ) ;
60- continue ;
61- }
53+ // Parse all records first
54+ const parsedRecords = records
55+ . map ( record => {
56+ const slug = record . get ( 'slug' ) as string ;
57+ const websiteJson = record . get ( 'website_json' ) as string ;
58+ const websiteActive = record . get ( 'website_active' ) === true ;
59+
60+ if ( ! slug ) {
61+ console . warn ( `Skipping record ${ record . id } : missing slug` ) ;
62+ return null ;
63+ }
6264
63- let data : any ;
64- try {
65- data = websiteJson ? JSON . parse ( websiteJson ) : { } ;
66- if ( websiteActive && data . version !== VERSION ) {
67- data = {
68- error : "Your JSON is outdated. Please update it!"
65+ let data : any ;
66+ try {
67+ data = websiteJson ? JSON . parse ( websiteJson ) : { } ;
68+ if ( websiteActive && data . version !== VERSION ) {
69+ data = { error : "Your JSON is outdated. Please update it!" } ;
6970 }
71+ } catch {
72+ console . error ( `Error parsing JSON for slug ${ slug } ` ) ;
73+ data = { error : "Error parsing JSON. Make sure the JSON is valid!" } ;
7074 }
71- } catch ( error ) {
72- console . error ( `Error parsing JSON for slug ${ slug } :` , error ) ;
73- data = {
74- error : "Error parsing JSON. Make sure the JSON is valid!"
75- } ;
75+
76+ return { recordId : record . id , slug, data, active : websiteActive } ;
77+ } )
78+ . filter ( ( r ) : r is NonNullable < typeof r > => r !== null ) ;
79+
80+ // Check for duplicate slugs in Airtable data and skip them
81+ const slugCounts = new Map < string , string [ ] > ( ) ;
82+ for ( const r of parsedRecords ) {
83+ const existing = slugCounts . get ( r . slug ) || [ ] ;
84+ existing . push ( r . recordId ) ;
85+ slugCounts . set ( r . slug , existing ) ;
86+ }
87+ const duplicateSlugs = new Set < string > ( ) ;
88+ for ( const [ slug , recordIds ] of slugCounts ) {
89+ if ( recordIds . length > 1 ) {
90+ console . error ( `Duplicate slug "${ slug } " found in Airtable for records: ${ recordIds . join ( ', ' ) } - skipping all` ) ;
91+ duplicateSlugs . add ( slug ) ;
7692 }
93+ }
94+ const deduplicatedRecords = parsedRecords . filter ( r => ! duplicateSlugs . has ( r . slug ) ) ;
7795
78- try {
79- await prisma . satellite . upsert ( {
80- where : { recordId : record . id } ,
81- update : {
82- slug,
83- data,
84- active : websiteActive ,
85- updatedAt : new Date ( ) ,
86- } ,
87- create : {
88- recordId : record . id ,
89- slug,
90- data,
91- active : websiteActive ,
92- } ,
93- } ) ;
94- console . log ( `✓ Synced: ${ slug } - Active: ${ websiteActive } ` ) ;
95- } catch ( upsertError : any ) {
96- if ( upsertError ?. code === 'P2002' ) {
97- const field = upsertError . meta ?. target ?. [ 0 ] || 'unknown' ;
98- const existing = await prisma . satellite . findFirst ( {
99- where : field === 'slug' ? { slug } : { recordId : record . id } ,
100- } ) ;
101- console . error (
102- `Unique constraint violation on "${ field } " for record ${ record . id } (slug: ${ slug } ).` ,
103- existing
104- ? `Conflicting record: id=${ existing . id } , recordId=${ existing . recordId } , slug=${ existing . slug } `
105- : 'Could not find conflicting record.'
106- ) ;
107- } else {
108- console . error ( `Failed to upsert record ${ record . id } (slug: ${ slug } ):` , upsertError ) ;
109- }
96+ // Fetch existing records by recordId
97+ const existingRecords = await prisma . satellite . findMany ( {
98+ where : { recordId : { in : deduplicatedRecords . map ( r => r . recordId ) } } ,
99+ } ) ;
100+ const existingByRecordId = new Map ( existingRecords . map ( r => [ r . recordId , r ] ) ) ;
101+
102+ const toCreate : typeof deduplicatedRecords = [ ] ;
103+ const toUpdate : { id : number ; data : typeof deduplicatedRecords [ 0 ] } [ ] = [ ] ;
104+
105+ for ( const record of deduplicatedRecords ) {
106+ const existing = existingByRecordId . get ( record . recordId ) ;
107+ if ( existing ) {
108+ toUpdate . push ( { id : existing . id , data : record } ) ;
109+ } else {
110+ toCreate . push ( record ) ;
110111 }
111112 }
112113
114+ // Batch create
115+ if ( toCreate . length > 0 ) {
116+ await prisma . satellite . createMany ( { data : toCreate } ) ;
117+ console . log ( `✓ Created ${ toCreate . length } new records` ) ;
118+ }
119+
120+ // Batch update (Prisma doesn't support batch update, so we do it in parallel)
121+ if ( toUpdate . length > 0 ) {
122+ await Promise . all (
123+ toUpdate . map ( ( { id, data } ) =>
124+ prisma . satellite . update ( {
125+ where : { id } ,
126+ data : {
127+ slug : data . slug ,
128+ data : data . data ,
129+ active : data . active ,
130+ updatedAt : new Date ( ) ,
131+ } ,
132+ } )
133+ )
134+ ) ;
135+ console . log ( `✓ Updated ${ toUpdate . length } existing records` ) ;
136+ }
137+
113138 console . log ( `[${ new Date ( ) . toISOString ( ) } ] Sync completed successfully` ) ;
114139 } catch ( error ) {
115140 console . error ( `[${ new Date ( ) . toISOString ( ) } ] Sync failed:` , error ) ;
0 commit comments