UNPKG

mongo-express-leufu

Version:

Web-based admin interface for MongoDB

718 lines (634 loc) 21.3 kB
"use strict"; var _ = require("lodash"); var bson = require("../bson"); var os = require("os"); var utils = require("../utils"); var csv = require("../csv"); const buildCollectionURL = utils.buildCollectionURL; var routes = function(config, socket) { var exp = {}; function validateCollectionName(name) { if (name === undefined || name.length === 0) { return { error: true, message: "You forgot to enter a collection name!" }; } // Collection names must begin with a letter, underscore, hyphen or slash, (tested v3.2.4) // and can contain only letters, underscores, hyphens, numbers, dots or slashes if (!name.match(/^[a-zA-Z_/-][a-zA-Z0-9._/-]*$/)) { return { error: true, message: "That collection name is invalid." }; } return { error: false }; } /* * Builds the Mongo query corresponding to the * Simple/Advanced parameters input. * Returns null if no query parameters were passed in request. */ exp._buildMongoQuery = function(req) { var result = null; var key = req.query.key; var value = req.query.value; var type = req.query.type && req.query.type.toUpperCase(); var jsonQuery = req.query.query; if (key && value) { // if it is a simple query, // 1. fist convert value to its actual type var converters = { // If type == J, convert value as json document J: function(value) { return JSON.parse(value); }, // If type == N, convert value to number N: function(value) { return Number(value); }, // If type == O, convert value to ObjectID O: function(value) { // Hex input var result = bson.hexToObjectId(value); if (!result) { // Basic validation // Check we have ObjectID() wrapper if ( value.toUpperCase().indexOf("OBJECTID(") === -1 || // missing the opening 'ObjectID(' value.indexOf(")") === -1 // missing the closing '(' ) { throw new Error("ObjectID(...) wrapper must be present"); // req.session.error = 'ObjectID(...) wrapper must be present'; // return res.redirect('back'); } result = bson.toObjectId(value); if (!value) { throw new Error("ObjectID is invalid"); } } return result; }, // If type == R, convert to RegExp R: function(value) { return new RegExp(value, "i"); }, // if type == S, no conversion done S: function(value) { return value; }, }; if (!converters[type]) { throw new Error("Invalid query type: " + type); } value = converters[type](value); // 2. then set query to it result = {}; result[key] = value; } else if (jsonQuery) { // if it is a complex query, take it as is; result = bson.toSafeBSON(jsonQuery); if (result === null) { throw new Error("Query entered is not valid"); } } // otherwise leave as null; return result; }; exp._getSort = function(req) { var sort = req.query.sort || {}; for (var i in sort) { sort[i] = parseInt(sort[i], 10); } return sort; }; exp._getQueryOptions = function(req) { var limit = config.options.documentsPerPage; var skip = parseInt(req.query.skip, 10) || 0; var sort = exp._getSort(req); var query_options = { sort: sort, limit: limit, skip: skip, }; return query_options; }; exp._getProjection = function(req) { var projection = {}; var jsonProjection = req.query.projection || ""; if (jsonProjection) { projection = bson.toSafeBSON(jsonProjection) || {}; } return projection; }; exp._getQuery = function(req, res) { return exp._buildMongoQuery(req, res) || {}; }; //view all entries in a collection exp.viewCollection = function(req, res) { req.query = req.query || {}; // might not be present in Express5 var query; var query_options; var projection; try { query = exp._getQuery(req, res); query_options = exp._getQueryOptions(req); projection = exp._getProjection(req, res); } catch (err) { req.session.error = err.message; return res.redirect("back"); } // determine default key var dbName = req.params.database; var collectionName = req.params.collection; var defaultKey = config.defaultKeyNames && config.defaultKeyNames[dbName] && config.defaultKeyNames[dbName][collectionName] ? config.defaultKeyNames[dbName][collectionName] : "_id"; var edKey = function(doc, defaultKey) { var defaultKeyAsArray = defaultKey.split("."); var val = doc; for (var i = 0; i < defaultKeyAsArray.length; i++) { if (val[defaultKeyAsArray[i]]) { val = val[defaultKeyAsArray[i]]; } } return val; }; var skip = query_options.skip; var limit = query_options.limit; var sort = query_options.sort; req.collection .find(query, projection, query_options) .toArray(function(err, items) { req.collection.stats(function(err, stats) { if (stats === undefined) { req.session.error = "Collection not found!"; return res.redirect("back"); } req.collection.indexes(function(err, indexes) { req.collection.count(query, null, function(err, count) { //Pagination //Have to do this here, swig template doesn't allow any calculations :( var prev; var prev2; var here; var next2; var next; var last; var pagination; prev = { page: Math.round((skip - limit) / limit) + 1, skip: skip - limit, }; prev2 = { page: Math.round((skip - limit * 2) / limit) + 1, skip: skip - limit * 2, }; next2 = { page: Math.round((skip + limit * 2) / limit) + 1, skip: skip + limit * 2, }; next = { page: Math.round((skip + limit) / limit) + 1, skip: skip + limit, }; here = Math.round(skip / limit) + 1; last = (Math.ceil(count / limit) - 1) * limit; pagination = count > limit; var docs = []; var columns = []; for (var i in items) { // Prep items with stubs so as not to send large info down the wire for (let prop in items[i]) { if ( utils.roughSizeOfObject(items[i][prop]) > config.options.maxPropSize ) { items[i][prop] = { attribu: prop, display: "*** LARGE PROPERTY ***", humanSz: utils.bytesToSize( utils.roughSizeOfObject(items[i][prop]) ), maxSize: utils.bytesToSize(config.options.maxPropSize), preview: JSON.stringify(items[i][prop]).substr(0, 25), roughSz: utils.roughSizeOfObject(items[i][prop]), _id: items[i]._id, }; } } // If after prepping the row is still too big if ( utils.roughSizeOfObject(items[i]) > config.options.maxRowSize ) { for (let prop in items[i]) { if ( prop !== "_id" && utils.roughSizeOfObject(items[i][prop]) > 200 ) { items[i][prop] = { attribu: prop, display: "*** LARGE ROW ***", humanSz: utils.bytesToSize( utils.roughSizeOfObject(items[i][prop]) ), maxSize: utils.bytesToSize(config.options.maxRowSize), preview: JSON.stringify(items[i][prop]).substr(0, 25), roughSz: utils.roughSizeOfObject(items[i][prop]), _id: items[i]._id, }; } } } docs[i] = items[i]; columns.push(Object.keys(items[i])); items[i] = bson.toString(items[i]); } var indexSizes = stats.indexSizes; for (var n = 0, nn = indexes.length; n < nn; n++) { indexes[n].size = indexSizes[indexes[n].name]; } // Generate an array of columns used by all documents visible on this page columns = _.uniq(_.flatten(columns)); var ctx = { title: "Viewing Collection: " + req.collectionName, documents: items, // Docs converted to strings docs: docs, // Original docs columns: columns, // All used columns count: count, // total number of docs returned by the query stats: stats, editorTheme: config.options.editorTheme, limit: limit, skip: skip, sort: sort, prev: prev, prev2: prev2, next2: next2, next: next, here: here, last: last, pagination: pagination, key: req.query.key, value: req.query.value, // value: type === 'O' ? ['ObjectID("', value, '")'].join('') : value, type: req.query.type, query: req.query.query, projection: req.query.projection, defaultKey: defaultKey, edKey: edKey, indexes: indexes, }; res.render("collection", ctx); }); }); }); }); }; exp.compactCollection = function(req, res) { req.db.command({ compact: req.collectionName }, function(err) { if (err) { req.session.error = "Error: " + err; return res.redirect("back"); } req.session.success = "Collection compacted!"; return res.redirect("back"); }); }; exp.exportCollection = function(req, res) { res.setHeader( "Content-Disposition", 'attachment; filename="' + encodeURI(req.collectionName) + "\"; filename*=UTF-8''" + encodeURI(req.collectionName) ); res.setHeader("Content-Type", "application/json"); var query; var query_options; var projection; try { query_options = { sort: exp._getSort(req), }; query = exp._getQuery(req, res); projection = exp._getProjection(req, res); } catch (err) { req.session.error = err.message; return res.redirect("back"); } req.collection .find(query, projection, query_options) .stream({ transform: function(item) { return bson.toJsonString(item) + os.EOL; }, }) .pipe(res); }; exp.exportColArray = function(req, res) { var query; var query_options; var projection; try { query_options = { sort: exp._getSort(req), }; query = exp._getQuery(req, res); projection = exp._getProjection(req, res); } catch (err) { req.session.error = err.message; return res.redirect("back"); } req.collection .find(query, projection, query_options) .toArray(function(err, items) { res.setHeader( "Content-Disposition", 'attachment; filename="' + encodeURI(req.collectionName) + "\"; filename*=UTF-8''" + encodeURI(req.collectionName) ); res.setHeader("Content-Type", "application/json"); res.write(bson.toJsonString(items)); res.end(); }); }; exp.exportCsv = function(req, res) { var query; var query_options; var projection; try { query_options = { sort: exp._getSort(req), }; query = exp._getQuery(req, res); projection = exp._getProjection(req, res); } catch (err) { req.session.error = err.message; return res.redirect("back"); } req.collection .find(query, projection, query_options) .toArray(function(err, items) { res.setHeader( "Content-Disposition", 'attachment; filename="' + encodeURI(req.collectionName) + "\"; filename*=UTF-8''" + encodeURI(req.collectionName) ); res.setHeader("Content-Type", "text/csv"); res.write(csv(items)); res.end(); }); }; exp.reIndex = function(req, res) { req.collection.reIndex(function(err) { if (err) { req.session.error = "Error: " + err; return res.redirect("back"); } req.session.success = "Index regenerated!"; return res.redirect("back"); }); }; exp.addIndex = function(req, res) { var doc = req.body.index; if (doc === undefined || doc.length === 0) { req.session.error = "You forgot to enter a index!"; return res.redirect("back"); } var docBSON; try { docBSON = bson.toBSON(doc); } catch (err) { req.session.error = "JSON is not valid!"; console.error(err); return res.redirect("back"); } req.collection.createIndex(docBSON, function(err) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } req.session.success = "Index created!"; res.redirect( buildCollectionURL(res.locals.baseHref, req.dbName, req.collectionName) ); }); }; exp.addCollection = function(req, res) { var name = req.body.collection; const validation = validateCollectionName(name); if (validation.error) { req.session.error = validation.message; return res.redirect("back"); } req.db.createCollection(name, function(err) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } req.updateCollections(req.db, req.dbName, function() { socket.emit("new_collection", { collection_name: name, db_name: req.dbName, }); req.session.success = "Collection created!"; res.redirect(buildCollectionURL(res.locals.baseHref, req.dbName, name)); }); }); }; exp.deleteCollection = function(req, res) { if (config.options.readOnly === true) { req.session.error = "Error: config.options.readOnly is set to true"; return res.redirect("back"); } var query = exp._buildMongoQuery(req); if (query) { // we're just deleting some of the documents var deleteOptions = null; req.collection.deleteMany(query, deleteOptions, function(err, opRes) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } req.session.success = opRes.result.n + ' documents deleted from "' + req.collectionName + '"'; socket.emit("delete_many", { n: opRes.result.n, collection_name: req.collectionName, db_name: req.dbName, }); res.redirect( res.locals.baseHref + "db/" + req.dbName + "/" + req.collectionName ); }); } else { // no query means we're dropping the whole collection req.collection.drop(function(err) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } req.updateCollections(req.db, req.dbName, function(err) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } req.session.success = 'Collection "' + req.collectionName + '" deleted!'; socket.emit("dropped_collection", { collection_name: req.collectionName, db_name: req.dbName, }); res.redirect(res.locals.baseHref + "db/" + req.dbName); }); }); } }; exp.renameCollection = function(req, res) { var name = req.body.collection; const validation = validateCollectionName(name); if (validation.error) { req.session.error = validation.message; return res.redirect("back"); } req.collection.rename(name, function(err) { if (err) { req.session.error = "Something went wrong: " + err; console.error(err); return res.redirect("back"); } const collection_old_name = req.collectionName; req.updateCollections(req.db, req.dbName, function(err) { if (err) { req.session.error = "Something went wrong: " + err; return res.redirect("back"); } req.session.success = "Collection renamed!"; socket.emit("rename_collection", { collection_name: name, collection_old_name, db_name: req.dbName, }); res.redirect(buildCollectionURL(res.locals.baseHref, req.dbName, name)); }); }); }; exp.updateCollections = function(req, res) { req.updateCollections(req.db, req.dbName, function(err) { if (err) { req.session.error = "Something went wrong: " + err; return res.redirect("back"); } req.session.success = "Collections Updated!"; socket.emit("collections_updated", { db_name: req.dbName }); res.redirect(res.locals.baseHref + "db/" + req.dbName); }); }; exp.dropIndex = function(req, res) { if (!req.query.name) { req.session.error = "Error: missing name parameter"; return res.redirect("back"); } if (config.options.readOnly === true) { req.session.error = "Error: config.options.readOnly is set to true"; return res.redirect("back"); } req.collection.dropIndex(req.query.name, function(err) { if (err) { req.session.error = "Error: " + err; return res.redirect("back"); } req.session.success = "Index deleted!"; return res.redirect("back"); }); }; exp.importCollection = function(req, res) { if (!req.files) { return res.status(400).send("Missing file"); } const keys = Object.keys(req.files); if ( keys.some(key => !req.files[key].data || !req.files[key].data.toString) ) { return res.status(400).send("Bad file"); } let docs = []; for (const key of keys) { const fileContent = req.files[key].data.toString("utf8"); let str; if (fileContent.trim()[0] === "[") { console.log("ARREGLO"); str = fileContent; } else { console.log("MONGOEXPORT"); str = `[${fileContent .replace(/}\s*\n\s*\{/gm, "},{") .replace(/\{"\$oid":"(.+?)"}/gm, '"$1"')}]`; } try { const arr = JSON.parse(str); docs = docs.concat(arr); } catch (err) { console.error(err); res.status(400).send("Bad file content"); return; } } docs = _.compact( _.map(docs, ({ _id, ...value }) => { if (_id) { const oid = _.get(_id, "$oid"); if (oid) { return { _id: oid, ...value }; } else if (_.isString(_id)) { return { _id, ...value }; } return; } return value; }) ); req.collection.insertMany(docs, { ordered: false }, (err, stats) => { if (err) { console.error("insert error: ", err.message); if (!stats) { res.status(400).send("Server error"); } } if (stats) { const n = _.get(stats, "result.n", _.get(stats, "nInserted", 0)); socket.emit("documents_inserted", { n, db_name: req.dbName, collection_name: req.collectionName, }); if (!err) { console.log("todo bien!"); req.session.success = `${n} document(s) inserted`; res.status(200).send(`${n} document(s) inserted`); } else { console.log( `${n} document(s) inserted but with errors!: ${err.message}` ); req.session.success = `${n} document(s) inserted but with errors!: ${ err.message }`; res .status(200) .send(`${n} document(s) inserted but with errors!: ${err.message}`); } } }); }; return exp; }; module.exports = routes;