Add DB seed script with admin bootstrap
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import db from '../db/index.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const dataDir = join(__dirname, '..', 'data');
|
||||
|
||||
const ghosts = JSON.parse(readFileSync(join(dataDir, 'ghosts.json'), 'utf8'));
|
||||
const abilities = JSON.parse(readFileSync(join(dataDir, 'abilities.json'), 'utf8'));
|
||||
const sets = JSON.parse(readFileSync(join(dataDir, 'sets.json'), 'utf8'));
|
||||
|
||||
// Some source ghosts have "-" (null) health/damage. Derive a sensible value from
|
||||
// the average of same type+rarity peers so AR combat always has valid numbers.
|
||||
function deriveStats(list) {
|
||||
const buckets = {};
|
||||
for (const g of list) {
|
||||
if (g.health == null || g.damage == null) continue;
|
||||
const k = `${g.type}-${g.rarity}`;
|
||||
(buckets[k] ??= { h: [], d: [] });
|
||||
buckets[k].h.push(g.health);
|
||||
buckets[k].d.push(g.damage);
|
||||
}
|
||||
const avg = (a) => (a.length ? Math.round(a.reduce((x, y) => x + y, 0) / a.length) : null);
|
||||
const fallback = { h: 338, d: 192 }; // global-ish midpoint if a whole bucket is empty
|
||||
for (const g of list) {
|
||||
const k = `${g.type}-${g.rarity}`;
|
||||
const b = buckets[k];
|
||||
if (g.health == null) g.health = (b && avg(b.h)) ?? fallback.h;
|
||||
if (g.damage == null) g.damage = (b && avg(b.d)) ?? fallback.d;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
deriveStats(ghosts);
|
||||
|
||||
const insertAbility = db.prepare(`
|
||||
INSERT INTO abilities (name, kind, charges, cooldown, effect)
|
||||
VALUES (@name, @kind, @charges, @cooldown, @effect)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
kind=excluded.kind, charges=excluded.charges,
|
||||
cooldown=excluded.cooldown, effect=excluded.effect
|
||||
`);
|
||||
|
||||
const insertGhost = db.prepare(`
|
||||
INSERT INTO ghosts
|
||||
(name, display_name, type, rarity, speed, range, charge_shot,
|
||||
health, damage, ability, is_boss, set_number, set_name, enabled)
|
||||
VALUES
|
||||
(@name, @name, @type, @rarity, @speed, @range, @chargeShot,
|
||||
@health, @damage, @ability, @isBoss, @setNumber, @setName, 1)
|
||||
`);
|
||||
|
||||
const seed = db.transaction(() => {
|
||||
// Only seed ghosts/abilities/sets if the ghosts table is empty,
|
||||
// so re-running seed never clobbers admin edits.
|
||||
const count = db.prepare('SELECT COUNT(*) AS n FROM ghosts').get().n;
|
||||
if (count === 0) {
|
||||
for (const a of abilities) {
|
||||
insertAbility.run({
|
||||
name: a.name, kind: a.kind ?? 'common',
|
||||
charges: a.charges ?? null, cooldown: a.cooldown ?? null, effect: a.effect ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
const idByName = {};
|
||||
for (const g of ghosts) {
|
||||
const info = insertGhost.run({ ...g, isBoss: g.isBoss ? 1 : 0 });
|
||||
idByName[g.name] = info.lastInsertRowid;
|
||||
}
|
||||
|
||||
// Build a set per canonical boss-linked LEGO reference.
|
||||
// Roster = the set's boss + a curated sampling of type-matched ghosts,
|
||||
// so each scan returns a playable mix across rarities.
|
||||
const insertSet = db.prepare(`
|
||||
INSERT INTO sets (code, set_number, set_name, boss_ghost_id, enabled)
|
||||
VALUES (@code, @setNumber, @setName, @bossId, 1)
|
||||
`);
|
||||
const linkGhost = db.prepare(
|
||||
'INSERT OR IGNORE INTO set_ghosts (set_id, ghost_id) VALUES (?, ?)'
|
||||
);
|
||||
|
||||
for (const s of sets) {
|
||||
const bossId = s.boss ? idByName[s.boss] : null;
|
||||
const bossGhost = ghosts.find((g) => g.name === s.boss);
|
||||
const code = `NN-${s.setNumber}`;
|
||||
const info = insertSet.run({
|
||||
code, setNumber: s.setNumber, setName: s.setName, bossId,
|
||||
});
|
||||
const setId = info.lastInsertRowid;
|
||||
|
||||
// roster: boss + up to 6 non-boss ghosts whose type matches the boss type
|
||||
const roster = new Set();
|
||||
if (bossId) roster.add(bossId);
|
||||
const pool = ghosts.filter(
|
||||
(g) => !g.isBoss && (!bossGhost || g.type === bossGhost.type)
|
||||
);
|
||||
// spread across rarities 1..4
|
||||
for (let r = 1; r <= 4; r++) {
|
||||
const pick = pool.filter((g) => g.rarity === r).slice(0, 2);
|
||||
for (const g of pick) roster.add(idByName[g.name]);
|
||||
}
|
||||
for (const gid of roster) linkGhost.run(setId, gid);
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure an admin user exists (bootstrap).
|
||||
const adminUser = process.env.ADMIN_USER || 'admin';
|
||||
const adminPass = process.env.ADMIN_PASS || 'changeme';
|
||||
const existing = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role='admin'").get().n;
|
||||
if (existing === 0) {
|
||||
const hash = bcrypt.hashSync(adminPass, 10);
|
||||
db.prepare(
|
||||
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
|
||||
).run(adminUser, hash, 'admin');
|
||||
console.log(`Created admin user "${adminUser}". CHANGE THE PASSWORD after first login.`);
|
||||
}
|
||||
});
|
||||
|
||||
seed();
|
||||
|
||||
const stats = {
|
||||
ghosts: db.prepare('SELECT COUNT(*) AS n FROM ghosts').get().n,
|
||||
abilities: db.prepare('SELECT COUNT(*) AS n FROM abilities').get().n,
|
||||
sets: db.prepare('SELECT COUNT(*) AS n FROM sets').get().n,
|
||||
rosterLinks: db.prepare('SELECT COUNT(*) AS n FROM set_ghosts').get().n,
|
||||
};
|
||||
console.log('Seed complete:', stats);
|
||||
Reference in New Issue
Block a user