From 7804ef842bfc537cfdafb517ce431b85ad705b28 Mon Sep 17 00:00:00 2001 From: taDuc Date: Sun, 19 Apr 2026 23:43:31 +0700 Subject: [PATCH] demo 20-4-2026 --- index.js | 13 +++++---- routes/rasterTiles.js | 4 +-- routes/sections.js | 4 +-- routes/tiles.js | 4 +-- swagger.js | 32 +++++++++++++++++---- utils/apiEnvelope.js | 65 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 utils/apiEnvelope.js diff --git a/index.js b/index.js index a1a61e8..d655cb1 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const geoRoutes = require("./routes/geometries"); const entityRoutes = require("./routes/entities"); const sectionRoutes = require("./routes/sections"); const { openApiSpec } = require("./swagger"); +const { envelopeResponses } = require("./utils/apiEnvelope"); const app = express(); @@ -15,12 +16,12 @@ app.use(cors()); app.use(express.json()); // serve MBTiles and geometry CRUD -app.use("/tiles", tileRoutes); -app.use("/raster-tiles", rasterTileRoutes); -app.use("/geometries", geoRoutes); -app.use("/entities", entityRoutes); -app.use("/sections", sectionRoutes); -app.use("/submissions", (req, res, next) => { +app.use("/tiles", envelopeResponses, tileRoutes); +app.use("/raster-tiles", envelopeResponses, rasterTileRoutes); +app.use("/geometries", envelopeResponses, geoRoutes); +app.use("/entities", envelopeResponses, entityRoutes); +app.use("/sections", envelopeResponses, sectionRoutes); +app.use("/submissions", envelopeResponses, (req, res, next) => { req.url = `/submissions${req.url}`; sectionRoutes(req, res, next); }); diff --git a/routes/rasterTiles.js b/routes/rasterTiles.js index 4f8a629..4f0637a 100644 --- a/routes/rasterTiles.js +++ b/routes/rasterTiles.js @@ -33,7 +33,7 @@ router.get("/:z/:x/:y", (req, res) => { const y = Number(req.params.y); if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y)) { - return res.status(400).send("Invalid tile coordinates"); + return res.status(400).json({ error: "Invalid tile coordinates" }); } const tmsY = (1 << z) - 1 - y; @@ -47,7 +47,7 @@ router.get("/:z/:x/:y", (req, res) => { `).get(z, x, tmsY); if (!tile) { - return res.status(404).send("Tile not found"); + return res.status(404).json({ error: "Tile not found" }); } res.setHeader("Content-Type", contentType); diff --git a/routes/sections.js b/routes/sections.js index 1f3b30e..5486670 100644 --- a/routes/sections.js +++ b/routes/sections.js @@ -289,9 +289,9 @@ router.post("/:sectionId/submit", (req, res) => { throw createHttpError("Section is not editable", 409); } - const commitId = normalizeId(req.body?.commit_id) || state.head_commit_id; + const commitId = state.head_commit_id; if (!commitId) { - throw createHttpError("Section has no commit to submit", 400); + throw createHttpError("Section has no head commit to submit", 400); } const commit = getCommitForSection(section.id, commitId); diff --git a/routes/tiles.js b/routes/tiles.js index 07b3546..d77f5ea 100644 --- a/routes/tiles.js +++ b/routes/tiles.js @@ -47,7 +47,7 @@ router.get("/:z/:x/:y", (req, res) => { const y = Number(req.params.y); if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y)) { - return res.status(400).send("Invalid tile coordinates"); + return res.status(400).json({ error: "Invalid tile coordinates" }); } // convert XYZ → TMS @@ -64,7 +64,7 @@ router.get("/:z/:x/:y", (req, res) => { const tile = stmt.get(z, x, tmsY); if (!tile) { - return res.status(404).send("Tile not found"); + return res.status(404).json({ error: "Tile not found" }); } res.setHeader("Content-Type", contentType); diff --git a/swagger.js b/swagger.js index 8603e10..c9649d5 100644 --- a/swagger.js +++ b/swagger.js @@ -24,9 +24,15 @@ const openApiSpec = { ErrorResponse: { type: "object", properties: { - error: { type: "string" }, + status: { type: "string", enum: ["error"] }, + data: { nullable: true }, + message: { type: "string" }, + errors: { + type: "array", + items: {}, + }, }, - required: ["error"], + required: ["status", "data", "message", "errors"], }, Entity: { type: "object", @@ -366,7 +372,7 @@ function objectResponse(description, ref) { description, content: { "application/json": { - schema: { $ref: ref }, + schema: successEnvelopeSchema({ $ref: ref }), }, }, }; @@ -377,15 +383,31 @@ function arrayResponse(description, itemRef) { description, content: { "application/json": { - schema: { + 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, diff --git a/utils/apiEnvelope.js b/utils/apiEnvelope.js new file mode 100644 index 0000000..ab3247c --- /dev/null +++ b/utils/apiEnvelope.js @@ -0,0 +1,65 @@ +function envelopeResponses(_req, res, next) { + const originalJson = res.json.bind(res); + + res.json = (payload) => { + if (isApiEnvelope(payload)) { + return originalJson(payload); + } + + const isError = res.statusCode >= 400; + const message = extractMessage(payload, isError); + const errors = extractErrors(payload, isError, message); + const data = isError ? null : payload; + + return originalJson({ + status: isError ? "error" : "success", + data, + message, + errors, + }); + }; + + next(); +} + +function isApiEnvelope(payload) { + return Boolean( + payload && + typeof payload === "object" && + !Array.isArray(payload) && + Object.prototype.hasOwnProperty.call(payload, "status") && + Object.prototype.hasOwnProperty.call(payload, "data") && + Object.prototype.hasOwnProperty.call(payload, "message") && + Object.prototype.hasOwnProperty.call(payload, "errors") + ); +} + +function extractMessage(payload, isError) { + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + if (typeof payload.message === "string" && payload.message.length) { + return payload.message; + } + if (typeof payload.error === "string" && payload.error.length) { + return payload.error; + } + } + + return isError ? "Request failed" : "OK"; +} + +function extractErrors(payload, isError, message) { + if (!isError) return []; + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + if (Array.isArray(payload.errors)) { + return payload.errors; + } + if (payload.error) { + return [payload.error]; + } + } + return message ? [message] : []; +} + +module.exports = { + envelopeResponses, +};