exercises

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

generate_test_products.js (12061B)


      1 #!/usr/bin/env node
      2 
      3 /**
      4  * Generate realistic product test data for OtelMart
      5  *
      6  * This script generates product data that matches the UI expectations:
      7  * - ProductCard interface (for list view)
      8  * - Product interface (for detail view)
      9  *
     10  * Generated fields:
     11  * - Basic info: name, brand, description
     12  * - Pricing: price, initial_price, discount
     13  * - Inventory: stock_quantity
     14  * - Categories: with hierarchical support
     15  * - Attributes: sizes, colors (for applicable products)
     16  * - Ratings: sample ratings for some products
     17  */
     18 
     19 const fs = require('fs');
     20 const {randomUUID} = require('crypto');
     21 
     22 // =====================================================================
     23 // Configuration
     24 // =====================================================================
     25 
     26 const NUM_PRODUCTS = 2000;
     27 const NUM_RATINGS = 500; // Number of sample ratings to generate
     28 const OUTPUT_FILE = './test_data.sql';
     29 
     30 // =====================================================================
     31 // Data Templates
     32 // =====================================================================
     33 
     34 const BRANDS = [
     35   'TechPro', 'CoreTech', 'SmartLife', 'HomeEssentials', 'StyleHub',
     36   'SportMax', 'EcoGear', 'PremiumChoice', 'ValueBrand', 'EliteGoods',
     37   'FastTrack', 'PureQuality', 'ModernHome', 'ActiveLife', 'ComfortZone'
     38 ];
     39 
     40 const PRODUCT_TYPES = {
     41   smartphones: ['iPhone Pro', 'Galaxy Ultra', 'Pixel Premium', 'OnePlus Flagship'],
     42   laptops: ['MacBook Pro', 'ThinkPad Elite', 'XPS Premium', 'Surface Laptop'],
     43   accessories: ['Wireless Mouse', 'Keyboard', 'USB Hub', 'Phone Case', 'Screen Protector', 'Charger', 'Cable', 'Earbuds'],
     44   smartHome: ['Smart Speaker', 'Security Camera', 'Thermostat', 'Video Doorbell', 'Light Bulb', 'Smart Lock'],
     45   mensClothing: ['T-Shirt', 'Jeans', 'Jacket', 'Hoodie', 'Shirt', 'Pants'],
     46   womensClothing: ['Dress', 'Blouse', 'Skirt', 'Leggings', 'Sweater'],
     47   shoes: ['Running Shoes', 'Sneakers', 'Boots', 'Sandals'],
     48   homeGarden: ['Garden Tools', 'Furniture', 'Decor', 'Kitchen Set'],
     49   sports: ['Yoga Mat', 'Dumbbells', 'Resistance Bands', 'Treadmill'],
     50   books: ['Fiction Novel', 'Cookbook', 'Self-Help Book', 'Technical Manual'],
     51   health: ['Vitamins', 'Face Cream', 'Shampoo', 'Supplements'],
     52   toys: ['Board Game', 'Action Figure', 'Puzzle', 'LEGO Set'],
     53   automotive: ['Car Cover', 'Floor Mats', 'Phone Mount', 'HDMI Cable']
     54 };
     55 
     56 const ADJECTIVES = [
     57   'Premium', 'Elite', 'Pro', 'Ultra', 'Smart', 'Eco-Friendly',
     58   'Wireless', 'Portable', 'Compact', 'Heavy-Duty', 'Deluxe',
     59   'Advanced', 'Professional', 'Classic', 'Modern', 'Innovative'
     60 ];
     61 
     62 const SIZES = {
     63   clothing: ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
     64   shoes: ['7', '8', '9', '10', '11', '12'],
     65   default: []
     66 };
     67 
     68 const COLORS = {
     69   electronics: ['Black', 'Silver', 'White', 'Space Gray'],
     70   clothing: ['Black', 'White', 'Navy', 'Gray', 'Red', 'Blue', 'Green'],
     71   shoes: ['Black', 'White', 'Brown', 'Gray'],
     72   default: []
     73 };
     74 
     75 // Map product types to categories (will be replaced with DB category IDs)
     76 const CATEGORY_MAP = {
     77   smartphones: 'Smartphones',
     78   laptops: 'Laptops',
     79   accessories: 'Accessories',
     80   smartHome: 'Smart Home',
     81   mensClothing: "Men's Clothing",
     82   womensClothing: "Women's Clothing",
     83   shoes: 'Shoes',
     84   homeGarden: 'Home & Garden',
     85   sports: 'Sports & Outdoors',
     86   books: 'Books & Media',
     87   health: 'Health & Beauty',
     88   toys: 'Toys & Games',
     89   automotive: 'Automotive'
     90 };
     91 
     92 // =====================================================================
     93 // Helper Functions
     94 // =====================================================================
     95 
     96 function randomInt(min, max) {
     97   return Math.floor(Math.random() * (max - min + 1)) + min;
     98 }
     99 
    100 function randomElement(array) {
    101   return array[randomInt(0, array.length - 1)];
    102 }
    103 
    104 function randomBool(probability = 0.5) {
    105   return Math.random() < probability;
    106 }
    107 
    108 let skuCounter = 10000;
    109 function generateSKU() {
    110   const prefix = randomElement(['PRD', 'SKU', 'ITM']);
    111   return `${prefix}-${skuCounter++}`;
    112 }
    113 
    114 let asinCounter = 100000;
    115 function generateASIN() {
    116   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    117   let asin = 'B';
    118   const counterStr = asinCounter.toString().padStart(8, '0');
    119   asin += counterStr;
    120   const checkChar = chars[asinCounter % chars.length];
    121   asin += checkChar;
    122   asinCounter++;
    123   return asin;
    124 }
    125 
    126 function generateDescription(productName, brand) {
    127   const templates = [
    128     `${productName} from ${brand}. High-quality product with excellent features and durability.`,
    129     `Experience the best with this ${productName}. Perfect for everyday use and built to last.`,
    130     `${brand}'s ${productName} combines style and functionality in one amazing package.`,
    131     `Premium ${productName} designed for maximum performance and user satisfaction.`,
    132     `Innovative ${productName} featuring cutting-edge technology and modern design.`
    133   ];
    134   return randomElement(templates);
    135 }
    136 
    137 function generatePrice() {
    138   const basePrice = randomInt(10, 500);
    139   const cents = randomElement([99, 95, 50, 00]);
    140   return `${basePrice}.${cents.toString().padStart(2, '0')}`;
    141 }
    142 
    143 function calculateDiscount(initialPrice, finalPrice) {
    144   const initial = parseFloat(initialPrice);
    145   const final = parseFloat(finalPrice);
    146 
    147   if (initial <= final) return null;
    148 
    149   const percentage = ((initial - final) / initial * 100).toFixed(0);
    150   return `${percentage}%`;
    151 }
    152 
    153 function getSizesForCategory(category) {
    154   if (category.includes('Clothing')) return SIZES.clothing;
    155   if (category.includes('Shoes')) return SIZES.shoes;
    156   return SIZES.default;
    157 }
    158 
    159 function getColorsForCategory(category) {
    160   if (category.includes('Electronics') || category.includes('Smart')) return COLORS.electronics;
    161   if (category.includes('Clothing')) return COLORS.clothing;
    162   if (category.includes('Shoes')) return COLORS.shoes;
    163   return COLORS.default;
    164 }
    165 
    166 // =====================================================================
    167 // Product Generation
    168 // =====================================================================
    169 
    170 function generateProduct(id, typeKey, category) {
    171   const productTypes = PRODUCT_TYPES[typeKey];
    172   const baseName = randomElement(productTypes);
    173   const adjective = randomBool(0.6) ? randomElement(ADJECTIVES) + ' ' : '';
    174   const number = randomInt(1000, 9999);
    175   const brand = randomElement(BRANDS);
    176 
    177   const productName = `${adjective}${baseName} ${number}`;
    178   const description = generateDescription(baseName, brand);
    179 
    180   // Pricing logic: 30% of products have discounts
    181   const hasDiscount = randomBool(0.3);
    182   let finalPrice, initialPrice, discount;
    183 
    184   if (hasDiscount) {
    185     initialPrice = generatePrice();
    186     const discountPercent = randomInt(10, 50);
    187     const final = parseFloat(initialPrice) * (1 - discountPercent / 100);
    188     finalPrice = final.toFixed(2);
    189     discount = calculateDiscount(initialPrice, finalPrice);
    190   } else {
    191     finalPrice = generatePrice();
    192     initialPrice = null;
    193     discount = null;
    194   }
    195 
    196   // Stock
    197   const stockQuantity = randomInt(0, 500);
    198 
    199   // Sizes and colors
    200   const sizes = getSizesForCategory(category);
    201   const colors = getColorsForCategory(category);
    202 
    203   const sizesArray = sizes.length > 0 && randomBool(0.7) ?
    204     `'{${sizes.slice(0, randomInt(2, sizes.length)).join(',')}}'` :
    205     'NULL';
    206 
    207   const colorsArray = colors.length > 0 && randomBool(0.8) ?
    208     `'{${colors.slice(0, randomInt(2, colors.length)).join(',')}}'` :
    209     'NULL';
    210 
    211   // Flags
    212   const availableForDelivery = randomBool(0.95);
    213   const availableForPickup = randomBool(0.3);
    214   const freeReturns = randomBool(0.6);
    215   const isActive = randomBool(0.98);
    216 
    217   return {
    218     id,
    219     uuid: randomUUID(),
    220     asin: generateASIN(),
    221     sku: generateSKU(),
    222     productName,
    223     brand,
    224     description,
    225     category,
    226     price: finalPrice,
    227     initialPrice,
    228     discount,
    229     stockQuantity,
    230     sizes: sizesArray,
    231     colors: colorsArray,
    232     availableForDelivery,
    233     availableForPickup,
    234     freeReturns,
    235     isActive
    236   };
    237 }
    238 
    239 // =====================================================================
    240 // Generate SQL
    241 // =====================================================================
    242 
    243 function escapeSQL(str) {
    244   if (str === null || str === undefined) return 'NULL';
    245   return `'${str.replace(/'/g, "''")}'`;
    246 }
    247 
    248 function generateProductSQL(product) {
    249   return `(
    250     ${escapeSQL(product.uuid)},
    251     ${escapeSQL(product.asin)},
    252     ${escapeSQL(product.sku)},
    253     ${escapeSQL(product.productName)},
    254     ${escapeSQL(product.brand)},
    255     ${escapeSQL(product.description)},
    256     (SELECT id FROM categories WHERE name = ${escapeSQL(product.category)}),
    257     ${product.price},
    258     ${product.initialPrice ? product.initialPrice : 'NULL'},
    259     ${escapeSQL(product.discount)},
    260     'USD',
    261     ${product.stockQuantity},
    262     ${product.sizes},
    263     ${product.colors},
    264     ${product.availableForDelivery},
    265     ${product.availableForPickup},
    266     ${product.freeReturns},
    267     ${product.isActive}
    268   )`;
    269 }
    270 
    271 function generateRatingSQL(productId, userId, rating, review) {
    272   return `(
    273     ${escapeSQL(randomUUID())},
    274     ${productId},
    275     ${escapeSQL(userId)},
    276     ${rating},
    277     ${escapeSQL(review)}
    278   )`;
    279 }
    280 
    281 // =====================================================================
    282 // Main Generation
    283 // =====================================================================
    284 
    285 console.log('Generating test product data...');
    286 
    287 const products = [];
    288 let productId = 1;
    289 
    290 // Distribute products across categories
    291 const typeKeys = Object.keys(PRODUCT_TYPES);
    292 const productsPerType = Math.floor(NUM_PRODUCTS / typeKeys.length);
    293 
    294 typeKeys.forEach((typeKey, index) => {
    295   const count = index === typeKeys.length - 1 ?
    296     NUM_PRODUCTS - products.length : // Last type gets remainder
    297     productsPerType;
    298 
    299   const category = CATEGORY_MAP[typeKey];
    300 
    301   for (let i = 0; i < count; i++) {
    302     products.push(generateProduct(productId++, typeKey, category));
    303   }
    304 });
    305 
    306 console.log(`Generated ${products.length} products`);
    307 
    308 // Generate SQL file
    309 let sql = `-- =====================================================================
    310 -- OTELMART TEST DATA
    311 -- Generated: ${new Date().toISOString()}
    312 -- Products: ${products.length}
    313 -- Ratings: ${NUM_RATINGS}
    314 -- =====================================================================
    315 
    316 SET search_path TO products, public;
    317 
    318 -- =====================================================================
    319 -- INSERT PRODUCTS
    320 -- =====================================================================
    321 
    322 INSERT INTO products (
    323   uuid, asin, sku, product_name, brand, description,
    324   category_id, price, initial_price, discount, currency,
    325   stock_quantity, sizes, colors,
    326   available_for_delivery, available_for_pickup, free_returns, is_active
    327 ) VALUES\n`;
    328 
    329 sql += products.map((p, i) =>
    330   generateProductSQL(p) + (i < products.length - 1 ? ',' : ';')
    331 ).join('\n');
    332 
    333 // Generate sample ratings
    334 sql += `\n\n-- =====================================================================
    335 -- INSERT SAMPLE RATINGS
    336 -- =====================================================================
    337 
    338 INSERT INTO ratings (uuid, product_id, user_id, rating, review) VALUES\n`;
    339 
    340 const ratingReviews = [
    341   'Great product! Highly recommended.',
    342   'Excellent quality and fast shipping.',
    343   'Good value for the price.',
    344   'Works perfectly as described.',
    345   'Very satisfied with this purchase.',
    346   'Amazing! Better than expected.',
    347   'Decent product, does the job.',
    348   'Not bad, but could be better.',
    349   'Fantastic quality!',
    350   null // Some ratings without reviews
    351 ];
    352 
    353 const ratings = [];
    354 for (let i = 0; i < NUM_RATINGS; i++) {
    355   const productId = randomInt(1, products.length);
    356   const userId = randomUUID();
    357   const rating = randomInt(3, 5); // Mostly positive ratings
    358   const review = randomElement(ratingReviews);
    359 
    360   ratings.push(generateRatingSQL(productId, userId, rating, review));
    361 }
    362 
    363 sql += ratings.map((r, i) =>
    364   r + (i < ratings.length - 1 ? ',' : ';')
    365 ).join('\n');
    366 
    367 sql += '\n\n-- Data generation complete\n';
    368 
    369 // Write to file
    370 fs.writeFileSync(OUTPUT_FILE, sql);
    371 
    372 console.log(`\nTest data generated successfully!`);
    373 console.log(`Output file: ${OUTPUT_FILE}`);
    374 console.log(`\nTo import:`);
    375 console.log(`psql -h localhost -p 5433 -U opentel_user -d opentel_db -f ${OUTPUT_FILE}`);