diff --git a/lib/cartodb/controllers/analyses.js b/lib/cartodb/controllers/analyses.js new file mode 100644 index 00000000..7760cc32 --- /dev/null +++ b/lib/cartodb/controllers/analyses.js @@ -0,0 +1,169 @@ +var step = require('step'); +var assert = require('assert'); +var dot = require('dot'); +dot.templateSettings.strip = false; +var PSQL = require('cartodb-psql'); + +var util = require('util'); +var BaseController = require('./base'); + +var cors = require('../middleware/cors'); +var userMiddleware = require('../middleware/user'); + +function AnalysesController(authApi, pgConnection) { + BaseController.call(this, authApi, pgConnection); +} + +util.inherits(AnalysesController, BaseController); + +module.exports = AnalysesController; + +AnalysesController.prototype.register = function(app) { + app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this)); +}; + +AnalysesController.prototype.sendResponse = function(req, res, resource) { + res.set('Cache-Control', 'public,max-age=10,must-revalidate'); + this.send(req, res, resource, 200); +}; + +AnalysesController.prototype.catalog = function(req, res) { + var self = this; + var username = req.context.user; + + step( + function reqParams() { + self.req2params(req, this); + }, + function catalogQuery(err) { + assert.ifError(err); + var pg = new PSQL(dbParamsFromReqParams(req.params)); + getMetadata(username, pg, this); + }, + function prepareResponse(err, results) { + assert.ifError(err); + + var analysisIdToTable = results.tables.reduce(function(analysisIdToTable, table) { + var analysisId = table.relname.split('_')[2]; + if (analysisId && analysisId.length === 40) { + analysisIdToTable[analysisId] = table; + } + return analysisIdToTable; + }, {}); + + var catalogWithTables = results.catalog.map(function(analysis) { + if (analysisIdToTable.hasOwnProperty(analysis.node_id)) { + analysis.table = analysisIdToTable[analysis.node_id]; + } + return analysis; + }); + + return catalogWithTables.sort(function(analysisA, analysisB) { + if (!!analysisA.table && !!analysisB.table) { + return analysisB.table.size - analysisA.table.size; + } + + if (!!analysisA.table) { + return -1; + } + + if (!!analysisB.table) { + return 1; + } + + return -1; + }); + }, + function sendResponse(err, catalogWithTables) { + if (err) { + if (err.message.match(/permission\sdenied/)) { + err = new Error('Unauthorized'); + err.http_status = 401; + } + self.sendError(req, res, err); + } else { + self.sendResponse(req, res, { catalog: catalogWithTables }); + } + } + ); +}; + +var catalogQueryTpl = dot.template( + 'SELECT analysis_def->>\'type\' as type, * FROM cartodb.cdb_analysis_catalog WHERE username = \'{{=it._username}}\'' +); + +var tablesQueryTpl = dot.template([ + "WITH analysis_tables AS (", + " SELECT", + " n.nspname AS nspname,", + " c.relname AS relname,", + " pg_total_relation_size(", + " format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))", + " ) AS size,", + " format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name", + " FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n", + " WHERE c.relnamespace = n.oid", + " AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'", + " AND n.nspname IN ('{{=it._username}}', 'public')", + ")", + "SELECT *, pg_size_pretty(size) as size_pretty", + "FROM analysis_tables", + "ORDER BY size DESC" +].join('\n')); + + +function getMetadata(username, pg, callback) { + var results = { + catalog: [], + tables: [] + }; + step( + function getCatalog() { + pg.query(catalogQueryTpl({_username: username}), this, true); // use read-only transaction + }, + function handleCatalog(err, resultSet) { + assert.ifError(err); + resultSet = resultSet || {}; + results.catalog = resultSet.rows || []; + this(); + }, + function getTables(err) { + assert.ifError(err); + pg.query(tablesQueryTpl({_username: username}), this, true); // use read-only transaction + }, + function handleTables(err, resultSet) { + assert.ifError(err); + resultSet = resultSet || {}; + results.tables = resultSet.rows || []; + this(); + }, + function finish(err) { + if (err) { + return callback(err); + } + + return callback(null, results); + } + ); +} + + +function dbParamsFromReqParams(params) { + var dbParams = {}; + if ( params.dbuser ) { + dbParams.user = params.dbuser; + } + if ( params.dbpassword ) { + dbParams.pass = params.dbpassword; + } + if ( params.dbhost ) { + dbParams.host = params.dbhost; + } + if ( params.dbport ) { + dbParams.port = params.dbport; + } + if ( params.dbname ) { + dbParams.dbname = params.dbname; + } + return dbParams; +} diff --git a/lib/cartodb/controllers/index.js b/lib/cartodb/controllers/index.js index 79f686ab..429a4b87 100644 --- a/lib/cartodb/controllers/index.js +++ b/lib/cartodb/controllers/index.js @@ -1,4 +1,5 @@ module.exports = { + Analyses: require('./analyses'), Layergroup: require('./layergroup'), Map: require('./map'), NamedMaps: require('./named_maps'), diff --git a/lib/cartodb/server.js b/lib/cartodb/server.js index 3fd73021..e51b39c8 100644 --- a/lib/cartodb/server.js +++ b/lib/cartodb/server.js @@ -222,6 +222,8 @@ module.exports = function(serverOptions) { new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app); + new controller.Analyses(authApi, pgConnection).register(app); + new controller.ServerInfo(versions).register(app); /*******************************************************************************************************************