deploy-legacy-appointment-deletions.mjs 3.8 KB

1234567891011121314151617181920212223242526272829
  1. #!/usr/bin/env node
  2. import fs from 'node:fs';
  3. import {createHash} from 'node:crypto';
  4. import {buildAppointmentDeletionGateway,patchAppointmentDeletionCapability} from './lib/legacy-appointment-deletions.mjs';
  5. const env=process.env,master=env.XIAOSHU_MASTER_KEY,token=env.XIAOSHU_SYNC_OPERATOR_TOKEN,out=env.XIAOSHU_DIRECT_DEPLOY_REPORT_DIR;
  6. if(!master||!token||!out)throw Error('Authorized deployment configuration and private backup directory required');
  7. fs.mkdirSync(out,{recursive:true,mode:0o700});
  8. const base=(env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',co=env.XIAOSHU_SYNC_COMPANY_ID||'7pIbDBJmKx';
  9. const hash=value=>createHash('sha256').update(value).digest('hex');
  10. async function api(path,method='GET',body){const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)}),d=await r.json();if(!r.ok||d.error)throw Error('Deployment API HTTP '+r.status);return d;}
  11. async function find(path){const rows=(await api('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path}),limit:'2'}))).results;if(rows.length>1)throw Error('Ambiguous deployment target');return rows[0];}
  12. const direct=await find('xiaoshu/sync/direct-v1');if(!direct)throw Error('Direct sync gateway missing');
  13. const match=direct.code.match(/windows=(\[.*?\]),q=/);if(!match)throw Error('Unknown existing migration windows');
  14. const code=buildAppointmentDeletionGateway(co,JSON.parse(match[1])),patched=patchAppointmentDeletionCapability(direct.code);new Function(patched);
  15. const path='xiaoshu/sync/appointment-deletions-v1',previous=await find(path);
  16. fs.writeFileSync(out+'/deletion-function-before.json',JSON.stringify(previous||null),{mode:0o600});fs.writeFileSync(out+'/direct-function-before.json',JSON.stringify(direct),{mode:0o600});
  17. const plan={directId:direct.objectId,beforeHash:hash(direct.code),afterHash:hash(patched),deletionHash:hash(code),path};
  18. fs.writeFileSync(out+'/deployment-plan.json',JSON.stringify(plan,null,2),{mode:0o600});
  19. if(!process.argv.includes('--apply')){console.log(JSON.stringify({...plan,prepared:true}));process.exit(0);}
  20. if(hash((await api('/classes/Function/'+direct.objectId)).code)!==plan.beforeHash)throw Error('Concurrent sync gateway change');
  21. const definition={name:'xiaoshu.sync.appointment-deletions.v1',path,type:'standalone',enabled:true,code,params:[],paramList:[],respType:'json',desc:'确认旧预约删除,保护新系统数据与教学凭据'};
  22. const saved=await api('/classes/Function'+(previous?'/'+previous.objectId:''),previous?'PUT':'POST',definition),id=previous?.objectId||saved.objectId;
  23. if(hash((await api('/classes/Function/'+id)).code)!==plan.deletionHash)throw Error('Deletion gateway readback failed');
  24. const status=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(20000)}),result=await status.json();
  25. if(!status.ok||!result.success||result.data.version!=='confirmed-ids-v1')throw Error('Deletion gateway authorization/status failed');
  26. if(hash((await api('/classes/Function/'+direct.objectId)).code)!==plan.beforeHash)throw Error('Concurrent sync gateway change');
  27. if(plan.beforeHash!==plan.afterHash)await api('/classes/Function/'+direct.objectId,'PUT',{code:patched});
  28. if(hash((await api('/classes/Function/'+direct.objectId)).code)!==plan.afterHash)throw Error('Direct gateway readback failed');
  29. fs.writeFileSync(out+'/deployment-verified.json',JSON.stringify({...plan,id,verified:true,at:new Date().toISOString()},null,2),{mode:0o600});console.log(JSON.stringify({deployed:true,sourceReadbackVerified:true,existingSyncCodePreserved:true}));