170 lines
5.0 KiB
JavaScript
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;
|
|
}
|