demo 20-4-2026

This commit is contained in:
taDuc
2026-04-19 23:43:31 +07:00
parent 34e709cea4
commit 7804ef842b
6 changed files with 105 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ const geoRoutes = require("./routes/geometries");
const entityRoutes = require("./routes/entities"); const entityRoutes = require("./routes/entities");
const sectionRoutes = require("./routes/sections"); const sectionRoutes = require("./routes/sections");
const { openApiSpec } = require("./swagger"); const { openApiSpec } = require("./swagger");
const { envelopeResponses } = require("./utils/apiEnvelope");
const app = express(); const app = express();
@@ -15,12 +16,12 @@ app.use(cors());
app.use(express.json()); app.use(express.json());
// serve MBTiles and geometry CRUD // serve MBTiles and geometry CRUD
app.use("/tiles", tileRoutes); app.use("/tiles", envelopeResponses, tileRoutes);
app.use("/raster-tiles", rasterTileRoutes); app.use("/raster-tiles", envelopeResponses, rasterTileRoutes);
app.use("/geometries", geoRoutes); app.use("/geometries", envelopeResponses, geoRoutes);
app.use("/entities", entityRoutes); app.use("/entities", envelopeResponses, entityRoutes);
app.use("/sections", sectionRoutes); app.use("/sections", envelopeResponses, sectionRoutes);
app.use("/submissions", (req, res, next) => { app.use("/submissions", envelopeResponses, (req, res, next) => {
req.url = `/submissions${req.url}`; req.url = `/submissions${req.url}`;
sectionRoutes(req, res, next); sectionRoutes(req, res, next);
}); });

View File

@@ -33,7 +33,7 @@ router.get("/:z/:x/:y", (req, res) => {
const y = Number(req.params.y); const y = Number(req.params.y);
if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(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; const tmsY = (1 << z) - 1 - y;
@@ -47,7 +47,7 @@ router.get("/:z/:x/:y", (req, res) => {
`).get(z, x, tmsY); `).get(z, x, tmsY);
if (!tile) { if (!tile) {
return res.status(404).send("Tile not found"); return res.status(404).json({ error: "Tile not found" });
} }
res.setHeader("Content-Type", contentType); res.setHeader("Content-Type", contentType);

View File

@@ -289,9 +289,9 @@ router.post("/:sectionId/submit", (req, res) => {
throw createHttpError("Section is not editable", 409); 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) { 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); const commit = getCommitForSection(section.id, commitId);

View File

@@ -47,7 +47,7 @@ router.get("/:z/:x/:y", (req, res) => {
const y = Number(req.params.y); const y = Number(req.params.y);
if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(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 // convert XYZ → TMS
@@ -64,7 +64,7 @@ router.get("/:z/:x/:y", (req, res) => {
const tile = stmt.get(z, x, tmsY); const tile = stmt.get(z, x, tmsY);
if (!tile) { if (!tile) {
return res.status(404).send("Tile not found"); return res.status(404).json({ error: "Tile not found" });
} }
res.setHeader("Content-Type", contentType); res.setHeader("Content-Type", contentType);

View File

@@ -24,9 +24,15 @@ const openApiSpec = {
ErrorResponse: { ErrorResponse: {
type: "object", type: "object",
properties: { 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: { Entity: {
type: "object", type: "object",
@@ -366,7 +372,7 @@ function objectResponse(description, ref) {
description, description,
content: { content: {
"application/json": { "application/json": {
schema: { $ref: ref }, schema: successEnvelopeSchema({ $ref: ref }),
}, },
}, },
}; };
@@ -377,15 +383,31 @@ function arrayResponse(description, itemRef) {
description, description,
content: { content: {
"application/json": { "application/json": {
schema: { schema: successEnvelopeSchema({
type: "array", type: "array",
items: { $ref: itemRef }, 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) { function jsonBody(schema) {
return { return {
required: true, required: true,

65
utils/apiEnvelope.js Normal file
View File

@@ -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,
};