/** * One-off: tenant/company code = nombre(4) + YYMM; username = code lowercase. * Usage: deno run -A scripts/migrate-tenant-codes.ts */ import { Database } from "jsr:@db/sqlite@0.12"; const PLATFORM = new URL("../data/platform.db", import.meta.url).pathname; const APP = new URL("../data/app.db", import.meta.url).pathname; function shortFromName(name: string): string { const slug = (name ?? "") .toString() .normalize("NFD") .replace(/\p{M}/gu, "") .toUpperCase() .replace(/[^A-Z0-9]+/g, "") .slice(0, 4); return slug.length >= 2 ? slug : (slug || "EMP").padEnd(2, "X").slice(0, 4); } function stampFromCreated(createdAt: string): string { const m = createdAt.match(/^(\d{4})-(\d{2})/); if (!m) { const d = new Date(); return `${String(d.getFullYear()).slice(-2)}${String(d.getMonth() + 1).padStart(2, "0")}`; } return `${m[1].slice(-2)}${m[2]}`; } const platform = new Database(PLATFORM); const app = new Database(APP); platform.exec("PRAGMA foreign_keys = ON;"); app.exec("PRAGMA foreign_keys = ON;"); const tenants = platform.prepare( "SELECT id, code, name, created_at FROM tenants ORDER BY id", ).all() as { id: number; code: string; name: string; created_at: string }[]; const used = new Set([ ...(app.prepare("SELECT code FROM companies").all() as { code: string }[]).map((r) => r.code), ...tenants.map((t) => t.code), ]); function allocate(name: string, createdAt: string, oldCode: string): string { const base = shortFromName(name); const stamp = stampFromCreated(createdAt); let candidate = `${base}${stamp}`; if (candidate === oldCode || !used.has(candidate)) return candidate; for (let n = 2; n < 1000; n++) { const suffix = String(n); const head = base.slice(0, Math.max(2, 4 - suffix.length)); candidate = `${head}${stamp}${suffix}`; if (candidate === oldCode || !used.has(candidate)) return candidate; } throw new Error(`No unique code for ${name}`); } type MapRow = { tenantId: number; oldCode: string; newCode: string; oldUser: string; newUser: string; }; const mappings: MapRow[] = []; for (const t of tenants) { const newCode = allocate(t.name, t.created_at, t.code); used.add(newCode); mappings.push({ tenantId: t.id, oldCode: t.code, newCode, oldUser: `adm.${t.code.toLowerCase()}`, newUser: newCode.toLowerCase(), }); } console.log("Migrating:", mappings); for (const m of mappings) { if (m.oldCode !== m.newCode) { platform.prepare("UPDATE tenants SET code = ? WHERE id = ?").run(m.newCode, m.tenantId); const principal = app.prepare( "SELECT id, code FROM companies WHERE tenant_id = ? AND kind = 'principal' ORDER BY id LIMIT 1", ).get(m.tenantId) as { id: number; code: string } | undefined; if (principal && principal.code === m.oldCode) { app.prepare("UPDATE companies SET code = ? WHERE id = ?").run(m.newCode, principal.id); app.prepare("UPDATE workers SET hire_type = ? WHERE company_id = ? AND hire_type = ?") .run(m.newCode, principal.id, m.oldCode); } } const clash = app.prepare("SELECT id FROM users WHERE username = ?").get(m.newUser); if (!clash) { app.prepare( "UPDATE users SET username = ? WHERE tenant_id = ? AND username = ?", ).run(m.newUser, m.tenantId, m.oldUser); // also catch if username already equals old code lowercase without adm. app.prepare( "UPDATE users SET username = ? WHERE tenant_id = ? AND username = ? AND username != ?", ).run(m.newUser, m.tenantId, m.oldCode.toLowerCase(), m.newUser); } else { // ensure old adm.* row is renamed only if target not taken by another user const old = app.prepare( "SELECT id FROM users WHERE tenant_id = ? AND username = ?", ).get(m.tenantId, m.oldUser) as { id: number } | undefined; if (old && (clash as { id: number }).id === old.id) { // same row already correct } else if (old) { console.warn(`Skip user rename ${m.oldUser} → ${m.newUser}: target exists`); } } } console.log("TENANTS", platform.prepare("SELECT id, code, name FROM tenants").all()); console.log( "PRINCIPALS", app.prepare("SELECT id, code, tenant_id, kind FROM companies WHERE kind = 'principal'").all(), ); console.log( "USERS", app.prepare("SELECT id, username, tenant_id, role FROM users").all(), ); platform.close(); app.close();