125 lines
5.6 KiB
JavaScript
125 lines
5.6 KiB
JavaScript
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),
|
|
skill_chip_min: existing?.skill_chip_min ?? null,
|
|
skill_chip_max: existing?.skill_chip_max ?? null,
|
|
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=?,skill_chip_min=?,skill_chip_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.skill_chip_min,
|
|
next.skill_chip_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,
|
|
skill_chip_min,skill_chip_max,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.skill_chip_min, next.skill_chip_max, 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();
|
|
}
|