const openApiSpec = { openapi: "3.0.3", info: { title: "Ultimate History Map API", version: "1.0.0", description: "API docs for tiles, geometries, and entities.", }, servers: [ { url: "http://localhost:3000", description: "Local", }, ], tags: [ { name: "System", description: "Health and meta endpoints" }, { name: "Tiles", description: "Vector and raster tile endpoints" }, { name: "Geometries", description: "Geometry CRUD and batch save" }, { name: "Entities", description: "Entity CRUD" }, ], components: { schemas: { ErrorResponse: { type: "object", properties: { error: { type: "string" }, }, required: ["error"], }, SuccessResponse: { type: "object", properties: { success: { type: "boolean" }, }, required: ["success"], }, Entity: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, slug: { type: "string", nullable: true }, description: { type: "string", nullable: true }, type_id: { type: "string" }, status: { type: "number" }, created_at: { type: "string", format: "date-time", nullable: true }, updated_at: { type: "string", format: "date-time", nullable: true }, geometry_count: { type: "number" }, }, required: ["id", "name", "type_id", "geometry_count"], }, EntityCreateInput: { type: "object", properties: { name: { type: "string" }, slug: { type: "string", nullable: true }, description: { type: "string", nullable: true }, type_id: { type: "string", nullable: true }, status: { type: "number", nullable: true }, }, required: ["name"], }, EntityUpdateInput: { type: "object", properties: { name: { type: "string" }, slug: { type: "string", nullable: true }, description: { type: "string", nullable: true }, type_id: { type: "string" }, status: { type: "number" }, }, }, EntityBatchCreateChange: { type: "object", properties: { action: { type: "string", enum: ["create"] }, entity: { $ref: "#/components/schemas/EntityCreateInput" }, }, required: ["action"], }, EntityBatchUpdateChange: { type: "object", properties: { action: { type: "string", enum: ["update"] }, id: { type: "string" }, entity: { $ref: "#/components/schemas/EntityUpdateInput" }, name: { type: "string" }, slug: { type: "string", nullable: true }, description: { type: "string", nullable: true }, type_id: { type: "string" }, status: { type: "number" }, }, required: ["action", "id"], }, EntityBatchDeleteChange: { type: "object", properties: { action: { type: "string", enum: ["delete"] }, id: { type: "string" }, }, required: ["action", "id"], }, EntityBatchPayload: { type: "object", properties: { changes: { type: "array", items: { oneOf: [ { $ref: "#/components/schemas/EntityBatchCreateChange" }, { $ref: "#/components/schemas/EntityBatchUpdateChange" }, { $ref: "#/components/schemas/EntityBatchDeleteChange" }, ], }, }, }, required: ["changes"], }, EntityBatchResponse: { type: "object", properties: { success: { type: "boolean" }, applied: { type: "number" }, created_entity_ids: { type: "array", items: { type: "string" }, }, }, required: ["success", "applied", "created_entity_ids"], }, GeoJSONGeometry: { type: "object", properties: { type: { type: "string", enum: [ "Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon", ], }, coordinates: { type: "array", items: {}, }, }, required: ["type", "coordinates"], }, GeometryFeature: { type: "object", properties: { type: { type: "string", enum: ["Feature"] }, properties: { type: "object", properties: { id: { type: "string" }, type: { type: "string", nullable: true }, time_start: { type: "number", nullable: true }, time_end: { type: "number", nullable: true }, binding: { type: "array", items: { type: "string" }, }, entity_id: { type: "string", nullable: true }, entity_ids: { type: "array", items: { type: "string" }, }, entity_name: { type: "string", nullable: true }, entity_names: { type: "array", items: { type: "string" }, }, entity_type_id: { type: "string", nullable: true }, }, required: ["id"], }, geometry: { $ref: "#/components/schemas/GeoJSONGeometry" }, }, required: ["type", "properties", "geometry"], }, GeometryFeatureCollection: { type: "object", properties: { type: { type: "string", enum: ["FeatureCollection"] }, features: { type: "array", items: { $ref: "#/components/schemas/GeometryFeature" }, }, }, required: ["type", "features"], }, GeometryUpsertInput: { type: "object", properties: { geometry: { $ref: "#/components/schemas/GeoJSONGeometry" }, type: { type: "string", nullable: true }, time_start: { type: "number", nullable: true }, time_end: { type: "number", nullable: true }, binding: { type: "array", items: { type: "string" }, }, entity_id: { type: "string", nullable: true }, entity_ids: { type: "array", items: { type: "string" }, }, }, required: ["geometry"], }, GeometryCreateResponse: { type: "object", properties: { id: { type: "string" }, }, required: ["id"], }, BatchCreateChange: { type: "object", properties: { action: { type: "string", enum: ["create"] }, feature: { $ref: "#/components/schemas/GeometryFeature" }, }, required: ["action", "feature"], }, BatchUpdateChange: { type: "object", properties: { action: { type: "string", enum: ["update"] }, id: { type: "string" }, geometry: { $ref: "#/components/schemas/GeoJSONGeometry" }, type: { type: "string", nullable: true }, time_start: { type: "number", nullable: true }, time_end: { type: "number", nullable: true }, binding: { type: "array", items: { type: "string" }, }, entity_id: { type: "string", nullable: true }, entity_ids: { type: "array", items: { type: "string" }, }, }, required: ["action", "id", "geometry"], }, BatchDeleteChange: { type: "object", properties: { action: { type: "string", enum: ["delete"] }, id: { type: "string" }, }, required: ["action", "id"], }, GeometryBatchPayload: { type: "object", properties: { changes: { type: "array", items: { oneOf: [ { $ref: "#/components/schemas/BatchCreateChange" }, { $ref: "#/components/schemas/BatchUpdateChange" }, { $ref: "#/components/schemas/BatchDeleteChange" }, ], }, }, }, required: ["changes"], }, GeometryBatchResponse: { type: "object", properties: { success: { type: "boolean" }, applied: { type: "number" }, }, required: ["success", "applied"], }, CombinedBatchPayload: { type: "object", properties: { entity_changes: { type: "array", items: { oneOf: [ { $ref: "#/components/schemas/EntityBatchCreateChange" }, { $ref: "#/components/schemas/EntityBatchUpdateChange" }, { $ref: "#/components/schemas/EntityBatchDeleteChange" }, ], }, }, geometry_changes: { type: "array", items: { oneOf: [ { $ref: "#/components/schemas/BatchCreateChange" }, { $ref: "#/components/schemas/BatchUpdateChange" }, { $ref: "#/components/schemas/BatchDeleteChange" }, ], }, }, }, }, CombinedBatchResponse: { type: "object", properties: { success: { type: "boolean" }, applied: { type: "number" }, entity_applied: { type: "number" }, geometry_applied: { type: "number" }, created_entity_ids: { type: "array", items: { type: "string" }, }, }, required: [ "success", "applied", "entity_applied", "geometry_applied", "created_entity_ids", ], }, MetadataResponse: { type: "object", additionalProperties: { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }], }, }, }, }, paths: { "/": { get: { tags: ["System"], summary: "Health check", responses: { 200: { description: "Server status text", content: { "text/plain": { schema: { type: "string" }, }, }, }, }, }, }, "/tiles/metadata/info": { get: { tags: ["Tiles"], summary: "Get vector tiles metadata", responses: { 200: { description: "MBTiles metadata", content: { "application/json": { schema: { $ref: "#/components/schemas/MetadataResponse" }, }, }, }, }, }, }, "/tiles/{z}/{x}/{y}": { get: { tags: ["Tiles"], summary: "Get vector tile by XYZ", parameters: [ { name: "z", in: "path", required: true, schema: { type: "integer" } }, { name: "x", in: "path", required: true, schema: { type: "integer" } }, { name: "y", in: "path", required: true, schema: { type: "integer" } }, ], responses: { 200: { description: "Tile binary", content: { "application/x-protobuf": { schema: { type: "string", format: "binary" }, }, "image/png": { schema: { type: "string", format: "binary" }, }, "image/jpeg": { schema: { type: "string", format: "binary" }, }, "application/octet-stream": { schema: { type: "string", format: "binary" }, }, }, }, 400: { description: "Invalid tile coordinates", }, 404: { description: "Tile not found", }, }, }, }, "/raster-tiles/metadata/info": { get: { tags: ["Tiles"], summary: "Get raster tiles metadata", responses: { 200: { description: "MBTiles metadata", content: { "application/json": { schema: { $ref: "#/components/schemas/MetadataResponse" }, }, }, }, }, }, }, "/raster-tiles/{z}/{x}/{y}": { get: { tags: ["Tiles"], summary: "Get raster tile by XYZ", parameters: [ { name: "z", in: "path", required: true, schema: { type: "integer" } }, { name: "x", in: "path", required: true, schema: { type: "integer" } }, { name: "y", in: "path", required: true, schema: { type: "integer" } }, ], responses: { 200: { description: "Tile binary", content: { "image/png": { schema: { type: "string", format: "binary" } }, "image/jpeg": { schema: { type: "string", format: "binary" } }, "image/webp": { schema: { type: "string", format: "binary" } }, "application/octet-stream": { schema: { type: "string", format: "binary" } }, }, }, 400: { description: "Invalid tile coordinates", }, 404: { description: "Tile not found", }, }, }, }, "/entities": { get: { tags: ["Entities"], summary: "List entities", parameters: [ { name: "q", in: "query", required: false, schema: { type: "string" }, description: "Search by name or slug", }, ], responses: { 200: { description: "Entity list", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Entity" }, }, }, }, }, }, }, post: { tags: ["Entities"], summary: "Create entity", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/EntityCreateInput" }, }, }, }, responses: { 201: { description: "Entity created", content: { "application/json": { schema: { $ref: "#/components/schemas/Entity" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 409: { description: "Unique conflict", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/entities/search": { get: { tags: ["Entities"], summary: "Search entities by name", parameters: [ { name: "name", in: "query", required: true, schema: { type: "string" }, description: "Entity name keyword", }, { name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 100 }, }, ], responses: { 200: { description: "Matched entity list", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Entity" }, }, }, }, }, }, }, }, "/entities/batch": { post: { tags: ["Entities"], summary: "Apply batch create/update/delete entities", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/EntityBatchPayload" }, }, }, }, responses: { 200: { description: "Batch applied", content: { "application/json": { schema: { $ref: "#/components/schemas/EntityBatchResponse" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 409: { description: "Unique conflict or cannot delete due to orphaned geometries", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/entities/{id}": { get: { tags: ["Entities"], summary: "Get entity by id", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, ], responses: { 200: { description: "Entity", content: { "application/json": { schema: { $ref: "#/components/schemas/Entity" }, }, }, }, 404: { description: "Not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, put: { tags: ["Entities"], summary: "Update entity", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, ], requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/EntityUpdateInput" }, }, }, }, responses: { 200: { description: "Entity updated", content: { "application/json": { schema: { $ref: "#/components/schemas/Entity" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 404: { description: "Not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 409: { description: "Unique conflict", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, delete: { tags: ["Entities"], summary: "Soft-delete entity", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, ], responses: { 200: { description: "Soft-delete success", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" }, }, }, }, 409: { description: "Entity is still the last active link of one or more geometries", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 404: { description: "Not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/geometries": { get: { tags: ["Geometries"], summary: "Query geometries by bbox, time, and entity", parameters: [ { name: "minLng", in: "query", required: true, schema: { type: "number" } }, { name: "minLat", in: "query", required: true, schema: { type: "number" } }, { name: "maxLng", in: "query", required: true, schema: { type: "number" } }, { name: "maxLat", in: "query", required: true, schema: { type: "number" } }, { name: "time", in: "query", required: false, schema: { type: "integer" } }, { name: "entity_id", in: "query", required: false, schema: { type: "string" } }, ], responses: { 200: { description: "Feature collection", content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryFeatureCollection" }, }, }, }, 400: { description: "Invalid query", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, post: { tags: ["Geometries"], summary: "Create geometry", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryUpsertInput" }, }, }, }, responses: { 200: { description: "Created geometry id", content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryCreateResponse" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 404: { description: "Entity not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/geometries/{id}": { put: { tags: ["Geometries"], summary: "Update geometry", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, ], requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryUpsertInput" }, }, }, }, responses: { 200: { description: "Updated", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 404: { description: "Not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, delete: { tags: ["Geometries"], summary: "Soft-delete geometry", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, ], responses: { 200: { description: "Soft-delete success", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" }, }, }, }, 404: { description: "Not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/geometries/batch": { post: { tags: ["Geometries"], summary: "Apply batch create/update/delete geometries", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryBatchPayload" }, }, }, }, responses: { 200: { description: "Batch applied", content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryBatchResponse" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, "/geometries/batch/combined": { post: { tags: ["Geometries", "Entities"], summary: "Apply entity batch and geometry batch in one transaction", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CombinedBatchPayload" }, }, }, }, responses: { 200: { description: "Combined batch applied", content: { "application/json": { schema: { $ref: "#/components/schemas/CombinedBatchResponse" }, }, }, }, 400: { description: "Invalid payload", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, 409: { description: "Conflict in entity changes (unique or orphan guard)", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }, }, }, }, }, }; module.exports = { openApiSpec, };