Files
Investidor-V2/tools/zones-openapi.mjs
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

447 lines
16 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'],
},
physical_category: {
type: 'string',
enum: [
'economic_zone',
'industrial_park',
'agro_industrial_park',
'industrial_logistics_park',
'industrial_free_zone',
'integrated_tourism_area',
'other',
'unknown',
],
},
legal_regime: {
type: 'string',
enum: ['zee', 'zfi', 'general', 'pending', 'not_applicable'],
},
development_status: {
type: 'string',
enum: [
'announced',
'planned',
'in_development',
'partially_operational',
'operational',
'suspended',
'unknown',
],
},
availability_status: {
type: 'string',
enum: [
'confirmed',
'unavailable',
'reported_unverified',
'not_informed',
],
},
area_components: { type: 'array', items: { type: 'object' } },
source_evidence: { type: 'array', items: { type: 'object' } },
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('zone-attachments')
? ['review', 'select-public', 'withdraw-public']
: path.includes('/admin/zones/')
? ['publish', 'suspend']
: ['validate', 'revoke'],
}
: uuid,
}));
if (
method === 'get' &&
(path.endsWith('/zones') || path.endsWith('/zones/export.csv'))
)
parameters.push(
{
name: 'q',
in: 'query',
schema: { type: 'string', maxLength: 120 },
},
{
name: 'category',
in: 'query',
schema: {
type: 'string',
enum: ['zee', 'zfi', 'industrial_park'],
},
},
{ name: 'physical_category', in: 'query', schema: string },
{ name: 'legal_regime', in: 'query', schema: string },
{ name: 'development_status', in: 'query', schema: string },
{ name: 'availability_status', in: 'query', schema: string },
{ name: 'activity', in: 'query', schema: string },
{ name: 'province_id', in: 'query', schema: uuid },
{ name: 'district_id', in: 'query', schema: uuid },
{ name: 'admin_post_id', in: 'query', schema: uuid },
{ name: 'locality_id', in: 'query', schema: uuid },
{ 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 privateRoute = admin || path.includes('/management/');
const operation = {
operationId:
route.action.split('\\').at(-1).replace('@', '_') +
'_' +
operations++,
summary: route.action.split('\\').at(-1),
parameters,
security: privateRoute ? [{ 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 (path.endsWith('/zones/export.csv'))
operation.responses['200'] = {
description:
'CSV filtered with the same server-side criteria as the public zone list.',
content: {
'text/csv': { schema: { type: 'string', format: 'binary' } },
},
};
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';
if (path.endsWith('/attachments')) {
operation.requestBody = {
required: true,
content: {
'multipart/form-data': {
schema: object(
{
file: { type: 'string', format: 'binary' },
kind: {
type: 'string',
enum: [
'legal_instrument',
'map',
'photo',
'study',
'other',
],
},
caption: { type: 'string', maxLength: 255 },
source_reference: {
type: 'string',
minLength: 5,
maxLength: 2000,
},
source_date: nullable(date),
rights_status: {
type: 'string',
enum: [
'pending',
'confirmed',
'restricted',
],
},
rights_basis: nullable(string),
},
[
'file',
'kind',
'caption',
'source_reference',
'rights_status',
],
),
},
},
};
} else if (path.includes('zone-attachments/{id}/{action}')) {
operation.requestBody = {
required: true,
content: {
'application/json': {
schema: object(
{
version_no: version,
decision: {
type: 'string',
enum: ['reviewed', 'rejected'],
},
rights_status: {
type: 'string',
enum: [
'pending',
'confirmed',
'restricted',
],
},
rights_basis: nullable(string),
personal_data_status: {
type: 'string',
enum: ['none', 'present', 'unknown'],
},
},
path.endsWith('/review')
? [
'version_no',
'decision',
'rights_status',
'personal_data_status',
]
: ['version_no'],
),
},
},
};
} else {
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.',
}),
);