server/api/DataApiRoute.js
Rama master · réplica de lectura del repositorio original.
const rateLimit = require("express-rate-limit");
const DataApiController = require("../controllers/DataApiController");
const verifyDataApiKey = require("../modules/verifyDataApiKey");
const { getDataApiLimits } = require("../modules/dataApiLimits");
const { incrementDataApiMetric } = require("../modules/dataApiMetrics");
const {
DataApiError,
acceptsJson,
ensureRequestId,
sendDataApiError,
setDataHeaders,
} = require("../modules/dataApiResponse");
const { failRun, startRun } = require("../modules/updateAudit");
function isDataApiEnabled() {
return process.env.CB_DATA_API_ENABLED !== "false";
}
function requestId(req, _res, next) {
ensureRequestId(req);
return next();
}
function requireEnabled(req, res, next) {
if (!isDataApiEnabled()) return sendDataApiError(req, res, new DataApiError("RESOURCE_NOT_FOUND"));
return next();
}
function validateHttpRequest(req, res, next) {
if (!acceptsJson(req)) return sendDataApiError(req, res, new DataApiError("NOT_ACCEPTABLE"));
if (req.method === "GET" && Object.keys(req.query || {}).length > 0) {
return sendDataApiError(req, res, new DataApiError("INVALID_REQUEST"));
}
if (req.method === "POST") {
if (!req.is("application/json")) {
return sendDataApiError(req, res, new DataApiError("UNSUPPORTED_MEDIA_TYPE"));
}
const rawBytes = Buffer.byteLength(req.rawBody || JSON.stringify(req.body || {}), "utf8");
if (rawBytes > getDataApiLimits().maxRequestBytes) {
incrementDataApiMetric("size_rejections", { resourceType: "request", type: "REQUEST_TOO_LARGE" });
return sendDataApiError(req, res, new DataApiError("REQUEST_TOO_LARGE"));
}
}
return next();
}
function createAuthLimiter() {
return rateLimit({
legacyHeaders: false,
limit: () => getDataApiLimits().authRateLimitMax,
standardHeaders: true,
windowMs: 60 * 1000,
requestWasSuccessful: (req) => Boolean(req.apiKeyAccess),
skipSuccessfulRequests: true,
handler: (req, res) => {
incrementDataApiMetric("authorization_failures", { code: "RATE_LIMITED" });
return sendDataApiError(req, res, new DataApiError("RATE_LIMITED"));
},
});
}
function positiveAuditId(value) {
if (!/^[1-9]\d*$/.test(`${value || ""}`)) return null;
const parsed = Number(value);
return Number.isSafeInteger(parsed) ? parsed : null;
}
async function auditAuthenticatedRateLimit(req) {
const isChart = Boolean(req.params.chart_id);
const traceContext = await startRun({
triggerType: "data_api",
entityType: isChart ? "chart_data" : "dataset_data",
status: "running",
apiKeyId: req.apiKeyAccess.apiKeyId,
teamId: req.apiKeyAccess.teamId,
projectId: positiveAuditId(req.params.project_id),
chartId: positiveAuditId(req.params.chart_id),
datasetId: positiveAuditId(req.params.dataset_id),
summary: {
apiVersion: "v1",
method: req.method,
requestId: req.id,
},
});
await failRun(traceContext, new DataApiError("RATE_LIMITED"), {
stage: "data_api",
summary: {
apiVersion: "v1",
errorCode: "RATE_LIMITED",
method: req.method,
requestId: req.id,
responseBytes: 0,
statusCode: 429,
},
});
}
function createKeyLimiter() {
return rateLimit({
keyGenerator: (req) => req.apiKeyAccess.apiKeyId,
legacyHeaders: false,
limit: () => getDataApiLimits().rateLimitMax,
standardHeaders: true,
windowMs: 60 * 1000,
handler: async (req, res) => {
incrementDataApiMetric("requests", { resourceType: "rate_limit", statusCode: 429 });
await auditAuthenticatedRateLimit(req);
return sendDataApiError(req, res, new DataApiError("RATE_LIMITED"));
},
});
}
function sendResult(res, result) {
setDataHeaders(res, result);
if (result.notModified) return res.status(304).end();
res.status(200).set("Content-Type", "application/json; charset=utf-8");
return res.end(result.serialized);
}
function routeHandler(controllerMethod) {
return async (req, res) => {
try {
return sendResult(res, await controllerMethod(req));
} catch (error) {
return sendDataApiError(req, res, error);
}
};
}
module.exports = (app) => {
const controller = new DataApiController();
const authLimiter = createAuthLimiter();
const keyLimiter = createKeyLimiter();
const access = [requestId, requireEnabled, validateHttpRequest, authLimiter, verifyDataApiKey(), keyLimiter];
app.get(
"/api/v1/teams/:team_id/datasets/:dataset_id/data",
...access,
routeHandler(controller.datasetData.bind(controller))
);
app.post(
"/api/v1/teams/:team_id/datasets/:dataset_id/data",
...access,
routeHandler(controller.datasetData.bind(controller))
);
app.get(
"/api/v1/projects/:project_id/charts/:chart_id/data",
...access,
routeHandler(controller.chartData.bind(controller))
);
app.post(
"/api/v1/projects/:project_id/charts/:chart_id/data",
...access,
routeHandler(controller.chartData.bind(controller))
);
return (req, res, next) => next();
};