/** * generate-cl-seed.cjs * * Reads both Punchlist_Reference sheets from the Excel templates and writes * sql/cl-seed.sql containing INSERT statements for: * - admin_categories (unique category values) * - admin_sub_categories (unique sub-category per category, with FK) * - admin_severities (unique severity values, normalised to title-case) * - admin_cl_records (every valid row – skips rows with no severity) * * Run inside the Docker app container: * node /workspace/scripts/generate-cl-seed.cjs * * Normalisation rules applied to severities: * - Leading / trailing whitespace stripped. * - Each word capitalised (title-case) so that "major", "Major" and * "MAJOR" all become "Major"; "on hold" / "On hold" both become "On Hold". * - Rows whose severity cell is empty, null or undefined are skipped entirely. */ 'use strict'; const XLSX = require('/app/node_modules/xlsx'); const fs = require('fs'); const path = require('path'); // ─── helpers ──────────────────────────────────────────────────────────────── /** Escape a value for use inside a SQL single-quoted string literal. * - Replaces CR/LF sequences and bare LF with a single space so the SQL * file stays on one logical line per value (avoids broken multi-line * string literals that confuse some SQL clients). * - Escapes embedded single quotes by doubling them. */ function esc(v) { if (v == null) return ''; return String(v) .replace(/\r\n|\r|\n/g, ' ') // collapse newlines to space .replace(/'/g, "''"); // escape single quotes } /** * Convert a string to title-case. * "on hold" → "On Hold" "major" → "Major" */ function toTitleCase(str) { return str .trim() .replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()); } // ─── load both files ───────────────────────────────────────────────────────── const FILES = [ '/workspace/cl_template/OVXXX_AURORA_ANT_CL_MW_V3.2_20220502.xlsx', '/workspace/cl_template/OVXXX_AURORA_CW_QC_MW_V3.2_20220502.xlsx', ]; const allRows = []; // { sort_order, category, sub_category, desc_en, desc_fr, desc_nl, severity } for (const filePath of FILES) { const wb = XLSX.readFile(filePath); const ws = wb.Sheets['Punchlist_Reference']; const data = XLSX.utils.sheet_to_json(ws, { header: 1 }); // Row 0 is the header; skip it. for (let i = 1; i < data.length; i++) { const row = data[i]; // Column A (index 0) must have a numeric sort_order; empty trailing rows skipped. if (row[0] == null) continue; const sortOrder = Number(row[0]); const category = String(row[4] ?? '').trim(); const subCat = String(row[5] ?? '').trim(); const descEn = String(row[6] ?? '').trim(); const descFr = String(row[7] ?? '').trim(); const descNl = String(row[8] ?? '').trim(); const severityRaw = row[9]; // Skip rows that carry no severity value. if (severityRaw == null || String(severityRaw).trim() === '') { console.warn(` Skipping row sort_order=${sortOrder}: no severity value`); continue; } const severity = toTitleCase(String(severityRaw)); allRows.push({ sortOrder, category, subCat, descEn, descFr, descNl, severity }); } } console.log(`Loaded ${allRows.length} valid rows total.`); // ─── derive unique lookup sets (insertion-order preserved via Map) ──────────── // Categories: unique by value const categoriesMap = new Map(); // value → true (just for dedup, order preserved) for (const r of allRows) { if (!categoriesMap.has(r.category)) categoriesMap.set(r.category, true); } const categories = [...categoriesMap.keys()]; // ordered list // Sub-categories: unique by (category, subCat) const subCatMap = new Map(); // "category|||subCat" → { category, subCat } for (const r of allRows) { const key = `${r.category}|||${r.subCat}`; if (!subCatMap.has(key)) subCatMap.set(key, { category: r.category, subCat: r.subCat }); } const subCats = [...subCatMap.values()]; // Severities: unique by normalised value const severitiesMap = new Map(); for (const r of allRows) { if (!severitiesMap.has(r.severity)) severitiesMap.set(r.severity, true); } const severities = [...severitiesMap.keys()]; console.log(`Categories: ${categories.length}, SubCategories: ${subCats.length}, Severities: ${severities.length}`); // ─── build SQL ─────────────────────────────────────────────────────────────── const lines = []; lines.push('-- ============================================================'); lines.push('-- cl-seed.sql'); lines.push('--'); lines.push('-- Auto-generated from Punchlist_Reference sheets in:'); lines.push('-- OVXXX_AURORA_ANT_CL_MW_V3.2_20220502.xlsx'); lines.push('-- OVXXX_AURORA_CW_QC_MW_V3.2_20220502.xlsx'); lines.push('--'); lines.push('-- Generated: ' + new Date().toISOString()); lines.push('--'); lines.push('-- Severity values are normalised to title-case.'); lines.push('-- Records with no severity are omitted.'); lines.push('-- Use INSERT IGNORE so re-running the script is idempotent.'); lines.push('-- ============================================================'); lines.push(''); lines.push('USE check_list;'); lines.push(''); // ── 1. admin_categories ──────────────────────────────────────────────────── lines.push('-- ── 1. Categories ────────────────────────────────────────────'); lines.push('INSERT IGNORE INTO admin_categories (value) VALUES'); const catValues = categories.map(c => ` ('${esc(c)}')`); lines.push(catValues.join(',\n') + ';'); lines.push(''); // ── 2. admin_sub_categories ─────────────────────────────────────────────── // FK to admin_categories resolved via a UNION ALL derived table. // (The VALUES ROW() syntax requires MariaDB ≥ 10.3.3; UNION ALL is universal.) lines.push('-- ── 2. Sub-categories (FK resolved via sub-select) ──────────'); lines.push('INSERT IGNORE INTO admin_sub_categories (value, category_id)'); lines.push('SELECT vals.value, c.id'); lines.push('FROM ('); const scValues = subCats.map( (sc, i) => ` ${i === 0 ? 'SELECT' : 'UNION ALL SELECT'} '${esc(sc.subCat)}' AS value, '${esc(sc.category)}' AS cat_value` ); lines.push(scValues.join('\n')); lines.push(') AS vals'); lines.push('JOIN admin_categories c ON c.value = vals.cat_value;'); lines.push(''); // ── 3. admin_severities ────────────────────────────────────────────────── lines.push('-- ── 3. Severities ───────────────────────────────────────────'); lines.push('INSERT IGNORE INTO admin_severities (value) VALUES'); const sevValues = severities.map(s => ` ('${esc(s)}')`); lines.push(sevValues.join(',\n') + ';'); lines.push(''); // ── 4. admin_cl_records ────────────────────────────────────────────────── lines.push('-- ── 4. Check-list records ───────────────────────────────────'); lines.push('INSERT IGNORE INTO admin_cl_records'); lines.push(' (sort_order, category, sub_category, severity, description_en, description_fr, description_nl)'); lines.push('VALUES'); const recValues = allRows.map(r => ` (${r.sortOrder}, '${esc(r.category)}', '${esc(r.subCat)}', '${esc(r.severity)}', '${esc(r.descEn)}', '${esc(r.descFr)}', '${esc(r.descNl)}')` ); lines.push(recValues.join(',\n') + ';'); lines.push(''); // ─── write output file ─────────────────────────────────────────────────────── const outPath = '/workspace/sql/cl-seed.sql'; fs.writeFileSync(outPath, lines.join('\n'), 'utf8'); console.log(`\nSQL written to ${outPath}`); console.log(` ${categories.length} categories`); console.log(` ${subCats.length} sub-categories`); console.log(` ${severities.length} severities`); console.log(` ${allRows.length} cl_records`);