This commit is contained in:
468
swagger.js
Normal file
468
swagger.js
Normal file
@@ -0,0 +1,468 @@
|
||||
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: {
|
||||
status: { type: "string", enum: ["error"] },
|
||||
data: { nullable: true },
|
||||
message: { type: "string" },
|
||||
errors: {
|
||||
type: "array",
|
||||
items: {},
|
||||
},
|
||||
},
|
||||
required: ["status", "data", "message", "errors"],
|
||||
},
|
||||
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: successEnvelopeSchema({ $ref: ref }),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function arrayResponse(description, itemRef) {
|
||||
return {
|
||||
description,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: successEnvelopeSchema({
|
||||
type: "array",
|
||||
items: { $ref: itemRef },
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function successEnvelopeSchema(dataSchema) {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", enum: ["success"] },
|
||||
data: dataSchema,
|
||||
message: { type: "string" },
|
||||
errors: {
|
||||
type: "array",
|
||||
items: {},
|
||||
},
|
||||
},
|
||||
required: ["status", "data", "message", "errors"],
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user