diff --git a/.gitignore b/.gitignore index ddff63b..d49120f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ bun.lockb package-lock.json yarn.lock pnpm-lock.yaml +csv_exports/ \ No newline at end of file diff --git a/helpers/README.md b/helpers/README.md new file mode 100644 index 0000000..98b92a7 --- /dev/null +++ b/helpers/README.md @@ -0,0 +1,74 @@ +# Helpers + +## CSV to JSON User Migration Script + +This script converts Clerk user export CSV files to the JSON format required to run the main script of this repo to import users in to Clerk. + +### Usage + +```bash +# Basic usage - outputs to users.json +npx tsx helpers/user_export_csv_to_json.ts csv_exports/your_export.csv + +# Specify custom output file +npx tsx helpers/user_export_csv_to_json.ts csv_exports/your_export.csv output/my_users.json +``` + +### Input CSV Format + +The script expects a CSV file with the following headers: +- `id` - The user ID (maps to `userId` in JSON) +- `first_name` - User's first name (optional) +- `last_name` - User's last name (optional) +- `primary_email_address` - User's email (required) +- `password_digest` - Hashed password (optional) +- `password_hasher` - Password hashing algorithm (optional) +- Other fields are ignored but can be present + +### Output JSON Format + +The script generates a JSON file compatible with Clerk's user migration format: + +```json +[ + { + "userId": "string", + "email": "email", + "firstName": "string (optional)", + "lastName": "string (optional)", + "password": "string (optional)", + "passwordHasher": "bcrypt" + } +] +``` + +### Features + +- Handles empty fields gracefully +- Validates required fields (userId and email) +- Skips invalid rows with warnings +- Supports various password hashers (bcrypt, argon2, etc.) +- Flexible CSV parsing with field count tolerance +- Clean console output with progress information + +### Example + +Input CSV: +```csv +id,first_name,last_name,username,primary_email_address,primary_phone_number,verified_email_addresses,unverified_email_addresses,verified_phone_numbers,unverified_phone_numbers,totp_secret,password_digest,password_hasher +user_123,John,Doe,,john@example.com,,john@example.com,,,,,$2a$10$hash123,bcrypt +``` + +Output JSON: +```json +[ + { + "userId": "user_123", + "email": "john@example.com", + "firstName": "John", + "lastName": "Doe", + "password": "$2a$10$hash123", + "passwordHasher": "bcrypt" + } +] +``` \ No newline at end of file diff --git a/helpers/user_export_csv_to_json.ts b/helpers/user_export_csv_to_json.ts new file mode 100755 index 0000000..74b5eed --- /dev/null +++ b/helpers/user_export_csv_to_json.ts @@ -0,0 +1,152 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from 'fs'; + +interface CSVRow { + id: string; + first_name: string; + last_name: string; + username: string; + primary_email_address: string; + primary_phone_number: string; + verified_email_addresses: string; + unverified_email_addresses: string; + verified_phone_numbers: string; + unverified_phone_numbers: string; + totp_secret: string; + password_digest: string; + password_hasher: string; +} + +interface UserRecord { + userId: string; + email: string; + firstName?: string; + lastName?: string; + password?: string; + passwordHasher?: string; +} + +function parseCSVLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + line = line.trim(); + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + inQuotes = !inQuotes; + } else if (char === ',' && !inQuotes) { + result.push(current.trim()); + current = ''; + } else { + current += char; + } + } + + result.push(current.trim()); + return result; +} + +function csvToJson(csvFilePath: string, outputPath: string): void { + try { + console.log(`Reading CSV file: ${csvFilePath}`); + const csvContent = readFileSync(csvFilePath, 'utf-8'); + const lines = csvContent.split('\n').filter(line => line.trim() !== ''); + + if (lines.length === 0) { + throw new Error('CSV file is empty'); + } + + const headers = parseCSVLine(lines[0]).map(h => h.trim()); + console.log('Headers found:', headers); + + const users: UserRecord[] = []; + + for (let i = 1; i < lines.length; i++) { + const values = parseCSVLine(lines[i]); + + if (values.length !== headers.length) { + console.warn(`Row ${i + 1} has ${values.length} values but expected ${headers.length}. Continuing with available data...`); + } + + const row: Record = {}; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + + const user: UserRecord = { + userId: row.id || '', + email: row.primary_email_address || '', + }; + + if (row.first_name && row.first_name.trim() !== '') { + user.firstName = row.first_name.trim(); + } + + if (row.last_name && row.last_name.trim() !== '') { + user.lastName = row.last_name.trim(); + } + + if (row.password_digest && row.password_digest.trim() !== '') { + user.password = row.password_digest.trim(); + } + + if (row.password_hasher && row.password_hasher.trim() !== '') { + user.passwordHasher = row.password_hasher.trim(); + } + + if (!user.userId || !user.email) { + console.warn(`Row ${i + 1} missing required userId or email. Skipping...`); + continue; + } + + users.push(user); + } + + console.log(`Processed ${users.length} users`); + + const jsonOutput = JSON.stringify(users, null, 2); + writeFileSync(outputPath, jsonOutput, 'utf-8'); + + console.log(`āœ… Successfully converted CSV to JSON!`); + console.log(`šŸ“ Output file: ${outputPath}`); + console.log(`šŸ‘„ Total users: ${users.length}`); + + if (users.length > 0) { + console.log('\nšŸ“‹ Sample of converted users:'); + console.log(JSON.stringify(users.slice(0, 3), null, 2)); + } + + } catch (error) { + console.error('āŒ Error converting CSV to JSON:', error); + process.exit(1); + } +} + +function main() { + const args = process.argv.slice(2); + + if (args.length < 1) { + console.log('Usage: node helpers/user_export_csv_to_json.ts [output-path]'); + console.log(''); + console.log('Examples:'); + console.log(' node helpers/user_export_csv_to_json.ts users.csv'); + console.log(' node helpers/user_export_csv_to_json.ts users.csv output/users.json'); + process.exit(1); + } + + const csvFilePath = args[0]; + const outputPath = args[1] || 'users.json'; + + csvToJson(csvFilePath, outputPath); +} + +if (process.argv[1]?.endsWith('user_export_csv_to_json.ts')) { + main(); +} + +export { csvToJson };