feat: 模块移动、swagger 导入

pull/663/head
bigfengyu 5 years ago
parent b332fed116
commit f15710d4c1

@ -22,40 +22,41 @@
"license": "ISC",
"dependencies": {
"@types/treeify": "^1.0.0",
"chalk": "^2.4.2",
"chalk": "^3.0.0",
"cross-env": "^6.0.3",
"graceful": "^1.0.2",
"is-md5": "^0.0.2",
"js-beautify": "^1.10.2",
"js-beautify": "^1.10.3",
"json5": "^2.1.1",
"kcors": "^2.2.2",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-generic-session": "^2.0.1",
"koa-generic-session": "^2.0.4",
"koa-logger": "^3.2.1",
"koa-redis": "^4.0.0",
"koa-router": "^7.4.0",
"koa-redis": "^4.0.1",
"koa-router": "^8.0.8",
"koa-send": "^5.0.0",
"koa-static": "^5.0.0",
"lodash": "^4.17.15",
"mariadb": "^2.1.2",
"mariadb": "^2.2.0",
"md5": "^2.2.1",
"mockjs": "1.1.0",
"moment": "^2.24.0",
"mysql": "^2.17.1",
"mysql2": "^2.0.0",
"nanoid": "^2.1.6",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"nanoid": "^2.1.11",
"node-fetch": "^2.6.0",
"node-print": "0.0.4",
"node-schedule": "^1.3.2",
"nodemailer": "^6.2.1",
"notevil": "^1.3.2",
"notevil": "^1.3.3",
"path-to-regexp": "^3.1.0",
"redis": "^2.8.0",
"redis": "^3.0.2",
"reflect-metadata": "^0.1.13",
"request": "^2.88.0",
"request": "^2.88.2",
"request-promise": "^4.2.5",
"sequelize": "^5.21.2",
"sequelize-typescript": "^1.0.0",
"sequelize": "^5.21.5",
"sequelize-typescript": "^1.1.0",
"svg-captcha": "^1.4.0",
"treeify": "^1.1.0",
"underscore": "^1.9.1",
@ -63,40 +64,42 @@
"vm2": "^3.8.4"
},
"devDependencies": {
"@types/chai": "^4.2.4",
"@ali/keycenter": "^2.1.0",
"@types/chai": "^4.2.10",
"@types/json5": "^0.0.30",
"@types/kcors": "^2.2.3",
"@types/koa": "^2.0.51",
"@types/koa": "^2.11.2",
"@types/koa-generic-session": "^1.0.3",
"@types/koa-logger": "^3.1.1",
"@types/koa-redis": "^3.0.3",
"@types/koa-router": "^7.0.42",
"@types/koa-redis": "^4.0.0",
"@types/koa-router": "^7.4.0",
"@types/koa-static": "^4.0.1",
"@types/lodash": "^4.14.145",
"@types/lodash": "^4.14.149",
"@types/md5": "^2.1.33",
"@types/mocha": "^5.2.7",
"@types/mockjs": "^1.0.2",
"@types/nanoid": "^2.1.0",
"@types/node": "^12.12.6",
"@types/node-schedule": "^1.2.4",
"@types/node": "^13.7.7",
"@types/node-schedule": "^1.3.0",
"@types/nodemailer": "^6.2.2",
"@types/redis": "^2.8.14",
"@types/request": "^2.48.3",
"@types/request-promise": "^4.1.44",
"@types/sequelize": "^4.28.6",
"@types/underscore": "^1.9.3",
"babel-eslint": "^10.0.3",
"@types/redis": "^2.8.16",
"@types/request": "^2.48.4",
"@types/request-promise": "^4.1.45",
"@types/sequelize": "^4.28.8",
"@types/underscore": "^1.9.4",
"babel-eslint": "^10.1.0",
"chai": "^4.2.0",
"mocha": "^6.2.2",
"nodemon": "^1.19.4",
"nodemon": "^2.0.2",
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"nyc": "^15.0.0",
"pre-commit": "^1.2.2",
"rimraf": "^3.0.0",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.16",
"standard": "^14.3.1",
"supertest": "^4.0.2",
"tslint": "^5.20.1",
"typescript": "^3.7.2"
"tslint": "^6.0.0",
"typescript": "^3.8.3"
},
"pre-commit": [
"check"

@ -18,7 +18,7 @@ const config: IConfigOptions = {
password: process.env.MYSQL_PASSWD ?? '',
database: process.env.MYSQL_SCHEMA ?? 'RAP2_DELOS_APP',
pool: {
max: 5,
max: 10,
min: 0,
idle: 10000
},

@ -7,6 +7,11 @@ const Op = Sequelize.Op
enum methods { GET = 'GET', POST = 'POST', PUT = 'PUT', DELETE = 'DELETE' }
export enum MoveOp {
MOVE = 1,
COPY = 2
}
@Table({ paranoid: true, freezeTableName: false, timestamps: true })
export default class Interface extends Model<Interface> {

@ -48,11 +48,10 @@ router.get('/account/list', isLoggedIn, async (ctx) => {
let { name } = ctx.query
if (name) {
Object.assign(where, {
[Op.or]: [{
fullname: {
[Op.like]: `%${name}%`
},
}],
[Op.or]: [
{ fullname: { [Op.like]: `%${name}%` } },
{ email: name },
],
})
}
let options = { where }

@ -1,11 +1,8 @@
import router from './router'
import { Repository, Interface, Property, DefaultVal } from '../models'
import { Repository, Interface, Property } from '../models'
import { QueryInclude } from '../models'
import Tree from './utils/tree'
import urlUtils from './utils/url'
import * as querystring from 'querystring'
import * as urlPkg from 'url'
import { Op } from 'sequelize'
import { MockService } from '../service/mock'
const attributes: any = { exclude: [] }
const pt = require('node-print').pt
@ -104,250 +101,16 @@ router.get('/app/plugin/:repositories', async (ctx) => {
ctx.body = result.join('\n')
})
const REG_URL_METHOD = /^\/?(get|post|delete|put)/i
// /app/mock/:repository/:method/:url
// X DONE 2.2 支持 GET POST PUT DELETE 请求
// DONE 2.2 忽略请求地址中的前缀斜杠
// DONE 2.3 支持所有类型的请求,这样从浏览器中发送跨越请求时不需要修改 method
router.all('/app/mock/:repositoryId(\\d+)/:url(.+)', async (ctx) => {
let app: any = ctx.app
app.counter.mock++
let { repositoryId, url } = ctx.params
let method = ctx.request.method
repositoryId = +repositoryId
if (REG_URL_METHOD.test(url)) {
REG_URL_METHOD.lastIndex = -1
method = REG_URL_METHOD.exec(url)[1].toUpperCase()
REG_URL_METHOD.lastIndex = -1
url = url.replace(REG_URL_METHOD, '')
}
let urlWithoutPrefixSlash = /(\/)?(.*)/.exec(url)[2]
// let urlWithoutSearch
// try {
// let urlParts = new URL(url)
// urlWithoutSearch = `${urlParts.origin}${urlParts.pathname}`
// } catch (e) {
// urlWithoutSearch = url
// }
// DONE 2.3 腐烂的 KISSY
// KISSY 1.3.2 会把路径中的 // 替换为 /。在浏览器端拦截跨域请求时,需要 encodeURIComponent(url) 以防止 http:// 被替换为 http:/。但是同时也会把参数一起编码,导致 route 的 url 部分包含了参数。
// 所以这里重新解析一遍!!!
let repository = await Repository.findByPk(repositoryId)
let collaborators: Repository[] = (await repository.$get('collaborators')) as Repository[]
let itf: Interface
let matchedItfList = await Interface.findAll({
attributes,
where: {
repositoryId: [repositoryId, ...collaborators.map(item => item.id)],
method,
url: {
[Op.like]: `%${urlWithoutPrefixSlash}%`,
}
}
})
function getRelativeURLWithoutParams(url: string) {
if (url.indexOf('http://') > -1) {
url = url.substring('http://'.length)
}
if (url.indexOf('https://') > -1) {
url = url.substring('https://'.length)
}
if (url.indexOf('/') > -1) {
url = url.substring(url.indexOf('/') + 1)
}
if (url.indexOf('?') > -1) {
url = url.substring(0, url.indexOf('?'))
}
return url
}
// matching by path
if (matchedItfList.length > 1) {
matchedItfList = matchedItfList.filter(x => {
const urlDoc = getRelativeURLWithoutParams(x.url)
const urlRequest = urlWithoutPrefixSlash
return urlDoc === urlRequest
})
}
// matching by params
if (matchedItfList.length > 1) {
const params = {
...ctx.request.query,
...ctx.request.body,
}
const paramsKeysCnt = Object.keys(params).length
matchedItfList = matchedItfList.filter(x => {
const parsedUrl = urlPkg.parse(x.url)
const pairs = parsedUrl.query ? parsedUrl.query.split('&').map(x => x.split('=')) : []
// 接口没有定义参数时看请求是否有参数
if (pairs.length === 0) {
return paramsKeysCnt === 0
}
// 接口定义参数时看每一项的参数是否一致
for (const p of pairs) {
const key = p[0]
const val = p[1]
if (params[key] != val) {
return false
}
}
return true
})
}
// 多个协同仓库的结果优先返回当前仓库的
if (matchedItfList.length > 1) {
const currProjMatchedItfList = matchedItfList.filter(x => x.repositoryId === repositoryId)
// 如果直接存在当前仓库的就当做结果集,否则放弃
if (currProjMatchedItfList.length > 0) {
matchedItfList = currProjMatchedItfList
}
}
for (const item of matchedItfList) {
itf = item
let url = item.url
if (url.charAt(0) === '/') {
url = url.substring(1)
}
if (url === urlWithoutPrefixSlash) {
break
}
}
if (!itf) {
// try RESTFul API search...
let list = await Interface.findAll({
attributes: ['id', 'url', 'method'],
where: {
repositoryId: [repositoryId, ...collaborators.map(item => item.id)],
method,
}
})
let listMatched = []
let relativeUrl = urlUtils.getRelative(url)
for (let item of list) {
let regExp = urlUtils.getUrlPattern(item.url) // 获取地址匹配正则
if (regExp.test(relativeUrl)) { // 检查地址是否匹配
let regMatchLength = regExp.exec(relativeUrl).length // 执行地址匹配
if (listMatched[regMatchLength]) { // 检查匹配地址中是否具有同group数量的数据
ctx.body = {
isOk: false,
errMsg: "匹配到多个同级别接口,请修改规则确保接口规则唯一性。"
}
return
}
listMatched[regMatchLength] = item // 写入数据
}
}
let loadDataId = 0
if (listMatched.length > 1) {
for (let matchedItem of listMatched) { // 循环匹配内的数据
if (matchedItem) { // 忽略为空的数据
loadDataId = matchedItem.id // 设置需查询的id
break
}
}
} else if (listMatched.length === 0) {
ctx.body = { isOk: false, errMsg: '未匹配到任何接口,请检查请求类型是否一致。' }
return
} else {
loadDataId = listMatched[0].id
}
itf = itf = await Interface.findByPk(loadDataId)
}
let interfaceId = itf.id
let properties = await Property.findAll({
attributes,
where: { interfaceId, scope: 'response' },
})
// default values override
const defaultVals = await DefaultVal.findAll({ where: { repositoryId } })
const defaultValsMap: {[key: string]: DefaultVal} = {}
for (const dv of defaultVals) {
defaultValsMap[dv.name] = dv
}
for (const p of properties) {
const dv = defaultValsMap[p.name]
if (!p.value && !p.rule && dv) {
p.value = dv.value
p.rule = dv.rule
}
}
// check required
if (~['GET', 'POST'].indexOf(method)) {
let requiredProperties = await Property.findAll({
attributes,
where: { interfaceId, scope: 'request', required: true },
})
let passed = true
let pFailed: Property | undefined
let params = method === 'GET' ? { ...ctx.request.query } : { ...ctx.request.body }
// http request中head的参数未添加会造成head中的参数必填勾选后即使header中有值也会检查不通过
params = Object.assign(params, ctx.request.headers)
for (const p of requiredProperties) {
if (typeof params[p.name] === 'undefined') {
passed = false
pFailed = p
break
}
}
if (!passed) {
ctx.body = {
isOk: false,
errMsg: `必选参数${pFailed.name}未传值。 Required parameter ${pFailed.name} has no value.`,
}
return
}
}
properties = properties.map((item: any) => item.toJSON())
await MockService.mock(ctx, { forceVerify: true })
})
// DONE 2.2 支持引用请求参数
let requestProperties: any = await Property.findAll({
attributes,
where: { interfaceId, scope: 'request' },
})
requestProperties = requestProperties.map((item: any) => item.toJSON())
let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties)
Object.assign(requestData, { ...ctx.params, ...ctx.query, ...ctx.body })
let data = Tree.ArrayToTreeToTemplateToData(properties, requestData)
if (data.__root__) {
data = data.__root__
}
ctx.type = 'json'
ctx.status = itf.status
ctx.body = JSON.stringify(data, undefined, 2)
const Location = data.Location
if (Location && itf.status === 301) {
ctx.redirect(Location)
return
}
if (itf && itf.url.indexOf('[callback]=') > -1) {
const query = querystring.parse(itf.url.substring(itf.url.indexOf('?') + 1))
const cbName = query['[callback]']
const cbVal = ctx.request.query[`${cbName}`]
if (cbVal) {
let body = typeof ctx.body === 'object' ? JSON.stringify(ctx.body, undefined, 2) : ctx.body
ctx.type = 'application/x-javascript'
ctx.body = cbVal + '(' + body + ')'
}
}
router.all('/app/mock-noverify/:repositoryId(\\d+)/:url(.+)', async ctx => {
await MockService.mock(ctx, { forceVerify: false })
})
// DONE 2.2 支持获取请求参数的模板、数据、Schema

@ -7,7 +7,9 @@ import Tree from './utils/tree'
import { AccessUtils, ACCESS_TYPE } from './utils/access'
import * as Consts from './utils/const'
import RedisService, { CACHE_KEY } from '../service/redis'
import RepositoryService from '../service/repository'
import MigrateService from '../service/migrate'
import OrganizationService from '../service/organization'
import { Op } from 'sequelize'
import { isLoggedIn } from './base'
@ -277,10 +279,15 @@ router.post('/repository/update', isLoggedIn, async (ctx, next) => {
ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY
return
}
if (
body.organizationId &&
!(await OrganizationService.canUserAccessOrganization(ctx.session.id, body.organizationId))
) {
ctx.body = '没有团队的权限'
return
}
delete body.creatorId
// DONE 2.2 支持转移仓库
// delete body.ownerId
delete body.organizationId
let result = await Repository.update(body, { where: { id: body.id } })
if (body.memberIds) {
let reloaded = await Repository.findByPk(body.id, {
@ -488,6 +495,24 @@ router.post('/module/update', isLoggedIn, async (ctx, next) => {
})
})
router.post('/module/move', isLoggedIn, async ctx => {
const { modId, op } = ctx.request.body
const repositoryId = ctx.request.body.repositoryId
if (!(await RepositoryService.canUserMoveModule(ctx.session.id, modId, repositoryId))) {
ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY
return
}
await RepositoryService.moveModule(op, modId, repositoryId)
ctx.body = {
data: {
isOk: true,
},
}
})
router.get('/module/remove', isLoggedIn, async (ctx, next) => {
let { id } = ctx.query
if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, ctx.session.id, +id)) {
@ -687,61 +712,21 @@ router.post('/interface/update', isLoggedIn, async (ctx, next) => {
})
})
router.post('/interface/move', isLoggedIn, async (ctx) => {
const OP_MOVE = 1
const OP_COPY = 2
router.post('/interface/move', isLoggedIn, async ctx => {
const { modId, itfId, op } = ctx.request.body
const itf = await Interface.findByPk(itfId)
if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, itfId)) {
const repositoryId = ctx.request.body.repositoryId || itf.repositoryId
if (!(await RepositoryService.canUserMoveInterface(ctx.session.id, itfId, repositoryId, modId))) {
ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY
return
}
if (op === OP_MOVE) {
itf.moduleId = modId
await Property.update({
moduleId: modId,
}, {
where: {
interfaceId: itf.id,
}
})
await itf.save()
} else if (op === OP_COPY) {
const { id, name, ...otherProps } = itf.toJSON() as Interface
const newItf = await Interface.create({
name: name + '副本',
...otherProps,
moduleId: modId,
})
const properties = await Property.findAll({
where: {
interfaceId: itf.id,
},
order: [['parentId', 'asc']],
})
// 解决parentId丢失的问题
let idMap = {}
for (const property of properties) {
const { id, parentId, ...props } = property.toJSON() as Property
// @ts-ignore
const newParentId = idMap[parentId + ''] ? idMap[parentId + ''] : -1
const newProperty = await Property.create({
...props,
interfaceId: newItf.id,
parentId: newParentId,
moduleId: modId,
})
// @ts-ignore
idMap[id + ''] = newProperty.id
}
await RepositoryService.moveInterface(op, itfId, repositoryId, modId)
}
ctx.body = {
data: {
isOk: true,
}
},
}
})
@ -963,6 +948,7 @@ router.post('/properties/update', isLoggedIn, async (ctx, next) => {
interfaceId: itfId
}
})
// 更新已存在的属性
for (let item of existingProperties) {
let affected = await Property.update(item, {
@ -1042,3 +1028,48 @@ router.post('/repository/import', isLoggedIn, async (ctx) => {
}
}
})
router.post('/repository/importswagger', isLoggedIn, async (ctx) => {
const { orgId, repositoryId, swagger, version = 1, mode = 'manual'} = ctx.request.body
// 权限判断
if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, orgId)) {
ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY
return
}
const result = await MigrateService.importRepoFromSwaggerDocUrl(orgId, ctx.session.id, swagger, version, mode, repositoryId)
ctx.body = {
isOk: result.code,
message: result.code === 'success' ? '导入成功' : '导入失败',
repository: {
id: 1,
}
}
})
router.post('/repository/importJSON', isLoggedIn , async ctx => {
const { data } = ctx.request.body
if (!(await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, data.id))) {
ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY
return
}
try {
await MigrateService.importRepoFromJSON(data, ctx.session.id)
ctx.body = {
isOk: true,
repository: {
id: data.id,
},
}
} catch (error) {
ctx.body = {
isOk: false,
message: '服务器错误,导入失败'
}
throw(error)
}
})

@ -1,10 +1,9 @@
import { Property } from "../../models"
import { Property } from '../../models'
import * as _ from 'underscore'
const { VM } = require('vm2')
import * as Mock from 'mockjs'
const { RE_KEY } = require('mockjs/src/mock/constant')
export default class Tree {
public static ArrayToTree(list: Property[]) {
let result: any = {
@ -14,7 +13,9 @@ export default class Tree {
}
let mapped: any = {}
list.forEach(item => { mapped[item.id] = item })
list.forEach(item => {
mapped[item.id] = item
})
function _parseChildren(parentId: any, children: any, depth: any) {
for (let id in mapped) {
@ -48,9 +49,13 @@ export default class Tree {
timeout: 1000
})
function parse(item: any, result: any) {
let rule = item.rule ? ('|' + item.rule) : ''
let rule = item.rule ? '|' + item.rule : ''
let value = item.value
if (item.value && item.value.indexOf('[') === 0 && item.value.substring(item.value.length - 1) === ']') {
if (
item.value &&
item.value.indexOf('[') === 0 &&
item.value.substring(item.value.length - 1) === ']'
) {
try {
result[item.name + rule] = vm.run(`(${item.value})`)
} catch (e) {
@ -79,7 +84,9 @@ export default class Tree {
try {
result[item.name + rule] = vm.run('(' + item.value + ')')
} catch (e) {
console.warn(`TreeToTemplate ${e.message}: ${item.type} { ${item.name}${rule}: ${item.value} }`) // TODO 2.2 怎么消除异常值?
console.warn(
`TreeToTemplate ${e.message}: ${item.type} { ${item.name}${rule}: ${item.value} }`,
) // TODO 2.2 怎么消除异常值?
result[item.name + rule] = item.value
}
break
@ -112,9 +119,9 @@ export default class Tree {
}
break
case 'Null':
// tslint:disable-next-line: no-null-keyword
result[item.name + rule] = null
break
// tslint:disable-next-line: no-null-keyword
result[item.name + rule] = null
break
}
}
}
@ -174,12 +181,12 @@ export default class Tree {
let result = data.replace(pattern, extra[eKey])
const p = propertyMap[key]
if (p) {
if (p.type === 'Number') {
result = +result || 1
} else if (p.type === 'Boolean') {
result = result === 'true' || !!+result
if (p.type === 'Number') {
result = +result || 1
} else if (p.type === 'Boolean') {
result = result === 'true' || !!+result
}
}
}
data = scopedData[key] = result
}
}
@ -203,12 +210,209 @@ export default class Tree {
// X Function.protytype.toJSON = Function.protytype.toString
// X RegExp.protytype.toJSON = RegExp.protytype.toString
public static stringifyWithFunctonAndRegExp(json: object) {
return JSON.stringify(json, (k, v) => {
k
if (typeof v === 'function') return v.toString()
if (v !== undefined && v !== null && v.exec) return v.toString()
else return v
}, 2)
return JSON.stringify(
json,
(k, v) => {
k
if (typeof v === 'function') return v.toString()
if (v !== undefined && v !== null && v.exec) return v.toString()
else return v
},
2,
)
}
// 把用户的 mock json 转换成 json-schema 再转换成 properties
public static jsonToArray(
json: any,
{
userId,
repositoryId,
moduleId,
interfaceId,
scope,
}: {
userId: number
repositoryId: number
moduleId: number
interfaceId: number
scope: 'request' | 'response'
},
) {
const isIncreamentNumberSequence = (numbers: any) =>
numbers.every(
(num: any) =>
typeof num === 'number' &&
((num: any, i: number) => i === 0 || num - numbers[i - 1] === 1),
)
function isPrimitiveType(type: string) {
return ['number', 'null', 'undefined', 'boolean', 'string'].indexOf(type.toLowerCase()) > -1
}
function mixItemsProperties(items: any) {
// 合并 item properties 的 key返回的 item 拥有导入 json 的所有 key
if (!items || !items.length) {
return {
properties: [],
}
} else if (items.length === 1) {
if (!items[0].properties) {
items[0].properties = []
}
return items[0]
} else {
const baseItem = items[0]
if (!baseItem.properties) {
baseItem.properties = []
}
const baseProperties = baseItem.properties
for (let i = 1; i < items.length; ++i) {
const item = items[i]
if (item.properties && item.properties.length) {
for (const p of item.properties) {
if (!baseProperties.find((e: any) => e.name === p.name)) {
baseProperties.push(p)
}
}
}
}
return baseItem
}
}
/** MockJS toJSONSchema bug length
* MockJS
* length schema
*/
const lengthAlias = '__mockjs_length_*#06#'
const replaceLength = (obj: any) => {
for (const k in obj) {
if (obj[k] && typeof obj[k] === 'object') {
replaceLength(obj[k])
} else {
// Do something with obj[k]
if (k === 'length') {
const v = obj[k]
delete obj[k]
obj[lengthAlias] = v
}
}
}
}
function handleJSONSchema(
schema: any,
parent = { id: -1 },
memoryProperties: any,
siblings?: any,
) {
if (!schema) {
return
}
const hasSiblings = siblings instanceof Array && siblings.length > 0
// DONE 2.1 需要与 Mock 的 rule.type 规则统一,首字符小写,好烦!应该忽略大小写!
if (schema.name === lengthAlias) {
schema.name = 'length'
}
let type = schema.type[0].toUpperCase() + schema.type.slice(1)
let rule = ''
if (type === 'Array' && schema.items && schema.items.length > 1) {
rule = schema.items.length + ''
}
let value = /Array|Object/.test(type) ? '' : schema.template
if (schema.items && schema.items.length) {
const childType = schema.items[0].type
if (isPrimitiveType(childType)) {
value = JSON.stringify(schema.template)
rule = ''
}
} else if (hasSiblings && isPrimitiveType(type)) {
// 如果是简单数据可以在这里进行合并
const valueArr = siblings.map((s: any) => s && s.template)
if (_.uniq(valueArr).length > 1) {
// 只有在数组里有不同元素时再合并
if (isIncreamentNumberSequence(valueArr)) {
// 如果是递增数字序列特殊处理
value = valueArr[0]
rule = '+1'
} else {
// 比如 [{a:1},{a:2}]
// 我们可以用 type: Array rule: +1 value: [1,2] 进行还原
value = JSON.stringify(valueArr)
type = 'Array'
rule = '+1'
}
}
}
type Property = {
name: any
type: any
rule: string
value: any
descripton: string
creator: any
repositoryId: any
moduleId: any
interfaceId: any
scope: any
parentId: number
memory: boolean
id: any
}
const property: Property = Object.assign(
{
name: schema.name,
type,
rule,
value,
descripton: '',
},
{
creator: userId,
repositoryId: repositoryId,
moduleId: moduleId,
interfaceId,
scope,
parentId: parent.id,
},
{
memory: true,
id: _.uniqueId('memory-'),
},
)
memoryProperties.push(property)
if (schema.properties) {
schema.properties.forEach((item: any) => {
const childSiblings = hasSiblings
? siblings.map(
(s: any) =>
(s && s.properties && s.properties.find((p: any) => p && p.name === item.name)) || null,
)
: undefined
handleJSONSchema(item, property, memoryProperties, childSiblings)
})
}
mixItemsProperties(schema.items).properties.forEach((item: any) => {
const siblings = schema.items.map(
(o: any) => o.properties.find((p: any) => p.name === item.name) || null,
)
handleJSONSchema(item, property, memoryProperties, siblings)
})
}
if (JSON.stringify(json).indexOf('length') > -1) {
// 递归查找替换 length 是一个重操作,先进行一次字符串查找,发现存在 length 字符再进行
replaceLength(json)
}
if (json instanceof Array) {
json = { _root_: json }
}
const schema = Mock.toJSONSchema(json)
const memoryProperties: any = []
if (schema.properties) {
schema.properties.forEach((item: any) => handleJSONSchema(item, undefined, memoryProperties))
}
return memoryProperties
}
}

@ -5,7 +5,7 @@ import * as redisStore from 'koa-redis'
import * as logger from 'koa-logger'
import * as serve from 'koa-static'
import * as cors from 'kcors'
import * as bodyParser from 'koa-body'
import * as body from 'koa-body'
import router from '../routes'
import config from '../config'
import { startTask } from '../service/task'
@ -16,6 +16,7 @@ appAny.counter = { users: {}, mock: 0 }
app.keys = config.keys
app.use(session({
// @ts-ignore
store: redisStore(config.redis)
}))
if (process.env.NODE_ENV === 'development' && process.env.TEST_MODE !== 'true') app.use(logger())
@ -51,8 +52,14 @@ app.use(async (ctx, next) => {
app.use(serve('public'))
app.use(serve('test'))
app.use(bodyParser({ multipart: true }))
app.use(
body({
multipart: true,
formLimit: '10mb',
textLimit: '10mb',
jsonLimit: '10mb',
}),
)
app.use(router.routes())
startTask()

@ -1,9 +1,105 @@
import { Repository, Module, Interface, Property, User } from "../models"
import { Repository, Module, Interface, Property, User, QueryInclude } from '../models'
import { SCOPES } from "../models/bo/property"
import * as md5 from 'md5'
import * as querystring from 'querystring'
import * as rp from 'request-promise'
const isMd5 = require('is-md5')
import Tree from '../routes/utils/tree'
import * as JSON5 from 'json5'
import RedisService, { CACHE_KEY } from "./redis"
import * as _ from 'lodash'
const SWAGGER_VERSION = {
1: '2.0'
}
const arrayToTree = (list) => {
const parseChildren = (list, parent) => {
list.forEach((item) => {
if (item.parent === parent.id) {
item.depth = parent.depth + 1
item.children = item.children || []
parent.children.push(item)
parseChildren(list, item)
}
})
return parent
}
return parseChildren(list, {
id: 'root',
name: 'root',
children: [],
depth: -1,
parent: -1
})
}
const REQUEST_TYPE_POS = {
path: 2,
query: 2,
header: 1,
formData: 3,
body: 3
}
/**
* @param parameters
* @param parent
* @param result swagger
* @param definitions swagger $ref definitions
*/
const parse = (parameters, parent, result, definitions) => {
for (let key = 0, len = parameters.length; key < len; key++) {
const param = parameters[key]
if (!param.$ref && !(param.items || {}).$ref) {
// 非对象或者数组的基础类型
result.push({
...param, parent,
id: `${parent}-${key}`
})
} else {
// 数组类型或者对象类型
let paramType = ''
if (param.items) { paramType = 'array' }
else { paramType = 'object' }
result.push({
...param, parent,
id: `${parent}-${key}`,
type: paramType
})
let refName
if (!param.items) {
refName = param.$ref.split('#/definitions/')[1]
delete result.find(item => item.id === `${parent}-${key}`)['$ref']
}
if (param.items) {
refName = param.items.$ref.split('#/definitions/')[1]
delete result.find(item => item.id === `${parent}-${key}`).items
}
const ref = definitions[refName]
if (ref && ref.properties) {
const properties = ref.properties
const list = []
for (const key in properties) {
list.push({
name: key,
...properties[key],
in: param.in, // response 无所谓不使用但是request 使用
required: (ref.required || []).indexOf(key) >= 0
})
}
parse(list, `${parent}-${key}`, result, definitions)
}
}
}
}
export default class MigrateService {
public static async importRepoFromRAP1ProjectData(orgId: number, curUserId: number, projectData: any): Promise<boolean> {
@ -128,6 +224,463 @@ export default class MigrateService {
result = safeEval('(' + result + ')')
return await this.importRepoFromRAP1ProjectData(orgId, curUserId, result)
}
/** 请求参对象->数组->标准树形对象 @param swagger @param parameters */
public static async swaggerToModelRequest(swagger: SwaggerData, parameters: Array<any>, method: string): Promise<any> {
const { definitions } = swagger
const result = []
if (method === 'get' || method === 'GET') {
parse(parameters, 'root', result, definitions)
} else if (method === 'post' || method === 'POST') {
let list = [] // 外层处理参数数据结果
const bodyObj = parameters.find(item => item.in === 'body') // body unique
if (!bodyObj) list = [ ...parameters ]
else {
const { schema } = bodyObj
if (!schema.$ref) {
// 没有按照接口规范返回数据结构,默认都是对象
list = parameters.filter(item => (item.in === 'query' || item.in === 'header'))
} else {
const refName = schema.$ref.split('#/definitions/')[1]
const ref = definitions[refName]
if (!ref) list = [ ...parameters.filter(item => (item.in === 'query' || item.in === 'header'))]
else {
const properties = ref.properties || {}
const bodyParameters = []
for (const key in properties) {
bodyParameters.push({
name: key,
...properties[key],
in: 'body',
required: (ref.required || []).indexOf(key) >= 0
})
}
list = [...bodyParameters, ...parameters.filter(item => (item.in === 'query' || item.in === 'header'))]
}
}
}
parse(list, 'root', result, definitions)
}
const tree = arrayToTree(JSON.parse(JSON.stringify(result)))
return tree
}
/**
* ->->
* swagger responses
* swagger responses200
* @param swagger
* @param response
*/
public static async swaggerToModelRespnse (swagger: SwaggerData, response: object): Promise<any> {
const { definitions } = swagger
const successObj = response['200']
if (!successObj) return []
const { schema } = successObj
if (!schema.$ref) {
// 没有按照接口规范返回数据结构,默认都是对象
return []
}
const parameters = []
const refName = schema.$ref.split('#/definitions/')[1]
const ref = definitions[refName]
if (ref && ref.properties) {
const properties = ref.properties
for (const key in properties) {
parameters.push({
name: key,
...properties[key],
in: 'body',
required: (ref.required || []).indexOf(key) >= 0
})
}
}
const result = []
parse(parameters, 'root', result, definitions)
const tree = arrayToTree(JSON.parse(JSON.stringify(result)))
return tree
}
public static async importRepoFromSwaggerProjectData(repositoryId: number, curUserId: number, swagger: SwaggerData): Promise<boolean> {
if (!swagger.paths || !swagger.swagger || !swagger.host) return false
let mCounter = 1 // 模块优先级顺序
let iCounter = 1 // 接口优先级顺序
let pCounter = 1 // 参数优先级顺序
async function processParam(p: SwaggerParameter, scope: SCOPES, interfaceId: number, moduleId: number, parentId?: number, ) {
const name = p.name
let description = ''
// 规则转化处理
let rule = ''
if (p.type === 'string' && p.minLength && p.maxLength ) {
rule = `${p.minLength}-${p.maxLength}`
} else if (p.type === 'string' && p.minLength && !p.maxLength) {
rule = `${p.minLength}`
} else if (p.type === 'string' && !p.minLength && p.maxLength) {
rule = `${p.required ? '1' : '0'}-${p.maxLength}`
}
if (p.type === 'string' && p.enum && p.enum.length > 0) {
description = `${description} 枚举值: ${p.enum.join()}`
}
if (p.type === 'integer' && p.minimum && p.maxinum) {
rule = `${p.minimum}-${p.maxinum}`
}
if (p.type === 'integer' && p.minimum && !p.maxinum) {
rule = `${p.minimum}`
}
if (p.type === 'integer' && !p.minimum && p.maxinum) {
rule = `${p.required ? '1' : '0'}-${p.maxinum}`
}
// 类型转化处理
let type = (p.type || 'string')
if (type === 'integer') type = 'number'
type = type[0].toUpperCase() + type.slice(1) // foo => Foo 首字母转化为大写
// 默认值转化处理
let value = p.default || ''
if (p.type === 'boolean') {
value = (p.default === true || p.default === false) ? p.default.toString() : ''
}
if (p.type === 'array' && p.default) {
value = typeof(p.default) === 'object' ? JSON.stringify(p.default) : p.default.toString()
}
if (/^function/.test(value)) type = 'Function' // @mock=function(){} => Function
if (/^\$order/.test(value)) { // $order => Array|+1
type = 'Array'
rule = '+1'
let orderArgs = /\$order\((.+)\)/.exec(value)
if (orderArgs) value = `[${orderArgs[1]}]`
}
const pCreated = await Property.create({
scope,
name,
rule,
value,
type,
required: p.required,
description: `${p.description || ''} ${description ? `|${description}` : ''}`,
priority: pCounter++,
interfaceId: interfaceId,
creatorId: curUserId,
moduleId: moduleId,
repositoryId: repositoryId,
parentId: parentId || -1,
pos: REQUEST_TYPE_POS[p.in],
memory: true
})
for (const subParam of p.children) {
processParam(subParam, scope, interfaceId, moduleId, pCreated.id)
}
}
let { tags = [], paths = {}, host = '' } = swagger
let pathTag: SwaggerTag[] = []
// 处理root tag中没有的情况
for (const action in paths) {
const apiObj = paths[action][Object.keys(paths[action])[0]]
const index = pathTag.findIndex((it: SwaggerTag) => {
return apiObj.tags.length > 0 && it.name === apiObj.tags[0]
} )
if (index < 0 && apiObj.tags.length > 0) pathTag.push({ name : apiObj.tags[0], description: tags.find(item => item.name === apiObj.tags[0]).description || '' })
}
tags = pathTag
for (const tag of tags) {
let repository: Partial<Repository>
let [repositoryModules] = await Promise.all([
Repository.findByPk(repositoryId, {
attributes: { exclude: [] },
include: [QueryInclude.RepositoryHierarchy],
order: [
[{ model: Module, as: 'modules' }, 'priority', 'asc'],
[
{ model: Module, as: 'modules' },
{ model: Interface, as: 'interfaces' },
'priority',
'asc'
]
]
})
])
repository = {
...repositoryModules.toJSON()
}
const findIndex = repository.modules.findIndex(item => { return item.name === tag.name }) // 判断是否存在模块
let mod = null
if (findIndex < 0) {
mod = await Module.create({
name: tag.name,
description: tag.description,
priority: mCounter++,
creatorId: curUserId,
repositoryId: repositoryId
})
} else {
mod = repository.modules[findIndex]
}
for (const action in paths) {
const apiObj = paths[action][Object.keys(paths[action])[0]]
const method = Object.keys(paths[action])[0]
const actionTags0 = apiObj.tags[0]
const url = action
if (actionTags0 === tag.name) {
// 判断接口是否存在在该模块中,如果不存在则创建接口,存在则更新接口信息
let [repositoryModules] = await Promise.all([
Repository.findByPk(repositoryId, {
attributes: { exclude: [] },
include: [QueryInclude.RepositoryHierarchy],
order: [
[{ model: Module, as: 'modules' }, 'priority', 'asc'],
[
{ model: Module, as: 'modules' },
{ model: Interface, as: 'interfaces' },
'priority',
'asc'
]
]
})
])
repository = {
...repositoryModules.toJSON()
}
const request = await this.swaggerToModelRequest(swagger, apiObj.parameters || {}, method)
const response = await this.swaggerToModelRespnse(swagger, apiObj.responses || {})
// 判断对应模块是否存在该接口
const index = repository.modules.findIndex(item => {
return item.id === mod.id && (item.interfaces.findIndex(it => (it.url || '').indexOf(url) >= 0 ) >= 0) // 已经存在接口
})
if (index < 0) {
// 创建接口
const itf = await Interface.create({
moduleId: mod.id,
name: `${apiObj.summary}`,
description: apiObj.description,
url: `https//${host}${url.replace('-test', '')}`,
priority: iCounter++,
creatorId: curUserId,
repositoryId: repositoryId,
method: method.toUpperCase()
})
for (const p of (request.children || [])) {
await processParam(p, SCOPES.REQUEST, itf.id, mod.id)
}
for (const p of (response.children || [])) {
await processParam(p, SCOPES.RESPONSE, itf.id, mod.id)
}
} else {
const findApi = repository.modules[index].interfaces.find(item => item.url.indexOf(url) >= 0)
// 更新接口
await Interface.update({
moduleId: mod.id,
name: `${apiObj.summary}`,
description: apiObj.description,
url: `https//${host}${url.replace('-test', '')}`,
repositoryId: repositoryId,
method: method.toUpperCase()
}, { where: { id: findApi.id } })
await Property.destroy({ where: { interfaceId: findApi.id } })
for (const p of (request.children || [])) {
await processParam(p, SCOPES.REQUEST, findApi.id, mod.id)
}
for (const p of (response.children || [])) {
await processParam(p, SCOPES.RESPONSE, findApi.id, mod.id)
}
}
}
}
}
return true
}
/** Swagger property */
public static async importRepoFromSwaggerDocUrl(orgId: number, curUserId: number, swagger: SwaggerData, version: number, mode: string, repositoryId: number): Promise<any> {
try {
if (!swagger) return { result: false, code: 'swagger'}
const { host = '', info = {} } = swagger
if (swagger.swagger === SWAGGER_VERSION[version]) {
let result
if (mode === 'manual') {
const repos = await Repository.findByPk(repositoryId)
const { creatorId, members, collaborators, ownerId, name } = repos
const body = {
creatorId: creatorId,
organizationId: orgId,
memberIds: (members || []).map((item: any) => item.id),
collaboratorIds: (collaborators || []).map((item: any) => item.id),
ownerId,
visibility: true,
name,
id: repositoryId,
description: `[host=${host}]${info.title || ''}`,
}
result = await Repository.update(body, { where: { id: repositoryId } })
} else if (mode === 'auto') {
result = await Repository.create({
id: 0,
name: info.title || 'swagger导入仓库',
description: info.description || 'swagger导入仓库',
visibility: true,
ownerId: curUserId,
creatorId: curUserId,
organizationId: orgId,
members: [],
collaborators: [],
collaboratorIdstring: '',
memberIds: [],
collaboratorIds: []
})
}
if (result[0] || result.id) {
const bol = await this.importRepoFromSwaggerProjectData(mode === 'manual' ? repositoryId : result.id, curUserId, swagger)
await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, result.id)
return { result: bol, code: 'success' }
}
} else {
return { result: true, code: 'version'}
}
} catch (err) {
return { result: false, code: 'error'}
}
}
/** 可以直接让用户把自己本地的 data 数据导入到 RAP 中 */
public static async importRepoFromJSON(data: JsonData, curUserId: number) {
function parseJSON(str: string) {
try {
const data = JSON5.parse(str)
return _.isObject(data) ? data : {}
} catch (error) {
return {}
}
}
const repositoryId = data.id
await Promise.all(
data.modules.map(async (modData, index) => {
const mod = await Module.create({
name: modData.name,
description: modData.description || '',
priority: index + 1,
creatorId: curUserId,
repositoryId,
})
await Promise.all(
modData.interfaces.map(async (iftData, index) => {
let properties = iftData.properties
const itf = await Interface.create({
moduleId: mod.id,
name: iftData.name,
description: iftData.description || '',
url: iftData.url,
priority: index + 1,
creatorId: curUserId,
repositoryId,
method: iftData.method,
})
if (!properties && (iftData.requestJSON || iftData.responseJSON)) {
const reqData = parseJSON(iftData.requestJSON)
const resData = parseJSON(iftData.responseJSON)
properties = [
...Tree.jsonToArray(reqData, {
interfaceId: itf.id,
moduleId: mod.id,
repositoryId,
scope: 'request',
userId: curUserId,
}),
...Tree.jsonToArray(resData, {
interfaceId: itf.id,
moduleId: mod.id,
repositoryId,
scope: 'response',
userId: curUserId,
}),
]
}
if (!properties) {
properties = []
}
const idMaps: any = {}
await Promise.all(
properties.map(async (pData, index) => {
const property = await Property.create({
scope: pData.scope,
name: pData.name,
rule: pData.rule,
value: pData.value,
type: pData.type,
description: pData.description,
priority: index + 1,
interfaceId: itf.id,
creatorId: curUserId,
moduleId: mod.id,
repositoryId,
parentId: -1,
})
idMaps[pData.id] = property.id
}),
)
await Promise.all(
properties.map(async pData => {
const newId = idMaps[pData.id]
const newParentId = idMaps[pData.parentId]
await Property.update(
{
parentId: newParentId,
},
{
where: {
id: newId,
},
},
)
}),
)
}),
)
}),
)
await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, repositoryId)
}
}
function getMethodFromRAP1RequestType(type: number) {
@ -145,24 +698,45 @@ function getMethodFromRAP1RequestType(type: number) {
}
}
// function getTypeFromRAP1DataType(dataType: string) {
// switch (dataType) {
// case 'number':
// return TYPES.NUMBER
// case 'string':
// return TYPES.STRING
// case 'boolean':
// return TYPES.BOOLEAN
// case 'object':
// return TYPES.OBJECT
// default:
// if (dataType && dataType.indexOf('array') > -1) {
// return TYPES.ARRAY
// } else {
// return TYPES.STRING
// }
// }
// }
interface JsonData {
/**
* repo id
*/
id: number
modules: {
name: string
description?: string
/**
*
* 1
*/
interfaces: {
name: string
url: string
/**
* GET POST
*/
method: string
description?: string
/**
*
*/
status: number
/**
*
*/
properties: Partial<Property>[]
/**
* json
*/
requestJSON: string
/**
* json
*/
responseJSON: string
}[]
}[]
}
interface OldParameter {
id: number
@ -173,3 +747,51 @@ interface OldParameter {
dataType: string
parameterList: OldParameter[]
}
interface SwaggerParameter {
name: string
in: string
description?: string
required: boolean
type: string
allowEmptyValue?: boolean
minLength?: number
maxLength?: number
format?: string
minimum?: number
maxinum?: number
default?: any
items?: SwaggerParameter[]
collectionFormat?: string
exclusiveMaximum?: number
exclusiveMinimum?: number
enum?: Array<any>
multipleOf?: number
uniqueItems?: boolean
pattern?: string
schema: any
children: SwaggerParameter[]
id: string
depth: number
}
interface SwaggerTag {
name: string
description?: string
}
interface SwaggerInfo {
description?: string
title?: string
version?: string
}
interface SwaggerData {
swagger: string
host: string
tags: SwaggerTag[]
paths: object
definitions?: object,
info?: SwaggerInfo
}

@ -0,0 +1,244 @@
import { Repository, Interface, Property, DefaultVal } from '../models'
import { Op } from 'sequelize'
import urlUtils from '../routes/utils/url'
import Tree from '../routes/utils/tree'
import * as urlPkg from 'url'
import * as querystring from 'querystring'
const REG_URL_METHOD = /^\/?(get|post|delete|put)/i
const attributes: any = { exclude: [] }
export class MockService {
public static async mock(ctx: any, option: { forceVerify: boolean } = { forceVerify: false }) {
const { forceVerify } = option
let app: any = ctx.app
app.counter.mock++
let { repositoryId, url } = ctx.params
let method = ctx.request.method
repositoryId = +repositoryId
if (REG_URL_METHOD.test(url)) {
REG_URL_METHOD.lastIndex = -1
method = REG_URL_METHOD.exec(url)[1].toUpperCase()
REG_URL_METHOD.lastIndex = -1
url = url.replace(REG_URL_METHOD, '')
}
let urlWithoutPrefixSlash = /(\/)?(.*)/.exec(url)[2]
let repository = await Repository.findByPk(repositoryId)
let collaborators: Repository[] = (await repository.$get('collaborators')) as Repository[]
let itf: Interface
let matchedItfList = await Interface.findAll({
attributes,
where: {
repositoryId: [repositoryId, ...collaborators.map(item => item.id)],
...(forceVerify ? { method } : {}),
url: {
[Op.like]: `%${urlWithoutPrefixSlash}%`,
},
},
})
function getRelativeURLWithoutParams(url: string) {
if (url.indexOf('http://') > -1) {
url = url.substring('http://'.length)
}
if (url.indexOf('https://') > -1) {
url = url.substring('https://'.length)
}
if (url.indexOf('/') > -1) {
url = url.substring(url.indexOf('/') + 1)
}
if (url.indexOf('?') > -1) {
url = url.substring(0, url.indexOf('?'))
}
return url
}
// matching by path
if (matchedItfList.length > 1) {
matchedItfList = matchedItfList.filter(x => {
const urlDoc = getRelativeURLWithoutParams(x.url)
const urlRequest = urlWithoutPrefixSlash
return urlDoc === urlRequest
})
}
// matching by params
if (matchedItfList.length > 1) {
const params = {
...ctx.request.query,
...ctx.request.body,
}
const paramsKeysCnt = Object.keys(params).length
matchedItfList = matchedItfList.filter(x => {
const parsedUrl = urlPkg.parse(x.url)
const pairs = parsedUrl.query ? parsedUrl.query.split('&').map(x => x.split('=')) : []
// 接口没有定义参数时看请求是否有参数
if (pairs.length === 0) {
return paramsKeysCnt === 0
}
// 接口定义参数时看每一项的参数是否一致
for (const p of pairs) {
const key = p[0]
const val = p[1]
if (params[key] != val) {
return false
}
}
return true
})
}
// 多个协同仓库的结果优先返回当前仓库的
if (matchedItfList.length > 1) {
const currProjMatchedItfList = matchedItfList.filter(x => x.repositoryId === repositoryId)
// 如果直接存在当前仓库的就当做结果集,否则放弃
if (currProjMatchedItfList.length > 0) {
matchedItfList = currProjMatchedItfList
}
}
for (const item of matchedItfList) {
itf = item
let url = item.url
if (url.charAt(0) === '/') {
url = url.substring(1)
}
if (url === urlWithoutPrefixSlash) {
break
}
}
if (!itf) {
// try RESTFul API search...
let list = await Interface.findAll({
attributes: ['id', 'url', 'method'],
where: {
repositoryId: [repositoryId, ...collaborators.map(item => item.id)],
method,
},
})
let listMatched = []
let relativeUrl = urlUtils.getRelative(url)
for (let item of list) {
let regExp = urlUtils.getUrlPattern(item.url) // 获取地址匹配正则
if (regExp.test(relativeUrl)) {
// 检查地址是否匹配
let regMatchLength = regExp.exec(relativeUrl).length // 执行地址匹配
if (listMatched[regMatchLength]) {
// 检查匹配地址中是否具有同group数量的数据
ctx.body = {
isOk: false,
errMsg: '匹配到多个同级别接口,请修改规则确保接口规则唯一性。',
}
return
}
listMatched[regMatchLength] = item // 写入数据
}
}
let loadDataId = 0
if (listMatched.length > 1) {
for (let matchedItem of listMatched) {
// 循环匹配内的数据
if (matchedItem) {
// 忽略为空的数据
loadDataId = matchedItem.id // 设置需查询的id
break
}
}
} else if (listMatched.length === 0) {
ctx.body = { isOk: false, errMsg: '未匹配到任何接口,请检查请求类型是否一致。' }
return
} else {
loadDataId = listMatched[0].id
}
itf = itf = await Interface.findByPk(loadDataId)
}
let interfaceId = itf.id
let properties = await Property.findAll({
attributes,
where: { interfaceId, scope: 'response' },
})
// default values override
const defaultVals = await DefaultVal.findAll({ where: { repositoryId } })
const defaultValsMap: { [key: string]: DefaultVal } = {}
for (const dv of defaultVals) {
defaultValsMap[dv.name] = dv
}
for (const p of properties) {
const dv = defaultValsMap[p.name]
if (!p.value && !p.rule && dv) {
p.value = dv.value
p.rule = dv.rule
}
}
// check required
if (forceVerify && ~['GET', 'POST'].indexOf(method)) {
let requiredProperties = await Property.findAll({
attributes,
where: { interfaceId, scope: 'request', required: true },
})
let passed = true
let pFailed: Property | undefined
let params = { ...ctx.request.query, ...ctx.request.body }
// http request中head的参数未添加会造成head中的参数必填勾选后即使header中有值也会检查不通过
params = Object.assign(params, ctx.request.headers)
for (const p of requiredProperties) {
if (typeof params[p.name] === 'undefined') {
passed = false
pFailed = p
break
}
}
if (!passed) {
ctx.body = {
isOk: false,
errMsg: `必选参数${pFailed.name}未传值。 Required parameter ${pFailed.name} has no value.`,
}
return
}
}
properties = properties.map((item: any) => item.toJSON())
// DONE 2.2 支持引用请求参数
let requestProperties: any = await Property.findAll({
attributes,
where: { interfaceId, scope: 'request' },
})
requestProperties = requestProperties.map((item: any) => item.toJSON())
let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties)
Object.assign(requestData, { ...ctx.params, ...ctx.query, ...ctx.body })
let data = Tree.ArrayToTreeToTemplateToData(properties, requestData)
if (data.__root__) {
data = data.__root__
}
ctx.type = 'json'
ctx.status = itf.status
ctx.body = JSON.stringify(data, undefined, 2)
const Location = data.Location
if (Location && itf.status === 301) {
ctx.redirect(Location)
return
}
if (itf && itf.url.indexOf('[callback]=') > -1) {
const query = querystring.parse(itf.url.substring(itf.url.indexOf('?') + 1))
const cbName = query['[callback]']
const cbVal = ctx.request.query[`${cbName}`]
if (cbVal) {
let body = typeof ctx.body === 'object' ? JSON.stringify(ctx.body, undefined, 2) : ctx.body
ctx.type = 'application/x-javascript'
ctx.body = cbVal + '(' + body + ')'
}
}
}
}

@ -1,8 +1,15 @@
import { Repository, RepositoriesMembers } from "../models"
import OrganizationService from "./organization"
import { Repository, RepositoriesMembers, Interface, Property, Module } from '../models'
import { MoveOp } from '../models/bo/interface'
import RedisService, { CACHE_KEY } from '../service/redis'
import { AccessUtils, ACCESS_TYPE } from '../routes/utils/access'
import OrganizationService from './organization'
export default class RepositoryService {
public static async canUserAccessRepository(userId: number, repositoryId: number, token?: string): Promise<boolean> {
public static async canUserAccessRepository(
userId: number,
repositoryId: number,
token?: string,
): Promise<boolean> {
const repo = await Repository.findByPk(repositoryId)
if (token && repo.token === token) return true
if (!repo) return false
@ -11,9 +18,138 @@ export default class RepositoryService {
where: {
userId,
repositoryId,
}
},
})
if (memberExistsNum > 0) return true
return OrganizationService.canUserAccessOrganization(userId, repo.organizationId)
}
}
public static async canUserMoveInterface(
userId: number,
itfId: number,
destRepoId: number,
destModuleId: number,
) {
return (
AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, userId, itfId) &&
AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, userId, destRepoId) &&
AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, userId, destModuleId)
)
}
public static async canUserMoveModule(userId: number, modId: number, destRepoId: number) {
return (
AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, userId, modId) &&
AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, userId, destRepoId)
)
}
public static async moveModule(op: MoveOp, modId: number, destRepoId: number, nameSuffix = '副本') {
const mod = await Module.findByPk(modId)
const fromRepoId = mod.repositoryId
if (op === MoveOp.MOVE) {
mod.repositoryId = destRepoId
await mod.save()
await Interface.update(
{
repositoryId: destRepoId,
},
{
where: {
moduleId: modId,
},
},
)
await Property.update(
{
repositoryId: destRepoId,
},
{
where: {
moduleId: modId,
},
},
)
} else if (op === MoveOp.COPY) {
const { id, name, ...otherProps } = mod.toJSON() as Module
const interfaces = await Interface.findAll({
where: {
moduleId: modId,
},
})
const newMod = await Module.create({
name: mod.name + nameSuffix,
...otherProps,
repositoryId: destRepoId,
})
const promises = interfaces.map(itf =>
RepositoryService.moveInterface(MoveOp.COPY, itf.id, destRepoId, newMod.id, ''),
)
await Promise.all(promises)
}
await Promise.all([
RedisService.delCache(CACHE_KEY.REPOSITORY_GET, fromRepoId),
RedisService.delCache(CACHE_KEY.REPOSITORY_GET, destRepoId),
])
}
public static async moveInterface(
op: MoveOp,
itfId: number,
destRepoId: number,
destModuleId: number,
nameSuffix = '副本'
) {
const itf = await Interface.findByPk(itfId)
const fromRepoId = itf.repositoryId
if (op === MoveOp.MOVE) {
itf.moduleId = destModuleId
itf.repositoryId = destRepoId
await Property.update(
{
moduleId: destModuleId,
repositoryId: destRepoId,
},
{
where: {
interfaceId: itf.id,
},
},
)
await itf.save()
} else if (op === MoveOp.COPY) {
const { id, name, ...otherProps } = itf.toJSON() as Interface
const newItf = await Interface.create({
name: name + nameSuffix,
...otherProps,
repositoryId: destRepoId,
moduleId: destModuleId,
})
const properties = await Property.findAll({
where: {
interfaceId: itf.id,
},
order: [['parentId', 'asc']],
})
// 解决parentId丢失的问题
let idMap: any = {}
for (const property of properties) {
const { id, parentId, ...props } = property.toJSON() as Property
const newParentId = idMap[parentId + ''] ? idMap[parentId + ''] : -1
const newProperty = await Property.create({
...props,
interfaceId: newItf.id,
parentId: newParentId,
repositoryId: destRepoId,
moduleId: destModuleId,
})
idMap[id + ''] = newProperty.id
}
}
await Promise.all([
RedisService.delCache(CACHE_KEY.REPOSITORY_GET, fromRepoId),
RedisService.delCache(CACHE_KEY.REPOSITORY_GET, destRepoId),
])
}
}

@ -12,11 +12,12 @@
"baseUrl": "./src",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitAny": false,
"skipLibCheck": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitUseStrict": true,
"suppressImplicitAnyIndexErrors": true,
"rootDir": "./src",
"paths": {
"*": [

@ -62,7 +62,7 @@
],
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-null-keyword": true,
"no-null-keyword": false,
"prefer-const": false,
"jsdoc-format": true
}

Loading…
Cancel
Save