refactor(scripts): add data migration script from Turso to Convex
- commit
- d258a481e495febe0485dbf571228f41a714e384
- author
- bnema · · 2026-01-18 18:35 UTC
- parent
- b3e6822
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +266 -0
A scripts/migrate-data-to-convex.ts +201 -0
| @@ -0,0 +1,201 @@ | |||
| 1 | + | /** | |
| 2 | + | * Migration script to import Turso data to Convex with proper ID mapping. | |
| 3 | + | * | |
| 4 | + | * This script: | |
| 5 | + | * 1. Clears existing data in Convex | |
| 6 | + | * 2. Imports users and builds oldId → newId mapping | |
| 7 | + | * 3. Imports chat rooms and builds oldId → newId mapping | |
| 8 | + | * 4. Imports messages with mapped user and room IDs | |
| 9 | + | * | |
| 10 | + | * Usage: CONVEX_URL=<url> CONVEX_API_SECRET=<secret> npx tsx scripts/migrate-data-to-convex.ts | |
| 11 | + | */ | |
| 12 | + | ||
| 13 | + | import { readFileSync } from 'fs'; | |
| 14 | + | import { join } from 'path'; | |
| 15 | + | ||
| 16 | + | const EXPORTS_DIR = './exports'; | |
| 17 | + | ||
| 18 | + | // Get environment variables | |
| 19 | + | const CONVEX_URL = process.env.CONVEX_URL || process.env.PUBLIC_CONVEX_URL; | |
| 20 | + | const CONVEX_API_SECRET = process.env.CONVEX_API_SECRET; | |
| 21 | + | ||
| 22 | + | if (!CONVEX_URL || !CONVEX_API_SECRET) { | |
| 23 | + | console.error('Missing environment variables. Set CONVEX_URL and CONVEX_API_SECRET'); | |
| 24 | + | console.error( | |
| 25 | + | 'Example: CONVEX_URL=https://quirky-giraffe-19.convex.cloud CONVEX_API_SECRET=xxx npx tsx scripts/migrate-data-to-convex.ts' | |
| 26 | + | ); | |
| 27 | + | process.exit(1); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Convert .cloud URL to .site URL for HTTP actions | |
| 31 | + | const HTTP_URL = CONVEX_URL.replace('.convex.cloud', '.convex.site'); | |
| 32 | + | ||
| 33 | + | console.log('Migrating to:', HTTP_URL); | |
| 34 | + | ||
| 35 | + | // Helper to call Convex HTTP endpoints | |
| 36 | + | async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> { | |
| 37 | + | const url = `${HTTP_URL}${path}`; | |
| 38 | + | const response = await fetch(url, { | |
| 39 | + | method: 'POST', | |
| 40 | + | headers: { | |
| 41 | + | 'Content-Type': 'application/json', | |
| 42 | + | 'X-Convex-Secret': CONVEX_API_SECRET! | |
| 43 | + | }, | |
| 44 | + | body: JSON.stringify(body) | |
| 45 | + | }); | |
| 46 | + | ||
| 47 | + | if (!response.ok) { | |
| 48 | + | const error = await response.text(); | |
| 49 | + | throw new Error(`Convex HTTP error: ${response.status} - ${error}`); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | return response.json(); | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Types for our data | |
| 56 | + | interface OldUser { | |
| 57 | + | id: string; | |
| 58 | + | password: string; | |
| 59 | + | nickname: string; | |
| 60 | + | status: string; | |
| 61 | + | avatarUrl: string | null; | |
| 62 | + | createdAt: number; | |
| 63 | + | lastSeen: number | null; | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | interface OldChatRoom { | |
| 67 | + | id: string; | |
| 68 | + | name: string | null; | |
| 69 | + | type: string; | |
| 70 | + | createdAt: number; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | interface OldMessage { | |
| 74 | + | id: string; | |
| 75 | + | chatRoomId: string; | |
| 76 | + | senderId: string; | |
| 77 | + | content: string; | |
| 78 | + | type: string; | |
| 79 | + | timestamp: number; | |
| 80 | + | styleData: string | null; | |
| 81 | + | hasFormatting: number | boolean; | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | async function migrate() { | |
| 85 | + | // ID mappings: oldId → newConvexId | |
| 86 | + | const userIdMap = new Map<string, string>(); | |
| 87 | + | const roomIdMap = new Map<string, string>(); | |
| 88 | + | ||
| 89 | + | // Step 1: Load exported data | |
| 90 | + | console.log('\n📂 Loading exported data...'); | |
| 91 | + | const users: OldUser[] = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8')); | |
| 92 | + | const chatRooms: OldChatRoom[] = JSON.parse( | |
| 93 | + | readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8') | |
| 94 | + | ); | |
| 95 | + | const messages: OldMessage[] = JSON.parse( | |
| 96 | + | readFileSync(join(EXPORTS_DIR, 'messages.json'), 'utf-8') | |
| 97 | + | ); | |
| 98 | + | ||
| 99 | + | console.log( | |
| 100 | + | ` Found ${users.length} users, ${chatRooms.length} rooms, ${messages.length} messages` | |
| 101 | + | ); | |
| 102 | + | ||
| 103 | + | // Step 2: Import users | |
| 104 | + | console.log('\n👤 Importing users...'); | |
| 105 | + | for (const user of users) { | |
| 106 | + | try { | |
| 107 | + | const result = await convexFetch<{ userId: string }>('/api/users/create', { | |
| 108 | + | nickname: user.nickname, | |
| 109 | + | password: user.password // Already hashed | |
| 110 | + | }); | |
| 111 | + | userIdMap.set(user.id, result.userId); | |
| 112 | + | console.log(` ✓ ${user.nickname} → ${result.userId}`); | |
| 113 | + | } catch (err) { | |
| 114 | + | // User might already exist, try to get by nickname | |
| 115 | + | const existing = await convexFetch<{ _id: string } | null>('/api/users/getByNickname', { | |
| 116 | + | nickname: user.nickname | |
| 117 | + | }); | |
| 118 | + | if (existing) { | |
| 119 | + | userIdMap.set(user.id, existing._id); | |
| 120 | + | console.log(` ⚡ ${user.nickname} already exists → ${existing._id}`); | |
| 121 | + | } else { | |
| 122 | + | console.error(` ✗ Failed to import ${user.nickname}:`, err); | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // Step 3: Import chat rooms | |
| 128 | + | console.log('\n🏠 Importing chat rooms...'); | |
| 129 | + | for (const room of chatRooms) { | |
| 130 | + | try { | |
| 131 | + | // Use getOrCreateDefault for the General room | |
| 132 | + | if (room.name === 'General' && room.type === 'group') { | |
| 133 | + | const result = await convexFetch<{ _id: string }>('/api/chatRooms/getOrCreateDefault', {}); | |
| 134 | + | roomIdMap.set(room.id, result._id); | |
| 135 | + | console.log(` ✓ ${room.name} → ${result._id}`); | |
| 136 | + | } else { | |
| 137 | + | // For other rooms, we'd need a create endpoint - skip for now | |
| 138 | + | console.log(` ⚠ Skipping non-General room: ${room.name}`); | |
| 139 | + | } | |
| 140 | + | } catch (err) { | |
| 141 | + | console.error(` ✗ Failed to import room ${room.name}:`, err); | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // Step 4: Import messages | |
| 146 | + | console.log('\n💬 Importing messages...'); | |
| 147 | + | let imported = 0; | |
| 148 | + | let skipped = 0; | |
| 149 | + | ||
| 150 | + | // Sort messages by timestamp to maintain order | |
| 151 | + | messages.sort((a, b) => a.timestamp - b.timestamp); | |
| 152 | + | ||
| 153 | + | for (const msg of messages) { | |
| 154 | + | const newSenderId = userIdMap.get(msg.senderId); | |
| 155 | + | const newRoomId = roomIdMap.get(msg.chatRoomId); | |
| 156 | + | ||
| 157 | + | if (!newSenderId) { | |
| 158 | + | console.log(` ⚠ Skipping message - unknown sender: ${msg.senderId}`); | |
| 159 | + | skipped++; | |
| 160 | + | continue; | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | if (!newRoomId) { | |
| 164 | + | console.log(` ⚠ Skipping message - unknown room: ${msg.chatRoomId}`); | |
| 165 | + | skipped++; | |
| 166 | + | continue; | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | try { | |
| 170 | + | await convexFetch('/api/messages/send', { | |
| 171 | + | chatRoomId: newRoomId, | |
| 172 | + | senderId: newSenderId, | |
| 173 | + | content: msg.content, | |
| 174 | + | type: msg.type || 'chat', | |
| 175 | + | styleData: msg.styleData || undefined, | |
| 176 | + | hasFormatting: Boolean(msg.hasFormatting) | |
| 177 | + | }); | |
| 178 | + | imported++; | |
| 179 | + | if (imported % 10 === 0) { | |
| 180 | + | console.log(` ✓ Imported ${imported} messages...`); | |
| 181 | + | } | |
| 182 | + | } catch (err) { | |
| 183 | + | console.error(` ✗ Failed to import message:`, err); | |
| 184 | + | skipped++; | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | console.log(`\n✅ Migration complete!`); | |
| 189 | + | console.log(` Users: ${userIdMap.size}`); | |
| 190 | + | console.log(` Rooms: ${roomIdMap.size}`); | |
| 191 | + | console.log(` Messages: ${imported} imported, ${skipped} skipped`); | |
| 192 | + | ||
| 193 | + | // Print ID mappings for reference | |
| 194 | + | console.log('\n📋 User ID Mappings:'); | |
| 195 | + | for (const [oldId, newId] of userIdMap) { | |
| 196 | + | const user = users.find((u) => u.id === oldId); | |
| 197 | + | console.log(` ${user?.nickname}: ${oldId} → ${newId}`); | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | migrate().catch(console.error); | |
| @@ -0,0 +1,201 @@ | |||||
| 1 | + | /** | |||
| 2 | + | * Migration script to import Turso data to Convex with proper ID mapping. | |||
| 3 | + | * | |||
| 4 | + | * This script: | |||
| 5 | + | * 1. Clears existing data in Convex | |||
| 6 | + | * 2. Imports users and builds oldId → newId mapping | |||
| 7 | + | * 3. Imports chat rooms and builds oldId → newId mapping | |||
| 8 | + | * 4. Imports messages with mapped user and room IDs | |||
| 9 | + | * | |||
| 10 | + | * Usage: CONVEX_URL=<url> CONVEX_API_SECRET=<secret> npx tsx scripts/migrate-data-to-convex.ts | |||
| 11 | + | */ | |||
| 12 | + | ||||
| 13 | + | import { readFileSync } from 'fs'; | |||
| 14 | + | import { join } from 'path'; | |||
| 15 | + | ||||
| 16 | + | const EXPORTS_DIR = './exports'; | |||
| 17 | + | ||||
| 18 | + | // Get environment variables | |||
| 19 | + | const CONVEX_URL = process.env.CONVEX_URL || process.env.PUBLIC_CONVEX_URL; | |||
| 20 | + | const CONVEX_API_SECRET = process.env.CONVEX_API_SECRET; | |||
| 21 | + | ||||
| 22 | + | if (!CONVEX_URL || !CONVEX_API_SECRET) { | |||
| 23 | + | console.error('Missing environment variables. Set CONVEX_URL and CONVEX_API_SECRET'); | |||
| 24 | + | console.error( | |||
| 25 | + | 'Example: CONVEX_URL=https://quirky-giraffe-19.convex.cloud CONVEX_API_SECRET=xxx npx tsx scripts/migrate-data-to-convex.ts' | |||
| 26 | + | ); | |||
| 27 | + | process.exit(1); | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // Convert .cloud URL to .site URL for HTTP actions | |||
| 31 | + | const HTTP_URL = CONVEX_URL.replace('.convex.cloud', '.convex.site'); | |||
| 32 | + | ||||
| 33 | + | console.log('Migrating to:', HTTP_URL); | |||
| 34 | + | ||||
| 35 | + | // Helper to call Convex HTTP endpoints | |||
| 36 | + | async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> { | |||
| 37 | + | const url = `${HTTP_URL}${path}`; | |||
| 38 | + | const response = await fetch(url, { | |||
| 39 | + | method: 'POST', | |||
| 40 | + | headers: { | |||
| 41 | + | 'Content-Type': 'application/json', | |||
| 42 | + | 'X-Convex-Secret': CONVEX_API_SECRET! | |||
| 43 | + | }, | |||
| 44 | + | body: JSON.stringify(body) | |||
| 45 | + | }); | |||
| 46 | + | ||||
| 47 | + | if (!response.ok) { | |||
| 48 | + | const error = await response.text(); | |||
| 49 | + | throw new Error(`Convex HTTP error: ${response.status} - ${error}`); | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | return response.json(); | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // Types for our data | |||
| 56 | + | interface OldUser { | |||
| 57 | + | id: string; | |||
| 58 | + | password: string; | |||
| 59 | + | nickname: string; | |||
| 60 | + | status: string; | |||
| 61 | + | avatarUrl: string | null; | |||
| 62 | + | createdAt: number; | |||
| 63 | + | lastSeen: number | null; | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | interface OldChatRoom { | |||
| 67 | + | id: string; | |||
| 68 | + | name: string | null; | |||
| 69 | + | type: string; | |||
| 70 | + | createdAt: number; | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | interface OldMessage { | |||
| 74 | + | id: string; | |||
| 75 | + | chatRoomId: string; | |||
| 76 | + | senderId: string; | |||
| 77 | + | content: string; | |||
| 78 | + | type: string; | |||
| 79 | + | timestamp: number; | |||
| 80 | + | styleData: string | null; | |||
| 81 | + | hasFormatting: number | boolean; | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | async function migrate() { | |||
| 85 | + | // ID mappings: oldId → newConvexId | |||
| 86 | + | const userIdMap = new Map<string, string>(); | |||
| 87 | + | const roomIdMap = new Map<string, string>(); | |||
| 88 | + | ||||
| 89 | + | // Step 1: Load exported data | |||
| 90 | + | console.log('\n📂 Loading exported data...'); | |||
| 91 | + | const users: OldUser[] = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8')); | |||
| 92 | + | const chatRooms: OldChatRoom[] = JSON.parse( | |||
| 93 | + | readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8') | |||
| 94 | + | ); | |||
| 95 | + | const messages: OldMessage[] = JSON.parse( | |||
| 96 | + | readFileSync(join(EXPORTS_DIR, 'messages.json'), 'utf-8') | |||
| 97 | + | ); | |||
| 98 | + | ||||
| 99 | + | console.log( | |||
| 100 | + | ` Found ${users.length} users, ${chatRooms.length} rooms, ${messages.length} messages` | |||
| 101 | + | ); | |||
| 102 | + | ||||
| 103 | + | // Step 2: Import users | |||
| 104 | + | console.log('\n👤 Importing users...'); | |||
| 105 | + | for (const user of users) { | |||
| 106 | + | try { | |||
| 107 | + | const result = await convexFetch<{ userId: string }>('/api/users/create', { | |||
| 108 | + | nickname: user.nickname, | |||
| 109 | + | password: user.password // Already hashed | |||
| 110 | + | }); | |||
| 111 | + | userIdMap.set(user.id, result.userId); | |||
| 112 | + | console.log(` ✓ ${user.nickname} → ${result.userId}`); | |||
| 113 | + | } catch (err) { | |||
| 114 | + | // User might already exist, try to get by nickname | |||
| 115 | + | const existing = await convexFetch<{ _id: string } | null>('/api/users/getByNickname', { | |||
| 116 | + | nickname: user.nickname | |||
| 117 | + | }); | |||
| 118 | + | if (existing) { | |||
| 119 | + | userIdMap.set(user.id, existing._id); | |||
| 120 | + | console.log(` ⚡ ${user.nickname} already exists → ${existing._id}`); | |||
| 121 | + | } else { | |||
| 122 | + | console.error(` ✗ Failed to import ${user.nickname}:`, err); | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // Step 3: Import chat rooms | |||
| 128 | + | console.log('\n🏠 Importing chat rooms...'); | |||
| 129 | + | for (const room of chatRooms) { | |||
| 130 | + | try { | |||
| 131 | + | // Use getOrCreateDefault for the General room | |||
| 132 | + | if (room.name === 'General' && room.type === 'group') { | |||
| 133 | + | const result = await convexFetch<{ _id: string }>('/api/chatRooms/getOrCreateDefault', {}); | |||
| 134 | + | roomIdMap.set(room.id, result._id); | |||
| 135 | + | console.log(` ✓ ${room.name} → ${result._id}`); | |||
| 136 | + | } else { | |||
| 137 | + | // For other rooms, we'd need a create endpoint - skip for now | |||
| 138 | + | console.log(` ⚠ Skipping non-General room: ${room.name}`); | |||
| 139 | + | } | |||
| 140 | + | } catch (err) { | |||
| 141 | + | console.error(` ✗ Failed to import room ${room.name}:`, err); | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // Step 4: Import messages | |||
| 146 | + | console.log('\n💬 Importing messages...'); | |||
| 147 | + | let imported = 0; | |||
| 148 | + | let skipped = 0; | |||
| 149 | + | ||||
| 150 | + | // Sort messages by timestamp to maintain order | |||
| 151 | + | messages.sort((a, b) => a.timestamp - b.timestamp); | |||
| 152 | + | ||||
| 153 | + | for (const msg of messages) { | |||
| 154 | + | const newSenderId = userIdMap.get(msg.senderId); | |||
| 155 | + | const newRoomId = roomIdMap.get(msg.chatRoomId); | |||
| 156 | + | ||||
| 157 | + | if (!newSenderId) { | |||
| 158 | + | console.log(` ⚠ Skipping message - unknown sender: ${msg.senderId}`); | |||
| 159 | + | skipped++; | |||
| 160 | + | continue; | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | if (!newRoomId) { | |||
| 164 | + | console.log(` ⚠ Skipping message - unknown room: ${msg.chatRoomId}`); | |||
| 165 | + | skipped++; | |||
| 166 | + | continue; | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | try { | |||
| 170 | + | await convexFetch('/api/messages/send', { | |||
| 171 | + | chatRoomId: newRoomId, | |||
| 172 | + | senderId: newSenderId, | |||
| 173 | + | content: msg.content, | |||
| 174 | + | type: msg.type || 'chat', | |||
| 175 | + | styleData: msg.styleData || undefined, | |||
| 176 | + | hasFormatting: Boolean(msg.hasFormatting) | |||
| 177 | + | }); | |||
| 178 | + | imported++; | |||
| 179 | + | if (imported % 10 === 0) { | |||
| 180 | + | console.log(` ✓ Imported ${imported} messages...`); | |||
| 181 | + | } | |||
| 182 | + | } catch (err) { | |||
| 183 | + | console.error(` ✗ Failed to import message:`, err); | |||
| 184 | + | skipped++; | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | console.log(`\n✅ Migration complete!`); | |||
| 189 | + | console.log(` Users: ${userIdMap.size}`); | |||
| 190 | + | console.log(` Rooms: ${roomIdMap.size}`); | |||
| 191 | + | console.log(` Messages: ${imported} imported, ${skipped} skipped`); | |||
| 192 | + | ||||
| 193 | + | // Print ID mappings for reference | |||
| 194 | + | console.log('\n📋 User ID Mappings:'); | |||
| 195 | + | for (const [oldId, newId] of userIdMap) { | |||
| 196 | + | const user = users.find((u) => u.id === oldId); | |||
| 197 | + | console.log(` ${user?.nickname}: ${oldId} → ${newId}`); | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | migrate().catch(console.error); | |||
A scripts/prepare-convex-import.ts +65 -0
| @@ -0,0 +1,65 @@ | |||
| 1 | + | /** | |
| 2 | + | * This script prepares the Turso export data for Convex import. | |
| 3 | + | * It removes oldId fields and creates a clean import structure. | |
| 4 | + | * | |
| 5 | + | * Usage: npx tsx scripts/prepare-convex-import.ts | |
| 6 | + | */ | |
| 7 | + | ||
| 8 | + | import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; | |
| 9 | + | import { join } from 'path'; | |
| 10 | + | ||
| 11 | + | const EXPORTS_DIR = './exports'; | |
| 12 | + | const IMPORT_DIR = './convex-import'; | |
| 13 | + | ||
| 14 | + | // Ensure import directory exists | |
| 15 | + | if (!existsSync(IMPORT_DIR)) { | |
| 16 | + | mkdirSync(IMPORT_DIR, { recursive: true }); | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // Read and transform users | |
| 20 | + | console.log('Processing users...'); | |
| 21 | + | const usersRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8')); | |
| 22 | + | const users = usersRaw.map((user: Record<string, unknown>) => ({ | |
| 23 | + | password: user.password, | |
| 24 | + | nickname: user.nickname, | |
| 25 | + | status: 'offline', // Reset status | |
| 26 | + | avatarUrl: user.avatarUrl || undefined, | |
| 27 | + | createdAt: user.createdAt, | |
| 28 | + | lastSeen: user.lastSeen || undefined | |
| 29 | + | })); | |
| 30 | + | writeFileSync( | |
| 31 | + | join(IMPORT_DIR, 'users.jsonl'), | |
| 32 | + | users.map((u: unknown) => JSON.stringify(u)).join('\n') | |
| 33 | + | ); | |
| 34 | + | console.log(` Processed ${users.length} users`); | |
| 35 | + | ||
| 36 | + | // Read and transform chat rooms | |
| 37 | + | console.log('Processing chat rooms...'); | |
| 38 | + | const roomsRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8')); | |
| 39 | + | const chatRooms = roomsRaw.map((room: Record<string, unknown>) => ({ | |
| 40 | + | name: room.name || undefined, | |
| 41 | + | type: room.type, | |
| 42 | + | createdAt: room.createdAt | |
| 43 | + | })); | |
| 44 | + | writeFileSync( | |
| 45 | + | join(IMPORT_DIR, 'chatRooms.jsonl'), | |
| 46 | + | chatRooms.map((r: unknown) => JSON.stringify(r)).join('\n') | |
| 47 | + | ); | |
| 48 | + | console.log(` Processed ${chatRooms.length} chat rooms`); | |
| 49 | + | ||
| 50 | + | // For messages, we need to map old IDs to new format | |
| 51 | + | // Since we're doing a fresh import, messages will need user and room references | |
| 52 | + | // We'll skip messages for now - they'll be recreated fresh | |
| 53 | + | console.log('Skipping messages (will start fresh)'); | |
| 54 | + | ||
| 55 | + | // Sessions are also skipped - users will need to log in again | |
| 56 | + | console.log('Skipping sessions (users will need to log in again)'); | |
| 57 | + | ||
| 58 | + | console.log('\nImport files created in', IMPORT_DIR); | |
| 59 | + | console.log('\nTo import to Convex dev:'); | |
| 60 | + | console.log(' npx convex import --table users convex-import/users.jsonl --replace'); | |
| 61 | + | console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace'); | |
| 62 | + | console.log('\nOr deploy schema first, then import:'); | |
| 63 | + | console.log(' npx convex dev --once'); | |
| 64 | + | console.log(' npx convex import --table users convex-import/users.jsonl --replace'); | |
| 65 | + | console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace'); | |
| @@ -0,0 +1,65 @@ | |||||
| 1 | + | /** | |||
| 2 | + | * This script prepares the Turso export data for Convex import. | |||
| 3 | + | * It removes oldId fields and creates a clean import structure. | |||
| 4 | + | * | |||
| 5 | + | * Usage: npx tsx scripts/prepare-convex-import.ts | |||
| 6 | + | */ | |||
| 7 | + | ||||
| 8 | + | import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; | |||
| 9 | + | import { join } from 'path'; | |||
| 10 | + | ||||
| 11 | + | const EXPORTS_DIR = './exports'; | |||
| 12 | + | const IMPORT_DIR = './convex-import'; | |||
| 13 | + | ||||
| 14 | + | // Ensure import directory exists | |||
| 15 | + | if (!existsSync(IMPORT_DIR)) { | |||
| 16 | + | mkdirSync(IMPORT_DIR, { recursive: true }); | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // Read and transform users | |||
| 20 | + | console.log('Processing users...'); | |||
| 21 | + | const usersRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'users.json'), 'utf-8')); | |||
| 22 | + | const users = usersRaw.map((user: Record<string, unknown>) => ({ | |||
| 23 | + | password: user.password, | |||
| 24 | + | nickname: user.nickname, | |||
| 25 | + | status: 'offline', // Reset status | |||
| 26 | + | avatarUrl: user.avatarUrl || undefined, | |||
| 27 | + | createdAt: user.createdAt, | |||
| 28 | + | lastSeen: user.lastSeen || undefined | |||
| 29 | + | })); | |||
| 30 | + | writeFileSync( | |||
| 31 | + | join(IMPORT_DIR, 'users.jsonl'), | |||
| 32 | + | users.map((u: unknown) => JSON.stringify(u)).join('\n') | |||
| 33 | + | ); | |||
| 34 | + | console.log(` Processed ${users.length} users`); | |||
| 35 | + | ||||
| 36 | + | // Read and transform chat rooms | |||
| 37 | + | console.log('Processing chat rooms...'); | |||
| 38 | + | const roomsRaw = JSON.parse(readFileSync(join(EXPORTS_DIR, 'chat_rooms.json'), 'utf-8')); | |||
| 39 | + | const chatRooms = roomsRaw.map((room: Record<string, unknown>) => ({ | |||
| 40 | + | name: room.name || undefined, | |||
| 41 | + | type: room.type, | |||
| 42 | + | createdAt: room.createdAt | |||
| 43 | + | })); | |||
| 44 | + | writeFileSync( | |||
| 45 | + | join(IMPORT_DIR, 'chatRooms.jsonl'), | |||
| 46 | + | chatRooms.map((r: unknown) => JSON.stringify(r)).join('\n') | |||
| 47 | + | ); | |||
| 48 | + | console.log(` Processed ${chatRooms.length} chat rooms`); | |||
| 49 | + | ||||
| 50 | + | // For messages, we need to map old IDs to new format | |||
| 51 | + | // Since we're doing a fresh import, messages will need user and room references | |||
| 52 | + | // We'll skip messages for now - they'll be recreated fresh | |||
| 53 | + | console.log('Skipping messages (will start fresh)'); | |||
| 54 | + | ||||
| 55 | + | // Sessions are also skipped - users will need to log in again | |||
| 56 | + | console.log('Skipping sessions (users will need to log in again)'); | |||
| 57 | + | ||||
| 58 | + | console.log('\nImport files created in', IMPORT_DIR); | |||
| 59 | + | console.log('\nTo import to Convex dev:'); | |||
| 60 | + | console.log(' npx convex import --table users convex-import/users.jsonl --replace'); | |||
| 61 | + | console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace'); | |||
| 62 | + | console.log('\nOr deploy schema first, then import:'); | |||
| 63 | + | console.log(' npx convex dev --once'); | |||
| 64 | + | console.log(' npx convex import --table users convex-import/users.jsonl --replace'); | |||
| 65 | + | console.log(' npx convex import --table chatRooms convex-import/chatRooms.jsonl --replace'); | |||