mongo-express-leufu
Version:
Web-based admin interface for MongoDB
718 lines (634 loc) • 21.3 kB
JavaScript
;
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;