forked from xiangwang25/whale-town-end-v2
57 lines
4.5 KiB
JavaScript
57 lines
4.5 KiB
JavaScript
// Run inside the backend runtime: node scripts/migrate_default_skins.cjs
|
|
// Apply only with --apply /protected/backup.json. Unrelated accounts/assets are unchanged.
|
|
const mysql = require('mysql2/promise');
|
|
const fs = require('node:fs');
|
|
const BOY = 'human_whale_directional_v2_8x4';
|
|
const RETIRED = 'classic_whale';
|
|
|
|
async function migrate(connection, backupPath) {
|
|
await connection.beginTransaction();
|
|
try {
|
|
const [profiles] = await connection.query(
|
|
"SELECT id,user_id,skin_id FROM user_profiles WHERE skin_id IN ('classic_whale','pending_initial_skin','') OR skin_id IS NULL FOR UPDATE");
|
|
const [assets] = await connection.query(
|
|
"SELECT * FROM user_assets WHERE asset_type='skin' AND asset_id='classic_whale' FOR UPDATE");
|
|
const userIds = [...new Set([...profiles, ...assets].map(row=>String(row.user_id)))];
|
|
const existingBoyAssets = userIds.length ? (await connection.query(
|
|
"SELECT id,user_id FROM user_assets WHERE asset_type='skin' AND asset_id=? AND user_id IN (?) FOR UPDATE",[BOY,userIds]))[0] : [];
|
|
const summary = {profiles:profiles.length,classicAssets:assets.length,affectedAccounts:userIds.length};
|
|
if (!backupPath) { await connection.rollback(); return {...summary,dryRun:true}; }
|
|
fs.writeFileSync(backupPath, JSON.stringify({createdAt:new Date().toISOString(),profiles,assets,existingBoyAssets},null,2)+'\n',{mode:0o600,flag:'wx'});
|
|
for (const userId of userIds) {
|
|
await connection.execute("INSERT INTO user_assets (user_id,asset_type,asset_id,source) VALUES (?,'skin',?,'default_skin_migration') ON DUPLICATE KEY UPDATE asset_id=VALUES(asset_id)",[userId,BOY]);
|
|
}
|
|
await connection.execute("UPDATE user_profiles SET skin_id=? WHERE skin_id IN ('classic_whale','pending_initial_skin','') OR skin_id IS NULL",[BOY]);
|
|
await connection.execute("DELETE FROM user_assets WHERE asset_type='skin' AND asset_id=?",[RETIRED]);
|
|
const [[check]] = await connection.query("SELECT (SELECT COUNT(*) FROM user_profiles WHERE skin_id='classic_whale') AS profiles, (SELECT COUNT(*) FROM user_assets WHERE asset_type='skin' AND asset_id='classic_whale') AS assets");
|
|
if (Number(check.profiles) || Number(check.assets)) throw new Error('Retired skin references remain');
|
|
await connection.commit();
|
|
return {...summary,dryRun:false,remainingClassicProfiles:0,remainingClassicAssets:0};
|
|
} catch (error) { await connection.rollback(); throw error; }
|
|
}
|
|
|
|
async function rollback(connection, backupPath) {
|
|
const backup=JSON.parse(fs.readFileSync(backupPath,'utf8'));
|
|
const hadBoy=new Set(backup.existingBoyAssets.map(row=>String(row.user_id)));
|
|
const users=new Set([...backup.profiles,...backup.assets].map(row=>String(row.user_id)));
|
|
await connection.beginTransaction();
|
|
try {
|
|
for(const row of backup.profiles) await connection.execute('UPDATE user_profiles SET skin_id=? WHERE id=? AND skin_id=?',[row.skin_id,row.id,BOY]);
|
|
for(const row of backup.assets) await connection.execute('INSERT INTO user_assets (id,user_id,asset_type,asset_id,source,metadata,acquired_at) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE asset_id=VALUES(asset_id)',[row.id,row.user_id,row.asset_type,row.asset_id,row.source,row.metadata?JSON.stringify(row.metadata):null,new Date(row.acquired_at)]);
|
|
for(const userId of users) if(!hadBoy.has(userId)) await connection.execute("DELETE FROM user_assets WHERE user_id=? AND asset_type='skin' AND asset_id=? AND source='default_skin_migration'",[userId,BOY]);
|
|
await connection.commit();
|
|
return {rolledBack:true,profiles:backup.profiles.length,assets:backup.assets.length};
|
|
}catch(e){await connection.rollback();throw e;}
|
|
}
|
|
|
|
async function main() {
|
|
const args=process.argv.slice(2);
|
|
if (args.length && (!['--apply','--rollback'].includes(args[0]) || args.length!==2)) throw new Error('Use --apply /protected/backup.json or no arguments for a dry run');
|
|
for(const key of ['DB_HOST','DB_USERNAME','DB_PASSWORD','DB_NAME']) if(!process.env[key]) throw new Error('Missing '+key);
|
|
const connection=await mysql.createConnection({host:process.env.DB_HOST,port:Number(process.env.DB_PORT||3306),user:process.env.DB_USERNAME,password:process.env.DB_PASSWORD,database:process.env.DB_NAME,charset:'utf8mb4',supportBigNumbers:true,bigNumberStrings:true});
|
|
try { console.log(JSON.stringify(await (args[0]==='--rollback'?rollback(connection,args[1]):migrate(connection,args[1])))); }
|
|
finally { await connection.end(); }
|
|
}
|
|
module.exports={migrate,rollback};
|
|
if(require.main===module) main().catch(e=>{console.error(e.message);process.exitCode=1;});
|