katheryn-website/scripts/import-csv.js

285 lines
7.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Import CSV data into Directus
*
* Usage: node import-csv.js <csv-file> <collection-name>
*
* 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 <csv-file> <collection-name>');
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);