44 lines
4 KiB
JavaScript
44 lines
4 KiB
JavaScript
|
|
/** Additive MCP schema. Dry run by default; use --apply only against the selected reviewed database. */
|
||
|
|
import PocketBase from 'pocketbase';
|
||
|
|
import {mkdtemp,writeFile} from 'node:fs/promises';
|
||
|
|
import {tmpdir} from 'node:os';
|
||
|
|
import {join,resolve} from 'node:path';
|
||
|
|
import {fileURLToPath} from 'node:url';
|
||
|
|
const text=name=>({name,type:'text',required:true,max:200});
|
||
|
|
const privateRules={listRule:null,viewRule:null,createRule:null,updateRule:null,deleteRule:null};
|
||
|
|
export const mcpSchemas=[
|
||
|
|
{name:'mcp_operations',type:'base',...privateRules,fields:[...['credential','tool','operation_id','payload_hash','status'].map(text),{name:'result',type:'json',maxSize:2000000},{name:'error_code',type:'text',max:200}],indexes:['CREATE UNIQUE INDEX idx_mcp_operation ON mcp_operations (credential, tool, operation_id)']},
|
||
|
|
{name:'mcp_jobs',type:'base',...privateRules,fields:[...['operation','batch','status'].map(text),{name:'attempts',type:'number',min:0},{name:'next_attempt_at',type:'date'},{name:'error_code',type:'text',max:200}],indexes:['CREATE UNIQUE INDEX idx_mcp_job_operation ON mcp_jobs (operation)']},
|
||
|
|
{name:'mcp_resource_locks',type:'base',...privateRules,fields:[text('resource_key'),text('owner'),{name:'expires_at',type:'date',required:true}],indexes:['CREATE UNIQUE INDEX idx_mcp_resource ON mcp_resource_locks (resource_key)']},
|
||
|
|
];
|
||
|
|
const normalizeIndex=s=>s.replace(/[`\s]/g,'').toLowerCase();
|
||
|
|
export function validateExistingSchema(existing,desired){
|
||
|
|
for(const key of Object.keys(privateRules))if(existing[key]!==null)throw Error(`Non-private MCP rule: ${existing.name}.${key}`);
|
||
|
|
for(const field of desired.fields){const old=existing.fields.find(f=>f.name===field.name);if(old&&(old.type!==field.type||Boolean(old.required)!==Boolean(field.required)||(field.maxSize&&old.maxSize<field.maxSize)||(field.max&&old.max<field.max)))throw Error(`Incompatible field: ${existing.name}.${field.name}`);}
|
||
|
|
}
|
||
|
|
async function main(){
|
||
|
|
if(process.argv.slice(2).some(a=>a!=='--apply'))throw Error('Only --apply is supported');
|
||
|
|
const pb=new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);pb.autoCancellation(false);
|
||
|
|
if(process.env.POCKETBASE_ADMIN_TOKEN)pb.authStore.save(process.env.POCKETBASE_ADMIN_TOKEN);
|
||
|
|
if(!pb.authStore.isValid)await pb.collection('_superusers').authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL,process.env.POCKETBASE_ADMIN_PASSWORD);
|
||
|
|
const apply=process.argv.includes('--apply');const backup=apply?await mkdtemp(join(tmpdir(),'mega-mcp-schema-')):null;
|
||
|
|
for(const desired of mcpSchemas){
|
||
|
|
let existing;try{existing=await pb.collections.getOne(desired.name);}catch(e){if(e.status!==404)throw e;}
|
||
|
|
if(existing)validateExistingSchema(existing,desired);
|
||
|
|
const fields=desired.fields.filter(f=>!existing?.fields.some(old=>old.name===f.name));
|
||
|
|
const indexes=desired.indexes.filter(i=>!existing?.indexes.some(old=>normalizeIndex(old)===normalizeIndex(i)));
|
||
|
|
console.log(JSON.stringify({collection:desired.name,create:!existing,addFields:fields.map(f=>f.name),addIndexes:indexes.length,apply}));
|
||
|
|
if(!apply)continue;
|
||
|
|
if(existing){
|
||
|
|
await writeFile(join(backup,desired.name+'.json'),JSON.stringify(existing,null,2),{mode:0o600});
|
||
|
|
const current=await pb.collections.getOne(existing.id);if(JSON.stringify(current)!==JSON.stringify(existing))throw Error('Schema changed during migration');
|
||
|
|
await pb.collections.update(existing.id,{fields:[...existing.fields,...fields],indexes:[...existing.indexes,...indexes]});
|
||
|
|
}else await pb.collections.create(desired);
|
||
|
|
const after=await pb.collections.getOne(desired.name);validateExistingSchema(after,desired);
|
||
|
|
for(const field of desired.fields)if(!after.fields.some(f=>f.name===field.name))throw Error('Missing field after migration');
|
||
|
|
for(const index of desired.indexes)if(!after.indexes.some(i=>normalizeIndex(i)===normalizeIndex(index)))throw Error('Missing index after migration');
|
||
|
|
}
|
||
|
|
if(backup)console.log('Schema backups: '+backup);
|
||
|
|
}
|
||
|
|
if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url))main().catch(e=>{console.error(e.status?`PocketBase HTTP ${e.status}`:e.message);process.exitCode=1;});
|