exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

generate_test_data.js (11248B)


      1 #!/usr/bin/env node
      2 /**
      3  * Generate Test Data for Products Schema
      4  *
      5  * Generates realistic test data matching the UI expectations:
      6  * - ProductCard interface (for listings)
      7  * - Product interface (for details)
      8  *
      9  * Output: SQL file ready to import
     10  */
     11 
     12 const fs = require('fs');
     13 const path = require('path');
     14 
     15 // Configuration
     16 const NUM_PRODUCTS = 2000;
     17 const NUM_RATINGS_PER_PRODUCT = { min: 5, max: 50 };
     18 
     19 // Sample data pools
     20 const BRANDS = [
     21   'TechPro', 'SmartGear', 'EliteHome', 'ActiveLife', 'StyleHub',
     22   'PureTech', 'FlexFit', 'MegaValue', 'ProMax', 'UltraGear',
     23   'CoreTech', 'PrimeTech', 'NexGen', 'FutureTech', 'DigitalWave'
     24 ];
     25 
     26 const CATEGORIES = [
     27   { name: 'Smartphones', root: 'Electronics', adjectives: ['Pro', 'Max', 'Ultra', 'Plus', 'Elite'] },
     28   { name: 'Laptops', root: 'Electronics', adjectives: ['Book', 'Pro', 'Air', 'Gaming', 'Business'] },
     29   { name: 'Accessories', root: 'Electronics', adjectives: ['Premium', 'Wireless', 'Smart', 'Pro', 'Plus'] },
     30   { name: 'Smart Home', root: 'Electronics', adjectives: ['Smart', 'Connected', 'Voice', 'AI', 'Auto'] },
     31   { name: 'Men\'s Clothing', root: 'Clothing', adjectives: ['Classic', 'Modern', 'Casual', 'Sport', 'Premium'] },
     32   { name: 'Women\'s Clothing', root: 'Clothing', adjectives: ['Elegant', 'Casual', 'Sport', 'Designer', 'Comfort'] },
     33   { name: 'Shoes', root: 'Clothing', adjectives: ['Running', 'Walking', 'Sport', 'Casual', 'Athletic'] }
     34 ];
     35 
     36 const PRODUCT_TYPES = {
     37   'Smartphones': ['Phone', 'Smartphone', 'Mobile Device', 'Handset'],
     38   'Laptops': ['Laptop', 'Notebook', 'Ultrabook', 'Chromebook'],
     39   'Accessories': ['Cable', 'Charger', 'Case', 'Stand', 'Mount', 'Adapter', 'Hub'],
     40   'Smart Home': ['Camera', 'Doorbell', 'Thermostat', 'Lock', 'Speaker', 'Light'],
     41   'Men\'s Clothing': ['Shirt', 'T-Shirt', 'Pants', 'Jeans', 'Jacket', 'Sweater'],
     42   'Women\'s Clothing': ['Dress', 'Blouse', 'Skirt', 'Pants', 'Jacket', 'Sweater'],
     43   'Shoes': ['Sneakers', 'Boots', 'Sandals', 'Loafers', 'Athletic Shoes']
     44 };
     45 
     46 const SIZES = {
     47   clothing: ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
     48   shoes: ['6', '7', '8', '9', '10', '11', '12'],
     49   tech: ['64GB', '128GB', '256GB', '512GB', '1TB']
     50 };
     51 
     52 const COLORS = ['Black', 'White', 'Silver', 'Gold', 'Blue', 'Red', 'Green', 'Gray', 'Rose Gold', 'Navy'];
     53 
     54 const DESCRIPTIONS = [
     55   'High-quality product designed for everyday use.',
     56   'Premium build quality with advanced features.',
     57   'Perfect for both professional and personal use.',
     58   'Innovative design combining style and functionality.',
     59   'Top-rated product with excellent performance.',
     60   'Durable construction built to last.',
     61   'Feature-rich product at an affordable price.',
     62   'Award-winning design with cutting-edge technology.',
     63   'User-friendly interface with powerful capabilities.',
     64   'Sleek and modern design for the contemporary lifestyle.'
     65 ];
     66 
     67 // Helper functions
     68 function randomInt(min, max) {
     69   return Math.floor(Math.random() * (max - min + 1)) + min;
     70 }
     71 
     72 function randomChoice(array) {
     73   return array[randomInt(0, array.length - 1)];
     74 }
     75 
     76 function randomChoices(array, count) {
     77   const shuffled = [...array].sort(() => 0.5 - Math.random());
     78   return shuffled.slice(0, count);
     79 }
     80 
     81 function generateUUID() {
     82   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
     83     const r = Math.random() * 16 | 0;
     84     const v = c === 'x' ? r : (r & 0x3 | 0x8);
     85     return v.toString(16);
     86   });
     87 }
     88 
     89 function generateASIN() {
     90   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
     91   return Array.from({ length: 10 }, () => randomChoice([...chars])).join('');
     92 }
     93 
     94 function sqlString(str) {
     95   if (str === null || str === undefined) return 'NULL';
     96   return `'${str.replace(/'/g, "''")}'`;
     97 }
     98 
     99 function sqlArray(arr) {
    100   if (!arr || arr.length === 0) return 'NULL';
    101   return `ARRAY[${arr.map(sqlString).join(', ')}]`;
    102 }
    103 
    104 function calculateDiscount(initial, final) {
    105   if (!initial || initial === final) return null;
    106   const percent = Math.round(((initial - final) / initial) * 100);
    107   return `${percent}%`;
    108 }
    109 
    110 // Generate products
    111 function generateProducts() {
    112   const products = [];
    113   const productsByCategory = new Map();
    114 
    115   for (let i = 0; i < NUM_PRODUCTS; i++) {
    116     const category = randomChoice(CATEGORIES);
    117     const brand = randomChoice(BRANDS);
    118     const productType = randomChoice(PRODUCT_TYPES[category.name] || ['Product']);
    119     const adjective = randomChoice(category.adjectives);
    120     const modelNumber = randomInt(1000, 9999);
    121 
    122     // Generate pricing
    123     const basePrice = randomInt(10, 2000);
    124     const hasDiscount = Math.random() > 0.6; // 40% of products have discount
    125     const finalPrice = hasDiscount ? basePrice * (1 - randomInt(10, 40) / 100) : basePrice;
    126     const initialPrice = hasDiscount ? basePrice : null;
    127 
    128     // Generate sizes and colors based on category
    129     let sizes = null;
    130     let colors = null;
    131 
    132     if (category.root === 'Clothing' && category.name !== 'Shoes') {
    133       sizes = randomChoices(SIZES.clothing, randomInt(3, 6));
    134       colors = randomChoices(COLORS, randomInt(2, 4));
    135     } else if (category.name === 'Shoes') {
    136       sizes = randomChoices(SIZES.shoes, randomInt(5, 7));
    137       colors = randomChoices(COLORS, randomInt(1, 3));
    138     } else if (category.name === 'Smartphones' || category.name === 'Laptops') {
    139       sizes = randomChoices(SIZES.tech, randomInt(2, 4));
    140       colors = randomChoices(COLORS.slice(0, 5), randomInt(2, 4));
    141     } else {
    142       colors = randomChoices(COLORS.slice(0, 5), randomInt(1, 3));
    143     }
    144 
    145     const product = {
    146       product_id: `PROD-${String(i + 1).padStart(6, '0')}`,
    147       uuid: generateUUID(),
    148       asin: generateASIN(),
    149       sku: `${category.name.substring(0, 3).toUpperCase()}-${brand.substring(0, 3).toUpperCase()}-${String(i + 1).padStart(6, '0')}`,
    150       gtin: Math.random() > 0.5 ? `${randomInt(1000000000000, 9999999999999)}` : null,
    151       product_name: `${brand} ${adjective} ${productType} ${modelNumber}`,
    152       brand: brand,
    153       description: randomChoice(DESCRIPTIONS),
    154       category_name: category.name,
    155       root_category_name: category.root,
    156       price: parseFloat(finalPrice.toFixed(2)),
    157       initial_price: initialPrice ? parseFloat(initialPrice.toFixed(2)) : null,
    158       discount: calculateDiscount(initialPrice, finalPrice),
    159       currency: 'USD',
    160       stock_quantity: randomInt(0, 500),
    161       sizes: sizes,
    162       colors: colors,
    163       url: `https://example.com/product/${i + 1}`,
    164       image_url: null,
    165       available_for_delivery: Math.random() > 0.1,
    166       available_for_pickup: Math.random() > 0.5,
    167       free_returns: Math.random() > 0.4,
    168       is_active: Math.random() > 0.05,
    169       deleted_at: null,
    170       data_timestamp: new Date().toISOString()
    171     };
    172 
    173     products.push(product);
    174 
    175     if (!productsByCategory.has(category.name)) {
    176       productsByCategory.set(category.name, []);
    177     }
    178     productsByCategory.get(category.name).push(product);
    179   }
    180 
    181   return { products, productsByCategory };
    182 }
    183 
    184 // Generate ratings
    185 function generateRatings(products) {
    186   const ratings = [];
    187   let ratingId = 1;
    188 
    189   products.forEach((product, idx) => {
    190     const numRatings = randomInt(NUM_RATINGS_PER_PRODUCT.min, NUM_RATINGS_PER_PRODUCT.max);
    191 
    192     for (let i = 0; i < numRatings; i++) {
    193       const rating = {
    194         uuid: generateUUID(),
    195         product_id: idx + 1, // Will be the serial ID
    196         user_id: generateUUID(),
    197         rating: randomInt(1, 5),
    198         review: Math.random() > 0.6 ? randomChoice([
    199           'Great product! Highly recommended.',
    200           'Good value for money.',
    201           'Exceeded my expectations.',
    202           'Exactly as described.',
    203           'Fast shipping and great quality.',
    204           'Not bad, but could be better.',
    205           'Satisfied with the purchase.',
    206           'Works perfectly!',
    207           'Very happy with this product.',
    208           'Would buy again.'
    209         ]) : null
    210       };
    211 
    212       ratings.push(rating);
    213       ratingId++;
    214     }
    215   });
    216 
    217   return ratings;
    218 }
    219 
    220 // Generate SQL
    221 function generateSQL(products, ratings) {
    222   let sql = `-- Generated test data for products schema
    223 -- Total products: ${products.length}
    224 -- Total ratings: ${ratings.length}
    225 -- Generated at: ${new Date().toISOString()}
    226 
    227 SET search_path TO products, public;
    228 
    229 -- ============================================================
    230 -- INSERT PRODUCTS
    231 -- ============================================================
    232 INSERT INTO products (
    233   product_id, uuid, asin, sku, gtin, product_name, brand, description,
    234   category_id, price, initial_price, discount, currency, stock_quantity,
    235   sizes, colors, url, image_url,
    236   available_for_delivery, available_for_pickup, free_returns,
    237   is_active, deleted_at, data_timestamp
    238 ) VALUES\n`;
    239 
    240   // Insert products
    241   products.forEach((p, idx) => {
    242     sql += `  (`;
    243     sql += `${sqlString(p.product_id)}, `;
    244     sql += `${sqlString(p.uuid)}, `;
    245     sql += `${sqlString(p.asin)}, `;
    246     sql += `${sqlString(p.sku)}, `;
    247     sql += `${sqlString(p.gtin)}, `;
    248     sql += `${sqlString(p.product_name)}, `;
    249     sql += `${sqlString(p.brand)}, `;
    250     sql += `${sqlString(p.description)}, `;
    251     sql += `(SELECT id FROM categories WHERE name = ${sqlString(p.category_name)}), `;
    252     sql += `${p.price}, `;
    253     sql += `${p.initial_price || 'NULL'}, `;
    254     sql += `${sqlString(p.discount)}, `;
    255     sql += `${sqlString(p.currency)}, `;
    256     sql += `${p.stock_quantity}, `;
    257     sql += `${sqlArray(p.sizes)}, `;
    258     sql += `${sqlArray(p.colors)}, `;
    259     sql += `${sqlString(p.url)}, `;
    260     sql += `${sqlString(p.image_url)}, `;
    261     sql += `${p.available_for_delivery}, `;
    262     sql += `${p.available_for_pickup}, `;
    263     sql += `${p.free_returns}, `;
    264     sql += `${p.is_active}, `;
    265     sql += `${sqlString(p.deleted_at)}, `;
    266     sql += `${sqlString(p.data_timestamp)}`;
    267     sql += `)${idx < products.length - 1 ? ',' : ';'}\n`;
    268   });
    269 
    270   sql += `\n-- ============================================================\n`;
    271   sql += `-- INSERT RATINGS\n`;
    272   sql += `-- ============================================================\n`;
    273   sql += `INSERT INTO ratings (uuid, product_id, user_id, rating, review) VALUES\n`;
    274 
    275   // Insert ratings
    276   ratings.forEach((r, idx) => {
    277     sql += `  (`;
    278     sql += `${sqlString(r.uuid)}, `;
    279     sql += `${r.product_id}, `;
    280     sql += `${sqlString(r.user_id)}, `;
    281     sql += `${r.rating}, `;
    282     sql += `${sqlString(r.review)}`;
    283     sql += `)${idx < ratings.length - 1 ? ',' : ';'}\n`;
    284   });
    285 
    286   return sql;
    287 }
    288 
    289 // Main execution
    290 console.log('Generating test data...');
    291 console.log(`- Products: ${NUM_PRODUCTS}`);
    292 console.log(`- Ratings per product: ${NUM_RATINGS_PER_PRODUCT.min}-${NUM_RATINGS_PER_PRODUCT.max}`);
    293 
    294 const { products, productsByCategory } = generateProducts();
    295 const ratings = generateRatings(products);
    296 
    297 console.log('\nGenerated:');
    298 console.log(`  ${products.length} products`);
    299 console.log(`  ${ratings.length} ratings`);
    300 console.log('\nProducts by category:');
    301 productsByCategory.forEach((prods, cat) => {
    302   console.log(`  ${cat}: ${prods.length}`);
    303 });
    304 
    305 const sql = generateSQL(products, ratings);
    306 const outputPath = path.join(__dirname, 'data', 'products_test_data.sql');
    307 
    308 fs.writeFileSync(outputPath, sql);
    309 console.log(`\nSQL file generated: ${outputPath}`);
    310 console.log('\nTo load data:');
    311 console.log(`  psql -h localhost -p 5433 -U opentel_user -d opentel_db -f ${outputPath}`);