|
3 | 3 | /** |
4 | 4 | * Sync Geometries Table with Storage Bucket |
5 | 5 | * |
6 | | - * This script ensures all GeoJSON files in the storage bucket are registered |
7 | | - * in the service_area_geometries table with proper metadata. |
| 6 | + * This script ensures the geometries table matches the storage bucket exactly: |
| 7 | + * - Inserts new entries for files that exist in bucket but not in table |
| 8 | + * - Updates entries with incorrect storage_url (e.g., pointing to wrong bucket) |
| 9 | + * - Removes orphaned entries for files that no longer exist in bucket |
| 10 | + * - Removes orphaned files from bucket that don't exist locally |
8 | 11 | * |
9 | 12 | * Usage: |
10 | 13 | * STAGING=true node sync-geometries-table.js (syncs staging) |
|
13 | 16 |
|
14 | 17 | import { createClient } from '@supabase/supabase-js' |
15 | 18 | import { config } from 'dotenv' |
| 19 | +import { readdirSync } from 'fs' |
16 | 20 |
|
17 | 21 | // Load .env file |
18 | 22 | if (!process.env.GITHUB_ACTIONS) { |
@@ -69,52 +73,156 @@ async function syncGeometriesTable() { |
69 | 73 | const existingNames = new Set(existingEntries?.map(e => e.geometry_name) || []) |
70 | 74 | console.log(` Found ${existingNames.size} existing entries in table\n`) |
71 | 75 |
|
72 | | - // Step 3: Insert missing entries |
| 76 | + // Step 3: Upsert entries (insert new, update existing with correct URLs) |
73 | 77 | console.log('📝 Syncing table with storage...') |
74 | 78 |
|
75 | 79 | let inserted = 0 |
76 | | - let skipped = 0 |
| 80 | + let updated = 0 |
| 81 | + let unchanged = 0 |
77 | 82 | let failed = 0 |
78 | 83 |
|
| 84 | + // Get full existing entries to compare URLs |
| 85 | + const { data: fullExistingEntries } = await supabase |
| 86 | + .from(table) |
| 87 | + .select('geometry_name, storage_url') |
| 88 | + |
| 89 | + const existingUrlMap = new Map( |
| 90 | + (fullExistingEntries || []).map(e => [e.geometry_name, e.storage_url]) |
| 91 | + ) |
| 92 | + |
79 | 93 | for (const file of geojsonFiles) { |
80 | 94 | const geometryName = file.name.replace('.geojson', '') |
81 | 95 |
|
82 | | - if (existingNames.has(geometryName)) { |
83 | | - console.log(` ⏭️ ${geometryName} (already exists)`) |
84 | | - skipped++ |
85 | | - continue |
86 | | - } |
87 | | - |
88 | | - // Create public URL |
| 96 | + // Create correct public URL for this environment's bucket |
89 | 97 | const { data: { publicUrl } } = supabase.storage |
90 | 98 | .from(bucket) |
91 | 99 | .getPublicUrl(file.name) |
92 | 100 |
|
93 | | - // Insert into table |
94 | | - const { error: insertError } = await supabase |
95 | | - .from(table) |
96 | | - .insert({ |
97 | | - geometry_name: geometryName, |
98 | | - display_name: geometryName.replace(/-/g, ' '), |
99 | | - storage_url: publicUrl, |
100 | | - file_size: file.metadata?.size || 0, |
101 | | - created_at: file.created_at || new Date().toISOString() |
102 | | - }) |
103 | | - |
104 | | - if (insertError) { |
105 | | - console.error(` ❌ ${geometryName}: ${insertError.message}`) |
106 | | - failed++ |
| 101 | + const existingUrl = existingUrlMap.get(geometryName) |
| 102 | + |
| 103 | + if (existingUrl === publicUrl) { |
| 104 | + // URL is already correct, skip |
| 105 | + unchanged++ |
| 106 | + continue |
| 107 | + } |
| 108 | + |
| 109 | + if (existingNames.has(geometryName)) { |
| 110 | + // Entry exists but URL is wrong - update it |
| 111 | + const { error: updateError } = await supabase |
| 112 | + .from(table) |
| 113 | + .update({ |
| 114 | + storage_url: publicUrl, |
| 115 | + file_size: file.metadata?.size || 0 |
| 116 | + }) |
| 117 | + .eq('geometry_name', geometryName) |
| 118 | + |
| 119 | + if (updateError) { |
| 120 | + console.error(` ❌ ${geometryName}: ${updateError.message}`) |
| 121 | + failed++ |
| 122 | + } else { |
| 123 | + console.log(` 🔄 ${geometryName} (updated URL)`) |
| 124 | + updated++ |
| 125 | + } |
107 | 126 | } else { |
108 | | - console.log(` ✅ ${geometryName} (inserted)`) |
109 | | - inserted++ |
| 127 | + // New entry - insert it |
| 128 | + const { error: insertError } = await supabase |
| 129 | + .from(table) |
| 130 | + .insert({ |
| 131 | + geometry_name: geometryName, |
| 132 | + display_name: geometryName.replace(/-/g, ' '), |
| 133 | + storage_url: publicUrl, |
| 134 | + file_size: file.metadata?.size || 0, |
| 135 | + created_at: file.created_at || new Date().toISOString() |
| 136 | + }) |
| 137 | + |
| 138 | + if (insertError) { |
| 139 | + console.error(` ❌ ${geometryName}: ${insertError.message}`) |
| 140 | + failed++ |
| 141 | + } else { |
| 142 | + console.log(` ✅ ${geometryName} (inserted)`) |
| 143 | + inserted++ |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + // Step 4: Read local geometry files to determine what should exist |
| 149 | + console.log('\n📂 Reading local geometry files...') |
| 150 | + let localFiles |
| 151 | + try { |
| 152 | + localFiles = readdirSync('./geometries') |
| 153 | + .filter(f => f.endsWith('.geojson')) |
| 154 | + .map(f => f.replace('.geojson', '')) |
| 155 | + } catch (err) { |
| 156 | + console.log(' ⚠️ Could not read local geometries folder (running in CI?)') |
| 157 | + localFiles = null |
| 158 | + } |
| 159 | + |
| 160 | + const localFileSet = localFiles ? new Set(localFiles) : null |
| 161 | + if (localFileSet) { |
| 162 | + console.log(` Found ${localFileSet.size} local geometry files`) |
| 163 | + } |
| 164 | + |
| 165 | + // Step 5: Clean up orphans (in table but not in local files) |
| 166 | + let deletedFromTable = 0 |
| 167 | + let deletedFromBucket = 0 |
| 168 | + |
| 169 | + if (localFileSet) { |
| 170 | + console.log('\n🧹 Cleaning up orphaned entries...') |
| 171 | + |
| 172 | + // Find orphans in table |
| 173 | + const bucketFileNames = new Set(geojsonFiles.map(f => f.name.replace('.geojson', ''))) |
| 174 | + const tableOrphans = [...existingNames].filter(name => !localFileSet.has(name)) |
| 175 | + const bucketOrphans = [...bucketFileNames].filter(name => !localFileSet.has(name)) |
| 176 | + |
| 177 | + // Delete orphans from table |
| 178 | + if (tableOrphans.length > 0) { |
| 179 | + console.log(` Found ${tableOrphans.length} orphaned table entries`) |
| 180 | + const { error: deleteError } = await supabase |
| 181 | + .from(table) |
| 182 | + .delete() |
| 183 | + .in('geometry_name', tableOrphans) |
| 184 | + |
| 185 | + if (deleteError) { |
| 186 | + console.error(` ❌ Error deleting from table: ${deleteError.message}`) |
| 187 | + } else { |
| 188 | + deletedFromTable = tableOrphans.length |
| 189 | + for (const orphan of tableOrphans) { |
| 190 | + console.log(` 🗑️ ${orphan} (removed from table)`) |
| 191 | + } |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + // Delete orphans from bucket |
| 196 | + if (bucketOrphans.length > 0) { |
| 197 | + console.log(` Found ${bucketOrphans.length} orphaned bucket files`) |
| 198 | + const filesToDelete = bucketOrphans.map(name => name + '.geojson') |
| 199 | + const { error: storageError } = await supabase.storage |
| 200 | + .from(bucket) |
| 201 | + .remove(filesToDelete) |
| 202 | + |
| 203 | + if (storageError) { |
| 204 | + console.error(` ❌ Error deleting from bucket: ${storageError.message}`) |
| 205 | + } else { |
| 206 | + deletedFromBucket = bucketOrphans.length |
| 207 | + for (const orphan of bucketOrphans) { |
| 208 | + console.log(` 🗑️ ${orphan} (removed from bucket)`) |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + if (tableOrphans.length === 0 && bucketOrphans.length === 0) { |
| 214 | + console.log(' ✅ No orphans found') |
110 | 215 | } |
111 | 216 | } |
112 | 217 |
|
113 | 218 | console.log('\n📊 Sync Summary:') |
114 | 219 | console.log(` ✅ Inserted: ${inserted}`) |
115 | | - console.log(` ⏭️ Skipped (existing): ${skipped}`) |
| 220 | + console.log(` 🔄 Updated: ${updated}`) |
| 221 | + console.log(` ⏭️ Unchanged: ${unchanged}`) |
| 222 | + console.log(` 🗑️ Deleted from table: ${deletedFromTable}`) |
| 223 | + console.log(` 🗑️ Deleted from bucket: ${deletedFromBucket}`) |
116 | 224 | console.log(` ❌ Failed: ${failed}`) |
117 | | - console.log(` 📁 Total files: ${geojsonFiles.length}`) |
| 225 | + console.log(` 📁 Local files: ${localFileSet ? localFileSet.size : 'N/A'}`) |
118 | 226 |
|
119 | 227 | if (failed === 0) { |
120 | 228 | console.log('\n✅ Geometries table synced successfully!') |
|
0 commit comments