@hicoder/express-core
Version:
Restful API exposure middleware for Express and Mongoose based framework. Provide Rest API automatically based on Mongoose schema. It can also work with angular-core to develop end to end MEAN stack web applications.
1,616 lines (1,491 loc) • 48 kB
JavaScript
const createError = require('http-errors');
const mongoose = require('mongoose');
const MddsUncategorized = 'MddsUncategorized';
const MddsAll = 'MddsAll';
const { exportAllExternal } = require('./rest_ctrl_export');
const {
emailAllErrorExternal,
emailAllCheckExternal,
emailAllExternal,
} = require('./rest_ctrl_email');
const createRegex = function (obj) {
const fieldRegex = function (field) {
return new RegExp(
// Escape all special characters except *
// Allow the use of * as a wildcard like % in SQL.
field.replace(/([.+?^=!:${}()|\[\]\/\\])/g, '\\$1').replace(/\*/g, '.*'),
'i'
);
};
if (typeof obj === 'string') {
return fieldRegex(obj);
}
//obj in {key: string} format
for (let prop in obj) {
obj[prop] = fieldRegex(obj[prop]);
}
return obj;
};
var capitalizeFirst = function (str) {
return str.charAt(0).toUpperCase() + str.substr(1);
};
var lowerFirst = function (str) {
return str.charAt(0).toLowerCase() + str.substr(1);
};
const checkAndSetValue = function (obj, schema) {
//obj in {item: value} format
for (let item in obj) {
if (item in schema.paths) {
let type = schema.paths[item].constructor.name;
if (type == 'SchemaDate') {
if (typeof obj[item] == 'string') {
//exact data provided:
let dt = new Date(obj[item]);
let y = dt.getFullYear(),
m = dt.getMonth(),
d = dt.getDate();
let d1 = new Date(y, m, d);
let d2 = new Date(y, m, d);
d2.setDate(d2.getDate() + 1);
obj[item] = { $gte: d1, $lt: d2 };
} else if (typeof obj[item] === 'object') {
//data range
let o = {};
if (obj[item]['from']) {
let dt = new Date(obj[item]['from']);
//let y = dt.getFullYear(), m = dt.getMonth(), d = dt.getDate();
//let d1 = new Date(y, m, d);
o['$gte'] = dt;
}
if (obj[item]['to']) {
let dt = new Date(obj[item]['to']);
//let y = dt.getFullYear(), m = dt.getMonth(), d = dt.getDate();
//let d2 = new Date(y, m, d);
//d2.setDate(d2.getDate() + 1);
o['$lt'] = dt;
}
obj[item] = o;
}
} else if (type === 'SchemaString') {
let userInput = obj[item];
if (userInput) {
userInput = new RegExp(
// Escape all special characters
userInput.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'),
'i'
);
}
obj[item] = userInput;
} else if (type == 'SchemaNumber') {
if (typeof obj[item] === 'object') {
//data range
let o = {};
if (typeof obj[item]['from'] === 'number') {
let dt = obj[item]['from'];
//let y = dt.getFullYear(), m = dt.getMonth(), d = dt.getDate();
//let d1 = new Date(y, m, d);
o['$gte'] = dt;
}
if (typeof obj[item]['to'] === 'number') {
let dt = obj[item]['to'];
//let y = dt.getFullYear(), m = dt.getMonth(), d = dt.getDate();
//let d2 = new Date(y, m, d);
//d2.setDate(d2.getDate() + 1);
o['$lte'] = dt;
}
obj[item] = o;
}
}
}
}
return obj;
};
const fieldReducerForRef = function (refObj, indexFields) {
let newRefObj = {};
if ('_id' in refObj) {
newRefObj['_id'] = refObj['_id'];
}
for (let indexField of indexFields) {
if (indexField in refObj) {
newRefObj[indexField] = refObj[indexField];
}
}
return newRefObj;
};
const objectReducerForRef = function (o, populateMap) {
if (typeof o !== 'object' || o == null) {
return o;
}
let obj = {};
for (let p in o) {
obj[p] = o[p];
}
for (let path in populateMap) {
let fields = populateMap[path].match(/\S+/g); // \S matches no space characters.
if (!fields) continue;
let indexFields = fields; //use all the fields
let newRefObj;
let refObj = obj[path];
if (typeof refObj !== 'object' || refObj == null) continue;
if (Array.isArray(refObj)) {
//list of ref objects
newRefObj = refObj.map((o) => fieldReducerForRef(o, indexFields)); //recursive call
} else {
newRefObj = fieldReducerForRef(refObj, indexFields);
}
//now only "_id" and the indexField will be left.
obj[path] = newRefObj;
}
return obj;
};
const getViewPopulates = function (schema, viewStr) {
let populates = [];
let viewFields = viewStr.match(/\S+/g) || [];
viewFields.forEach((item) => {
if (item in schema.paths) {
let type = schema.paths[item].constructor.name;
switch (type) {
case 'SchemaArray':
if (schema.paths[item].caster.options.ref) {
let ref = schema.paths[item].caster.options.ref;
if (ref) populates.push([item, ref]);
}
break;
case 'ObjectId':
let ref = schema.paths[item].options.ref;
if (ref) populates.push([item, ref]);
break;
default:
break;
}
}
});
return populates;
};
const resultReducerForRef = function (result, populateMap) {
if (Object.keys(populateMap).length == 0) {
//not ref fields
return result;
}
if (typeof result !== 'object' || result == null) {
//array is also object
return result;
}
let r;
if (Array.isArray(result)) {
r = result.map((obj) => objectReducerForRef(obj, populateMap));
} else {
r = objectReducerForRef(result, populateMap);
}
return r;
};
const objectReducerForView = function (obj, viewStr) {
//console.log("***obj: ", obj);
//console.log("***viewStr: ", viewStr);
if (typeof obj !== 'object' || obj == null) {
return obj;
}
let fields = viewStr.match(/\S+/g); // \S matches no space characters.
if (!fields) return obj;
let newObj = {};
newObj['_id'] = obj['_id'];
if ('archived' in obj) {
newObj['archived'] = obj['archived']; // keep archived
}
for (let path of fields) {
if (path in obj) newObj[path] = obj[path];
}
//console.log("***newObj: ", newObj);
return newObj;
};
const resultReducerForView = function (result, viewStr) {
if (!viewStr) {
//not specified. Return everything
return result;
}
if (typeof result !== 'object' || result == null) {
//array is also object
return result;
}
if (Array.isArray(result)) {
result = result.map((obj) => objectReducerForView(obj, viewStr));
} else {
result = objectReducerForView(result, viewStr);
}
return result;
};
const ownerPatch = function (query, owner, req) {
if (owner && owner.enable) {
if (owner.type === 'module') {
query.mmodule_name = req.mddsModuleName;
} else if (owner.type === 'user') {
if (req.muser) {
// user logged in
if (!!owner.field) {
query[owner.field] = req.muser._id;
} else {
query.muser_id = req.muser._id;
}
}
}
}
return query;
};
const searchObjPatch = function (query, mraBE) {
const searchObj = mraBE.searchObj || {};
for (const p in searchObj) {
query[p] = searchObj[p];
}
return query;
};
const processMraBE = function (mraBE, owner) {
if (typeof mraBE !== 'object') {
return {
listOnlyAllowSearch: false,
};
}
if (Array.isArray(mraBE.listOnlyAllowSearchOn)) {
mraBE.listOnlyAllowSearch = true;
mraBE.listOnlyAllowSearchOn.push('_id');
if (owner && owner.enable) {
if (owner.type === 'module') {
mraBE.listOnlyAllowSearchOn.push('mmodule_name');
} else if (owner.type === 'user') {
if (!!owner.field) {
mraBE.listOnlyAllowSearchOn.push(owner.field);
} else {
mraBE.listOnlyAllowSearchOn.push('muser_id');
}
}
}
mraBE.listOnlyAllowSearchOn = mraBE.listOnlyAllowSearchOn.filter(
(x, idx) => mraBE.listOnlyAllowSearchOn.indexOf(x) === idx
)
} else {
mraBE.listOnlyAllowSearch = false;
}
return mraBE;
};
const searchAllowed = function (query, mraBE) {
//TODO: consider $and, $or type of search
if (!mraBE.listOnlyAllowSearch) return true;
for (let p in query) {
if (mraBE.listOnlyAllowSearchOn.includes(p)) {
// at least one search fields available
return true;
}
}
return false;
};
const processPages = function (
__per_page,
__page,
PER_PAGE,
MAX_PER_PAGE,
count
) {
if (isNaN(__per_page) || __per_page <= 0) {
__per_page = PER_PAGE;
} else if (__per_page > MAX_PER_PAGE) {
__per_page = MAX_PER_PAGE;
}
let maxPageNum = Math.ceil(count / (__per_page * 1.0));
if (isNaN(__page)) __page = 1;
if (__page > maxPageNum) __page = maxPageNum;
if (__page <= 0) __page = 1;
let skipCount = (__page - 1) * __per_page;
return [__per_page, __page, maxPageNum, skipCount];
};
const fieldValueSearchAllowed = function (field, mraBE) {
const fields = mraBE.valueSearchFields || [];
return fields.includes(field);
};
class RestController {
constructor(options) {
this.schema_collection = {};
this.views_collection = {}; // views in [briefView, detailView, CreateView, EditView, SearchView, IndexView, associationView] format
this.model_collection = {};
this.populate_collection = {};
this.owner_config = {}; // {enable: true, type: 'user | module'
this.mraBE_collection = {};
this.tags_collection = {};
this.mddsProperties = options || {};
}
loadContextVarsByName(name) {
const schema = this.schema_collection[name];
const model = this.model_collection[name];
const views = this.views_collection[name];
const populates = this.populate_collection[name];
const owner = this.owner_config[name];
const mraBE = this.mraBE_collection[name];
if (!schema || !model || !views || !populates || !owner || !mraBE) {
throw createError(500, 'Cannot load context from name ' + name);
}
return { name, schema, model, views, populates, owner, mraBE };
}
loadContextVars(req) {
//let url = req.originalUrl
//let arr = url.split('/');
//if (arr.length < 2) throw(createError(500, "Cannot identify context name from routing path: " + url))
//let name = arr[arr.length-2].toLowerCase();
let name = req.meanRestSchemaName.toLowerCase();
return this.loadContextVarsByName(name);
}
getPopulatesRefFields(ref) {
let views = this.views_collection[ref.toLowerCase()]; //view registered with lowerCase
if (!views) return null;
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView, AssociationView] format
if (views[6]) {
return [views[5], views[6]]
}
return [views[5], views[0]]; //indexView and birefView. Brief view is for association population
}
register(
schemaName,
schema,
views,
model,
moduleName,
ownerConfig,
mraBE,
tags
) {
let name = schemaName.toLowerCase();
this.schema_collection[name] = schema;
this.views_collection[name] = views;
this.model_collection[name] = model;
this.owner_config[name] = ownerConfig;
this.mraBE_collection[name] = processMraBE(mraBE, ownerConfig);
this.tags_collection[name] = tags; // schema tags for special logic handling
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
this.populate_collection[name] = {
//populates in a array, each populate is an array, too, with [field, ref]
//eg: [["person", "Person"], ["comments", "Comments"]]
briefView: getViewPopulates(schema, views[0]),
detailView: getViewPopulates(schema, views[1]),
};
if (mraBE && mraBE.createObjects) {
let cnt = 0;
for (let obj of mraBE.createObjects) {
model.create(obj, function (err, result) {
if (err) {
console.error(
` ~~ mraBE Initialization: failed to create object for schema ${schemaName}: `,
err.message
);
return;
}
console.log(
` ~~ mraBE Initialization: create object for schema ${schemaName}: `,
++cnt
);
});
}
}
}
getModelNameByTag(tag) {
for (let schemaName in this.tags_collection) {
let tags = this.tags_collection[schemaName];
if (tags && tags.includes(tag)) {
return schemaName;
}
}
return undefined;
}
getAll(req, res, next) {
return this.searchAll(req, res, next, {});
}
async getRefObjectsFromId(req, schm, idArray) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVarsByName(schm.toLowerCase());
let query = {
_id: { $in: idArray.map((x) => mongoose.Types.ObjectId(x)) },
};
query = ownerPatch(query, owner, req);
query = searchObjPatch(query, mraBE);
if (!searchAllowed(query, mraBE)) {
throw new Error('Search not allowed.');
}
try {
let docs = await model.find(query).exec();
return docs;
} catch (err) {
throw err;
}
}
async getRefObjectsAll(req, schm) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVarsByName(schm.toLowerCase());
let query = {};
query = ownerPatch(query, owner, req);
query = searchObjPatch(query, mraBE);
if (!searchAllowed(query, mraBE)) {
throw new Error('Search not allowed.');
}
try {
// TODO: handle large number of documents...
let docs = await model.find(query).exec();
return docs;
} catch (err) {
throw err;
}
}
getPopulateInfo(theView, morePopulateField) {
const populateArray = [];
const populateMap = {};
theView.forEach((p) => {
const fields = this.getPopulatesRefFields(p[1]);
//fields is [indexFields, briefFieds]
if (fields != null) {
//only push when the ref schema is found
populateArray.push({ path: p[0], select: fields[0] }); //let's use indexFields for now
populateMap[p[0]] = morePopulateField === p[0] ? fields[1] : fields[0]; // use briefFields, or indexFields
}
});
return [populateArray, populateMap];
}
// Handling other MddsActins post request
// 1. /mddsaction/emailing
// 2. /mddsaction/export
async PostActionsAll(req, res, next, actionType) {
let body = req.body;
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
return next(createError(400, 'Bad document in body.'));
}
}
if (actionType === '/mddsaction/emailing') {
try {
emailAllCheckExternal(req, this);
} catch (err) {
return next(err);
}
} else if (actionType === '/mddsaction/export') {
} else {
return next(createError(400, `Action ${actionType} is not supported.`));
}
const searchContext = body ? body.search : {};
let rows = [];
let emailAllResult = { success: 0, fail: 0, queuing: 0, error: null };
const PER_PAGE = 400; //query 400 each time. SES limit is 500;
for (let p = 1; ; p++) {
req.query['__page'] = String(p);
req.query['__per_page'] = String(PER_PAGE);
let output;
try {
const [o, items] = await this.searchAllExec(req, searchContext);
output = o;
output.items = items; // un-reduced items
} catch (err) {
//handle DB query error
if (actionType === '/mddsaction/export') {
return next(err);
} else if (actionType === '/mddsaction/emailing') {
return emailAllErrorExternal(req, res, next, emailAllResult, err);
}
}
rows = rows.concat(output.items);
// handle each search chunk
if (actionType === '/mddsaction/emailing') {
try {
let result = await this.emailAll(req, output.items);
emailAllResult.success += result.success;
emailAllResult.fail += result.fail;
emailAllResult.queuing += result.queuing;
emailAllResult.error = result.error;
} catch (err) {
return emailAllErrorExternal(req, res, next, emailAllResult, err);
}
}
let { page, total_pages, total_count } = output;
if (page === total_pages) {
//done all query
break;
}
}
// handle all chunk;
if (actionType === '/mddsaction/export') {
return this.exportAll(req, res, next, rows);
} else if (actionType === '/mddsaction/emailing') {
return res.send(emailAllResult);
}
return next(createError(400, `Action ${actionType} not supported.`));
}
async emailAll(req, rows) {
return await emailAllExternal(req, rows, this);
}
exportAll(req, res, next, rows) {
return exportAllExternal(req, res, next, rows, this);
}
async searchAllExec(req, searchContext) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
let query = {};
//get the query parameters ?a=b&c=d, but filter out unknown ones
const PER_PAGE = 25,
MAX_PER_PAGE = 1000;
let __page = 1;
let __per_page = PER_PAGE;
let __sort, __order;
let __categoryBy,
__categoryProvided,
__listCategoryShowMore,
__categoryCand;
let __categoryBy2,
__categoryProvided2,
__listCategoryShowMore2,
__categoryCand2;
let __asso;
for (let prop in req.query) {
if (prop === '__page') {
__page = parseInt(req.query[prop]);
} else if (prop === '__per_page') {
__per_page = parseInt(req.query[prop]);
} else if (prop === '__sort') {
__sort = req.query[prop];
} else if (prop === '__order') {
__order = req.query[prop];
} else if (prop === '__categoryBy') {
__categoryBy = req.query[prop];
} else if (prop === '__listCategoryShowMore') {
__listCategoryShowMore = req.query[prop];
} else if (prop === '__categoryCand') {
__categoryCand = req.query[prop];
} else if (prop === '__categoryProvided') {
__categoryProvided = req.query[prop];
} else if (prop === '__categoryBy2') {
__categoryBy2 = req.query[prop];
} else if (prop === '__listCategoryShowMore2') {
__listCategoryShowMore2 = req.query[prop];
} else if (prop === '__categoryCand2') {
__categoryCand2 = req.query[prop];
} else if (prop === '__categoryProvided2') {
__categoryProvided2 = req.query[prop];
} else if (prop === '__asso') {
__asso = req.query[prop];
} else if (prop in schema.paths) {
query[prop] = req.query[prop];
}
}
let __categoryFieldRef, __categoryFieldRef2;
for (let p of populates.briefView) {
//an array, with [field, ref]
if (p[0] === __categoryBy) {
__categoryFieldRef = p[1];
} else if (p[0] === __categoryBy2) {
__categoryFieldRef2 = p[1];
}
}
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
const briefView = views[0];
// __asso will be populated with brief view
const [populateArray, populateMap] = this.getPopulateInfo(
populates.briefView,
__asso
);
let count = 0;
if (searchContext) {
//console.log("searchContext is ....", searchContext);
// searchContext ={'_id': xxx, '$and': [{'$or', []},{'$and', []}]}
let searchQuery = searchContext;
if (searchContext._id) {
searchQuery = {_id: searchContext._id};
} else if (searchContext['$and']) {
for (let subContext of searchContext['$and']) {
if ('$or' in subContext) {
if (subContext['$or'].length == 0) subContext['$or'] = [{}];
subContext['$or'] = subContext['$or'].map((x) => createRegex(x));
} else if ('$and' in subContext) {
if (subContext['$and'].length == 0) subContext['$and'] = [{}];
subContext['$and'] = subContext['$and'].map((x) =>
checkAndSetValue(x, schema)
);
}
}
}
//merge the url query and body query
query = searchQuery;
//console.log("query is ....", query['$and'][0]['$or'], query['$and'][1]['$and']);
}
query = ownerPatch(query, owner, req);
query = searchObjPatch(query, mraBE);
if (!searchAllowed(query, mraBE)) {
throw new Error('Search not allowed.');
}
let originCategoriesAll = [[], []];
let categoriesAll = [[], []]; // all reference documents that are used by this model
let categoriesDocumentsAll = [[], []]; // all documents from referenc collection (used and not used)
let categoriesCounts = [[], []]; // document counts based on categories
let categoryObjectsIndexAll = [[], []];
let categoryObjectsBriefAll = [[], []];
let categoryObjectsAll = [[], []];
const cateDef = [
{
categoryBy: __categoryBy,
categoryProvided: __categoryProvided,
categoryFieldRef: __categoryFieldRef,
listCategoryShowMore: __listCategoryShowMore,
categoryCand: __categoryCand,
},
{
categoryBy: __categoryBy2,
categoryProvided: __categoryProvided2,
categoryFieldRef: __categoryFieldRef2,
listCategoryShowMore: __listCategoryShowMore2,
categoryCand: __categoryCand2,
},
];
for (let i = 0; i < cateDef.length; i++) {
const cate = cateDef[i];
if (cate.categoryBy && !cate.categoryProvided) {
// need to query DB to get the category first.
try {
let catQuery = {};
catQuery = ownerPatch(catQuery, owner, req);
catQuery = searchObjPatch(catQuery, mraBE);
if (!searchAllowed(catQuery, mraBE)) {
throw new Error('Search not allowed.');
}
originCategoriesAll[i] = await model
.find(catQuery)
.distinct(cate.categoryBy)
.exec(); // returns array of distinct field values. Value is unwinded for array type.
const aggregatePipes = [
{ $match: catQuery },
{ $unwind: `$${cate.categoryBy}` }, // support array field
{ $group: { _id: `$${cate.categoryBy}`, count: { $sum: 1 } } },
];
let cateCounts = await model.aggregate(aggregatePipes).exec();
cateCounts = JSON.parse(JSON.stringify(cateCounts));
const cateCountsObj = {};
for (const c of cateCounts) {
let k = c['_id'];
if (k === null) coninue; // ignore null field; k = MddsUncategorized;
cateCountsObj[k] = c['count'];
}
/*[ { _id: 5de16d0db8c1b52671ff717f, count: 1 },
{ _id: null, count: 64 },
{ _id: 5de17192518aa428ae761e70, count: 1 } ]*/
// Order based on alphebetic
originCategoriesAll[i].sort();
if (i === 1) {
// reverse (eg: time based.)
originCategoriesAll[i].reverse();
}
categoriesAll[i] = originCategoriesAll[i];
if (cate.categoryFieldRef) {
// it's an ref field
categoriesAll[i] = await this.getRefObjectsFromId(
req,
cate.categoryFieldRef,
originCategoriesAll[i]
);
categoriesDocumentsAll[i] = await this.getRefObjectsAll(
req,
cate.categoryFieldRef
);
const tempIds = categoriesAll[i].map((x) => x['_id'].toString());
categoriesDocumentsAll[i] = categoriesDocumentsAll[i].filter(
(x) => !tempIds.includes(x['_id'].toString())
); // remove duplicate ones
categoriesAll[i] = categoriesAll[i].concat(
categoriesDocumentsAll[i]
); // merge
}
// categoriesAll could be ref object, or just simple value. Put it to parent objects.
categoryObjectsAll[i] = categoriesAll[i].map((x) => {
const obj = {};
obj[cate.categoryBy] = x;
return obj;
});
categoryObjectsAll[i] = JSON.parse(
JSON.stringify(categoryObjectsAll[i])
);
// get the index population of the category fields
const [indexPopulateArray, indexPopulateMap] = this.getPopulateInfo(
populates.briefView,
null
);
categoryObjectsIndexAll[i] = resultReducerForRef(
categoryObjectsAll[i],
indexPopulateMap
);
// get the indexed ref objects, or just simple value if not ref.
categoriesAll[i] = categoryObjectsIndexAll[i].map(
(x) => x[cate.categoryBy]
);
if (cate.categoryFieldRef) {
originCategoriesAll[i] = categoriesAll[i].map((x) => x['_id']);
}
for (const c of originCategoriesAll[i]) {
categoriesCounts[i].push(cateCountsObj[c] || 0);
}
// categoriesCounts[i].push(cateCountsObj[MddsUncategorized] || 0);
if (i === 0) {
/*
let totalCnt = 0;
for (let j = 0; j < categoriesCounts[i].length; j++) {
totalCnt += categoriesCounts[i][j];
}
// put total cnt in front.
categoriesCounts[i].splice(0, 0, totalCnt);
*/
let totalCnt = await model.countDocuments(catQuery).exec(); // returns array of distinct field values. Value is unwinded for array type.
categoriesCounts[i].splice(0, 0, totalCnt);
}
catQuery[cate.categoryBy] = { $in: [null, []] };
let uncategorizedCnt = await model.countDocuments(catQuery).exec(); // returns array of distinct field values. Value is unwinded for array type.
categoriesCounts[i].push(uncategorizedCnt);
// get the biref population of the category fields
if (cate.listCategoryShowMore) {
const [briefPopulateArray, briefPopulateMap] = this.getPopulateInfo(
populates.briefView,
cate.categoryBy
);
categoryObjectsBriefAll[i] = resultReducerForRef(
categoryObjectsAll[i],
briefPopulateMap
).map((x) => x[cate.categoryBy]); // put only the biref-ed ref or simple value
}
// db.someCollection.aggregate([{ $match: { age: { $gte: 21 }}}, {"$group" : {_id:"$source", count:{$sum:1}}} ])
} catch (err) {
throw err;
}
}
if (!cate.categoryProvided && originCategoriesAll[i].length > 0) {
// user get a link, it has cateory Candidate, but no categoryProvided
if (originCategoriesAll[i].includes(cate.categoryCand)) {
// candidate found
query[cate.categoryBy] = cate.categoryCand;
} else if (cate.categoryCand === MddsUncategorized) {
// uncategorized request from front end. use null.
// query[cate.categoryBy] = null;
query[cate.categoryBy] = { $in: [null, []] };
} else {
if (i === 0) {
// Search all. don't put to query
// do nothing
} else {
// take the first category as query filter
query[cate.categoryBy] = originCategoriesAll[i][0];
}
}
}
}
try {
count = await model.countDocuments(query).exec();
} catch (err) {
throw err;
}
const [perPage, pageNum, maxPageNum, skipCount] = processPages(
__per_page,
__page,
PER_PAGE,
MAX_PER_PAGE,
count
);
let srt = {};
if (__sort && __order) srt[__sort] = __order;
//let dbExec = model.find(query, briefView)
let dbExec = model
.find(query) //return every thing for the document
.sort(srt)
.skip(skipCount)
.limit(perPage);
for (let pi = 0; pi < populateArray.length; pi++) {
let p = populateArray[pi];
//dbExec = dbExec.populate(p);
dbExec = dbExec.populate(p.path); //only give the reference path. return everything
}
try {
let result = await dbExec.exec();
let output = {
total_count: count,
total_pages: maxPageNum,
page: pageNum,
per_page: perPage,
items: result,
categoryBy: __categoryBy,
categories: categoryObjectsIndexAll[0],
categoriesCounts: categoriesCounts[0],
categoriesBrief: categoryObjectsBriefAll[0],
categoryBy2: __categoryBy2,
categories2: categoryObjectsIndexAll[1],
categoriesCounts2: categoriesCounts[1],
categoriesBrief2: categoryObjectsBriefAll[1],
};
output = JSON.parse(JSON.stringify(output));
const items = output.items;
output.items = resultReducerForRef(output.items, populateMap);
output.items = resultReducerForView(output.items, briefView);
return [output, items];
} catch (err) {
throw err;
}
}
async searchAll(req, res, next, searchContext) {
try {
const [output, items] = await this.searchAllExec(req, searchContext);
return res.send(output);
} catch (err) {
return next(err);
}
}
async searchFieldValues(req, res, next, fieldValueSearch) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
let query = {};
//get the query parameters ?a=b&c=d, but filter out unknown ones
let __field;
let __field_value;
const MAX_LIMIT = 1000;
let __limit = 25;
const sortValues = ['count', 'value']; // sort based on count, or field value
let __sort = 'count'; // default: sort based on count
for (let prop in req.query) {
if (prop === '__field') {
__field = req.query[prop];
} else if (prop === '__field_value') {
__field_value = req.query[prop];
} else if (prop === '__limit') {
let lmt = parseInt(req.query[prop]);
__limit =
isNaN(lmt) || lmt <= 0 ? __limit : lmt > MAX_LIMIT ? MAX_LIMIT : lmt;
} else if (prop === '__sort') {
let srt = req.query[prop];
__sort = sortValues.includes(srt) ? srt : __sort;
}
}
if (!__field) {
return next(createError(400, 'Field is not provided'));
}
if (!fieldValueSearchAllowed(__field, mraBE)) {
return next(
createError(400, `Field value search for '${__field}' is now allowed`)
);
}
let catQuery = {};
let fieldQuery = {};
if (__field_value) {
const v = createRegex(__field_value);
catQuery[__field] = v;
fieldQuery['_id'] = v;
}
let sortO = { count: -1 };
if (__sort === 'value') {
sortO = { _id: 1 };
}
catQuery = ownerPatch(catQuery, owner, req);
catQuery = searchObjPatch(catQuery, mraBE);
if (!searchAllowed(query, mraBE)) {
return next(
createError(400, `Search not allowed`)
);
}
const aggregatePipes = [
{ $match: catQuery },
{ $unwind: `$${__field}` }, // unwind will unpack the array, if field is of type array.
{ $group: { _id: `$${__field}`, count: { $sum: 1 } } }, // group and sum
{ $match: fieldQuery }, // pick fields matching the given field value;
{ $sort: sortO },
{ $limit: __limit },
];
// count for each value
let cateCounts = await model.aggregate(aggregatePipes).exec();
cateCounts = JSON.parse(JSON.stringify(cateCounts));
return res.send(cateCounts);
}
getSamples(req, res, next, searchContext) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
const briefView = views[0];
const [populateArray, populateMap] = this.getPopulateInfo(
populates.briefView,
null
);
let query = {};
//get the query parameters ?a=b&c=d, but filter out unknown ones
const PER_PAGE = 25,
MAX_PER_PAGE = 1000;
let __page = 1;
let __per_page = PER_PAGE;
for (let prop in req.query) {
if (prop === '__page') __page = parseInt(req.query[prop]);
else if (prop === '__per_page') __per_page = parseInt(req.query[prop]);
else if (prop in schema.paths) {
query[prop] = req.query[prop];
}
}
let count = 0;
if (searchContext) {
//console.log("searchContext is ....", searchContext);
// searchContext ={'$and', [{'$or', []},{'$and', []}]}
if (searchContext['$and']) {
for (let subContext of searchContext['$and']) {
if ('$or' in subContext) {
if (subContext['$or'].length == 0) subContext['$or'] = [{}];
subContext['$or'] = subContext['$or'].map((x) => createRegex(x));
} else if ('$and' in subContext) {
if (subContext['$and'].length == 0) subContext['$and'] = [{}];
subContext['$and'] = subContext['$and'].map((x) =>
checkAndSetValue(x, schema)
);
}
}
}
//merge the url query and body query
query = searchContext;
//console.log("query is ....", query['$and'][0]['$or'], query['$and'][1]['$and']);
}
query = ownerPatch(query, owner, req);
query = searchObjPatch(query, mraBE);
if (!searchAllowed(query, mraBE)) {
return next(
createError(400, `Search not allowed`)
);
}
model.countDocuments(query).exec(function (err, cnt) {
if (err) {
return next(err);
}
count = cnt;
const [perPage, pageNum, maxPageNum, skipCount] = processPages(
__per_page,
__page,
PER_PAGE,
MAX_PER_PAGE,
count
);
//let dbExec = model.find(query, briefView)
let dbExec = model
.find(query) //return every thing for the document
.skip(skipCount)
.limit(perPage);
for (let pi = 0; pi < populateArray.length; pi++) {
let p = populateArray[pi];
//dbExec = dbExec.populate(p);
dbExec = dbExec.populate(p.path); //only give the reference path. return everything
}
dbExec.exec(function (err, result) {
if (err) return next(err);
let output = {
total_count: count,
total_pages: maxPageNum,
page: pageNum,
per_page: perPage,
items: result,
};
output = JSON.parse(JSON.stringify(output));
output.items = resultReducerForRef(output.items, populateMap);
output.items = resultReducerForView(output.items, briefView);
return res.send(output);
});
});
}
async getDetailsByIdExec(req) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
/*
let action = "";
if (req.query) {
action = req.query['action'];
}
*/
let originalUrl = req.originalUrl;
let detailView;
if (originalUrl.includes('/mddsaction/post')) {
detailView = views[3]; //return based on edit view
} else {
detailView = views[1];
}
const [populateArray, populateMap] = this.getPopulateInfo(
populates.detailView,
null
);
let idParam = name + 'Id';
let id = req.params[idParam];
//let dbExec = model.findById(id, detailView)
let dbExec = model.findById(id); //return every thing for the document
for (let pi = 0; pi < populateArray.length; pi++) {
let p = populateArray[pi];
//dbExec = dbExec.populate(p);
dbExec = dbExec.populate(p.path); //only give the reference path. return everything for reference
}
let result = await dbExec.exec();
result = JSON.parse(JSON.stringify(result));
let reducedResult = resultReducerForRef(result, populateMap);
reducedResult = resultReducerForView(reducedResult, detailView);
return [reducedResult, result];
}
async getDetailsById(req, res, next) {
try {
let [reducedResult, result] = await this.getDetailsByIdExec(req);
return res.send(reducedResult);
} catch (err) {
return next(err);
}
}
HardDeleteById(req, res, next) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
let idParam = name + 'Id';
let id = req.params[idParam];
model.findByIdAndDelete(id).exec((err, result) => {
if (err) {
return next(err);
}
this.handleHooks('delete', result, mraBE, req);
return res.send();
});
}
async deleteManyByIds(req, res, next, ids) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
let query = { _id: { $in: ids } };
let docs = [];
if (this.hasHooks('delete', mraBE)) {
try {
docs = await model.find(query).exec();
} catch (err) {
console.error('query docs faied ', err);
}
}
model.deleteMany(query).exec((err, results) => {
if (err) {
return next(err);
}
for (let doc of docs) {
this.handleHooks('delete', doc, mraBE, req);
}
return res.send();
});
}
archiveManyByIds(req, res, next, ids) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
model.archive({ _id: { $in: ids } }, function (err, result) {
if (err) {
return next(err);
}
return res.send();
});
}
unarchiveManyByIds(req, res, next, ids) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
model.unarchive({ _id: { $in: ids } }, function (err, result) {
if (err) {
return next(err);
}
return res.send();
});
}
async CreateExec(req) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
let body = req.body;
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
throw createError(400, 'Bad ' + name + ' document.');
}
}
body = ownerPatch(body, owner, req);
let result = await model.create(body);
this.handleHooks('insert', result, mraBE, req);
return result;
}
async Create(req, res, next) {
let result;
try {
result = await this.CreateExec(req);
res.send(result);
} catch (err) {
return next(err);
}
}
Update(req, res, next) {
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
//views in [briefView, detailView, CreateView, EditView, SearchView, IndexView] format
let body = req.body;
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
return next(createError(404, 'Bad ' + name + ' document.'));
}
}
let idParam = name + 'Id';
let id = req.params[idParam];
let editViewStr = views[3];
let viewFields = editViewStr.match(/\S+/g) || [];
if (schema.options.useSaveInsteadOfUpdate) {
model.findOne({ _id: id }, (err, result) => {
if (err) {
return next(err);
}
let mapFields = {};
let hasMap = false;
let changes = {};
for (let field in body) {
let income = body[field];
let existing = result[field];
if (income != existing) { // object will always be added.
changes[field] = {old: existing, new: income};
}
if (existing instanceof Map) {
// first remove the map filed, and save it for later update.
mapFields[field] = income;
income = undefined;
hasMap = true;
}
//all fields from client
result[field] = income;
}
for (let field of viewFields) {
if (!(field in body)) {
//not in body means user deleted this field
changes[field] = {old: result[field], new: undefined}
// delete result[field]
result[field] = undefined;
}
}
result = ownerPatch(result, owner, req);
result.save((err) => {
if (err) {
return next(err);
}
// put map fields to the result
for (let field in mapFields) {
result[field] = mapFields[field];
}
if (!hasMap) {
this.handleHooks('update', result, mraBE, req, changes);
return res.send();
}
// update second time for the map field.
// Use update for performance (assume map fields don't need save hooks)
model.updateOne({ _id: id }, mapFields, (err) => {
if (err) {
return next(err);
}
this.handleHooks('update', result, mraBE, req, changes);
return res.send();
});
});
});
} else {
//all top-level update keys that are not $atomic operation names are treated as $set operations
model.updateOne({ _id: id }, body, (err, result) => {
if (err) {
return next(err);
}
this.handleHooks('update', result, mraBE, req);
return res.send();
});
}
}
PostActions(req, res, next) {
/*
if (req.query) {
action = req.query['action'];
}
*/
let body = req.body;
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
return next(createError(400, 'Bad document in body.'));
}
}
let action = req.path;
let ids;
switch (action) {
case '/mddsaction/delete':
ids = body;
this.deleteManyByIds(req, res, next, ids);
break;
case '/mddsaction/archive':
ids = body;
this.archiveManyByIds(req, res, next, ids);
break;
case '/mddsaction/unarchive':
ids = body;
this.unarchiveManyByIds(req, res, next, ids);
break;
case '/mddsaction/getfieldvalues':
let fieldValueSearch = body ? body.fieldValueSearch : {};
this.searchFieldValues(req, res, next, fieldValueSearch);
break;
case '/mddsaction/get':
let searchContext = body ? body.search : {};
this.searchAll(req, res, next, searchContext);
break;
default:
if (action.startsWith('/mddsaction/')) {
this.PostActionsAll(req, res, next, action);
} else {
return next(createError(404, 'Bad Action: ' + action));
}
}
}
zInterfaceCall(req, res, next) {
if (!req.zInterface) {
return next(createError(400, 'Bad Request: interface not defined'));
}
let ifname = req.zInterface.name;
let action = req.zInterface.action;
const {
name,
schema,
model,
views,
populates,
owner,
mraBE,
} = this.loadContextVars(req);
if (!mraBE || !mraBE.zInterfaces || !mraBE.zInterfaces[action]) {
return next(
createError(
400,
`Bad Request: ${action} interface not defined: ${ifname}`
)
);
}
const targetInterfaces = mraBE.zInterfaces[action].filter((x) => {
if (x.name === ifname) return true;
return false;
});
if (targetInterfaces.length === 0) {
return next(
createError(
400,
`Bad Request: ${action} interface not defined: ${ifname}`
)
);
}
const fn = targetInterfaces[0].fn;
return fn(req, res, next, this);
}
//Return a promise.
ModelExecute(modelName, apiName, ...params) {
const modelname = modelName.toLowerCase();
let model = this.model_collection[modelname];
if (!model || !model[apiName]) {
let err = new Error(
`model ${modelname} or mode API ${apiName} doesn't exit`
);
return new Promise(function (resolve, reject) {
reject(err);
});
}
// For APIs that return Promise
if (apiName == 'create') {
return model.create.apply(model, params);
} else if (apiName == 'insertMany') {
return model.insertMany.apply(model, params);
}
// For APIs that return Query
let dbExe = model[apiName].apply(model, params);
return dbExe.exec();
}
ModelExecute2(modelName, apis) {
const modelname = modelName.toLowerCase();
let model = this.model_collection[modelname];
if (!model) {
let err = new Error(
`model ${modelname} doesn't exit`
);
return new Promise(function (resolve, reject) {
reject(err);
});
}
let dbExe;
for (let item of apis) {
let apiName = item[0];
let apiArgs = item[1];
try {
if (!dbExe) dbExe = model[apiName].apply(model, apiArgs);
else dbExe = dbExe[apiName].apply(dbExe, apiArgs);
} catch (err) {
return new Promise(function (resolve, reject) {
reject(err);
});
}
}
return dbExe.exec();
}
async handleHooks(action, data, mraBE, req, changes) {
//action: insert, update
const restController = this;
const moduleName = req.mddsModuleName;
const schemaName = req.meanRestSchemaName;
const muser = req.muser || {};
// 1. check emailer hooks
const { emailer, emailerObj } = this.mddsProperties || {};
if (emailer) {
const emailerConf = mraBE.emailer || {};
const replacement = emailerConf.replacement || {};
const emailHooks = emailerConf.hooks || {};
if (emailHooks[action]) {
let hooksArr = emailHooks[action];
if (!Array.isArray(hooksArr)) {
hooksArr = [emailHooks[action]];
}
for (let func of hooksArr) {
// TODO: add error handling.
func(
emailer,
data,
replacement,
emailerObj,
restController,
changes
);
}
}
}
// 2. check .... hooks
const hooks = mraBE.hooks || {};
if (hooks[action]) {
let hooksArr = hooks[action];
if (!Array.isArray(hooksArr)) {
hooksArr = [hooks[action]];
}
for (let func of hooksArr) {
// TODO: add error handling.
func(data, restController, changes);
}
}
// 3. check history hooks
if (mraBE.enableHistory) {
// TODO: add error handling.
this.ModelExecute(
'MddsCoreHistory',
'create',
{
oid: data._id,
module: moduleName,
schema: schemaName,
action,
user: muser._id,
document: JSON.stringify(data),
} //search criteria
);
}
}
hasHooks(action, mraBE) {
// 1. check emailer hooks
const { emailer, emailerObj } = this.mddsProperties || {};
if (emailer) {
const emailerConf = mraBE.emailer || {};
const emailHooks = emailerConf.hooks || {};
if (emailHooks[action]) {
return true;
}
}
// 2. check .... hooks
const hooks = mraBE.hooks || {};
if (hooks[action]) {
return true;
}
// 3. Check history hooks
if (mraBE.enableHistory) {
return true;
}
return false;
}
}
module.exports = RestController;