import { createServer } from 'vite'; import { readFile } from 'node:fs/promises'; import { randomUUID } from 'node:crypto'; import { isDeepStrictEqual } from 'node:util'; import { resolve } from 'node:path'; const sourcePath = resolve( process.argv[2] || './scripts/data/resurgence-attributes.json', ); const records = JSON.parse(await readFile(sourcePath, 'utf8')); if (!Array.isArray(records) || !records.length) throw new Error('The Attribute import source is empty or invalid.'); const identities = new Set(); for (const record of records) { const identity = `${String(record.name).normalize('NFKC').trim().toLocaleLowerCase()}\u0000${String(record.unit ?? '')}`; if (identities.has(identity)) throw new Error( `Duplicate Attribute identity in import source: ${record.name} (${record.unit || 'flat'})`, ); identities.add(identity); if ( !['Offensive', 'Defensive'].includes(record.type) || !['Weapon', 'Skill', 'Character'].includes(record.category) || !['Additive', 'Multiplicative'].includes(record.calculationType) || !['', '%', 's', 'Shots', 'm'].includes(record.unit ?? '') ) { throw new Error( `Invalid Attribute classification in import source: ${record.name}`, ); } for (const range of [record.gearRange, record.weaponRange]) { if (range === undefined) continue; if ( !Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite) || range[0] > range[1] ) { throw new Error(`Invalid source range in import source: ${record.name}`); } } } const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom', logLevel: 'error', }); try { const { getDatabase } = await vite.ssrLoadModule( '/src/lib/server/database.ts', ); const { auditRecord, recordIsacAudit } = await vite.ssrLoadModule( '/src/lib/server/audit.ts', ); const database = await getDatabase(); const actor = await ( await database.prepare(`SELECT users.id FROM users LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN user_roles link ON link.user_id=users.id WHERE users.role='admin' OR link.role_id='role-admin' ORDER BY CASE WHEN users.username=? COLLATE NOCASE OR profile.display_name=? COLLATE NOCASE THEN 0 ELSE 1 END,users.created_at LIMIT 1`) ).get('Aewyn', 'Aewyn'); if (!actor?.id) throw new Error( 'No administrator account is available to own the Attribute import audit events.', ); let created = 0; let updated = 0; let unchanged = 0; await database.exec('BEGIN'); try { for (const record of records) { const name = String(record.name).normalize('NFKC').trim(); const unit = String(record.unit ?? ''); const existing = await ( await database.prepare( 'SELECT * FROM stat_attributes WHERE name=? COLLATE NOCASE AND unit=?', ) ).get(name, unit); const id = String(existing?.id ?? randomUUID()); const now = new Date().toISOString(); const next = { ...(existing ?? {}), id, name, description: existing?.description ?? null, type: record.type, category: record.category, calculation_type: record.calculationType, unit, gear_min: record.gearRange?.[0] ?? existing?.gear_min ?? null, gear_max: record.gearRange?.[1] ?? existing?.gear_max ?? null, gear_reference_level: Number(existing?.gear_reference_level ?? 50), weapon_min: record.weaponRange?.[0] ?? existing?.weapon_min ?? null, weapon_max: record.weaponRange?.[1] ?? existing?.weapon_max ?? null, weapon_reference_level: Number(existing?.weapon_reference_level ?? 50), cap_value: existing?.cap_value ?? null, active: 1, created_at: String(existing?.created_at ?? now), updated_at: now, }; const comparableExisting = existing ? { ...existing, updated_at: now } : null; if (comparableExisting && isDeepStrictEqual(comparableExisting, next)) { unchanged++; continue; } const before = existing ? await auditRecord('stat_attributes', id) : null; if (existing) { await ( await database.prepare(`UPDATE stat_attributes SET name=?,description=?,type=?,category=?,calculation_type=?,unit=?, gear_min=?,gear_max=?,weapon_min=?,weapon_max=?, cap_value=?,active=1,updated_at=? WHERE id=?`) ).run( next.name, next.description, next.type, next.category, next.calculation_type, next.unit, next.gear_min, next.gear_max, next.weapon_min, next.weapon_max, next.cap_value, now, id, ); updated++; } else { await ( await database.prepare(`INSERT INTO stat_attributes( id,name,description,type,category,calculation_type,unit, gear_min,gear_max,gear_reference_level,weapon_min,weapon_max,weapon_reference_level, cap_value,active,created_at,updated_at ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)`) ).run( id, next.name, next.description, next.type, next.category, next.calculation_type, next.unit, next.gear_min, next.gear_max, next.gear_reference_level, next.weapon_min, next.weapon_max, next.weapon_reference_level, next.cap_value, now, now, ); created++; } await recordIsacAudit({ actorId: String(actor.id), category: 'game_data', action: existing ? 'attribute.updated' : 'attribute.created', entityId: id, before, after: await auditRecord('stat_attributes', id), }); } await database.exec('COMMIT'); } catch (error) { await database.exec('ROLLBACK'); throw error; } console.log( `Imported ${records.length} Attribute definitions: ${created} created, ${updated} updated, ${unchanged} unchanged.`, ); console.log(`Source: ${sourcePath}`); } finally { await vite.close(); }