research-cleanup-maintenance.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. import fs from 'node:fs';
  2. import crypto from 'node:crypto';
  3. import {request,adminHeaders,functionsUrl,redact} from '../server/scripts/platform-client.mjs';
  4. import {save,load,unwrap,read} from './lib/research-enrichment.mjs';
  5. const name='ksfResearchCleanup20261111',endpointWork='work/library-cleanup-20261111',work=process.env.KSF_CLEANUP_WORK||endpointWork;
  6. fs.mkdirSync(work,{recursive:true});
  7. const requestedMode=process.argv[2];
  8. const mode=requestedMode==='inventory-after'?'inventory':requestedMode;
  9. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  10. if(mode==='deploy') {
  11. const source=fs.readFileSync('server/src/research-cleanup-maintenance.mjs','utf8').replace(/^export /gm,'');
  12. const helper=fs.readFileSync('server/voc-library/research_cleanup.py','utf8'),tests=fs.readFileSync('server/voc-library/test_research_cleanup.py','utf8');
  13. const entry=`\nasync function handler(request,response){const crypto=await import('node:crypto');const fs=await import('node:fs/promises');const {spawn}=await import('node:child_process');return createResearchCleanupMaintenance({Parse,crypto,fs,spawn,helperSource:${JSON.stringify(helper)},testSource:${JSON.stringify(tests)},loadDatabase:async()=>{const m=await import('parse-server/lib/Config.js');const C=m.default?.default||m.default;const a=C.get('FKieRbIoGt_main').database?.adapter;if(a?.constructor?.name!=='PostgresStorageAdapter'||typeof a._client?.tx!=='function')throw Error('INCOMPATIBLE_DATABASE');return a._client;}})(request,response);}\n`;
  14. const code=source+entry;new Function(code);
  15. const matches=(await request('GET','/classes/Function?'+new URLSearchParams({where:JSON.stringify({$or:[{name},{path:'/'+name}]}),limit:'3'}))).results;
  16. if(matches.length>1||matches.some(x=>x.name!==name||x.path!=='/'+name))throw Error('FUNCTION_CONFLICT');
  17. if(matches.length) {
  18. const prior=load(endpointWork+'/maintenance-deployed.json');
  19. if(sha(matches[0].code)!==prior.sha256)throw Error('UNKNOWN_FUNCTION_BASELINE');
  20. save(work+'/maintenance-before-'+Date.now()+'.json',matches[0]);
  21. }
  22. let id=matches[0]?.objectId;
  23. if(!id)id=(await request('POST','/classes/Function',{name,path:'/'+name,code,ACL:{}})).objectId;
  24. else if(matches[0].code!==code)await request('PUT','/classes/Function/'+id,{code});
  25. const back=await request('GET','/classes/Function/'+id);
  26. if(back.code!==code)throw Error('FUNCTION_READBACK_FAILED');
  27. save(endpointWork+'/maintenance-deployed.json',{ok:true,name,objectId:id,sha256:sha(code),deployedAt:new Date().toISOString()});
  28. fs.writeFileSync(endpointWork+'/maintenance-deployed.js',code);
  29. console.log(JSON.stringify({ok:true,name,sha256:sha(code)}));
  30. } else if(['inventory','runtime','selftest','plan','apply','status'].includes(mode)) {
  31. const input=process.argv[3]?load(process.argv[3]):{};
  32. if(mode==='apply'){
  33. const planned=unwrap(load(work+'/plan-result.json'));
  34. if(!planned.ok||!planned.retainedRowsVerified||!planned.sourceRowsPreserved||!planned.foreignKeysVerified||planned.planSha256!==input.planSha256)throw Error('SUCCESSFUL_CLEANUP_PLAN_REQUIRED');
  35. input.expectedResult={generation:planned.generation,fingerprint:planned.fingerprint};
  36. const current=await read('contract');save(work+'/contract-before-apply.json',current);
  37. if(current.generation!==input.plan.baseGeneration)throw Error('GENERATION_CHANGED_SINCE_CLEANUP_PLAN');
  38. }
  39. const startedAt=new Date().toISOString();
  40. const res=await fetch(functionsUrl,{method:'POST',headers:adminHeaders,redirect:'error',signal:AbortSignal.timeout(580000),body:JSON.stringify({path:'/'+name,params:{...input,action:mode}})});
  41. const raw=Buffer.from(await res.arrayBuffer());
  42. fs.writeFileSync(work+'/'+requestedMode+'-response-'+Date.now()+'.json',raw);
  43. const response={http:res.status,body:JSON.parse(raw),startedAt,finishedAt:new Date().toISOString()};
  44. save(work+'/'+requestedMode+'-result.json',response);
  45. const r=unwrap(response);
  46. if(!r.ok)throw Error(JSON.stringify(r.error));
  47. console.log(JSON.stringify(mode==='inventory'?{ok:r.ok,generation:r.generation,products:r.products.length,social:r.social.length}:r));
  48. } else if(mode==='remove-endpoint') {
  49. const d=load(endpointWork+'/maintenance-deployed.json');
  50. const old=await request('GET','/classes/Function/'+d.objectId);
  51. if(sha(old.code)!==d.sha256)throw Error('UNKNOWN_FUNCTION_BASELINE');
  52. await request('DELETE','/classes/Function/'+d.objectId);
  53. const remaining=await request('GET','/classes/Function?'+new URLSearchParams({where:JSON.stringify({name}),limit:'1'}));
  54. if(remaining.results.length)throw Error('MAINTENANCE_ENDPOINT_STILL_PRESENT');
  55. save(work+'/maintenance-removed.json',{ok:true,name,objectId:d.objectId,removedAt:new Date().toISOString()});
  56. console.log(JSON.stringify({ok:true,removed:true,name}));
  57. } else throw Error('MAINTENANCE_MODE_REQUIRED');