UNPKG

mock-restful-api

Version:

This is dev support mock RESTful API. Refer to django-rest-framework implementation.

459 lines (444 loc) 14 kB
const { validateSubmitData } = require("./validator.js"); const utils = require("./utils.js"); const logger = require("./logger.js"); const { LookupEnum, MethodEnum } = require("./enums.js"); const FIELD_SEPARATOR = "__"; /** * 根据 fieldName 从 row 中找出对应的值 * @param {Ojbect} row * @param {string} fieldName * @returns */ const findRowValueByFieldName = (row, fieldName) => { let value; if (utils.isBlank(fieldName) || !utils.isDict(row)) { // 字段为空; row 为空或者数组时不做处理 return value; } value = row[fieldName]; if (value !== undefined) { // 有值直接取 return value; } const info = fieldName.split(FIELD_SEPARATOR); if (info.length === 1) { value = row[info[0]]; } else { // 递归层级寻找 const data = row[info[0]]; const childName = info.slice(1).join(FIELD_SEPARATOR); value = findRowValueByFieldName(data, childName); } return value; }; /** * 根据value的类型,转换targetValue值的类型与之对应 * @param {any} value * @param {any} targetValue * @returns */ const transTargetValueType = (value, targetValue) => { if (utils.isAbsNumber(value)) { if (utils.isArray(targetValue)) { targetValue = targetValue.map(v => (utils.isBlank(v) ? v : Number(v))); } else if (!utils.isBlank(targetValue)) { targetValue = Number(targetValue); if (Number.isNaN(targetValue)) { // 数字只能与数字比较 targetValue = undefined; } } } else if (utils.isAbsBoolean(value)) { const transToBool = v => (utils.isBooleanFalse(v) ? false : utils.isBooleanTrue(v) ? true : v); if (utils.isArray(targetValue)) { targetValue = targetValue.map(v => transToBool(v)); } else { targetValue = transToBool(targetValue); if (!utils.isAbsBoolean(targetValue)) { // 不是 boolean 对比 targetValue = undefined; } } } return targetValue; }; /** * 处理 "a,b" 这样的数据为 ["a", "b"] * @param {string} value * @returns array 返回值一定是数组 */ const parseCsvValue = value => { if (utils.isBlank(value)) { value = []; } else if (utils.isString(value)) { // 支持 key=a,b,c 类型的数据 value = value.split(","); } else if (!utils.isArray(value)) { value = [value]; } return value; }; /** * 将value和targetValue,根据不同的lookup进行不同的比较 * @param {any} value * @param {str} lookup * @param {any} targetValue * @returns bool, 满足条件返回true */ const compareValueByLookup = (value, lookup, targetValue) => { if (utils.isBlank(targetValue)) { // 无目标,则默认匹配 return true; } if (value === undefined && lookup !== LookupEnum.IS_NULL) { // isnull特殊; 其他值未定义的情况,一律匹配失败 return false; } if ((utils.isArray(targetValue) && targetValue.length === 0) || utils.isDict(targetValue)) { // 目标值数组不能为空;不能是字典 return false; } // 转换需要的类型 let success = false; switch (lookup) { case LookupEnum.EXACT: { targetValue = transTargetValueType(value, targetValue); // 不用 ===,query中解析出来的数字是字符串,兼容 1 == "1" success = value == targetValue; break; } case LookupEnum.IS_NULL: { if (utils.isBooleanTrue(targetValue)) { success = utils.isNull(value); } else if (utils.isBooleanFalse(targetValue)) { success = !utils.isNull(value); } else { success = false; } break; } case LookupEnum.IN: { // 用 == 为了兼容 1 == "1" 的场景 targetValue = parseCsvValue(targetValue); success = utils.isArray(targetValue) && targetValue.filter(v => v == value).length > 0; break; } case LookupEnum.STARTSWITH: { success = utils.isString(value) && value.startsWith(String(targetValue)); break; } case LookupEnum.ENDSWITH: { success = utils.isString(value) && value.endsWith(String(targetValue)); break; } case LookupEnum.CONTAINS: { success = utils.isString(value) && value.indexOf(String(targetValue)) > -1; break; } case LookupEnum.REGEX: { success = utils.isString(value) && value.match(new RegExp(targetValue)); break; } case LookupEnum.RANGE: { if (!utils.allowCompareRange(value)) { success = false; } else { targetValue = parseCsvValue(targetValue); // 默认query解析 range=a&range=b 可得到数组 if (targetValue.length > 2) { throw Error(`The range value must be an array of length 2.`); } let [start, end] = targetValue; if (utils.isBlank(start) && utils.isBlank(end)) { // 都为空,则通过 success = true; } else if (utils.isBlank(start)) { success = utils.allowCompareRange(end) && value <= end; } else if (utils.isBlank(end)) { success = utils.allowCompareRange(start) && value >= start; } else { success = utils.allowCompareRange(start) && utils.allowCompareRange(end) && value >= start && value <= end; } } break; } case LookupEnum.LT: { success = utils.allowCompareRange(value) && utils.allowCompareRange(targetValue) && value < targetValue; break; } case LookupEnum.LTE: { success = utils.allowCompareRange(value) && utils.allowCompareRange(targetValue) && value <= targetValue; break; } case LookupEnum.GT: { success = utils.allowCompareRange(value) && utils.allowCompareRange(targetValue) && value > targetValue; break; } case LookupEnum.GTE: { success = utils.allowCompareRange(value) && utils.allowCompareRange(targetValue) && value >= targetValue; break; } } return success; }; const handleFilterRows = (filterFields, searchFields, rows, query) => { if (utils.isBlank(query)) { return rows; } if (!utils.isDict(query)) { // 格式不对 return rows; } // 初始化 filters const filters = []; for (let fieldName in filterFields) { const lookups = filterFields[fieldName]; for (let lookup of lookups) { let value = query[`${fieldName}${FIELD_SEPARATOR}${lookup}`]; if (value === undefined && lookup === LookupEnum.EXACT) { value = query[fieldName]; } if (!utils.isBlank(value)) { // 不为空才有效 filters.push({ fieldName, lookup, targetValue: value }); } } } let results = rows.filter(row => { for (let item of filters) { const { fieldName, lookup, targetValue } = item; const value = findRowValueByFieldName(row, fieldName); const isMatch = compareValueByLookup(value, lookup, targetValue); if (!isMatch) { // 只要有未匹配到的,直接结束 return false; } } const { search } = query; if (utils.isBlank(search) || !utils.isArray(searchFields) || searchFields.length === 0) { // 无search, 上面filters又全部通过 return true; } // 处理 searchFields for (let fieldName of searchFields) { const value = findRowValueByFieldName(row, fieldName); const isMatch = compareValueByLookup(value, LookupEnum.CONTAINS, search); if (isMatch) { // 只要有一个匹配到的,则算搜索成功 return true; } } // search 所有未匹配 return false; }); return results; }; const handleSortRows = (rows, ordering, orderingFields) => { const results = [...rows]; let orderList = parseCsvValue(ordering); if (orderList.length === 0) { // 不排序 return results; } if (!utils.isArray(orderingFields) || orderingFields.length === 0) { // 未配置搜索条件 logger.warn(`ordering=${ordering} is not effective because "orderingFields" is not configured.`); return results; } orderList = orderList .map(order => { let isAsc = true; // 默认升序 let fieldName = order; if (fieldName.startsWith("-")) { isAsc = false; fieldName = fieldName.substring(1); } else if (fieldName.startsWith("+")) { fieldName = fieldName.substring(1); } if (!orderingFields.includes(fieldName)) { // 限定了排序范围 fieldName = undefined; } return { isAsc, fieldName, order }; }) .filter(item => item.fieldName); if (orderList.length > 0) { results.sort((a, b) => { // 返回值: >0 a在b后;<0 a在b前;=0 保持a、b顺序不变 let ret = 0; for (let item of orderList) { let { isAsc, fieldName } = item; let _ret; const v1 = findRowValueByFieldName(a, fieldName); const v2 = findRowValueByFieldName(b, fieldName); if (v1 === v2) { continue; } _ret = compareValueByLookup(v1, LookupEnum.LT, v2); if (_ret) { // v1 < v2 ret = isAsc ? -1 : 1; break; } else { // v1 > v2 ret = isAsc ? 1 : -1; break; } } return ret; }); } return results; }; const queryRows = (query, config) => { const { filter_fields: filterFields, search_fields: searchFields, ordering: defaultOrdering, ordering_fields: orderingFields, rows } = config; // 刷选 + 搜索 let results = handleFilterRows(filterFields, searchFields, rows, query); // 排序 if (utils.isDict(query)) { const { ordering } = query; results = handleSortRows(results, ordering || defaultOrdering, orderingFields); } // 返回结果 return results; }; const findRowByPk = (pkValue, query, config) => { let results = queryRows(query, config); const { pk_field: pkField } = config; results = results.filter(row => utils.isDict(row) && row[pkField] == pkValue); const row = results.length > 0 ? results[0] : undefined; return row; }; const genRowCreateNewPk = (pkField, rows) => { const pks = rows.map(item => item[pkField]).filter(v => v && utils.isNumber(v)); let last = 0; if (pks.length > 0) { pks.sort(); // 排序 last = pks[pks.length - 1]; } const pk = Number(last) + 1; return pk; }; const initRestfulResponse = (req, filePath, route) => { const configData = global.jsonConfig[filePath]; if (!utils.isDict(configData)) { return { code: 404, text: `path=${route.path} Not Found` }; } let response = {}; const { query } = req; const { config } = configData; const { detail } = route; const { pk_field: pkField, rules, page_size: defaultSize } = config; let { rows } = config; if (!detail) { switch (req.method.toUpperCase()) { case MethodEnum.GET: { // 列表 const results = queryRows(query, config); // 分页 const page = Number(query?.page || 1); const pageSize = Number(query?.page_size || defaultSize); const pageRows = results.slice((page - 1) * pageSize, page * pageSize); response = { json: { count: results.length, results: pageRows } }; break; } case MethodEnum.POST: { // 创建 const row = req.body; try { validateSubmitData(row, rules); if (utils.isNull(row[pkField])) { // 没有pk,自动生成一个 row[pkField] = genRowCreateNewPk(pkField, rows); } rows.push(row); response = { json: row, code: 201 }; } catch (err) { logger.error(`${req.method} ${req.path} error: ${err}`); response = { json: { detail: err.message }, code: 400 }; } break; } default: { response = { code: 405, json: { detail: `Method "${req.method}" not allowed.` } }; break; } } } else { if (!utils.isDict(req.params) || utils.isBlank(req.params.pk)) { response = { code: 404, text: "Not Found params.pk" }; return response; } const pkValue = req.params.pk; const row = findRowByPk(pkValue, query, config); if (!row) { response = { code: 404, text: "Not Found by query" }; } else { switch (req.method) { case MethodEnum.GET: { // 详情 response = { json: row, code: 200 }; break; } case MethodEnum.PUT: case MethodEnum.PATCH: { // 更新 const data = req.body || {}; const partial = req.method === MethodEnum.PATCH; try { validateSubmitData(data, rules, partial); if (!partial) { // 非局部更新、移除所有 for (let key in row) { if (key !== pkField) { // 不删除主键 delete row[key]; } } } for (let key in data) { if (key === pkField) { // 不更新主键 continue; } row[key] = data[key]; } response = { json: row, code: 200 }; } catch (err) { logger.error(err); response = { json: { detail: err.message }, code: 400 }; } break; } case MethodEnum.DELETE: { // 删除 let idx; for (idx = 0; idx < rows.length; idx++) { if (rows[idx][pkField] == row[pkField]) { break; } } rows.splice(idx, 1); response = { json: row, code: 204 }; break; } default: { response = { code: 405, json: { detail: `Method "${req.method}" not allowed.` } }; break; } } } } return response; }; module.exports = { findRowValueByFieldName, transTargetValueType, compareValueByLookup, handleFilterRows, handleSortRows, queryRows, findRowByPk, genRowCreateNewPk, initRestfulResponse };