braziw-plugin-dbedit
Version:
Braziw js Mongo DB object editor / scaffolding.
617 lines (479 loc) • 19 kB
JavaScript
;
const moment = web.require('moment-timezone');
const NON_EXISTENT_OBJ_ID = '5e848994cd0f31435cfdcea7';
module.exports = function({
modelName,
sort,
cols,
labels,
listView,
showAddButton = true,
pageTitle,
displayName,
saveParams,
rowsPerPage,
caseInsensitiveSorting,
includeVirtuals,
populate,
query,
queryFilter,
enableDangerousClientFiltering = false,
handlers,
shouldShowDeleteAction = true,
shouldShowEditAction = true,
editActionRedirect,
editActionOnClick,
parentTemplate,
prefixActionCol = true,
addUrl,
editUrl,
saveUrl,
colMap = {},
beforeQuery,
beforeRender,
beforeDelete,
beforeRenderAction,
handlePostRequest,
searchable = [],
searchableNumFieldsPerRow = 2,
searchableStyle = "max-width: 600px;",
sortable = [],
showExport = false,
exportHandlers = {},
exportCols,
} = {}) {
return {
get: async function(req, res) {
//customizable through get parameters:
const querySort = (req.query.sort && JSON.parse(req.query.sort)) || sort;
const queryCols = cols || (req.query.cols && JSON.parse(req.query.cols)); //db columns to add
const queryLabels = labels || (req.query.labels && JSON.parse(req.query.labels)); //if cols is specified, and this is not, just convert to title case
const queryListView = listView || (req.query.listView);
const queryPageTitle = pageTitle || (req.query.pageTitle);
const queryDisplayName = displayName || (req.query.displayName);
let querySaveParams = (saveParams && web.objectUtils.isFunction(saveParams) ? await saveParams(req) : saveParams) || ((enableDangerousClientFiltering && req.query.saveParams) || "");
if (querySaveParams && querySaveParams.substr(0, 1) !== "&") {
querySaveParams = "&" + querySaveParams;
}
let myShowAddButton;
if (showAddButton !== undefined) {
if (showAddButton.permission) {
myShowAddButton = req.user.hasPermission(showAddButton.permission);
} else {
myShowAddButton = showAddButton;
}
} else {
myShowAddButton = (enableDangerousClientFiltering && req.query.showAddButton === 'Y');
}
let myShouldShowEditAction;
if (shouldShowEditAction.permission) {
myShouldShowEditAction = req.user.hasPermission(shouldShowEditAction.permission);
} else {
myShouldShowEditAction = shouldShowEditAction;
}
parentTemplate = parentTemplate || web.cms.conf.adminTemplate;
// this replaces the db query
const queryQuery = query || (enableDangerousClientFiltering && req.query.query && JSON.parse(req.query.query));
// this extends the db query
const myQueryFilter = (enableDangerousClientFiltering && req.query.filter && JSON.parse(req.query.filter)) || await getQueryFilter(queryFilter, req);
let queryShouldShowDeleteAction;
if (shouldShowDeleteAction !== undefined) {
if (shouldShowDeleteAction.permission) {
queryShouldShowDeleteAction = req.user.hasPermission(shouldShowDeleteAction.permission);
} else {
queryShouldShowDeleteAction = shouldShowDeleteAction
}
} else {
queryShouldShowDeleteAction = (enableDangerousClientFiltering && req.query.shouldShowDeleteAction === 'Y');
}
const queryEditActionRedirect = editActionRedirect || (enableDangerousClientFiltering && req.query.editActionRedirect);
const queryEditActionOnClick = editActionOnClick || (enableDangerousClientFiltering && req.query.editActionOnClick);
// need to copy as not to retain the objects from memory
const mySearchable = searchable.map(a => ({...a}));
let queryPrefixActionCol = enableDangerousClientFiltering && req.query.prefixActionCol === 'Y';
prefixActionCol = prefixActionCol || queryPrefixActionCol;
const queryModel = (enableDangerousClientFiltering && req.query.model);
const modelStr = modelName || queryModel;
//should be in cache by this time (assumption)
const model = web.cms.dbedit.utils.searchModel(modelStr);
const modelAttr = model.getModelDictionary();
const modelSchema = modelAttr.schema;
const modelAttrName = modelAttr.name;
const modelDisplayName = queryDisplayName || modelAttr.displayName || modelAttr.name;
const modelConf = web.cms.dbedit.utils.getModelConf(modelAttrName) || {};
const sortDefault = {};
sortDefault[web.cms.dbedit.conf.updateDtCol] = -1;
const tableId = getPrefix(model);
let _rowsPerPage = (req.query[tableId + '_rowsPerPage'] && parseInt(req.query[tableId + '_rowsPerPage']))
|| rowsPerPage || web.conf.defaultRowsPerPage;
modelConf.sort = querySort || modelConf.sort || sortDefault;
let myCols = [];
let myPrefixActionCol;
if (prefixActionCol.permissionsAny) {
myPrefixActionCol = req.user.hasAnyPermission(...prefixActionCol.permissionsAny);
} else {
myPrefixActionCol = prefixActionCol;
}
if (myPrefixActionCol) {
myCols.push({id: '_action', style: 'text-align: center;', excludeExport: true});
}
let colsToAdd = queryCols || modelConf.cols;
if (colsToAdd) {
myCols.push(...colsToAdd);
} else {
colsToAdd = [];
const maxColsDisplay = 4;
let counter = 0;
for (let i in modelSchema) {
myCols.push(i);
colsToAdd.push(i);
counter++;
if (counter > maxColsDisplay) {
break;
}
}
}
let myLabels = [];
if (myPrefixActionCol) {
myLabels.push('Action');
}
let labelsToAdd = queryLabels || modelConf.labels;
if (labelsToAdd) {
myLabels.push(...labelsToAdd);
} else {
for (let colToAdd of colsToAdd || []) {
let colName = web.objectUtils.isString(colToAdd) ? colToAdd : colToAdd.id;
myLabels.push(web.cms.dbedit.utils.camelToTitle(colName));
}
}
const redirectAfter = encodeURIComponent(req.url);
const myHandlers = Object.assign({}, handlers || modelConf.handlers || {});
if (!saveUrl) {
saveUrl = 'save';
}
addUrl = addUrl || showAddButton.url || saveUrl;
if (!editUrl) {
editUrl = saveUrl;
}
let colIds = [];
let myColCopy = [...myCols]; // prevent change of orig array during visibility checking
for (let col of myColCopy || []) {
if (web.objectUtils.isString(col)) {
colIds.push(col);
colMap[col] = Object.assign({}, colMap[col]);
} else {
let isVisible = true;
if (col.visible !== undefined) {
if (web.objectUtils.isFunction(col.visible)) {
isVisible = await col.visible(myCols, req);
} else {
isVisible = col.visible;
}
}
if (isVisible) {
colIds.push(col.id);
colMap[col.id] = Object.assign({}, col, colMap[col.id]);
} else {
myCols.splice(myCols.indexOf(col), 1);
}
}
}
if (colMap['_action'] && !myHandlers['_action']) {
const _csrf = req.csrfToken();
myHandlers['_action'] = async function(record, column, opts) {
let actionArr = [];
let queryModelParam = '';
let queryModelHidden = '';
let editActionOnClickStr = '';
if (queryModel) {
queryModelParam = '&model=' + encodeURIComponent(queryModel);
queryModelHidden = `<input type="hidden" name="model" value="${queryModelParam}">`
}
if (queryEditActionOnClick) {
editActionOnClickStr = `onclick="${editActionOnClick}"`
}
let saveUrl = queryEditActionRedirect || 'save';
if (myShouldShowEditAction) {
actionArr.push(
`<a title="Edit" ${editActionOnClickStr} class="btn btn-link btn-flatten-m" href="${editUrl}?_backUrl=${encodeURIComponent(req.url)}&_id=${record._id.toString()}${queryModelParam}${querySaveParams}">
<i class="fa fa-pencil"></i>
</a>`
)
}
if (queryShouldShowDeleteAction) {
actionArr.push(
`<button title="Delete" name="ACTION_DELETE" type="submit" class="btn btn-link text-danger btn-flatten-m" onclick="return confirm(\'Are you sure you want to remove this ${modelDisplayName.toLowerCase()}?\')">
<i class="fa fa-remove"></i>
</button>`
);
}
if (beforeRenderAction) {
await beforeRenderAction(actionArr, record, req);
}
return (`<div style="text-align: center; white-space: nowrap;"><form method="POST" style="display: inline;">
<input type="hidden" name="_id" value="${web.stringUtils.escapeHTML(record._id.toString())}">
<input type="hidden" name="_csrf" value="${web.stringUtils.escapeHTML(_csrf)}">
<input type="hidden" name="_backUrl" value="${web.stringUtils.escapeHTML(req.url)}">
${queryModelHidden}
${actionArr.join('')}
</form></div>`)
}
}
for (let colName of colIds) {
let dbCol = modelAttr.schema[colName];
if (web.conf.timezone && dbCol && dbCol.type === Date) {
if (!myHandlers[colName]) {
myHandlers[colName] = async function(record, column, opts) {
let rawVal = record[colName];
if (rawVal) {
let dateFormat = (colMap[colName] && colMap[colName].dateFormat);
if (!dateFormat) {
dateFormat = 'MM/DD hh:mmA';
}
if (rawVal.getFullYear() !== (new Date()).getFullYear()) {
dateFormat = 'MM/DD/YYYY hh:mmA';
}
return moment.tz(rawVal, web.conf.timezone).format(dateFormat);
}
return "";
};
}
} else if (colMap[colName] && colMap[colName].lookup) {
if (!myHandlers[colName]) {
myHandlers[colName] = async function(record, column, opts) {
let rawVal = record[colName];
if (rawVal) {
return colMap[colName].lookup[rawVal] || rawVal;
}
return "";
};
}
}
}
let myQuery = Object.assign({}, queryQuery || modelConf.query); //else query everything
if (myQueryFilter) {
myQuery = Object.assign(myQuery, myQueryFilter);
}
let fieldPerRowCounter = 0;
let visibleLength = 0;
let hasSearchVal = false;
let searchableQuery = await getSearchableQuery(req, mySearchable, myQuery, {
forEach: async function(searchObj) {
if (web.objectUtils.isFunction(searchObj.hidden)) {
searchObj.hiddenComputed = await searchObj.hidden(searchObj, req);
} else {
searchObj.hiddenComputed = searchObj.hidden;
}
if (!searchObj.hiddenComputed) {
visibleLength++;
}
if (searchObj.val) {
hasSearchVal = true;
}
if (searchObj.inputValues) {
if (web.objectUtils.isFunction(searchObj.inputValues)) {
searchObj.inputValuesComputed = await searchObj.inputValues(searchObj, req);
} else {
searchObj.inputValuesComputed = searchObj.inputValues;
}
}
fieldPerRowCounter++;
if (fieldPerRowCounter !== 1 && fieldPerRowCounter <= searchableNumFieldsPerRow) {
searchObj.attachToPrevious = true;
}
if (fieldPerRowCounter > searchableNumFieldsPerRow) {
fieldPerRowCounter = 1;
}
}
});
mySearchable.visibleLength = visibleLength;
mySearchable.hasSearchVal = hasSearchVal;
myQuery = Object.assign(myQuery, searchableQuery);
if (beforeQuery) {
await beforeQuery(myQuery, req, res, model);
}
const table = await web.renderTable(req, model, {
tableId: tableId,
query: myQuery,
columns: myCols,
labels: myLabels,
includeVirtuals: includeVirtuals,
populate: populate,
sort: modelConf.sort,
sortable: sortable,
clientSortFunc: 'goSortColumn',
rowsPerPage: _rowsPerPage,
caseInsensitiveSorting: caseInsensitiveSorting,
handlers: myHandlers,
req: req,
});
const myListView = queryListView || modelConf.view || web.cms.dbedit.conf.listView;
const myPageTitle = queryPageTitle || modelConf.pageTitle || (modelDisplayName + ' List');
let count;
if (!myQuery || isObjectEmpty(myQuery)) {
count = await model.estimatedDocumentCount();
} else {
count = await model.countDocuments(myQuery);
}
let rowsPerPageSelectVals = [10, 100, 500, 1000];
if (_rowsPerPage && rowsPerPageSelectVals.indexOf(_rowsPerPage) === -1) {
rowsPerPageSelectVals.push(_rowsPerPage);
rowsPerPageSelectVals.sort(function(a, b){
return a - b;
});
}
let options = {
table: table,
tableId: tableId,
rowsPerPageSelectVals: rowsPerPageSelectVals,
rowsPerPage: _rowsPerPage,
redirectAfter: redirectAfter,
saveParams: querySaveParams,
pageTitle: myPageTitle,
modelName: modelAttrName,
queryModelName: queryModel,
showAddButton: myShowAddButton,
searchable: mySearchable,
searchableStyle: searchableStyle,
modelDisplayName: modelDisplayName,
filter: (myQueryFilter && JSON.stringify(myQueryFilter)),
sort: (querySort && JSON.stringify(querySort)),
count: count,
parentTemplate: parentTemplate,
addUrl: addUrl,
origListView: web.cms.dbedit.conf.listView,
showExport: showExport,
}
if (beforeRender) {
await beforeRender(req, res, options);
}
res.render(myListView, options);
},
post: async function(req, res) {
const modelStr = modelName || (enableDangerousClientFiltering && req.body.model);
const recId = req.body._id;
const model = web.cms.dbedit.utils.searchModel(modelStr);
if (req.body.hasOwnProperty("ACTION_DELETE")) {
if (!enableDangerousClientFiltering && !shouldShowDeleteAction) {
throw new Error("Invalid access");
}
const modelAttr = model.getModelDictionary();
const modelSchema = modelAttr.schema;
const redirectAfter = req.body._backUrl;
if (!recId) {
throw new Error("Invalid request [3]");
}
let rec = await model.findOne({_id:recId}).exec();
if (!rec) {
throw new Error("Record not found.");
}
try {
if (beforeDelete) {
await beforeDelete(rec, req, res);
}
if (web.ext && web.ext.dbUtils) {
await web.ext.dbUtils.remove(rec, req);
} else {
await rec.remove();
}
} catch (ex) {
console.warn("Error deleting", ex.message);
if (ex.errors) {
for (const [key, value] of Object.entries(ex.errors)) {
req.flash('error', web.objectUtils.resolvePath(value, 'properties.message') || ex.message);
}
} else {
let errStr = ex.message || "Error deleting record.";
req.flash('error', errStr);
}
res.redirect(req.url);
return;
}
req.flash('info', "Record has been deleted.");
res.redirect(redirectAfter);
} if (req.body.hasOwnProperty("_ACTION_EXPORT_CSV")) {
const mySearchable = searchable.map(a => ({...a}));
let myQuery = Object.assign({}, query, await getQueryFilter(queryFilter, req));
let searchableQuery = await getSearchableQuery(req, mySearchable, myQuery);
Object.assign(myQuery, searchableQuery);
let myHandlers = Object.assign({}, handlers, exportHandlers);
if (beforeQuery) {
await beforeQuery(myQuery, req, res, model);
}
await web.cms.dbedit.csvUtils.downloadCsv(req, res, model, {
cols: exportCols || cols,
query: myQuery,
populate: populate,
sort: sort,
handlers: myHandlers,
includeVirtuals: includeVirtuals,
});
} else if (handlePostRequest) {
await handlePostRequest(req, res);
}
}
}
}
function getPrefix(ModelObj) {
return ModelObj.modelName.toLowerCase();
}
function isObjectEmpty(obj) {
if (Object.getOwnPropertyNames) {
return (Object.getOwnPropertyNames(obj).length === 0);
} else {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
}
}
async function getQueryFilter(queryFilter, req) {
return (web.objectUtils.isFunction(queryFilter) ? await queryFilter(req) : queryFilter)
}
async function getSearchableQuery(req, mySearchable, myQuery, {
forEach
} = {}) {
let searchableQuery = {};
for (let searchObj of mySearchable) {
let sParamVal = req.query['f_' + searchObj.id] || myQuery[searchObj.id] || searchObj.default;
if (sParamVal) {
searchObj.val = sParamVal;
if (searchObj.queryHandler) {
let addtlSearchQuery = await searchObj.queryHandler(req, searchObj.val, searchObj, searchableQuery);
if (addtlSearchQuery === null) {
addtlSearchQuery = {};
// force none existent ID
addtlSearchQuery[searchObj.id] = NON_EXISTENT_OBJ_ID;
}
Object.assign(searchableQuery, addtlSearchQuery);
} else {
let assignedSearchVal = sParamVal;
if (assignedSearchVal == '$NULL') {
assignedSearchVal = null;
}
let isString = web.objectUtils.isString(assignedSearchVal);
if (isString) {
assignedSearchVal = assignedSearchVal.trim()
}
if (isString
&& (searchObj.wildcardSearch || searchObj.wildcardSearch === undefined)) {
// exact search if enclosed in quotes
let isExactSearch = assignedSearchVal[0] === '"' && assignedSearchVal[assignedSearchVal.length - 1] === '"';
if (isExactSearch) {
assignedSearchVal = assignedSearchVal.substr(1, assignedSearchVal.length - 2);
} else {
assignedSearchVal = web.cms.dbedit.searchUtils.wrapSearch(assignedSearchVal);
}
}
searchableQuery[searchObj.id] = assignedSearchVal;
}
}
if (forEach) {
await forEach(searchObj);
}
}
return searchableQuery;
}