/** * Sync Laravel $fillable + $timestamps from CodeIgniter *Model.php (sibling app_codeigniter). * Run: node tools/sync_fillable_from_codeigniter.mjs */ import { readdir, readFile, writeFile } from "node:fs/promises"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const laravelRoot = join(__dirname, ".."); const workspaceRoot = join(laravelRoot, ".."); const ciModelsDir = join(workspaceRoot, "app_codeigniter", "app", "Models"); const laravelModelsDir = join(laravelRoot, "app", "Models"); const manualCiToLaravel = new Map([ ["SettingsModel.php", "Setting.php"], ]); /** CI models that must not sync (legacy duplicate of another model). */ const skipCiBasenames = new Set([ "FlagModel.php", // table `flag`; Laravel uses CurrentFlagModel → current_flag ]); const skipLaravel = new Set([ "Teacher.php", "Communication.php", "EventCharge.php", "ClassModel.php", "Calendar.php", ]); function ciFileToLaravel(ciBasename) { if (manualCiToLaravel.has(ciBasename)) return manualCiToLaravel.get(ciBasename); const m = ciBasename.match(/^(.+)Model\.php$/); if (!m) return null; const base = m[1]; if (base === "Parent" || base === "Class") return `${base}Model.php`; return `${base}.php`; } /** Find array after `protected $allowedFields =` (flexible spacing). */ function extractAllowedFieldsArrayLiteral(src) { const m = src.match(/protected\s+\$allowedFields\s*=\s*/); if (!m || m.index === undefined) return null; let pos = m.index + m[0].length; while (pos < src.length && /\s/.test(src[pos])) pos++; if (pos >= src.length || src[pos] !== "[") return null; const start = pos; let depth = 0; for (let i = pos; i < src.length; i++) { const c = src[i]; if (c === "[") depth++; else if (c === "]") { depth--; if (depth === 0) return src.slice(start, i + 1); } } return null; } function parseQuotedStrings(arrayLiteral) { const out = []; const re = /'((?:\\'|[^'])*)'/g; let m; while ((m = re.exec(arrayLiteral))) out.push(m[1].replace(/\\'/g, "'")); return out; } function renderFillable(items) { const lines = items.map((it) => { const escaped = String(it).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); return ` '${escaped}',`; }); return `[\n${lines.join("\n")}\n ]`; } function replaceFillable(lv, inner) { const re = /protected\s+\$fillable\s*=\s*\[[\s\S]*?\];/m; if (!re.test(lv)) return null; return lv.replace(re, `protected $fillable = ${inner};`); } function replaceTimestamps(lv, useTs) { const line = useTs ? " public $timestamps = true;" : " public $timestamps = false;"; // Normalize any leading whitespace so re-sync does not duplicate lines or pile up indent. if (/^\s*public\s+\$timestamps\s*=\s*(true|false)\s*;/m.test(lv)) return lv.replace(/^\s*public\s+\$timestamps\s*=\s*(true|false)\s*;/m, line); return lv.replace(/(class\s+\w+[^\n]*\n\{)/, `$1\n${line}\n`); } let updated = 0, skipped = 0; const missing = []; const files = await readdir(ciModelsDir).catch(() => []); for (const ciBasename of files) { if (!ciBasename.endsWith("Model.php")) continue; if (skipCiBasenames.has(ciBasename)) { console.error(`Skip CI (legacy): ${ciBasename}`); skipped++; continue; } const laravelBasename = ciFileToLaravel(ciBasename); if (!laravelBasename) continue; if (skipLaravel.has(laravelBasename)) { console.error(`Skip (alias/base): ${laravelBasename}`); skipped++; continue; } const lvPath = join(laravelModelsDir, laravelBasename); try { await readFile(lvPath, "utf8"); } catch { missing.push(`${laravelBasename} (from ${ciBasename})`); continue; } const ciSrc = await readFile(join(ciModelsDir, ciBasename), "utf8"); let lvSrc = await readFile(lvPath, "utf8"); let useTs = true; const tm = ciSrc.match(/protected\s+\$useTimestamps\s*=\s*(true|false)\s*;/); if (tm) useTs = tm[1] === "true"; const allowedBody = extractAllowedFieldsArrayLiteral(ciSrc); if (!allowedBody) { console.error(`No allowedFields in ${ciBasename}`); skipped++; continue; } const fields = parseQuotedStrings(allowedBody); if (!fields.length) { console.error(`Empty allowedFields in ${ciBasename}`); skipped++; continue; } if (!/protected\s+\$fillable\s*=\s*\[/.test(lvSrc)) { console.error(`No $fillable in ${laravelBasename}`); skipped++; continue; } const rendered = renderFillable(fields); let next = replaceFillable(lvSrc, rendered); if (next == null) { skipped++; continue; } next = replaceTimestamps(next, useTs); if (next === lvSrc) { console.error(`Unchanged: ${laravelBasename}`); skipped++; continue; } await writeFile(lvPath, next, "utf8"); console.log(`Updated ${laravelBasename}`); updated++; } console.log(`\nDone. Updated: ${updated}, skipped: ${skipped}`); if (missing.length) { console.error(`Missing (${missing.length}): ${missing.slice(0, 25).join(", ")}${missing.length > 25 ? "…" : ""}`); process.exitCode = 2; }