Files
shd.build/scripts/import-resurgence-attributes.mjs
T
jb dd84dff055
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s
feat: add community publishing and news syndication
2026-08-01 21:42:09 -04:00

190 lines
6.4 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();
}