migrate-operator-accounts.mjs 3.2 KB

123456789101112131415161718192021
  1. #!/usr/bin/env node
  2. import {readFileSync,mkdirSync,writeFileSync} from 'node:fs';
  3. import {randomBytes,createHash} from 'node:crypto';
  4. const sql=readFileSync(new URL('./sql/operator-accounts.sql',import.meta.url),'utf8');
  5. if(process.argv.includes('--validate')){console.log('Operator account migration ready: '+createHash('sha256').update(sql).digest('hex'));process.exit(0);}
  6. if(!process.env.XIAOSHU_MASTER_KEY)throw Error('缺少 XIAOSHU_MASTER_KEY');
  7. const root=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),co=process.env.XIAOSHU_COMPANY_ID||'7pIbDBJmKx';
  8. async function request(path,method='GET',body){const r=await fetch(root+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main','X-Parse-Master-Key':process.env.XIAOSHU_MASTER_KEY},...(body?{body:JSON.stringify(body)}:{})});const d=await r.json();if(!r.ok||d.error)throw Error(d.error||'HTTP '+r.status);return d;}
  9. const fields={company:{type:'Pointer',targetClass:'Company'},actorId:{type:'String'},actorName:{type:'String'},targetId:{type:'String'},targetName:{type:'String'},action:{type:'String'},reason:{type:'String'},before:{type:'Object'},after:{type:'Object'}};
  10. let schema;try{schema=await request('/schemas/OperatorAccountAudit');}catch(e){if(!/exist|schema|class/i.test(e.message))throw e;}
  11. const dir='tmp/operator-account-migration-'+Date.now();mkdirSync(dir,{recursive:true,mode:0o700});writeFileSync(dir+'/schema-before.json',JSON.stringify(schema||null),{mode:0o600});
  12. console.log('Preflight ready; schema backup: '+dir);if(!process.argv.includes('--apply'))process.exit(0);
  13. const missing=Object.fromEntries(Object.entries(fields).filter(([key])=>!schema?.fields?.[key]));
  14. await request('/schemas/OperatorAccountAudit',schema?'PUT':'POST',{className:'OperatorAccountAudit',fields:missing,classLevelPermissions:{find:{},get:{},count:{},create:{},update:{},delete:{},addField:{}}});
  15. const suffix=Date.now()+'_'+randomBytes(5).toString('hex'),path='xiaoshu/system/operators-'+suffix;let uid='',fid='';
  16. try{
  17. const actor=await request('/users','POST',{username:'operator_migration_'+suffix,password:randomBytes(32).toString('base64url'),company:{__type:'Pointer',className:'Company',objectId:co}});uid=actor.objectId;
  18. const code=`async function handler(request,response){if(!request.user||request.user.id!==${JSON.stringify(uid)})return response.status(403).json({success:false});try{await Psql.none(${JSON.stringify(sql)});return response.json({success:true});}catch(e){return response.status(500).json({success:false,message:String(e.message)});}}`;
  19. fid=(await request('/classes/Function','POST',{name:path,path,code,enabled:true,company:{__type:'Pointer',className:'Company',objectId:co}})).objectId;
  20. const r=await fetch(root.replace(/\/parse$/,'')+'/api/functions/'+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:actor.sessionToken,params:{}})}),d=await r.json();if(!r.ok||!d.success)throw Error(d.message||'Migration failed');console.log('Operator schema and locked transaction installed');
  21. }finally{if(fid)await request('/classes/Function/'+fid,'DELETE');if(uid)await request('/users/'+uid,'DELETE');console.log('Migration fixtures cleaned');}