yapi-next/vendors/server/middleware/mockServer.js
2023-06-27 18:59:45 +08:00

387 lines
12 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const yapi = require('../yapi.js');
const projectModel = require('../models/project.js');
const interfaceModel = require('../models/interface.js');
const mockExtra = require('../../common/mock-extra.js');
const { schemaValidator } = require('../../common/utils.js');
const _ = require('underscore');
const Mock = require('mockjs');
const variable = require('../../client/constants/variable.js')
/**
*
* @param {*} apiPath /user/tom
* @param {*} apiRule /user/:username
*/
function matchApi(apiPath, apiRule) {
let apiRules = apiRule.split('/');
let apiPaths = apiPath.split('/');
let pathParams = {
__weight: 0
};
if (apiPaths.length !== apiRules.length) {
return false;
}
for (let i = 0; i < apiRules.length; i++) {
if (apiRules[i]) {
apiRules[i] = apiRules[i].trim();
} else {
continue;
}
if (
apiRules[i].length > 2 &&
apiRules[i][0] === '{' &&
apiRules[i][apiRules[i].length - 1] === '}'
) {
pathParams[apiRules[i].substr(1, apiRules[i].length - 2)] = apiPaths[i];
} else if (apiRules[i].indexOf(':') === 0) {
pathParams[apiRules[i].substr(1)] = apiPaths[i];
} else if (
apiRules[i].length > 2 &&
apiRules[i].indexOf('{') > -1 &&
apiRules[i].indexOf('}') > -1
) {
let params = [];
apiRules[i] = apiRules[i].replace(/\{(.+?)\}/g, function(src, match) {
params.push(match);
return '([^\\/\\s]+)';
});
apiRules[i] = new RegExp(apiRules[i]);
if (!apiRules[i].test(apiPaths[i])) {
return false;
}
let matchs = apiPaths[i].match(apiRules[i]);
params.forEach((item, index) => {
pathParams[item] = matchs[index + 1];
});
} else {
if (apiRules[i] !== apiPaths[i]) {
return false;
}else{
pathParams.__weight++;
}
}
}
return pathParams;
}
function parseCookie(str) {
if (!str || typeof str !== 'string') {
return str;
}
if (str.split(';')[0]) {
let c = str.split(';')[0].split('=');
return { name: c[0], value: c[1] || '' };
}
return null;
}
function handleCorsRequest(ctx) {
let header = ctx.request.header;
ctx.set('Access-Control-Allow-Origin', header.origin);
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEADER, PATCH, OPTIONS');
ctx.set('Access-Control-Allow-Headers', header['access-control-request-headers']);
ctx.set('Access-Control-Allow-Credentials', true);
ctx.set('Access-Control-Max-Age', 1728000);
ctx.body = 'ok';
}
// 必填字段是否填写好
function mockValidator(interfaceData, ctx) {
let i,
j,
l,
len,
noRequiredArr = [];
let method = interfaceData.method.toUpperCase() || 'GET';
// query 判断
for (i = 0, l = interfaceData.req_query.length; i < l; i++) {
let curQuery = interfaceData.req_query[i];
if (curQuery && typeof curQuery === 'object' && curQuery.required === '1') {
if (!ctx.query[curQuery.name]) {
noRequiredArr.push(curQuery.name);
}
}
}
// form 表单判断
if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'form') {
for (j = 0, len = interfaceData.req_body_form.length; j < len; j++) {
let curForm = interfaceData.req_body_form[j];
if (curForm && typeof curForm === 'object' && curForm.required === '1') {
if (
ctx.request.body[curForm.name] ||
(ctx.request.body.fields && ctx.request.body.fields[curForm.name]) ||
(ctx.request.body.files && ctx.request.body.files[curForm.name])
) {
continue;
}
noRequiredArr.push(curForm.name);
}
}
}
let validResult;
// json schema 判断
if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'json' && interfaceData.req_body_is_json_schema === true) {
const schema = yapi.commons.json_parse(interfaceData.req_body_other);
const params = yapi.commons.json_parse(ctx.request.body);
validResult = schemaValidator(schema, params);
}
if (noRequiredArr.length > 0 || (validResult && !validResult.valid)) {
let message = `错误信息:`;
message += noRequiredArr.length > 0 ? `缺少必须字段 ${noRequiredArr.join(',')} ` : '';
message += validResult && !validResult.valid ? `schema 验证请求参数 ${validResult.message}` : '';
return {
valid: false,
message
};
}
return { valid: true };
}
module.exports = async (ctx, next) => {
// no used variable 'hostname' & 'config'
// let hostname = ctx.hostname;
// let config = yapi.WEBCONFIG;
let path = ctx.path;
let header = ctx.request.header;
if (path.indexOf('/mock/') !== 0) {
if (next) await next();
return true;
}
let paths = path.split('/');
let projectId = paths[2];
paths.splice(0, 3);
path = '/' + paths.join('/');
ctx.set('Access-Control-Allow-Origin', header.origin);
ctx.set('Access-Control-Allow-Credentials', true);
// ctx.set('Access-Control-Allow-Origin', '*');
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 400, 'projectId不能为空'));
}
let projectInst = yapi.getInst(projectModel),
project;
try {
project = await projectInst.get(projectId);
} catch (e) {
return (ctx.body = yapi.commons.resReturn(null, 403, e.message));
}
if (!project) {
return (ctx.body = yapi.commons.resReturn(null, 400, '不存在的项目'));
}
let interfaceData, newpath;
let interfaceInst = yapi.getInst(interfaceModel);
try {
newpath = path.substr(project.basepath.length);
interfaceData = await interfaceInst.getByPath(project._id, newpath, ctx.method);
let queryPathInterfaceData = await interfaceInst.getByQueryPath(project._id, newpath, ctx.method);
//处理query_path情况 url 中有 ?params=xxx
if (!interfaceData || interfaceData.length != queryPathInterfaceData.length) {
let i,
l,
j,
len,
curQuery,
match = false;
for (i = 0, l = queryPathInterfaceData.length; i < l; i++) {
match = false;
let currentInterfaceData = queryPathInterfaceData[i];
curQuery = currentInterfaceData.query_path;
if (!curQuery || typeof curQuery !== 'object' || !curQuery.path) {
continue;
}
for (j = 0, len = curQuery.params.length; j < len; j++) {
if (ctx.query[curQuery.params[j].name] !== curQuery.params[j].value) {
continue;
}
if (j === len - 1) {
match = true;
}
}
if (match) {
interfaceData = [currentInterfaceData];
break;
}
// if (i === l - 1) {
// interfaceData = [];
// }
}
}
//处理动态路由
if (!interfaceData || interfaceData.length === 0) {
let newData = await interfaceInst.getVar(project._id, ctx.method);
let findInterface;
let weight = 0;
_.each(newData, item => {
let m = matchApi(newpath, item.path);
if (m !== false) {
if(m.__weight >= weight){
findInterface = item;
}
delete m.__weight;
ctx.request.query = Object.assign(m, ctx.request.query);
return true;
}
return false;
});
if (!findInterface) {
//非正常跨域预检请求回应
if (ctx.method === 'OPTIONS' && ctx.request.header['access-control-request-method']) {
return handleCorsRequest(ctx);
}
return (ctx.body = yapi.commons.resReturn(
null,
404,
`不存在的api, 当前请求path为 ${newpath} 请求方法为 ${
ctx.method
} ,请确认是否定义此请求。`
));
}
interfaceData = [await interfaceInst.get(findInterface._id)];
}
if (interfaceData.length > 1) {
return (ctx.body = yapi.commons.resReturn(null, 405, '存在多个api请检查数据库'));
} else {
interfaceData = interfaceData[0];
}
// 必填字段是否填写好
if (project.strice) {
const validResult = mockValidator(interfaceData, ctx);
if (!validResult.valid) {
return (ctx.body = yapi.commons.resReturn(
null,
404,
`接口字段验证不通过, ${validResult.message}`
));
}
}
let res;
// mock 返回值处理
res = interfaceData.res_body;
try {
if (interfaceData.res_body_type === 'json') {
if (interfaceData.res_body_is_json_schema === true) {
//json-schema
const schema = yapi.commons.json_parse(interfaceData.res_body);
res = yapi.commons.schemaToJson(schema, {
alwaysFakeOptionals: true
});
} else {
// console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data'))
// 处理 format-data
if (
_.isString(ctx.request.header['content-type']) &&
ctx.request.header['content-type'].indexOf('multipart/form-data') > -1
) {
ctx.request.body = ctx.request.body.fields;
}
// console.log('body', ctx.request.body)
res = mockExtra(yapi.commons.json_parse(interfaceData.res_body), {
query: ctx.request.query,
body: ctx.request.body,
params: Object.assign({}, ctx.request.query, ctx.request.body)
});
// console.log('res',res)
}
try {
res = Mock.mock(res);
} catch (e) {
console.log('err', e.message);
yapi.commons.log(e, 'error');
}
}
let context = {
projectData: project,
interfaceData: interfaceData,
ctx: ctx,
mockJson: res,
resHeader: {},
httpCode: 200,
delay: 0
};
if (project.is_mock_open && project.project_mock_script) {
// 项目层面的mock脚本解析
let script = project.project_mock_script;
await yapi.commons.handleMockScript(script, context);
}
await yapi.emitHook('mock_after', context);
let handleMock = new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, context.delay);
});
await handleMock;
if (context.resHeader && typeof context.resHeader === 'object') {
for (let i in context.resHeader) {
let cookie;
if (i === 'Set-Cookie') {
if (context.resHeader[i] && typeof context.resHeader[i] === 'string') {
cookie = parseCookie(context.resHeader[i]);
if (cookie && typeof cookie === 'object') {
ctx.cookies.set(cookie.name, cookie.value, {
maxAge: 864000000,
httpOnly: false
});
}
} else if (context.resHeader[i] && Array.isArray(context.resHeader[i])) {
context.resHeader[i].forEach(item => {
cookie = parseCookie(item);
if (cookie && typeof cookie === 'object') {
ctx.cookies.set(cookie.name, cookie.value, {
maxAge: 864000000,
httpOnly: false
});
}
});
}
} else {
ctx.set(i, context.resHeader[i]);
}
}
}
ctx.status = context.httpCode;
ctx.body = context.mockJson;
return;
} catch (e) {
yapi.commons.log(e, 'error');
return (ctx.body = {
errcode: 400,
errmsg: '解析出错请检查。Error: ' + e.message,
data: null
});
}
} catch (e) {
yapi.commons.log(e, 'error');
return (ctx.body = yapi.commons.resReturn(null, 409, e.message));
}
};