#!/usr/bin/env node require('dotenv').config({ path: '.env.local' }); const AWS = require('aws-sdk'); const { v4: uuidv4 } = require('uuid'); // Configure AWS AWS.config.update({ region: process.env.AWS_REGION || 'eu-central-1' }); const dynamodb = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments'; // Color mappings for common filament colors const colorMappings = { 'Black': '#000000', 'White': '#FFFFFF', 'Red': '#FF0000', 'Blue': '#0000FF', 'Green': '#00FF00', 'Yellow': '#FFFF00', 'Orange': '#FFA500', 'Purple': '#800080', 'Gray': '#808080', 'Grey': '#808080', 'Silver': '#C0C0C0', 'Gold': '#FFD700', 'Brown': '#964B00', 'Pink': '#FFC0CB', 'Cyan': '#00FFFF', 'Magenta': '#FF00FF', 'Beige': '#F5F5DC', 'Transparent': '#FFFFFF00', // Specific Bambu Lab colors 'Mistletoe Green': '#50C878', 'Indingo Purple': '#4B0082', 'Jade White': '#F0F8FF', 'Hot Pink': '#FF69B4', 'Cocoa Brown': '#D2691E', 'Cotton Candy Cloud': '#FFB6C1', 'Sunflower Yellow': '#FFDA03', 'Scarlet Red': '#FF2400', 'Mandarin Orange': '#FF8C00', 'Marine Blue': '#0066CC', 'Charcoal': '#36454F', 'Ivory White': '#FFFFF0', 'Ash Gray': '#B2BEB5', 'Cobalt Blue': '#0047AB', 'Turquoise': '#40E0D0', 'Nardo Gray': '#4A4A4A', 'Bright Green': '#66FF00', 'Glow Green': '#90EE90', 'Black Walnut': '#5C4033', 'Jeans Blue': '#5670A1', 'Forest Green': '#228B22', 'Lavender Purple': '#B57EDC' }; function getColorHex(colorName) { // Try exact match first if (colorMappings[colorName]) { return colorMappings[colorName]; } // Try to find color in the name for (const [key, value] of Object.entries(colorMappings)) { if (colorName.toLowerCase().includes(key.toLowerCase())) { return value; } } return null; } function parseInventory(oldFilament) { let total = 1; let vacuum = 0; let opened = 0; // Parse kolicina (quantity) if (oldFilament.kolicina) { const qty = parseInt(oldFilament.kolicina); if (!isNaN(qty)) { total = qty; } } // Parse vakum field if (oldFilament.vakum) { const vakumLower = oldFilament.vakum.toLowerCase(); if (vakumLower.includes('vakuum') || vakumLower.includes('vakum')) { // Check for multiplier const match = vakumLower.match(/x(\d+)/); vacuum = match ? parseInt(match[1]) : 1; } } // Parse otvoreno field if (oldFilament.otvoreno) { const otvorenoLower = oldFilament.otvoreno.toLowerCase(); if (otvorenoLower.includes('otvorena') || otvorenoLower.includes('otvoreno')) { // Check for multiplier const match = otvorenoLower.match(/(\d+)x/); if (match) { opened = parseInt(match[1]); } else { const match2 = otvorenoLower.match(/x(\d+)/); opened = match2 ? parseInt(match2[1]) : 1; } } } // Calculate available const available = vacuum + opened; const inUse = Math.max(0, total - available); return { total: total || 1, available: available, inUse: inUse, locations: { vacuum: vacuum, opened: opened, printer: 0 } }; } function determineStorageCondition(oldFilament) { if (oldFilament.vakum && oldFilament.vakum.toLowerCase().includes('vakuum')) { return 'vacuum'; } if (oldFilament.otvoreno && oldFilament.otvoreno.toLowerCase().includes('otvorena')) { return 'opened'; } return 'sealed'; } function parseMaterial(oldFilament) { const base = oldFilament.tip || 'PLA'; let modifier = null; if (oldFilament.finish && oldFilament.finish !== 'Basic' && oldFilament.finish !== '') { modifier = oldFilament.finish; } // Handle special PLA types if (base === 'PLA' && modifier) { // These are actually base materials, not modifiers if (modifier === 'PETG' || modifier === 'ABS' || modifier === 'TPU') { return { base: modifier, modifier: null }; } } return { base, modifier }; } function generateSKU(brand, material, color) { const brandCode = brand.substring(0, 3).toUpperCase(); const materialCode = material.base.substring(0, 3); const colorCode = color.name.substring(0, 3).toUpperCase(); const random = Math.random().toString(36).substring(2, 5).toUpperCase(); return `${brandCode}-${materialCode}-${colorCode}-${random}`; } function migrateFilament(oldFilament) { const material = parseMaterial(oldFilament); const inventory = parseInventory(oldFilament); const colorHex = getColorHex(oldFilament.boja); const newFilament = { // Keep existing fields id: oldFilament.id || uuidv4(), sku: generateSKU(oldFilament.brand, material, { name: oldFilament.boja }), // Product info brand: oldFilament.brand, type: oldFilament.tip || 'PLA', material: material, color: { name: oldFilament.boja || 'Unknown', hex: colorHex }, // Physical properties weight: { value: 1000, // Default to 1kg unit: 'g' }, diameter: 1.75, // Standard diameter // Inventory inventory: inventory, // Pricing pricing: { purchasePrice: oldFilament.cena ? parseFloat(oldFilament.cena) : null, currency: 'RSD', supplier: null, purchaseDate: null }, // Condition condition: { isRefill: oldFilament.refill === 'Da', openedDate: oldFilament.otvoreno ? new Date().toISOString() : null, expiryDate: null, storageCondition: determineStorageCondition(oldFilament), humidity: null }, // Metadata tags: [], notes: null, images: [], // Timestamps createdAt: oldFilament.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), lastUsed: null, // Keep old structure temporarily for backwards compatibility _legacy: { tip: oldFilament.tip, finish: oldFilament.finish, boja: oldFilament.boja, refill: oldFilament.refill, vakum: oldFilament.vakum, otvoreno: oldFilament.otvoreno, kolicina: oldFilament.kolicina, cena: oldFilament.cena, status: oldFilament.status } }; // Add tags based on properties if (material.modifier === 'Silk') newFilament.tags.push('silk'); if (material.modifier === 'Matte') newFilament.tags.push('matte'); if (material.modifier === 'CF') newFilament.tags.push('engineering', 'carbon-fiber'); if (material.modifier === 'Wood') newFilament.tags.push('specialty', 'wood-fill'); if (material.modifier === 'Glow') newFilament.tags.push('specialty', 'glow-in-dark'); if (material.base === 'PETG') newFilament.tags.push('engineering', 'chemical-resistant'); if (material.base === 'ABS') newFilament.tags.push('engineering', 'high-temp'); if (material.base === 'TPU') newFilament.tags.push('flexible', 'engineering'); if (newFilament.condition.isRefill) newFilament.tags.push('refill', 'eco-friendly'); return newFilament; } async function migrateData() { console.log('Starting migration to new data structure...'); try { // Scan all existing items const scanParams = { TableName: TABLE_NAME }; const items = []; let lastEvaluatedKey = null; do { if (lastEvaluatedKey) { scanParams.ExclusiveStartKey = lastEvaluatedKey; } const result = await dynamodb.scan(scanParams).promise(); items.push(...result.Items); lastEvaluatedKey = result.LastEvaluatedKey; } while (lastEvaluatedKey); console.log(`Found ${items.length} items to migrate`); // Check if already migrated if (items.length > 0 && items[0].material && items[0].inventory) { console.log('Data appears to be already migrated!'); const confirm = process.argv.includes('--force'); if (!confirm) { console.log('Use --force flag to force migration'); return; } } // Migrate each item const migratedItems = items.map(item => migrateFilament(item)); // Show sample console.log('\nSample migrated data:'); console.log(JSON.stringify(migratedItems[0], null, 2)); // Update items in batches const chunks = []; for (let i = 0; i < migratedItems.length; i += 25) { chunks.push(migratedItems.slice(i, i + 25)); } console.log(`\nUpdating ${migratedItems.length} items in DynamoDB...`); for (const chunk of chunks) { const params = { RequestItems: { [TABLE_NAME]: chunk.map(item => ({ PutRequest: { Item: item } })) } }; await dynamodb.batchWrite(params).promise(); console.log(`Updated ${chunk.length} items`); } console.log('\n✅ Migration completed successfully!'); // Show summary const summary = { totalItems: migratedItems.length, brands: [...new Set(migratedItems.map(i => i.brand))], materials: [...new Set(migratedItems.map(i => i.material.base))], modifiers: [...new Set(migratedItems.map(i => i.material.modifier).filter(Boolean))], storageConditions: [...new Set(migratedItems.map(i => i.condition.storageCondition))], totalInventory: migratedItems.reduce((sum, i) => sum + i.inventory.total, 0), availableInventory: migratedItems.reduce((sum, i) => sum + i.inventory.available, 0) }; console.log('\nMigration Summary:'); console.log(JSON.stringify(summary, null, 2)); } catch (error) { console.error('Migration failed:', error); process.exit(1); } } // Run migration if (require.main === module) { console.log('Data Structure Migration Tool'); console.log('============================'); console.log('This will migrate all filaments to the new structure'); console.log('Old data will be preserved in _legacy field\n'); migrateData(); }