const openApiSpec = { openapi: "3.0.3", info: { title: "Ultimate History Map API", version: "1.0.0", description: "Read APIs plus section review workflow. Direct geometry/entity mutations are intentionally not exposed.", }, servers: [ { url: "http://localhost:3000", description: "Local", }, ], tags: [ { name: "System", description: "Health and OpenAPI endpoints" }, { name: "Tiles", description: "Vector and raster tile endpoints" }, { name: "Geometries", description: "Published geometry read endpoints" }, { name: "Entities", description: "Published entity read endpoints" }, { name: "Sections", description: "Draft, commit, and submit workflow" }, { name: "Submissions", description: "Submission review workflow" }, ], components: { schemas: { ErrorResponse: { type: "object", properties: { error: { type: "string" }, }, required: ["error"], }, Entity: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, slug: { type: "string", nullable: true }, description: { type: "string", nullable: true }, type_id: { type: "string", nullable: true }, status: { type: "number", nullable: true }, geometry_count: { type: "number" }, created_at: { type: "string", nullable: true }, updated_at: { type: "string", nullable: true }, }, required: ["id", "name", "geometry_count"], }, GeoJSONGeometry: { type: "object", properties: { type: { type: "string" }, coordinates: { type: "array", items: {} }, }, required: ["type", "coordinates"], }, GeometryFeature: { type: "object", properties: { type: { type: "string", enum: ["Feature"] }, properties: { type: "object" }, 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"], }, Section: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string", nullable: true }, user_id: { type: "string", nullable: true }, created_by: { type: "string", nullable: true }, state: { type: "object" }, }, required: ["id", "title", "state"], }, SectionCommit: { type: "object", properties: { id: { type: "string" }, section_id: { type: "string" }, commit_no: { type: "number" }, kind: { type: "string" }, snapshot: { type: "object", nullable: true }, }, required: ["id", "section_id", "commit_no", "kind"], }, SectionSubmission: { type: "object", properties: { id: { type: "string" }, section_id: { type: "string" }, commit_id: { type: "string" }, status: { type: "string" }, snapshot: { type: "object", nullable: true }, }, required: ["id", "section_id", "commit_id", "status"], }, }, }, paths: { "/": { get: { tags: ["System"], summary: "Health check", responses: { 200: { description: "Server is running" } }, }, }, "/docs.json": { get: { tags: ["System"], summary: "OpenAPI JSON", responses: { 200: { description: "OpenAPI document" } }, }, }, "/tiles/metadata/info": { get: { tags: ["Tiles"], summary: "Vector tile metadata", responses: { 200: { description: "MBTiles metadata" } }, }, }, "/tiles/{z}/{x}/{y}": { get: { tags: ["Tiles"], summary: "Vector tile", parameters: tileParameters(), responses: { 200: { description: "Tile binary" }, 400: { description: "Invalid tile coordinates" }, 404: { description: "Tile not found" }, }, }, }, "/raster-tiles/metadata/info": { get: { tags: ["Tiles"], summary: "Raster tile metadata", responses: { 200: { description: "MBTiles metadata" } }, }, }, "/raster-tiles/{z}/{x}/{y}": { get: { tags: ["Tiles"], summary: "Raster tile", parameters: tileParameters(), responses: { 200: { description: "Tile binary" }, 400: { description: "Invalid tile coordinates" }, 404: { description: "Tile not found" }, }, }, }, "/geometries": { get: { tags: ["Geometries"], summary: "Query published geometries by bbox, time, and entity", parameters: [ queryParam("minLng", "number", true), queryParam("minLat", "number", true), queryParam("maxLng", "number", true), queryParam("maxLat", "number", true), queryParam("time", "integer", false), queryParam("entity_id", "string", false), ], responses: { 200: { description: "Feature collection", content: { "application/json": { schema: { $ref: "#/components/schemas/GeometryFeatureCollection" }, }, }, }, 400: responseRef("Invalid query"), }, }, }, "/entities": { get: { tags: ["Entities"], summary: "List published entities", parameters: [queryParam("q", "string", false)], responses: { 200: arrayResponse("Entity list", "#/components/schemas/Entity"), }, }, }, "/entities/search": { get: { tags: ["Entities"], summary: "Search published entities by name", parameters: [ queryParam("name", "string", true), queryParam("limit", "integer", false), ], responses: { 200: arrayResponse("Matched entities", "#/components/schemas/Entity"), }, }, }, "/entities/{id}": { get: { tags: ["Entities"], summary: "Get published entity by id", parameters: [pathParam("id")], responses: { 200: objectResponse("Entity", "#/components/schemas/Entity"), 404: responseRef("Entity not found"), }, }, }, "/sections": { get: { tags: ["Sections"], summary: "List sections", responses: { 200: arrayResponse("Sections", "#/components/schemas/Section") }, }, post: { tags: ["Sections"], summary: "Create section", requestBody: jsonBody({ type: "object", required: ["title"], properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string", nullable: true }, user_id: { type: "string" }, created_by: { type: "string" }, }, }), responses: { 201: objectResponse("Created section", "#/components/schemas/Section"), 400: responseRef("Invalid payload"), 409: responseRef("Section id already exists"), }, }, }, "/sections/{sectionId}/editor": { get: { tags: ["Sections"], summary: "Open section editor and acquire lock", parameters: [ pathParam("sectionId"), queryParam("user_id", "string", false), ], responses: { 200: { description: "Editor payload" }, 404: responseRef("Section not found"), 409: responseRef("Section locked by another user"), }, }, }, "/sections/{sectionId}/lock": sectionActorPost("Acquire section lock"), "/sections/{sectionId}/unlock": sectionActorPost("Release section lock"), "/sections/{sectionId}/commits": { get: { tags: ["Sections"], summary: "List section commits", parameters: [ pathParam("sectionId"), queryParam("include_snapshot", "string", false), ], responses: { 200: arrayResponse("Commits", "#/components/schemas/SectionCommit") }, }, post: { tags: ["Sections"], summary: "Create reviewed workflow commit", description: "Direct geometry/entity mutations are not exposed. Commit snapshots are the mutation path for geometry/entity changes.", parameters: [pathParam("sectionId")], requestBody: jsonBody({ type: "object", required: ["snapshot", "created_by"], properties: { snapshot: { type: "object" }, created_by: { type: "string" }, expected_version: { type: "number" }, expected_head_commit_id: { type: "string", nullable: true }, title: { type: "string", nullable: true }, note: { type: "string", nullable: true }, }, }), responses: { 201: { description: "Created commit" }, 400: responseRef("Invalid snapshot"), 409: responseRef("Section conflict"), }, }, }, "/sections/{sectionId}/restore": { post: { tags: ["Sections"], summary: "Restore a previous commit", parameters: [pathParam("sectionId")], responses: { 201: { description: "Restore commit created" }, 400: responseRef("Invalid payload"), 404: responseRef("Commit not found"), 409: responseRef("Section conflict"), }, }, }, "/sections/{sectionId}/submit": { post: { tags: ["Sections"], summary: "Submit head commit for review", parameters: [pathParam("sectionId")], responses: { 201: objectResponse("Submission", "#/components/schemas/SectionSubmission"), 400: responseRef("Invalid payload"), 409: responseRef("Section conflict"), }, }, }, "/sections/{sectionId}/submissions": { get: { tags: ["Sections"], summary: "List section submissions", parameters: [ pathParam("sectionId"), queryParam("include_snapshot", "string", false), ], responses: { 200: arrayResponse("Submissions", "#/components/schemas/SectionSubmission") }, }, }, "/submissions/{submissionId}/approve": submissionReviewPost("Approve submission"), "/submissions/{submissionId}/reject": submissionReviewPost("Reject submission"), }, }; function pathParam(name) { return { name, in: "path", required: true, schema: { type: "string" } }; } function queryParam(name, type, required) { return { name, in: "query", required, schema: { type } }; } function tileParameters() { return [pathParam("z"), pathParam("x"), pathParam("y")]; } function responseRef(description) { return { description, content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }; } function objectResponse(description, ref) { return { description, content: { "application/json": { schema: { $ref: ref }, }, }, }; } function arrayResponse(description, itemRef) { return { description, content: { "application/json": { schema: { type: "array", items: { $ref: itemRef }, }, }, }, }; } function jsonBody(schema) { return { required: true, content: { "application/json": { schema }, }, }; } function sectionActorPost(summary) { return { post: { tags: ["Sections"], summary, parameters: [pathParam("sectionId")], requestBody: jsonBody({ type: "object", properties: { user_id: { type: "string" }, user: { type: "string" }, }, }), responses: { 200: { description: "Updated section state" }, 400: responseRef("user_id is required"), 404: responseRef("Section not found"), 409: responseRef("Section conflict"), }, }, }; } function submissionReviewPost(summary) { return { post: { tags: ["Submissions"], summary, parameters: [pathParam("submissionId")], requestBody: jsonBody({ type: "object", properties: { reviewed_by: { type: "string" }, user_id: { type: "string" }, review_note: { type: "string", nullable: true }, }, }), responses: { 200: objectResponse("Updated submission", "#/components/schemas/SectionSubmission"), 404: responseRef("Submission not found"), 409: responseRef("Submission conflict"), }, }, }; } module.exports = { openApiSpec, };