Files
jb 57068b61cd
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s
feat: expand ISAC content modeling
2026-08-03 01:23:20 -04:00

140 lines
4.9 KiB
JavaScript

import { connect } from '@tursodatabase/database';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import process from 'node:process';
const backupArgument = process.argv[2];
if (!backupArgument) {
console.error('Usage: npm run db:restore -- <backup-file>');
process.exit(2);
}
const backupPath = resolve(backupArgument);
const databasePath = resolve(
process.env.TDR_DATABASE_PATH || './data/shd-build-development.db',
);
const backup = JSON.parse(await readFile(backupPath, 'utf8'));
if (
backup?.format !== 'shd-build-database-backup' ||
backup?.version !== 1 ||
!Array.isArray(backup.tables)
) {
throw new Error(
'The selected file is not a supported SHD.build database backup.',
);
}
const quoteIdentifier = (identifier) =>
`"${String(identifier).replaceAll('"', '""')}"`;
const database = await connect(databasePath);
// Restores are deliberately limited to empty databases. This prevents an
// accidental merge from overwriting contributor-entered records.
for (const table of backup.tables) {
const existing = await (
await database.prepare(
`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}`,
)
).get();
if (Number(existing?.count) > 0) {
throw new Error(
`Restore refused: table "${table.name}" is not empty. Use a new migrated database.`,
);
}
}
let restoredRows = 0;
const attributeTable = backup.tables.find(
(table) => table.name === 'stat_attributes',
);
const legacySkillChipProfiles = attributeTable?.rows.some(
(row) => 'skill_chip_min' in row || 'skill_chip_max' in row,
);
const legacySkillChipRanges = new Map(
(attributeTable?.rows ?? []).map((row) => [
String(row.id),
{ min: row.skill_chip_min, max: row.skill_chip_max },
]),
);
const restoredLegacySecondaries = new Set();
await database.exec('BEGIN');
try {
for (const table of backup.tables) {
for (const row of table.rows) {
if (
legacySkillChipProfiles &&
table.name === 'skill_chip_primary_options'
)
continue;
const restoredRow = { ...row };
if (legacySkillChipProfiles && table.name === 'stat_attributes') {
delete restoredRow.skill_chip_min;
delete restoredRow.skill_chip_max;
}
if (
legacySkillChipProfiles &&
table.name === 'skill_chip_secondary_options'
) {
delete restoredRow.skill_chip_set_id;
const statId = String(restoredRow.stat_attribute_id ?? '');
if (restoredLegacySecondaries.has(statId)) continue;
restoredLegacySecondaries.add(statId);
}
const columns = Object.keys(restoredRow);
if (!columns.length) continue;
const columnSql = columns.map(quoteIdentifier).join(', ');
const placeholders = columns.map(() => '?').join(', ');
const insert = await database.prepare(
`INSERT INTO ${quoteIdentifier(table.name)} (${columnSql}) VALUES (${placeholders})`,
);
await insert.run(...columns.map((column) => restoredRow[column]));
restoredRows++;
}
}
if (legacySkillChipProfiles) {
const primaryAttributes = await (
await database.prepare(`SELECT id,active FROM stat_attributes WHERE name IN (
'Skill Duration','Skill Radius','Skill Cooldown Recovery','Skill Intensity'
)`)
).all();
if (
primaryAttributes.length !== 4 ||
primaryAttributes.some((attribute) => Number(attribute.active) !== 1)
)
throw new Error(
'Legacy restore requires exactly four active fixed Skill Chip primary Attributes.',
);
for (const attribute of primaryAttributes) {
const statId = String(attribute.id);
await (
await database.prepare(
'INSERT INTO skill_chip_primary_options(stat_attribute_id) VALUES(?)',
)
).run(statId);
const range = legacySkillChipRanges.get(statId);
if (!range || range.min === null || range.max === null) continue;
for (const quality of ['Standard', 'Specialized', 'Superior', 'High-end'])
await (
await database.prepare(`INSERT INTO skill_chip_primary_quality_values
(stat_attribute_id,quality,min_value,max_value) VALUES(?,?,?,?)`)
).run(statId, quality, range.min, range.max);
}
for (const statId of restoredLegacySecondaries) {
const range = legacySkillChipRanges.get(statId);
if (!range || range.min === null || range.max === null) continue;
for (const quality of ['Specialized', 'Superior', 'High-end'])
await (
await database.prepare(`INSERT INTO skill_chip_secondary_quality_values
(stat_attribute_id,quality,min_value,max_value) VALUES(?,?,?,?)`)
).run(statId, quality, range.min, range.max);
}
}
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
console.log(`Restored ${restoredRows} rows into ${databasePath}.`);
console.log(`Source backup: ${backupPath}`);