40 lines
6.5 KiB
JavaScript
40 lines
6.5 KiB
JavaScript
import fs from 'node:fs';
|
|
const routes = JSON.parse(fs.readFileSync(process.argv[2] ?? 'C:/Users/macbook/AppData/Local/Temp/zones-routes.json', 'utf8').replace(/^\uFEFF/, ''));
|
|
const string = {type: 'string'};
|
|
const uuid = {type: 'string', format: 'uuid'};
|
|
const date = {type: 'string', format: 'date'};
|
|
const version = {type: 'integer', minimum: 1};
|
|
const nullable = schema => ({...schema, nullable: true});
|
|
const object = (properties, required = []) => ({type: 'object', properties, required});
|
|
const relation = {association_type: {type: 'string', enum: ['official','influence','proximity','potential','historical']},role: {type:'string',enum:['primary','beneficiary','served','enabled','dependent','related','potential','historical']},source_reference: {type:'string',minLength:10,maxLength:2000},justification:{type:'string',minLength:10,maxLength:3000},legal_source_id:nullable(uuid),valid_from:date,valid_until:nullable(date),priority:{type:'integer',minimum:0,maximum:100},is_primary:{type:'boolean'}};
|
|
const relationRequired=['association_type','role','source_reference','justification','valid_from'];
|
|
const schemas = {
|
|
Version: object({version_no:version},['version_no']),
|
|
Zone: object({version_no:version,code:{type:'string',maxLength:80},category:{type:'string',enum:['zee','zfi','industrial_park','pending']},name:{type:'string',maxLength:255},name_en:nullable(string),description:nullable(string),description_en:nullable(string),province:nullable(string),district:nullable(string),operator_id:nullable(uuid),legal_source_id:nullable(uuid)},['code','category','name']),
|
|
LegalSource: object({title:string,url:{type:'string',format:'uri',description:'HTTPS reference only; no remote download.'},diploma:string,article:string,competent_entity:string,valid_from:date,valid_until:nullable(date)},['title','url','diploma','article','competent_entity','valid_from']),
|
|
Link: object({opportunity_id:{type:'integer',minimum:1},...relation},['opportunity_id',...relationRequired]),
|
|
Relation: object({cluster_id:nullable(uuid),corridor_id:nullable(uuid),project_id:nullable(uuid),...relation},relationRequired),
|
|
Geometry: object({geometry:object({type:{type:'string',enum:['Point','LineString','Polygon','MultiPolygon']},coordinates:{type:'array',items:{}}},['type','coordinates']),purpose:{type:'string',enum:['official_perimeter','influence','site','alternative']},legal_source_id:nullable(uuid),source_reference:string,validating_entity:string,observed_at:date},['geometry','purpose','source_reference','validating_entity','observed_at']),
|
|
};
|
|
schemas.Zone.description='PUT requires version_no. Save withdraws publication and requires a new independent review. No official status inferred from category.';
|
|
schemas.Relation.description='Exactly one of cluster_id, corridor_id, project_id. Official association requires legal_source_id. Unique zone-target pair; duplicate returns 409.';
|
|
schemas.Link.description='Official association requires legal_source_id. Creates or reuses UUID mapping for existing opportunity; duplicate zone-item pair returns 409. Potential remains potential after review.';
|
|
schemas.Geometry.description='GeoJSON geometry in EPSG:4326, 2D, valid, nonempty, max 200000 encoded bytes. Official perimeter requires Polygon/MultiPolygon and legal source. No status inferred from geometry.';
|
|
const paths={}; let operations=0;
|
|
for(const route of routes){
|
|
const path='/'+route.uri; const method=route.method.split('|')[0].toLowerCase(); const admin=path.includes('/admin/');
|
|
const parameters=[...path.matchAll(/\{([^}]+)\}/g)].map(match=>({name:match[1],in:'path',required:true,schema:match[1]==='action'?{type:'string',enum:path.includes('/admin/zones/')?['publish','suspend']:['validate','revoke']}:uuid}));
|
|
if(method==='get' && path.endsWith('/zones')) parameters.push({name:'q',in:'query',schema:{type:'string',maxLength:120}},{name:'category',in:'query',schema:{type:'string',enum:['zee','zfi','industrial_park']}},{name:'page',in:'query',schema:version});
|
|
if(path.endsWith('/zone-map')) parameters.push({name:'zone_id',in:'query',schema:uuid},{name:'bbox[]',in:'query',style:'form',explode:true,schema:{type:'array',minItems:4,maxItems:4,items:{type:'number'}},description:'west,south,east,north; no antimeridian wrap.'});
|
|
const creates=method==='post'&&!path.includes('{action}');
|
|
const operation={operationId:route.action.split('\\').at(-1).replace('@','_')+'_'+operations++,summary:route.action.split('\\').at(-1),parameters,security:admin?[{laravelSession:[]}]:[],responses:{[creates?'201':'200']:{description:'Success. JSON; paginated lists. Private management responses use Cache-Control: private, no-store.',content:{'application/json':{schema:{type:'object'}}}},'403':{description:'Missing manager/MFA/designated role or self-review.'},'404':{description:'Not found or not publicly visible.'},'409':{description:'Stale version or duplicate relation.'},'419':{description:'Session/CSRF validation failed.'},'422':{description:'Invalid data, source, geometry, overlap or workflow.'},'429':{description:'Rate limit exceeded.'}}};
|
|
if(method!=='get'){
|
|
const schema=path.includes('{action}')?'Version':path.endsWith('/legal-sources')?'LegalSource':path.endsWith('/links')?'Link':path.endsWith('/relations')?'Relation':path.endsWith('/geometries')?'Geometry':'Zone';
|
|
operation.requestBody={required:true,content:{'application/json':{schema:{$ref:'#/components/schemas/'+schema}}}};
|
|
}
|
|
(paths[path]??={})[method]=operation;
|
|
}
|
|
const doc={openapi:'3.0.3',info:{title:'InvestMoz zone foundation API',version:'2026-09-28',description:'Implemented local routes only. Session cookies and CSRF; no bearer protocol invented. Admin: verified manager with confirmed MFA; legal/GIS/publish actions also require configured designated role. 30 requests/minute for management; public API uses application throttle. Approval in the portal is documentary review, not an administrative act. API v1 remains compatible and adds optional zee_id/zfi_id/park_id filters. Response schemas are deliberately open pending full contract typing.'},paths,components:{securitySchemes:{laravelSession:{type:'apiKey',in:'cookie',name:'laravel_session',description:'Actual session cookie name is configured by Laravel. Send X-XSRF-TOKEN from the session; no credentials embedded.'}},schemas}};
|
|
fs.writeFileSync('docs/investmoz/zones/openapi.json',JSON.stringify(doc,null,2)+'\n');
|
|
console.log(JSON.stringify({documented_operations:operations,registered_routes:routes.length,scope:'Route/method coverage; not exhaustive response-schema conformance.'}));
|