egg-jianghu
Version:
egg-jianghu
328 lines (292 loc) • 10 kB
JavaScript
;
const { BizError, errorInfoEnum } = require('../../constant/error');
const _ = require('lodash');
const validateUtil = require('../../common/validateUtil');
const commonUtil = require("../../common/commonUtil");
function validate(ctx, body) {
const { resourceData, appDataSchema } = ctx.packageResource;
const { operation } = resourceData;
const appData = body.appData || {};
const actionData = appData.actionData || {};
if (
![
"select",
"insert",
"update",
"delete",
"jhInsert",
"jhUpdate",
"jhDelete",
].includes(operation)
) {
throw new BizError(errorInfoEnum.resource_sql_operation_invalid);
}
if (!_.isEmpty(appDataSchema)) {
validateUtil.validate(appDataSchema, appData, "appData");
}
// 创建 or 更新时不能指定 主键id ===> 避免无操作
delete actionData.id;
}
/**
* 构建 Where 语句
* @param jianghuKnex
* @param ctx
* @param requestBody
*/
async function buildWhereCondition(jianghuKnex, ctx, requestBody) {
const appData = requestBody.appData || {};
const { resourceData, accessControlTable } = ctx.packageResource;
const userInfo = ctx.userInfo || {};
const { userId } = userInfo || {};
// insert 不需要 where 语句
if (resourceData.operation === "insert") {
return "";
}
// 服务端部分,主要来自 resource 数据的 resourceData
// 但如果配置了 dataAccessControl 数据,则以 dataAccessControl 的 resourceData 为准
let accessControl = null;
if (accessControlTable) {
accessControl = await jianghuKnex(accessControlTable)
.where({ userId })
.first();
}
const backendResourceData = accessControl
? JSON.parse(accessControl.resourceData || "{}")
: resourceData;
backendResourceData.table = resourceData.table;
backendResourceData.operation = resourceData.operation;
const backendWhere = await buildWhereConditionFromResourceData(
backendResourceData,
ctx,
userInfo
);
// 前端部分,来自前端传过来的 actionData,不支持 whereKnex
delete appData.whereKnex;
const frontendWhere = await buildWhereConditionFromAppData(appData);
return backendWhere + frontendWhere;
}
/**
* 从 resourceData 构建 Where 语句
* (ctx, userInfo 参数用于数据 eval 环境)
* @param resourceData
* @param ctx
* @param userInfo
*/
// eslint-disable-next-line no-unused-vars
async function buildWhereConditionFromResourceData(
resourceData,
ctx,
userInfo
) {
if (!resourceData) {
return "";
}
const backendAppData = {};
// 如:{ "where": { "field1": "ctx.someData" } }
["where", "whereLike", "whereIn"].forEach((appDataKey) => {
const expressionObject = resourceData[appDataKey];
if (!expressionObject) {
return;
}
const valueObject = {};
_.forEach(expressionObject, (value, key) => {
// eslint-disable-next-line no-eval
valueObject[key] = commonUtil.eval({ evalString: value, ctx, userInfo });
});
backendAppData[appDataKey] = valueObject;
});
// 如:{ "whereOptions": "ctx.someList" }
["whereOptions", "whereOrOptions"].forEach((appDataKey) => {
const expressionObject = resourceData[appDataKey];
if (!expressionObject) {
return;
}
const valueObject = [];
_.forEach(expressionObject, (value, key) => {
// eslint-disable-next-line no-eval
const evalString = value[value.length - 1];
value[value.length - 1] = commonUtil.eval({ evalString, ctx, userInfo });
valueObject.push(value);
});
backendAppData[appDataKey] = valueObject;
});
return buildWhereConditionFromAppData(backendAppData);
}
/**
* 从 appData 构建 Where 语句
*
* @param appData
* @param appData.where
* @param appData.whereLike
* @param appData.whereIn
* @param appData.whereOptions
* @param appData.whereOrOptions
* @param appData.limit
* @param appData.offset
* @param appData.orderBy
* @param appData.whereKnex
*/
async function buildWhereConditionFromAppData({
where = {},
whereLike = {},
whereIn = {},
whereOptions = [],
whereOrOptions = [],
whereKnex = "",
limit,
offset,
orderBy = [],
}) {
// where
let wherePart = "";
if (!_.isEmpty(where)) {
wherePart = `.where(${JSON.stringify(where)})`;
}
// whereLike
let whereLikePart = "";
if (!_.isEmpty(whereLike)) {
for (const key in whereLike) {
const value = whereLike[key] || "";
whereLikePart = whereLikePart + `.where('${key}', 'like', '%${value}%')`;
}
}
// whereIn
let whereInPart = "";
if (!_.isEmpty(whereIn)) {
Object.entries(whereIn).forEach(([key, value]) => {
whereInPart += `.whereIn('${key}', ${JSON.stringify(value)})`;
});
}
// whereOptions: [['name', '=', 'zhangshan'],['level', '>', 3],['a', 100]]
let whereOptionsPart = "";
if (!_.isEmpty(whereOptions)) {
whereOptions.forEach((whereOption) => {
if (whereOption.length === 3) {
whereOptionsPart += `.where('${whereOption[0]}', '${whereOption[1]}', '${whereOption[2]}')`;
} else if (whereOption.length === 2) {
whereOptionsPart += `.where('${whereOption[0]}', '${whereOption[1]}')`;
} else {
throw new BizError(errorInfoEnum.resource_sql_where_options_invalid);
}
});
}
// whereOrOptions,相当于 and ( statement1 or statement2 )
// [['name', '=', 'zhangshan'],['level', '>', 3],['a', 100]]
let whereOrOptionsPart = "";
if (!_.isEmpty(whereOrOptions)) {
whereOrOptionsPart += ".where(function() { this";
whereOrOptions.forEach((whereOrOption) => {
if (whereOrOption.length === 3) {
whereOrOptionsPart += `.orWhere('${whereOrOption[0]}', '${whereOrOption[1]}', '${whereOrOption[2]}')`;
} else if (whereOrOption.length === 2) {
whereOrOptionsPart += `.orWhere('${whereOrOption[0]}', '${whereOrOption[1]}')`;
} else {
throw new BizError(errorInfoEnum.resource_sql_where_options_invalid);
}
});
whereOrOptionsPart += "})";
}
// limit offset
let limitAndOffset = "";
if (limit) {
limitAndOffset += `.limit(${limit})`;
}
if (offset) {
limitAndOffset += `.offset(${offset})`;
}
// orderBy:.orderBy([{ column: 'email' }, { column: 'age', order: 'desc' }])
let orderByPart = "";
if (!_.isEmpty(orderBy)) {
orderByPart = `.orderBy(${JSON.stringify(orderBy)})`;
}
return (
wherePart +
whereLikePart +
whereInPart +
whereOptionsPart +
whereOrOptionsPart +
whereKnex +
orderByPart +
limitAndOffset
);
}
async function runKnexFunction(knexFunctionString, args = {}) {
// eslint-disable-next-line no-empty-function
const AsyncFunction = Object.getPrototypeOf(async function() {
}).constructor;
const knexCommandCountFunc = new AsyncFunction(..._.keys(args), knexFunctionString);
return await knexCommandCountFunc(..._.values(args));
}
/**
* 执行 sql resource
*
* actionData 数据参数
* where jianghuKnex 查询条件
* whereLike 模糊查询
* whereOrOptions or查询
* whereOptions jianghuKnex 原生的 where 三元查询
* - [['name', '=', 'zhangshan'],['level', '>', 3]]
* whereIn in查询
* whereKnex 直接写 knex 语句,只在 resourceData 中有效
* offset, limit 分页查询
* - .limit(10).offset(30)
* orderBy 排序
* - .orderBy([{ column: 'email' }, { column: 'age', order: 'desc' }])
* fields 要查询的字段,不传表示查询所有字段 (fields 字段暂时只能配在 resource 表中)
* - ["id", ...]
*
* @param root0
* @param root0.jianghuKnex
* @param root0.ctx
*/
async function sqlResource({ jianghuKnex, ctx }) {
const requestBody = ctx.request.body;
const appData = requestBody.appData || {};
const actionData = appData.actionData || {};
const { resourceData } = ctx.packageResource;
const { table, operation, fields } = resourceData;
const { limit } = appData;
// 校验并处理数据
validate(ctx, requestBody);
// 1. where 构建:前后端合并
const whereCondition = await buildWhereCondition(jianghuKnex, ctx, requestBody);
// 2. 翻页场景需要 count 计算
let count;
if (limit) {
let knexCommandCountString = `return await jianghuKnex('${table}')${whereCondition}.count('*', {as: 'count'});`;
// 去掉 limit, offset, orderBy
knexCommandCountString = knexCommandCountString.replace(/\.limit\([^\)]+\)/, '').replace(/\.offset\([^\)]+\)/, '').replace(/\.orderBy\([^\)]+\)/, '');
const result = await runKnexFunction(knexCommandCountString, { jianghuKnex, actionData, ctx });
count = result[0].count;
}
// 3. jianghuKnex 执行
let rows = null;
await jianghuKnex.transaction(async trx => {
let knexArgs = [ 'select', 'delete', 'jhDelete' ].includes(operation) ? '' : 'actionData';
if (operation === 'select' && !_.isEmpty(fields)) {
knexArgs = 'fields';
}
const knexCommandCountString = `return await trx('${table}', ctx)${whereCondition}.${operation}(${knexArgs});`;
rows = await runKnexFunction(knexCommandCountString, { trx, actionData, fields, ctx });
});
return { rows, count };
}
async function serviceResource({ ctx }) {
const requestBody = ctx.request.body;
const appData = requestBody.appData || {};
const actionData = appData.actionData || {};
const { packageResource: { resourceData } } = ctx;
const { service, serviceFunction } = resourceData;
const serviceTmp = ctx.service[service];
if (!serviceTmp) {
throw new BizError(errorInfoEnum.resource_service_not_found);
}
const serviceFunctionTmp = serviceTmp[serviceFunction];
if (!serviceFunctionTmp) {
throw new BizError(errorInfoEnum.resource_service_method_not_found);
}
// 注意: 这里必须 'ctx.service[serviceName][methodName]' 这样 写; 否则service无法获取egg 相关属性
return await ctx.service[service][serviceFunction](actionData, ctx);
}
module.exports.sqlResource = sqlResource;
module.exports.serviceResource = serviceResource;