UNPKG

@hosoft/restful-api-framework

Version:

Base framework of the headless cms HoServer provided by http://helloreact.cn

698 lines (599 loc) 25.2 kB
/** * 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') const QueryConverter = require('./query-converter') const { Sequelize } = require('sequelize') const JSON_TYPES = [ Constants.API_FIELD_TYPE.array, Constants.API_FIELD_TYPE.mix, Constants.API_FIELD_TYPE.object, Constants.API_FIELD_TYPE['array-of-object'], Constants.API_FIELD_TYPE['array-of-boolean'], Constants.API_FIELD_TYPE['array-of-char'], Constants.API_FIELD_TYPE['array-of-number'], Constants.API_FIELD_TYPE['array-of-objectId'] ] /** * adapter for relation database */ class RdbAdapter extends Model { constructor(modelMeta, nativeModel) { super(modelMeta, nativeModel) /** * check and convert id * @param id */ this.getObjectId = (id) => { if (!id) { return String(mongoose.Types.ObjectId()) } if (typeof id === 'object') { return String(id) } return id } /** * count model */ this.count = async (query, groupBy, options) => { let result const dbQuery = this._makeDbQuery(query) const queryOptions = { where: dbQuery, } if (options && options.logging) { queryOptions.logging = options.logging delete options.logging } if (groupBy) { queryOptions.group = groupBy instanceof Array ? groupBy : [groupBy] queryOptions.attributes = [] if (groupBy instanceof Array) { queryOptions.attributes = _.concat(queryOptions.attributes, groupBy) } else { queryOptions.attributes.push(groupBy) } queryOptions.attributes.push([Sequelize.fn('count', Sequelize.col('*')), 'count']) queryOptions.raw = true result = await nativeModel.findAll(queryOptions) } else { result = await nativeModel.count(queryOptions) } return result } /** * find model list by given query condition * @param query */ this.find = async (query, options, selectFields) => { let result if (!options) { options = {} } const dbQuery = this._makeDbQuery(query) const queryOptions = { where: dbQuery, attributes: QueryConverter.convertOutputFields(selectFields) } if (options && options.logging) { queryOptions.logging = options.logging delete options.logging } if (options.distinct) { let dictinctFields = options.distinct if (typeof dictinctFields === 'string') { dictinctFields = [dictinctFields] } if (!queryOptions.attributes) { queryOptions.attributes = [] } for (const f of dictinctFields) { queryOptions.attributes.push([Sequelize.fn('DISTINCT', Sequelize.col(f)), f]) } } if (options.sort) { queryOptions.order = QueryConverter.convertOrder(options.sort) } // keep compatible if (options.order) { queryOptions.order = QueryConverter.convertOrder(options.order) } const limit = options.limit || options.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 } if (limit) { queryOptions.limit = limit } if (skip) { queryOptions.offset = skip } let groupBy = options.group_by if (groupBy) { const prop = this.getProperty(groupBy) if (prop && prop.prop_type === 'date') { groupBy = Sequelize.fn('date_trunc', 'day', Sequelize.col(groupBy)) } queryOptions.group = groupBy } if (_.get(options, 'lean') !== false) { queryOptions.raw = true } if (options.paginate || options.page) { const dbResult = await nativeModel.findAndCountAll(queryOptions) const total = dbResult.count const pageSize = limit || Constants.PAGE_SIZE const current = options.page || 1 const pages = Math.ceil(total / pageSize) const dataList = dbResult.rows if (dataList && dataList.length > 0) { for (const row of dataList) { this._removeUnwantedFields(row, this.meta) } } result = { pagination: { total: total, pageSize: pageSize, pages: pages, current: current, prev: current > 1 ? current - 1 : undefined, next: current + 1 <= pages ? current + 1 : undefined }, list: dataList } } else { result = await nativeModel.findAll(queryOptions) if (result && result.length > 0) { for (const row of result) { this._removeUnwantedFields(row, this.meta) } } } return result } /** * find model detail */ this.findOne = async (query, options, selectFields) => { let result = null try { const dbQuery = this._makeDbQuery(query) const queryOptions = { where: dbQuery, attributes: QueryConverter.convertOutputFields(selectFields) } if (options && options.logging) { queryOptions.logging = options.logging delete options.logging } if (_.get(options, 'lean') !== false) { queryOptions.raw = true } result = await nativeModel.findOne(queryOptions) } catch (ex) { logger.error('findOne exception: ' + ex.message + ', ' + ex.stack) return Promise.reject({ message: ex.message || ex.toString(), code: ErrorCodes.GENERAL_ERR_QUERY_FAIL }) } if (result) { this._removeUnwantedFields(result, this.meta) } 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, options) 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) // equals for objectId 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 { existRecord.changed(propPath.split('.')[0], true) await existRecord.save(options) 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 { name } = this.getIdField('') const replace = query.replace || inputData.replace delete query.replace delete inputData.replace // delete inputData[name] - 无法更新 model sync 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) let result = await existRecord.save(options) result = _.get(result, '_changed') || {} return { [name]: existRecord[name], result } } else { const dbQuery = this._makeDbQuery(query) const updatedCount = await this.nativeModel.update(inputData, { where: dbQuery }, options) return { nModified: updatedCount[0] } } } 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 matched records, please check query condition', 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) existRecord.changed(propPath.split('.')[0], true) await existRecord.save(options) // 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 * TODO: update 和 updateMany 这块不清楚,update 还做了更新多个的事情 */ this.updateMany = async (dataList, options) => { 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 } = idField for (const row of dataList) { if (!(row[name] && row.data)) { logger.warn('updateMany, invalid data: ' + JSON.stringify(row)) continue } this.nativeModel.update(row.data, { where: { [name]: row[name] } }, options) result.push(row[name]) } 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 dbQuery = this._makeDbQuery(query) const deletedCount = await nativeModel.destroy({ where: dbQuery }, options) return { [name]: query[name], deletedCount } } 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 } = this.getIdField(propPath) const subId = query[`${propPath}.${name}`] _.remove(subRecord, (p) => (p[name].equals ? p[name].equals(subId) : p[name] === subId)) try { existRecord.changed(propPath.split('.')[0], true) await existRecord.save(options) 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 dbQuery = this._makeDbQuery(query) const deletedCount = await nativeModel.destroy({ where: dbQuery }, options) return { query: query, deletedCount } } 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) } /** * sync model with database after model schema updated by admin * @param query * @returns {Promise<void>} */ this.sync = async (query) => { return nativeModel.sync({ force: false, alter: true }) } /** * 2 is for rdb */ this.getDbType = () => 2 } _removeUnwantedFields(result, modelMeta) { for (const prop of modelMeta.properties) { const propVal = result[prop.name] if (propVal == undefined) { continue } if (!prop.output_flag_mod) { delete result[prop.name] } else if (prop.properties && prop.properties.length > 0) { if (propVal instanceof Array) { for (const v of propVal) { if (v && typeof propVal !== 'object') { break } this._removeUnwantedFields(v, prop) } } else if (typeof propVal === 'object') { this._removeUnwantedFields(propVal, prop) } } } } _makeDbQuery(query) { if (!query) { return {} } const dbQuery = {} for (const key in query) { if (key[0] === '$') { const subQuery = query[key] if (subQuery instanceof Array) { if (!dbQuery[key]) { dbQuery[key] = [] } for (const subq of subQuery) { const subDbq = this._makeDbQuery(subq) dbQuery[key].push(subDbq) } } else { dbQuery[key] = this._makeDbQuery(subQuery) } } } for (const propPath in this._propertyList) { if (query[propPath] !== undefined) { const parts = propPath.split('.') let hasArray = false let curPath = parts[0] let prop = this._propertyList[curPath] if (JSON_TYPES.includes(prop.prop_type)) { let queryPath = curPath + '->"$' if (prop.prop_type.indexOf('array') > -1) { queryPath += '[*]' hasArray = true } for (let i = 1; i < parts.length; i++) { curPath += '.' + parts[i] prop = this._propertyList[curPath] queryPath += '.' + prop.name if (prop.prop_type.indexOf('array') > -1) { queryPath += '[*]' hasArray = true } } queryPath += '"' // now is last prop if (!dbQuery.$and) { dbQuery.$and = [] } let queryData = InputConverter.convertData(query[propPath], prop.prop_type) if (prop.prop_type.indexOf('array') > -1) { if (queryData && typeof queryData === 'string') { queryData = `"${queryData.replace('"', '\\\\"')}"` } dbQuery.$and.push( Sequelize.literal(`json_contains(${queryPath}, '${queryData.replace("'", "\\'")}')`) ) } else if (hasArray) { const parts = queryPath.split('->') dbQuery.$and.push( Sequelize.literal( `json_search(\`${parts[0]}\`, 'one', '${queryData.replace("'", "\\'")}', null, ${ parts[1] || null }) is not null` ) ) } else if ( prop.prop_type === Constants.API_FIELD_TYPE.number || prop.prop_type === Constants.API_FIELD_TYPE.boolean ) { dbQuery.$and.push(Sequelize.literal(`${queryPath} = ${queryData}`)) } else { dbQuery.$and.push(Sequelize.literal(`${queryPath} = '${queryData.replace("'", "\\'")}'`)) } } else { dbQuery[propPath] = InputConverter.convertData(query[propPath], prop.prop_type) } } } 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 = RdbAdapter