Files
alrahma_sunday_school_api/tools/sync_fillable_from_codeigniter.mjs
T
2026-04-23 00:04:35 -04:00

170 lines
5.0 KiB
JavaScript

/**
* 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;
}