#!/usr/bin/env node /** * Import CSV data into Directus * * Usage: node import-csv.js * * Example: node import-csv.js ../data/artworks.csv artworks */ const fs = require('fs'); const path = require('path'); const https = require('https'); const readline = require('readline'); // Configuration const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://katheryn-cms.jeffemmett.com'; const DIRECTUS_TOKEN = process.env.DIRECTUS_TOKEN; // Field mapping from Airtable to Directus // Adjust these based on your actual Airtable columns const FIELD_MAPPINGS = { artworks: { 'Name': 'title', 'Title': 'title', 'Description': 'description', 'Year': 'year', 'Medium': 'medium', 'Dimensions': 'dimensions', 'Price': 'price', 'Status': 'status', 'Series': 'series_name', // Will need to resolve to ID 'Tags': 'tag_names', // Will need to resolve to IDs 'Zettle ID': 'zettle_product_id', 'Image URL': 'image_url', // Will need to upload }, series: { 'Name': 'name', 'Description': 'description', }, events: { 'Name': 'title', 'Title': 'title', 'Start Date': 'start_date', 'End Date': 'end_date', 'Location': 'location', 'Description': 'description', } }; function parseCSV(content) { const lines = content.split('\n'); if (lines.length === 0) return []; // Parse header const header = parseCSVLine(lines[0]); const records = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = parseCSVLine(line); const record = {}; header.forEach((col, idx) => { record[col] = values[idx] || ''; }); records.push(record); } return records; } function parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; } function mapFields(record, collection) { const mapping = FIELD_MAPPINGS[collection] || {}; const mapped = {}; for (const [airtableField, value] of Object.entries(record)) { const directusField = mapping[airtableField] || airtableField.toLowerCase().replace(/\s+/g, '_'); // Skip empty values if (value === '' || value === undefined) continue; // Type conversions if (directusField === 'year' || directusField === 'price') { const num = parseFloat(value.replace(/[^0-9.-]/g, '')); if (!isNaN(num)) mapped[directusField] = num; } else if (directusField === 'status') { // Map common status values const statusMap = { 'available': 'published', 'sold': 'sold', 'draft': 'draft', 'published': 'published', 'archived': 'archived' }; mapped[directusField] = statusMap[value.toLowerCase()] || 'draft'; } else if (directusField.endsWith('_date')) { // Parse dates const date = new Date(value); if (!isNaN(date)) { mapped[directusField] = date.toISOString().split('T')[0]; } } else { mapped[directusField] = value; } } // Set default status if not present if (!mapped.status) { mapped.status = 'draft'; } return mapped; } async function createItem(collection, data) { return new Promise((resolve, reject) => { const url = new URL(`/items/${collection}`, DIRECTUS_URL); const postData = JSON.stringify(data); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(url, options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { const result = JSON.parse(body); if (res.statusCode >= 200 && res.statusCode < 300) { resolve(result.data); } else { reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(result.errors || result)}`)); } } catch (e) { reject(new Error(`Parse error: ${body}`)); } }); }); req.on('error', reject); req.write(postData); req.end(); }); } async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log('Usage: node import-csv.js '); console.log(''); console.log('Example: node import-csv.js ../data/artworks.csv artworks'); console.log(''); console.log('Environment variables:'); console.log(' DIRECTUS_URL - Directus API URL (default: https://katheryn-cms.jeffemmett.com)'); console.log(' DIRECTUS_TOKEN - Directus admin token (required)'); console.log(''); console.log('Supported collections: artworks, series, events'); process.exit(1); } if (!DIRECTUS_TOKEN) { console.error('Error: DIRECTUS_TOKEN environment variable is required'); console.error(''); console.error('To get a token:'); console.error('1. Log in to Directus admin at ' + DIRECTUS_URL); console.error('2. Go to Settings → Access Tokens'); console.error('3. Create a new static token'); process.exit(1); } const [csvFile, collection] = args; const csvPath = path.resolve(csvFile); if (!fs.existsSync(csvPath)) { console.error(`Error: File not found: ${csvPath}`); process.exit(1); } console.log(`Importing ${csvPath} into ${collection}...`); console.log(''); const content = fs.readFileSync(csvPath, 'utf-8'); const records = parseCSV(content); console.log(`Found ${records.length} records`); console.log(''); // Show first record for verification if (records.length > 0) { console.log('Sample record (first row):'); console.log(JSON.stringify(records[0], null, 2)); console.log(''); const mapped = mapFields(records[0], collection); console.log('Mapped to Directus:'); console.log(JSON.stringify(mapped, null, 2)); console.log(''); } // Ask for confirmation const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); await new Promise(resolve => { rl.question(`Import ${records.length} records into '${collection}'? (y/N) `, (answer) => { rl.close(); if (answer.toLowerCase() !== 'y') { console.log('Aborted'); process.exit(0); } resolve(); }); }); // Import records let success = 0; let failed = 0; for (let i = 0; i < records.length; i++) { const record = records[i]; const mapped = mapFields(record, collection); // Remove fields that need special handling delete mapped.series_name; delete mapped.tag_names; delete mapped.image_url; try { const result = await createItem(collection, mapped); console.log(`[${i + 1}/${records.length}] Created: ${mapped.title || mapped.name || result.id}`); success++; } catch (err) { console.error(`[${i + 1}/${records.length}] Failed: ${err.message}`); failed++; } // Small delay to avoid rate limiting await new Promise(r => setTimeout(r, 100)); } console.log(''); console.log(`Import complete: ${success} succeeded, ${failed} failed`); } main().catch(console.error);