@hosoft/restful-api-framework
Version:
Base framework of the headless cms HoServer provided by http://helloreact.cn
583 lines (494 loc) • 20.7 kB
JavaScript
/**
* HoServer API Server Ver 2.0
* Copyright http://hos.helloreact.cn
*
* create: 2020/07/01
**/
const _ = require('lodash')
const Constants = require('../../base/constants/constants')
const ErrorCodes = require('../../base/constants/error-codes')
const InputConverter = require('../common/input-converter')
const Model = require('../../models/Model')
const mongoose = require('mongoose')
/**
* mongoose adapter
*/
class MongoAdapter extends Model {
constructor(modelMeta, nativeModel) {
super(modelMeta, nativeModel)
/**
* check and convert id
* @param id
*/
this.getObjectId = (id) => {
if (id) {
return typeof id === 'string' ? mongoose.Types.ObjectId(id) : id
} else {
return mongoose.Types.ObjectId()
}
}
/**
* count model
*/
this.count = async (query) => {
const countQuery = [{ $match: query }]
const groupByQuery = { _id: 1, count: { $sum: 1 } }
const groupBy = query.group_by
let propModel = null
if (groupBy) {
delete query.group_by
let subProperty = modelMeta
let curPath = ''
const routePaths = groupBy.split('.')
for (let i = 0; i < routePaths.length; i++) {
if (curPath) {
curPath += '.'
}
subProperty = subProperty.properties.find((item) => item.name === routePaths[i])
if (subProperty.prop_type.startsWith('array-of')) {
countQuery.push({ $unwind: '$' + curPath + subProperty.name })
}
curPath += subProperty.name
}
propModel = subProperty
groupByQuery._id = '$' + groupBy
}
countQuery.push({ $group: groupByQuery })
if (propModel) {
// TODO: sub model count
const outField = { $project: { _id: 0, count: 1 } }
outField.$project[propModel.name] = '$_id'
countQuery.push(outField)
return nativeModel.aggregate(countQuery)
} else {
const data = await nativeModel.aggregate(countQuery)
return data.length > 0 ? data[0].count : 0
}
}
/**
* find model list by given query condition
* @param query
*/
this.find = async (query, options, selectFields) => {
let result
if (!options) {
options = {}
}
if (!selectFields) {
selectFields = {}
}
const dbQuery = this._makeDbQuery(query)
// aggregate query
if (options.aggregate_query) {
const dataAggregate = []
if (options.sort) {
dataAggregate.push({ $sort: options.sort })
}
const limit = options.limit || options.page_size || Constants.PAGE_SIZE
let skip = 0
if (options.page) {
if (options.offset) {
skip = options.offset
} else {
skip = (options.page - 1) * limit
}
} else if (options.offset) {
skip = options.offset
}
dataAggregate.push({ $limit: skip + limit })
dataAggregate.push({ $skip: skip })
const aggregateQuery = [{ $match: dbQuery }, { $project: selectFields }]
let groupBy = options.group_by
if (groupBy) {
// TODO: 更多统计方法
const outFields = { _id: 0, count: '$count' }
outFields[groupBy] = '$_id'
const prop = this.getProperty(groupBy)
if (prop && prop.prop_type === 'date') {
groupBy = { $dateToString: { format: '%Y-%m-%d', date: '$' + groupBy } }
}
aggregateQuery.push({
$group: {
_id: typeof groupBy === 'string' ? '$' + groupBy : groupBy,
count: { $sum: 1 }
}
})
aggregateQuery.push({
$project: outFields
})
}
aggregateQuery.push({
$facet: {
list: dataAggregate,
pagination: [{ $count: 'total' }]
}
})
result = (await nativeModel.aggregate(aggregateQuery))[0]
if (!result || result.list.length === 0) {
return result
}
const total = result.pagination[0].total
const pageSize = options.limit || Constants.PAGE_SIZE
const current = options.page || 1
const pages = Math.ceil(total / pageSize)
result.pagination = {
total: total,
pageSize: pageSize,
pages: pages,
current: current,
next: Math.min(current + 1, pages)
}
} else if (options.paginate || options.page) {
const dbOptions = { ...options, select: selectFields }
dbOptions.lean = dbOptions.lean !== false
// query DB
result = await nativeModel.paginate(dbQuery, dbOptions)
if (!result || result.list.length === 0) {
return result
}
} else {
if (options.distinct) {
result = nativeModel.distinct(options.distinct, dbQuery)
} else {
result = nativeModel.find(dbQuery, selectFields)
}
if (options.limit) {
result = result.limit(options.limit)
}
if (options.sort) {
result = result.sort(options.sort)
}
if (options.lean !== false) {
result = result.lean()
}
}
return result
}
/**
* find model detail
*/
this.findOne = async (query, options, selectFields) => {
let result = null
try {
const dbQuery = this._makeDbQuery(query)
if (!options) {
options = {}
}
if (!selectFields) {
selectFields = {}
}
if (options.lean !== false) {
result = await nativeModel.findOne(dbQuery, selectFields).lean()
} else {
result = await nativeModel.findOne(dbQuery, selectFields)
}
} catch (ex) {
logger.error('findOne exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({ message: ex.message || ex.toString(), code: ErrorCodes.GENERAL_ERR_QUERY_FAIL })
}
return result
}
/**
* create model
* @param inputData object data
*/
this.create = async (inputData, options) => {
try {
this.makeId(inputData, '', true)
const newModel = await this.nativeModel.create(inputData)
const { name } = this.getIdField('')
return { [name]: newModel[name] }
} catch (ex) {
logger.error('create exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_CREATE_FAIL
})
}
}
this.createSub = async (propName, query, inputData, options) => {
const existRecord = await this.findOne(query, { lean: false })
if (!existRecord) {
return Promise.reject({ message: 'record not found', code: ErrorCodes.GENERAL_ERR_NOT_FOUND })
}
const subModel = this.getProperty(propName)
this.makeId(inputData, subModel, true)
const idField = this.getIdField('')
if (!(idField && existRecord[idField.name])) {
return Promise.reject({ message: 'invalid input', code: ErrorCodes.GENERAL_ERR_PARAM })
}
const modelNames = propName.split('.')
const propPath = propName.substr(this.name.length + 1)
// create modelMeta property
let subRecord = existRecord
for (let i = 1; i < modelNames.length; i++) {
subRecord = subRecord[modelNames[i]]
}
if (!(subRecord instanceof Array)) {
return Promise.reject({ message: 'property is not an array', code: ErrorCodes.GENERAL_ERR_PARAM })
}
const { name } = this.getIdField(propPath)
// TODO: object id equals
if (
subRecord.find((r) => (r[name].equals ? r[name].equals(inputData[name]) : r[name] === inputData[name]))
) {
return Promise.reject({
message: `${name} with ${inputData[name]} already exist`,
code: ErrorCodes.GENERAL_ERR_EXIST
})
}
// push new child to the array
subRecord.push(inputData)
try {
await existRecord.save()
return {
[idField.name]: existRecord[idField.name],
[`${propPath}.${name}`]: inputData[name]
}
} catch (ex) {
logger.error('createSub exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_CREATE_FAIL
})
}
}
/**
* update model record
*/
this.update = async (query, inputData, options) => {
if (_.isEmpty(query) && !_.get(options, 'force')) {
return Promise.reject({
message: 'unsafe update, please set force=true if you really want',
code: ErrorCodes.GENERAL_ERR_DELETE_FAIL
})
}
try {
const replace = query.replace || inputData.replace
delete query.replace
delete inputData.replace
if (replace) {
const existRecord = await this.findOne(query, { lean: false })
if (!existRecord) {
return Promise.reject({
message: 'record not find:' + this.name,
code: ErrorCodes.GENERAL_ERR_NOT_FOUND
})
}
this._setObjectProps(this, existRecord, inputData, true)
const result = await existRecord.save()
const { name } = this.getIdField('')
return { [name]: existRecord[name], result }
} else {
return this.nativeModel.updateMany(query, { $set: inputData })
}
} catch (ex) {
logger.error('update exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_UPDATE_FAIL
})
}
}
/**
* update model sub record
*/
this.updateSub = async (propName, query, inputData, options) => {
try {
const records = await this.find(query, { lean: false })
if (records.length === 0) {
return Promise.reject({
message: 'record not find:' + this.name,
code: ErrorCodes.GENERAL_ERR_NOT_FOUND
})
} else if (records.length > 1) {
return Promise.reject({
message: 'find more than one match records, please check query condition: ' + this.name,
code: ErrorCodes.GENERAL_ERR_UPDATE_FAIL
})
}
const existRecord = records[0]
const idField = this.getIdField('')
if (!(idField && existRecord[idField.name])) {
return Promise.reject({ message: 'invalid input', code: ErrorCodes.GENERAL_ERR_PARAM })
}
const propPath = propName.substr(this.name.length + 1)
const property = this.getProperty(propPath)
const replace = query.replace !== undefined ? !!query.replace : !!inputData.replace
delete inputData.replace
const subRecord = this.getObjectProp(existRecord, propName, query)
this._setObjectProps(property, subRecord, inputData, replace)
await existRecord.save()
// const { name } = this.getIdField(propPath)
// const subIdName = name ? `${propPath}.${name}` : propPath
return {
[idField.name]: existRecord[idField.name],
...query
}
} catch (ex) {
logger.error('updateSub exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_UPDATE_FAIL
})
}
}
/**
* batch update model records
*/
this.updateMany = async (dataList, options) => {
const bulkOperate = this._native.collection.initializeOrderedBulkOp()
const idField = this.getIdField('')
if (!idField) {
return Promise.reject({
message: 'model has no id field: ' + this.name,
code: ErrorCodes.GENERAL_ERR_UPDATE_FAIL
})
}
const result = []
const { name, type } = idField
for (const row of dataList) {
if (!(row[name] && row.data)) {
logger.warn('updateMany, invalid data: ' + JSON.stringify(row))
continue
}
const query = { [name]: InputConverter.convertData(row[name], type, true) }
bulkOperate.find(query).updateOne({ $set: row.data })
result.push(row[name])
}
await bulkOperate.execute()
return result
}
/**
* delete model
*/
this.delete = async (query, options) => {
try {
const { name } = this.getIdField('')
if (!(name && query[name])) {
return Promise.reject({ message: 'invalid input', code: ErrorCodes.GENERAL_ERR_PARAM })
}
const result = await nativeModel.deleteOne(query)
return { [name]: query[name], ...result }
} catch (ex) {
logger.error('delete exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_DELETE_FAIL
})
}
}
/**
* delete model sub record
*/
this.deleteSub = async (subModel, query, options) => {
const existRecord = await this.findOne(query, { lean: false })
if (!existRecord) {
return Promise.reject({ message: 'record not found', code: ErrorCodes.GENERAL_ERR_DELETE_FAIL })
}
const idField = this.getIdField('')
if (!(idField && existRecord[idField.name])) {
return Promise.reject({ message: 'invalid input', code: ErrorCodes.GENERAL_ERR_PARAM })
}
let propPath = subModel
const modelNamePrefix = `${this._meta.name}.`
if (propPath.indexOf(modelNamePrefix) === 0) {
propPath = propPath.substr(modelNamePrefix.length)
}
const subRecord = this.getObjectProp(existRecord, propPath)
const { name, type } = this.getIdField(propPath)
const subId = query[`${propPath}.${name}`]
// TODO: object id equals
if (type === Constants.API_FIELD_TYPE.objectId) {
_.remove(subRecord, (p) => p[name].equals(subId))
} else {
_.remove(subRecord, (p) => p[name] === subId)
}
try {
existRecord.markModified(propPath)
await existRecord.save()
return {
[idField.name]: existRecord[idField.name],
[`${propPath}.${name}`]: subId
}
} catch (ex) {
logger.error('deleteSub exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_DELETE_FAIL
})
}
}
/**
* batch delete model
*/
this.deleteMany = async (query, options) => {
try {
if (_.isEmpty(query) && !_.get(options, 'force')) {
return Promise.reject({
message: 'unsafe delete, please set force=true if you really want',
code: ErrorCodes.GENERAL_ERR_DELETE_FAIL
})
}
const result = await nativeModel.deleteMany(query)
return { query: query, ...result }
} catch (ex) {
logger.error('deleteMany exception: ' + ex.message + ', ' + ex.stack)
return Promise.reject({
message: ex.message || ex.toString(),
code: ErrorCodes.GENERAL_ERR_DELETE_FAIL
})
}
}
/**
* aggregate query
* @param query
* @returns {Promise<void>}
*/
this.aggregate = async (query) => {
return nativeModel.aggregate(query)
}
/**
* 1 is for mongodb
*/
this.getDbType = () => 1
}
_makeDbQuery(query) {
if (!query) {
return {}
}
const dbQuery = {}
for (const propPath in this._propertyList) {
if (query[propPath] !== undefined) {
const prop = this._propertyList[propPath]
dbQuery[propPath] = InputConverter.convertData(query[propPath], prop.prop_type, true)
}
}
return dbQuery
}
_setObjectProps(subModel, recordObj, inputObj, replace) {
if (!inputObj) return
for (const prop of subModel.properties) {
const queryKey = prop.name // parentName ? parentName + '.' + prop.name : prop.name
if (replace === true) {
// sub prop ignore unique
if (
(prop.input_flag !== 2 || inputObj[queryKey]) &&
(!prop.unique || (prop.array_level > 0 && inputObj[queryKey]))
) {
recordObj[prop.name] = inputObj[queryKey]
}
} else if (prop.properties && prop.properties.length > 0 && prop.prop_type.indexOf('array') < 0) {
const subModel = recordObj[prop.name]
if (subModel) {
this._setObjectProps(prop, subModel, inputObj[queryKey], replace)
}
} else if (inputObj[queryKey] !== undefined) {
if (prop.prop_type.indexOf('array') < 0 || inputObj[queryKey] instanceof Array) {
recordObj[prop.name] = inputObj[queryKey]
}
}
}
}
}
module.exports = MongoAdapter