@hosoft/restful-api-framework
Version:
Base framework of the headless cms HoServer provided by http://helloreact.cn
448 lines (382 loc) • 13.7 kB
JavaScript
/**
* HoServer API Server Ver 2.0
* Copyright http://hos.helloreact.cn
*
* create: 2018/7/15
**/
const _ = require('lodash')
const BaseHelper = require('./base-helper')
const mongoose = require('mongoose')
/**
* fill list data field with relation table records
*
* if set targetQuery,will add the query condition when query target table
* if set outField,will put the relation records to outField, by default use [field]_rel
*
* is_recursive if recursive to set the relation fields for relation table
*/
const populateData = async (
srcData,
srcProp,
targetModel,
targetProp,
outFieldName,
selectFields,
targetQuery,
isRecursive = false
) => {
let { srcDocs, result } = _prepareResult(srcData)
const count = srcDocs.length
if (count === 0) {
return result
}
// to improve the performance for read sub property data
if (srcProp.indexOf('.') > 0) {
const srcFields = srcProp.split('.')
const subSrcDocs = []
// e.g. students.id_list
_traverseSubProperties(subSrcDocs, srcDocs, srcFields, 0)
srcDocs = subSrcDocs
srcProp = srcFields[srcFields.length - 1]
}
let srcPropArr = []
for (let i = 0; i < srcDocs.length; i++) {
const item = srcDocs[i]
const p = item[srcProp]
if (p instanceof Array) {
srcPropArr.push(...p)
} else if (p) {
srcPropArr.push(p)
}
}
if (srcPropArr.length === 0) {
return result
}
srcPropArr = _.uniq(srcPropArr, srcProp)
const multiResult = !!(srcPropArr.length > 1)
const queryOptions = {}
queryOptions[targetProp] = multiResult ? { $in: srcPropArr } : srcPropArr[0]
if (targetQuery) {
Object.assign(queryOptions, targetQuery)
}
let targetResult = null
if (selectFields) {
if (typeof selectFields === 'string') {
selectFields = selectFields.split(' ')
}
let findTargetProp = false
const targetPropPathSegs = targetProp.split('.')
let propPath = ''
for (const seg of targetPropPathSegs) {
propPath = propPath ? propPath + '.' + seg : seg
if (selectFields.indexOf(propPath) > -1) {
findTargetProp = true
break
}
}
if (!findTargetProp) {
selectFields.push(targetProp)
}
const selFieldsQuery = {}
for (let i = 0; i < selectFields.length; i++) {
let f = selectFields[i].trim()
if (f.startsWith('-')) {
f = f.substr(1).trim()
if (f) selFieldsQuery[f] = 0
} else if (f) {
selFieldsQuery[f] = 1
}
}
// prettier-ignore
targetResult = multiResult
? await targetModel.find(queryOptions, null, selFieldsQuery)
: await targetModel.findOne(queryOptions, null, selFieldsQuery)
} else {
const defOutFields = targetModel.getDefaultOutFields()
// prettier-ignore
targetResult = multiResult
? await targetModel.find(queryOptions, null, defOutFields)
: await targetModel.findOne(queryOptions, null, defOutFields)
}
// it's better to avoid recursive to improve performance
if (isRecursive) {
const defSelFields = {}
// only scan first layer
for (const prop of targetModel.properties) {
if (prop.relations && prop.relations.rel_type && (prop.output_flag === 1 || prop.output_flag === 4)) {
const relType = prop.relations.rel_type / 1
// ignore object relation field
/*
if (relType === 1) {
const relField = {
name: prop.name + '_rel',
is_recursive: false,
rel_fields: [],
};
defSelFields.push(relField);
}
*/
if ([2, 3].indexOf(relType) > -1) {
defSelFields[prop.name] = 1
}
}
targetResult = await populateModel(targetResult, targetModel, defSelFields, false /* don't recursive */)
}
}
// fill data
for (const srcItem of srcDocs) {
const p = srcItem[srcProp]
let relationItem = null
if (p instanceof Array) {
relationItem = !multiResult
? targetResult
: targetResult.filter((item) => {
const targetPropVal = _getJsonPropVal(item, targetProp)
if (typeof targetPropVal === 'object' && targetPropVal.equals) {
return p.findIndex((r) => r.equals(targetPropVal)) > -1
} else {
return p.indexOf(targetPropVal) > -1
}
})
if (relationItem) {
const concatItems = relationItem instanceof Array ? relationItem : [relationItem._doc || relationItem]
const resultItem = srcItem instanceof mongoose.Model ? srcItem._doc : srcItem
if (outFieldName) {
if (!resultItem[outFieldName]) {
resultItem[outFieldName] = []
}
for (const item of concatItems) {
delete item._id
resultItem[outFieldName].push(item)
}
} else {
if (!resultItem[srcProp + '_rel']) {
resultItem[srcProp + '_rel'] = []
}
for (const item of concatItems) {
delete item._id
resultItem[srcProp + '_rel'].push(item)
}
}
}
} else {
relationItem = !multiResult
? targetResult
: targetResult.find((item) => {
const targetPropVal = _getJsonPropVal(item, targetProp)
if (typeof targetPropVal === 'object' && targetPropVal.equals) {
return targetPropVal.equals(srcItem[srcProp])
} else {
return targetPropVal === srcItem[srcProp]
}
})
const resultItem = srcItem instanceof mongoose.Model ? srcItem._doc : srcItem
if (relationItem) {
delete relationItem._id
if (outFieldName) {
resultItem[outFieldName] = relationItem._doc || relationItem
} else {
resultItem[srcProp + '_rel'] = relationItem._doc || relationItem
}
} else {
resultItem[outFieldName || srcProp + '_rel'] = null
}
}
}
return result
}
/**
* fill a single field
*/
const populateField = async (srcData, propModel, propFullName, outPropName, relFields, is_recursive = false) => {
const relType = propModel.relations.rel_type / 1
if (relType === 1 && propModel.relations.name) {
if (propModel.relations.field) {
const targetModel = BaseHelper.getModel(propModel.relations.name)
if (!targetModel) {
logger.error('getListQuery relations target modelMeta not found: ' + propModel.relations.name)
} else {
// if relation fields not set, use default output fields
if (!relFields || relFields.length === 0) {
relFields = targetModel.getDefaultOutFields()
if (relFields.length === 0) {
relFields = null
}
}
srcData = await populateData(
srcData,
propFullName,
targetModel,
propModel.relations.field,
'',
relFields,
propModel.relations.rel_query,
is_recursive
)
}
}
} else if (relType === 2) {
const enumObj = BaseHelper.getPropertyEnum(propModel)
if (enumObj) {
srcData = fillDictData(srcData, propFullName, enumObj)
}
} else if (relType === 3) {
const enumObj = await BaseHelper.getSystemDict(propModel.relations.name)
srcData = fillDictData(srcData, propFullName, enumObj)
}
return srcData
}
/**
* fill model relation table
* @param srcData source record list
* @param model the object model for source record
* @param populateFields which fields to populate
*/
const populateModel = async (srcData, model, populateFields, is_recursive = false) => {
if (!srcData) return
if (typeof model === 'string') {
model = BaseHelper.getModel(model)
}
if (typeof populateFields == 'string') {
populateFields = populateFields.split(/[,;\s]/).filter((f) => !!f.trim())
}
if (!(populateFields && populateFields.length > 0)) {
populateFields = model.getDefaultOutFields()
}
const populatedFields = {}
for (const field of populateFields) {
const propNames = field.split('.')
let propPath = ''
let outPropName = field
if (propNames.length > 1) {
propPath = field.substring(0, field.lastIndexOf('.') + 1)
outPropName = field.substring(field.lastIndexOf('.') + 1)
}
let propModel = model
while (propNames.length > 0) {
let propName = propNames[0]
propNames.splice(0, 1)
// remove _rel
if (propName.endsWith('_rel')) {
propName = propName.substr(0, propName.length - 4)
}
if (propName.startsWith('-')) {
propName = propName.substr(1)
}
// TODO: $ is mongoose specialized
if (propName !== '$') {
propModel = propModel.properties.find((p) => p.name === propName)
} else {
console.log('populate array field')
}
}
if (!propModel) {
logger.error('find modelMeta property fail, please check your output settings: ', field)
}
if (!(propModel.relations && propModel.relations.rel_type)) {
continue
}
const fieldName = propPath + propModel.name
if (populatedFields[fieldName]) {
continue
}
srcData = await populateField(srcData, propModel, fieldName, outPropName, null, false)
populatedFields[fieldName] = true
}
return srcData
}
/**
* fill dict item text, for client render
*/
const fillDictData = (srcData, srcProp, dictObj, outField) => {
let { srcDocs, result } = _prepareResult(srcData)
if (!srcDocs || (srcDocs instanceof Array && srcDocs.length === 0)) {
return result
}
if (srcProp.indexOf('.') > 0) {
const srcFields = srcProp.split('.')
const subSrcDocs = []
_traverseSubProperties(subSrcDocs, srcDocs, srcFields, 0)
srcDocs = subSrcDocs
srcProp = srcFields[srcFields.length - 1]
}
for (const srcItem of srcDocs) {
const p = srcItem[srcProp]
const item = srcItem._doc || srcItem
if (dictObj[p]) {
if (outField) {
item[outField] = dictObj[p]
} else {
item[srcProp + '_rel'] = dictObj[p]
}
}
}
return result
}
/************************************************
* below private functions
* **********************************************/
const _prepareResult = (srcData) => {
let srcDocs = srcData
let result = null
if (srcData.docs) {
srcDocs = srcData.docs
result = srcDocs
} else if (srcData.list) {
srcDocs = srcData.list
result = srcData
} else {
result = srcDocs
if (!(srcDocs instanceof Array)) {
srcDocs = [srcDocs]
}
}
return { srcDocs, result }
}
/**
* Get sub properties from result list. used for later fill relation data.
* e.g. students.id_list, will get sub prop students,then append students to result,
*/
const _traverseSubProperties = (result, docs, subProps, subPropIndex = 0) => {
for (const doc of docs) {
let item = doc._doc || doc
for (let i = subPropIndex; i < subProps.length - 1; i++) {
const f = subProps[i]
if (!item[f]) {
item[f] = {}
}
item = item[f]
if (item instanceof Array) {
_traverseSubProperties(result, item, subProps, i + 1)
}
}
if (!(item instanceof Array)) {
result.push(item)
}
}
}
const _getJsonPropVal = (jsonData, propPath) => {
if (!(jsonData instanceof Object) || typeof propPath === 'undefined') {
return null
}
propPath = propPath.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
propPath = propPath.replace(/^\./, '') // strip a leading dot
let result = jsonData
const pathArray = propPath.split('.')
for (let i = 0, n = pathArray.length; i < n; ++i) {
const key = pathArray[i]
if (key in result) {
result = result[key]
} else {
result = null
break
}
}
return result
}
module.exports = {
populateModel,
populateField,
populateData,
fillDictData
}