first commit
This commit is contained in:
29
app/controllers/cache_status_controller.js
Normal file
29
app/controllers/cache_status_controller.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
function CacheStatusController(tableCache) {
|
||||
this.tableCache = tableCache;
|
||||
}
|
||||
|
||||
CacheStatusController.prototype.route = function (app) {
|
||||
app.get(global.settings.base_url + '/cachestatus', this.handleCacheStatus.bind(this));
|
||||
};
|
||||
|
||||
CacheStatusController.prototype.handleCacheStatus = function (req, res) {
|
||||
var tableCacheValues = this.tableCache.values();
|
||||
var totalExplainKeys = tableCacheValues.length;
|
||||
var totalExplainHits = _.reduce(tableCacheValues, function(memo, res) {
|
||||
return memo + res.hits;
|
||||
}, 0);
|
||||
|
||||
res.send({
|
||||
explain: {
|
||||
pid: process.pid,
|
||||
hits: totalExplainHits,
|
||||
keys : totalExplainKeys
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CacheStatusController;
|
||||
207
app/controllers/copy_controller.js
Normal file
207
app/controllers/copy_controller.js
Normal file
@@ -0,0 +1,207 @@
|
||||
'use strict';
|
||||
|
||||
const userMiddleware = require('../middlewares/user');
|
||||
const errorMiddleware = require('../middlewares/error');
|
||||
const authorizationMiddleware = require('../middlewares/authorization');
|
||||
const connectionParamsMiddleware = require('../middlewares/connection-params');
|
||||
const { initializeProfilerMiddleware } = require('../middlewares/profiler');
|
||||
const rateLimitsMiddleware = require('../middlewares/rate-limit');
|
||||
const dbQuotaMiddleware = require('../middlewares/db-quota');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimitsMiddleware;
|
||||
const errorHandlerFactory = require('../services/error_handler_factory');
|
||||
const StreamCopy = require('../services/stream_copy');
|
||||
const StreamCopyMetrics = require('../services/stream_copy_metrics');
|
||||
const zlib = require('zlib');
|
||||
const { PassThrough } = require('stream');
|
||||
|
||||
function CopyController(metadataBackend, userDatabaseService, userLimitsService, logger) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userDatabaseService = userDatabaseService;
|
||||
this.userLimitsService = userLimitsService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
CopyController.prototype.route = function (app) {
|
||||
const { base_url } = global.settings;
|
||||
|
||||
const copyFromMiddlewares = endpointGroup => {
|
||||
return [
|
||||
initializeProfilerMiddleware('copyfrom'),
|
||||
userMiddleware(this.metadataBackend),
|
||||
rateLimitsMiddleware(this.userLimitsService, endpointGroup),
|
||||
authorizationMiddleware(this.metadataBackend),
|
||||
connectionParamsMiddleware(this.userDatabaseService),
|
||||
validateCopyQuery(),
|
||||
dbQuotaMiddleware(),
|
||||
handleCopyFrom(this.logger),
|
||||
errorHandler(),
|
||||
errorMiddleware()
|
||||
];
|
||||
};
|
||||
|
||||
const copyToMiddlewares = endpointGroup => {
|
||||
return [
|
||||
initializeProfilerMiddleware('copyto'),
|
||||
userMiddleware(this.metadataBackend),
|
||||
rateLimitsMiddleware(this.userLimitsService, endpointGroup),
|
||||
authorizationMiddleware(this.metadataBackend),
|
||||
connectionParamsMiddleware(this.userDatabaseService),
|
||||
validateCopyQuery(),
|
||||
handleCopyTo(this.logger),
|
||||
errorHandler(),
|
||||
errorMiddleware()
|
||||
];
|
||||
};
|
||||
|
||||
app.post(`${base_url}/sql/copyfrom`, copyFromMiddlewares(RATE_LIMIT_ENDPOINTS_GROUPS.COPY_FROM));
|
||||
app.get(`${base_url}/sql/copyto`, copyToMiddlewares(RATE_LIMIT_ENDPOINTS_GROUPS.COPY_TO));
|
||||
};
|
||||
|
||||
|
||||
function handleCopyTo (logger) {
|
||||
return function handleCopyToMiddleware (req, res, next) {
|
||||
const sql = req.query.q;
|
||||
const { userDbParams, user } = res.locals;
|
||||
const filename = req.query.filename || 'carto-sql-copyto.dmp';
|
||||
|
||||
// it is not sure, nginx may choose not to compress the body
|
||||
// but we want to know it and save it in the metrics
|
||||
// https://github.com/CartoDB/CartoDB-SQL-API/issues/515
|
||||
const isGzip = req.get('accept-encoding') && req.get('accept-encoding').includes('gzip');
|
||||
|
||||
const streamCopy = new StreamCopy(sql, userDbParams);
|
||||
const metrics = new StreamCopyMetrics(logger, 'copyto', sql, user, isGzip);
|
||||
|
||||
res.header("Content-Disposition", `attachment; filename=${encodeURIComponent(filename)}`);
|
||||
res.header("Content-Type", "application/octet-stream");
|
||||
|
||||
streamCopy.getPGStream(StreamCopy.ACTION_TO, (err, pgstream) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
pgstream
|
||||
.on('data', data => metrics.addSize(data.length))
|
||||
.on('error', err => {
|
||||
metrics.end(null, err);
|
||||
pgstream.unpipe(res);
|
||||
|
||||
return next(err);
|
||||
})
|
||||
.on('end', () => metrics.end( streamCopy.getRowCount(StreamCopy.ACTION_TO) ))
|
||||
.pipe(res)
|
||||
.on('close', () => {
|
||||
const err = new Error('Connection closed by client');
|
||||
pgstream.emit('cancelQuery', err);
|
||||
pgstream.emit('error', err);
|
||||
})
|
||||
.on('error', err => {
|
||||
pgstream.emit('error', err);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleCopyFrom (logger) {
|
||||
return function handleCopyFromMiddleware (req, res, next) {
|
||||
const sql = req.query.q;
|
||||
const { userDbParams, user, dbRemainingQuota } = res.locals;
|
||||
const isGzip = req.get('content-encoding') === 'gzip';
|
||||
const COPY_FROM_MAX_POST_SIZE = global.settings.copy_from_max_post_size || 2 * 1024 * 1024 * 1024; // 2 GB
|
||||
const COPY_FROM_MAX_POST_SIZE_PRETTY = global.settings.copy_from_max_post_size_pretty || '2 GB';
|
||||
|
||||
const streamCopy = new StreamCopy(sql, userDbParams);
|
||||
const metrics = new StreamCopyMetrics(logger, 'copyfrom', sql, user, isGzip);
|
||||
|
||||
streamCopy.getPGStream(StreamCopy.ACTION_FROM, (err, pgstream) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req
|
||||
.on('data', data => isGzip ? metrics.addGzipSize(data.length) : undefined)
|
||||
.on('error', err => {
|
||||
metrics.end(null, err);
|
||||
pgstream.emit('error', err);
|
||||
})
|
||||
.on('close', () => {
|
||||
const err = new Error('Connection closed by client');
|
||||
pgstream.emit('cancelQuery', err);
|
||||
pgstream.emit('error', err);
|
||||
})
|
||||
.pipe(isGzip ? zlib.createGunzip() : new PassThrough())
|
||||
.on('error', err => {
|
||||
err.message = `Error while gunzipping: ${err.message}`;
|
||||
metrics.end(null, err);
|
||||
pgstream.emit('error', err);
|
||||
})
|
||||
.on('data', data => {
|
||||
metrics.addSize(data.length);
|
||||
|
||||
if(metrics.size > dbRemainingQuota) {
|
||||
const quotaError = new Error('DB Quota exceeded');
|
||||
pgstream.emit('cancelQuery', err);
|
||||
pgstream.emit('error', quotaError);
|
||||
}
|
||||
if((metrics.gzipSize || metrics.size) > COPY_FROM_MAX_POST_SIZE) {
|
||||
const maxPostSizeError = new Error(
|
||||
`COPY FROM maximum POST size of ${COPY_FROM_MAX_POST_SIZE_PRETTY} exceeded`
|
||||
);
|
||||
pgstream.emit('cancelQuery', err);
|
||||
pgstream.emit('error', maxPostSizeError);
|
||||
}
|
||||
})
|
||||
.pipe(pgstream)
|
||||
.on('error', err => {
|
||||
metrics.end(null, err);
|
||||
req.unpipe(pgstream);
|
||||
return next(err);
|
||||
})
|
||||
.on('end', () => {
|
||||
metrics.end( streamCopy.getRowCount(StreamCopy.ACTION_FROM) );
|
||||
|
||||
const { time, rows } = metrics;
|
||||
|
||||
if (!rows) {
|
||||
return next(new Error("No rows copied"));
|
||||
}
|
||||
|
||||
res.send({
|
||||
time,
|
||||
total_rows: rows
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function validateCopyQuery () {
|
||||
return function validateCopyQueryMiddleware (req, res, next) {
|
||||
const sql = req.query.q;
|
||||
|
||||
if (!sql) {
|
||||
return next(new Error("SQL is missing"));
|
||||
}
|
||||
|
||||
if (!sql.toUpperCase().startsWith("COPY ")) {
|
||||
return next(new Error("SQL must start with COPY"));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function errorHandler () {
|
||||
return function errorHandlerMiddleware (err, req, res, next) {
|
||||
if (res.headersSent) {
|
||||
console.error("EXCEPTION REPORT: " + err.stack);
|
||||
const errorHandler = errorHandlerFactory(err);
|
||||
res.write(JSON.stringify(errorHandler.getResponse()));
|
||||
res.end();
|
||||
} else {
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = CopyController;
|
||||
14
app/controllers/generic_controller.js
Normal file
14
app/controllers/generic_controller.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
function GenericController() {
|
||||
}
|
||||
|
||||
GenericController.prototype.route = function (app) {
|
||||
app.options('*', this.handleRequest.bind(this));
|
||||
};
|
||||
|
||||
GenericController.prototype.handleRequest = function(req, res) {
|
||||
res.end();
|
||||
};
|
||||
|
||||
module.exports = GenericController;
|
||||
35
app/controllers/health_check_controller.js
Normal file
35
app/controllers/health_check_controller.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
var HealthCheck = require('../monitoring/health_check');
|
||||
|
||||
function HealthCheckController() {
|
||||
this.healthCheck = new HealthCheck(global.settings.disabled_file);
|
||||
}
|
||||
|
||||
HealthCheckController.prototype.route = function (app) {
|
||||
app.get(global.settings.base_url + '/health', this.handleHealthCheck.bind(this));
|
||||
};
|
||||
|
||||
HealthCheckController.prototype.handleHealthCheck = function (req, res) {
|
||||
var healthConfig = global.settings.health || {};
|
||||
if (!!healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
this.healthCheck.check(function(err) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.status(ok ? 200 : 503).send(response);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.status(200).send({enabled: false, ok: true});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = HealthCheckController;
|
||||
253
app/controllers/job_controller.js
Normal file
253
app/controllers/job_controller.js
Normal file
@@ -0,0 +1,253 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
|
||||
const bodyParserMiddleware = require('../middlewares/body-parser');
|
||||
const userMiddleware = require('../middlewares/user');
|
||||
const { initializeProfilerMiddleware, finishProfilerMiddleware } = require('../middlewares/profiler');
|
||||
const authorizationMiddleware = require('../middlewares/authorization');
|
||||
const connectionParamsMiddleware = require('../middlewares/connection-params');
|
||||
const errorMiddleware = require('../middlewares/error');
|
||||
const rateLimitsMiddleware = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimitsMiddleware;
|
||||
|
||||
function JobController(metadataBackend, userDatabaseService, jobService, statsdClient, userLimitsService) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userDatabaseService = userDatabaseService;
|
||||
this.jobService = jobService;
|
||||
this.statsdClient = statsdClient;
|
||||
this.userLimitsService = userLimitsService;
|
||||
}
|
||||
|
||||
module.exports = JobController;
|
||||
|
||||
JobController.prototype.route = function (app) {
|
||||
const { base_url } = global.settings;
|
||||
const jobMiddlewares = composeJobMiddlewares(
|
||||
this.metadataBackend,
|
||||
this.userDatabaseService,
|
||||
this.jobService,
|
||||
this.statsdClient,
|
||||
this.userLimitsService
|
||||
);
|
||||
|
||||
app.get(
|
||||
`${base_url}/jobs-wip`,
|
||||
bodyParserMiddleware(),
|
||||
listWorkInProgressJobs(this.jobService),
|
||||
sendResponse(),
|
||||
errorMiddleware()
|
||||
);
|
||||
app.post(
|
||||
`${base_url}/sql/job`,
|
||||
bodyParserMiddleware(),
|
||||
checkBodyPayloadSize(),
|
||||
jobMiddlewares('create', createJob, RATE_LIMIT_ENDPOINTS_GROUPS.JOB_CREATE)
|
||||
);
|
||||
app.get(
|
||||
`${base_url}/sql/job/:job_id`,
|
||||
bodyParserMiddleware(),
|
||||
jobMiddlewares('retrieve', getJob, RATE_LIMIT_ENDPOINTS_GROUPS.JOB_GET)
|
||||
);
|
||||
app.delete(
|
||||
`${base_url}/sql/job/:job_id`,
|
||||
bodyParserMiddleware(),
|
||||
jobMiddlewares('cancel', cancelJob, RATE_LIMIT_ENDPOINTS_GROUPS.JOB_DELETE)
|
||||
);
|
||||
};
|
||||
|
||||
function composeJobMiddlewares (metadataBackend, userDatabaseService, jobService, statsdClient, userLimitsService) {
|
||||
return function jobMiddlewares (action, jobMiddleware, endpointGroup) {
|
||||
const forceToBeMaster = true;
|
||||
|
||||
return [
|
||||
initializeProfilerMiddleware('job'),
|
||||
userMiddleware(metadataBackend),
|
||||
rateLimitsMiddleware(userLimitsService, endpointGroup),
|
||||
authorizationMiddleware(metadataBackend, forceToBeMaster),
|
||||
connectionParamsMiddleware(userDatabaseService),
|
||||
jobMiddleware(jobService),
|
||||
setServedByDBHostHeader(),
|
||||
finishProfilerMiddleware(),
|
||||
logJobResult(action),
|
||||
incrementSuccessMetrics(statsdClient),
|
||||
sendResponse(),
|
||||
incrementErrorMetrics(statsdClient),
|
||||
errorMiddleware()
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
function cancelJob (jobService) {
|
||||
return function cancelJobMiddleware (req, res, next) {
|
||||
const { job_id } = req.params;
|
||||
|
||||
jobService.cancel(job_id, (err, job) => {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('cancelJob');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = job.serialize();
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getJob (jobService) {
|
||||
return function getJobMiddleware (req, res, next) {
|
||||
const { job_id } = req.params;
|
||||
|
||||
jobService.get(job_id, (err, job) => {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('getJob');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = job.serialize();
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createJob (jobService) {
|
||||
return function createJobMiddleware (req, res, next) {
|
||||
const params = Object.assign({}, req.query, req.body);
|
||||
|
||||
var data = {
|
||||
user: res.locals.user,
|
||||
query: params.query,
|
||||
host: res.locals.userDbParams.host,
|
||||
port: global.settings.db_batch_port || res.locals.userDbParams.port,
|
||||
pass: res.locals.userDbParams.pass,
|
||||
dbname: res.locals.userDbParams.dbname,
|
||||
dbuser: res.locals.userDbParams.user
|
||||
};
|
||||
|
||||
jobService.create(data, (err, job) => {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('createJob');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.job_id = job.job_id;
|
||||
|
||||
res.statusCode = 201;
|
||||
res.body = job.serialize();
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function listWorkInProgressJobs (jobService) {
|
||||
return function listWorkInProgressJobsMiddleware (req, res, next) {
|
||||
jobService.listWorkInProgressJobs((err, list) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = list;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function checkBodyPayloadSize () {
|
||||
return function checkBodyPayloadSizeMiddleware(req, res, next) {
|
||||
const payload = JSON.stringify(req.body);
|
||||
|
||||
if (payload.length > MAX_LIMIT_QUERY_SIZE_IN_BYTES) {
|
||||
return next(new Error(getMaxSizeErrorMessage(payload)), res);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
const ONE_KILOBYTE_IN_BYTES = 1024;
|
||||
const MAX_LIMIT_QUERY_SIZE_IN_KB = 16;
|
||||
const MAX_LIMIT_QUERY_SIZE_IN_BYTES = MAX_LIMIT_QUERY_SIZE_IN_KB * ONE_KILOBYTE_IN_BYTES;
|
||||
|
||||
function getMaxSizeErrorMessage(sql) {
|
||||
return util.format([
|
||||
'Your payload is too large: %s bytes. Max size allowed is %s bytes (%skb).',
|
||||
'Are you trying to import data?.',
|
||||
'Please, check out import api http://docs.cartodb.com/cartodb-platform/import-api/'
|
||||
].join(' '),
|
||||
sql.length,
|
||||
MAX_LIMIT_QUERY_SIZE_IN_BYTES,
|
||||
Math.round(MAX_LIMIT_QUERY_SIZE_IN_BYTES / ONE_KILOBYTE_IN_BYTES)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports.MAX_LIMIT_QUERY_SIZE_IN_BYTES = MAX_LIMIT_QUERY_SIZE_IN_BYTES;
|
||||
module.exports.getMaxSizeErrorMessage = getMaxSizeErrorMessage;
|
||||
|
||||
function setServedByDBHostHeader () {
|
||||
return function setServedByDBHostHeaderMiddleware (req, res, next) {
|
||||
const { userDbParams } = res.locals;
|
||||
|
||||
if (userDbParams.host) {
|
||||
res.header('X-Served-By-DB-Host', res.locals.userDbParams.host);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function logJobResult (action) {
|
||||
return function logJobResultMiddleware (req, res, next) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.info(JSON.stringify({
|
||||
type: 'sql_api_batch_job',
|
||||
username: res.locals.user,
|
||||
action: action,
|
||||
job_id: req.params.job_id || res.locals.job_id
|
||||
}));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
const METRICS_PREFIX = 'sqlapi.job';
|
||||
|
||||
function incrementSuccessMetrics (statsdClient) {
|
||||
return function incrementSuccessMetricsMiddleware (req, res, next) {
|
||||
if (statsdClient !== undefined) {
|
||||
statsdClient.increment(`${METRICS_PREFIX}.success`);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementErrorMetrics (statsdClient) {
|
||||
return function incrementErrorMetricsMiddleware (err, req, res, next) {
|
||||
if (statsdClient !== undefined) {
|
||||
statsdClient.increment(`${METRICS_PREFIX}.error`);
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
res.status(res.statusCode || 200).send(res.body);
|
||||
};
|
||||
}
|
||||
275
app/controllers/query_controller.js
Normal file
275
app/controllers/query_controller.js
Normal file
@@ -0,0 +1,275 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var CachedQueryTables = require('../services/cached-query-tables');
|
||||
const pgEntitiesAccessValidator = require('../services/pg-entities-access-validator');
|
||||
var queryMayWrite = require('../utils/query_may_write');
|
||||
|
||||
var formats = require('../models/formats');
|
||||
|
||||
var sanitize_filename = require('../utils/filename_sanitizer');
|
||||
var getContentDisposition = require('../utils/content_disposition');
|
||||
const bodyParserMiddleware = require('../middlewares/body-parser');
|
||||
const userMiddleware = require('../middlewares/user');
|
||||
const errorMiddleware = require('../middlewares/error');
|
||||
const authorizationMiddleware = require('../middlewares/authorization');
|
||||
const connectionParamsMiddleware = require('../middlewares/connection-params');
|
||||
const timeoutLimitsMiddleware = require('../middlewares/timeout-limits');
|
||||
const { initializeProfilerMiddleware } = require('../middlewares/profiler');
|
||||
const rateLimitsMiddleware = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimitsMiddleware;
|
||||
|
||||
var ONE_YEAR_IN_SECONDS = 31536000; // 1 year time to live by default
|
||||
|
||||
function QueryController(metadataBackend, userDatabaseService, tableCache, statsd_client, userLimitsService) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.statsd_client = statsd_client;
|
||||
this.userDatabaseService = userDatabaseService;
|
||||
this.queryTables = new CachedQueryTables(tableCache);
|
||||
this.userLimitsService = userLimitsService;
|
||||
}
|
||||
|
||||
QueryController.prototype.route = function (app) {
|
||||
const { base_url } = global.settings;
|
||||
const forceToBeMaster = false;
|
||||
|
||||
const queryMiddlewares = () => {
|
||||
return [
|
||||
bodyParserMiddleware(),
|
||||
initializeProfilerMiddleware('query'),
|
||||
userMiddleware(this.metadataBackend),
|
||||
rateLimitsMiddleware(this.userLimitsService, RATE_LIMIT_ENDPOINTS_GROUPS.QUERY),
|
||||
authorizationMiddleware(this.metadataBackend, forceToBeMaster),
|
||||
connectionParamsMiddleware(this.userDatabaseService),
|
||||
timeoutLimitsMiddleware(this.metadataBackend),
|
||||
this.handleQuery.bind(this),
|
||||
errorMiddleware()
|
||||
];
|
||||
};
|
||||
|
||||
app.all(`${base_url}/sql`, queryMiddlewares());
|
||||
app.all(`${base_url}/sql.:f`, queryMiddlewares());
|
||||
};
|
||||
|
||||
// jshint maxcomplexity:21
|
||||
QueryController.prototype.handleQuery = function (req, res, next) {
|
||||
var self = this;
|
||||
// extract input
|
||||
var body = (req.body) ? req.body : {};
|
||||
// clone so don't modify req.params or req.body so oauth is not broken
|
||||
var params = _.extend({}, req.query, body);
|
||||
var sql = params.q;
|
||||
var limit = parseInt(params.rows_per_page);
|
||||
var offset = parseInt(params.page);
|
||||
var orderBy = params.order_by;
|
||||
var sortOrder = params.sort_order;
|
||||
var requestedFormat = params.format;
|
||||
var format = _.isArray(requestedFormat) ? _.last(requestedFormat) : requestedFormat;
|
||||
var requestedFilename = params.filename;
|
||||
var filename = requestedFilename;
|
||||
var requestedSkipfields = params.skipfields;
|
||||
|
||||
const { user: username, userDbParams: dbopts, authDbParams, userLimits, authorizationLevel } = res.locals;
|
||||
|
||||
var skipfields;
|
||||
var dp = params.dp; // decimal point digits (defaults to 6)
|
||||
var gn = "the_geom"; // TODO: read from configuration FILE
|
||||
|
||||
req.aborted = false;
|
||||
req.on("close", function() {
|
||||
if (req.formatter && _.isFunction(req.formatter.cancel)) {
|
||||
req.formatter.cancel();
|
||||
}
|
||||
req.aborted = true; // TODO: there must be a builtin way to check this
|
||||
});
|
||||
|
||||
function checkAborted(step) {
|
||||
if ( req.aborted ) {
|
||||
var err = new Error("Request aborted during " + step);
|
||||
// We'll use status 499, same as ngnix in these cases
|
||||
// see http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error
|
||||
err.http_status = 499;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// sanitize and apply defaults to input
|
||||
dp = (dp === "" || _.isUndefined(dp)) ? '6' : dp;
|
||||
format = (format === "" || _.isUndefined(format)) ? 'json' : format.toLowerCase();
|
||||
filename = (filename === "" || _.isUndefined(filename)) ? 'cartodb-query' : sanitize_filename(filename);
|
||||
sql = (sql === "" || _.isUndefined(sql)) ? null : sql;
|
||||
limit = (!_.isNaN(limit)) ? limit : null;
|
||||
offset = (!_.isNaN(offset)) ? offset * limit : null;
|
||||
|
||||
// Accept both comma-separated string or array of comma-separated strings
|
||||
if ( requestedSkipfields ) {
|
||||
if ( _.isString(requestedSkipfields) ) {
|
||||
skipfields = requestedSkipfields.split(',');
|
||||
} else if ( _.isArray(requestedSkipfields) ) {
|
||||
skipfields = [];
|
||||
_.each(requestedSkipfields, function(ele) {
|
||||
skipfields = skipfields.concat(ele.split(','));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
skipfields = [];
|
||||
}
|
||||
|
||||
//if ( -1 === supportedFormats.indexOf(format) )
|
||||
if ( ! formats.hasOwnProperty(format) ) {
|
||||
throw new Error("Invalid format: " + format);
|
||||
}
|
||||
|
||||
if (!_.isString(sql)) {
|
||||
throw new Error("You must indicate a sql query");
|
||||
}
|
||||
|
||||
var formatter;
|
||||
|
||||
if ( req.profiler ) {
|
||||
req.profiler.done('init');
|
||||
}
|
||||
|
||||
// 1. Get the list of tables affected by the query
|
||||
// 2. Setup headers
|
||||
// 3. Send formatted results back
|
||||
// 4. Handle error
|
||||
step(
|
||||
function queryExplain() {
|
||||
var next = this;
|
||||
|
||||
checkAborted('queryExplain');
|
||||
|
||||
var pg = new PSQL(authDbParams);
|
||||
|
||||
var skipCache = authorizationLevel === 'master';
|
||||
|
||||
self.queryTables.getAffectedTablesFromQuery(pg, sql, skipCache, function(err, result) {
|
||||
if (err) {
|
||||
var errorMessage = (err && err.message) || 'unknown error';
|
||||
console.error("Error on query explain '%s': %s", sql, errorMessage);
|
||||
}
|
||||
return next(null, result);
|
||||
});
|
||||
},
|
||||
function setHeaders(err, affectedTables) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var mayWrite = queryMayWrite(sql);
|
||||
if ( req.profiler ) {
|
||||
req.profiler.done('queryExplain');
|
||||
}
|
||||
|
||||
checkAborted('setHeaders');
|
||||
if(!pgEntitiesAccessValidator.validate(affectedTables, authorizationLevel)) {
|
||||
const syntaxError = new SyntaxError("system tables are forbidden");
|
||||
syntaxError.http_status = 403;
|
||||
throw(syntaxError);
|
||||
}
|
||||
|
||||
var FormatClass = formats[format];
|
||||
formatter = new FormatClass();
|
||||
req.formatter = formatter;
|
||||
|
||||
|
||||
// configure headers for given format
|
||||
var use_inline = !requestedFormat && !requestedFilename;
|
||||
res.header("Content-Disposition", getContentDisposition(formatter, filename, use_inline));
|
||||
res.header("Content-Type", formatter.getContentType());
|
||||
|
||||
// set cache headers
|
||||
var cachePolicy = req.query.cache_policy;
|
||||
if (cachePolicy === 'persist') {
|
||||
res.header('Cache-Control', 'public,max-age=' + ONE_YEAR_IN_SECONDS);
|
||||
} else {
|
||||
var maxAge = (mayWrite) ? 0 : ONE_YEAR_IN_SECONDS;
|
||||
res.header('Cache-Control', 'no-cache,max-age='+maxAge+',must-revalidate,public');
|
||||
}
|
||||
|
||||
// Only set an X-Cache-Channel for responses we want Varnish to cache.
|
||||
var skipNotUpdatedAtTables = true;
|
||||
if (!!affectedTables && affectedTables.getTables(skipNotUpdatedAtTables).length > 0 && !mayWrite) {
|
||||
res.header('X-Cache-Channel', affectedTables.getCacheChannel(skipNotUpdatedAtTables));
|
||||
res.header('Surrogate-Key', affectedTables.key(skipNotUpdatedAtTables).join(' '));
|
||||
}
|
||||
|
||||
if(!!affectedTables) {
|
||||
res.header('Last-Modified',
|
||||
new Date(affectedTables.getLastUpdatedAt(Number(new Date()))).toUTCString());
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function generateFormat(err){
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
checkAborted('generateFormat');
|
||||
|
||||
// TODO: drop this, fix UI!
|
||||
sql = new PSQL.QueryWrapper(sql).orderBy(orderBy, sortOrder).window(limit, offset).query();
|
||||
|
||||
var opts = {
|
||||
username: username,
|
||||
dbopts: dbopts,
|
||||
sink: res,
|
||||
gn: gn,
|
||||
dp: dp,
|
||||
skipfields: skipfields,
|
||||
sql: sql,
|
||||
filename: filename,
|
||||
bufferedRows: global.settings.bufferedRows,
|
||||
callback: params.callback,
|
||||
abortChecker: checkAborted,
|
||||
timeout: userLimits.timeout
|
||||
};
|
||||
|
||||
if ( req.profiler ) {
|
||||
opts.profiler = req.profiler;
|
||||
opts.beforeSink = function() {
|
||||
req.profiler.done('beforeSink');
|
||||
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
|
||||
};
|
||||
}
|
||||
|
||||
if (dbopts.host) {
|
||||
res.header('X-Served-By-DB-Host', dbopts.host);
|
||||
}
|
||||
formatter.sendResponse(opts, this);
|
||||
},
|
||||
function errorHandle(err){
|
||||
formatter = null;
|
||||
|
||||
if (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
if ( req.profiler ) {
|
||||
req.profiler.sendStats();
|
||||
}
|
||||
if (self.statsd_client) {
|
||||
if ( err ) {
|
||||
self.statsd_client.increment('sqlapi.query.error');
|
||||
} else {
|
||||
self.statsd_client.increment('sqlapi.query.success');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
if (self.statsd_client) {
|
||||
self.statsd_client.increment('sqlapi.query.error');
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = QueryController;
|
||||
18
app/controllers/version_controller.js
Normal file
18
app/controllers/version_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var version = {
|
||||
cartodb_sql_api: require(__dirname + '/../../package.json').version
|
||||
};
|
||||
|
||||
function VersionController() {
|
||||
}
|
||||
|
||||
VersionController.prototype.route = function (app) {
|
||||
app.get(global.settings.base_url + '/version', this.handleVersion.bind(this));
|
||||
};
|
||||
|
||||
VersionController.prototype.handleVersion = function (req, res) {
|
||||
res.send(version);
|
||||
};
|
||||
|
||||
module.exports = VersionController;
|
||||
Reference in New Issue
Block a user