From f15710d4c17af6d17c95effbcc2c649ed173938a Mon Sep 17 00:00:00 2001 From: bigfengyu Date: Fri, 20 Mar 2020 17:45:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A8=A1=E5=9D=97=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E3=80=81swagger=20=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 67 ++-- src/config/config.dev.ts | 2 +- src/models/bo/interface.ts | 5 + src/routes/account.ts | 9 +- src/routes/mock.ts | 249 +------------- src/routes/repository.ts | 127 ++++--- src/routes/utils/tree.ts | 244 ++++++++++++-- src/scripts/app.ts | 13 +- src/service/migrate.ts | 660 +++++++++++++++++++++++++++++++++++-- src/service/mock.ts | 244 ++++++++++++++ src/service/repository.ts | 146 +++++++- tsconfig.json | 3 +- tslint.json | 2 +- 13 files changed, 1393 insertions(+), 378 deletions(-) create mode 100644 src/service/mock.ts diff --git a/package.json b/package.json index 32e43d8..617a9d6 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/config/config.dev.ts b/src/config/config.dev.ts index 3c99c64..d00767b 100644 --- a/src/config/config.dev.ts +++ b/src/config/config.dev.ts @@ -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 }, diff --git a/src/models/bo/interface.ts b/src/models/bo/interface.ts index 6988cbe..3fef790 100644 --- a/src/models/bo/interface.ts +++ b/src/models/bo/interface.ts @@ -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 { diff --git a/src/routes/account.ts b/src/routes/account.ts index b273dd9..0763e75 100644 --- a/src/routes/account.ts +++ b/src/routes/account.ts @@ -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 } diff --git a/src/routes/mock.ts b/src/routes/mock.ts index 8991597..10ae2d7 100644 --- a/src/routes/mock.ts +++ b/src/routes/mock.ts @@ -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 diff --git a/src/routes/repository.ts b/src/routes/repository.ts index e6acacf..602319a 100644 --- a/src/routes/repository.ts +++ b/src/routes/repository.ts @@ -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) + } + + +}) diff --git a/src/routes/utils/tree.ts b/src/routes/utils/tree.ts index a4c6ef8..9d6ead5 100644 --- a/src/routes/utils/tree.ts +++ b/src/routes/utils/tree.ts @@ -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 + } } diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 877e51c..044c537 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -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() diff --git a/src/service/migrate.ts b/src/service/migrate.ts index 826abfd..de569d4 100644 --- a/src/service/migrate.ts +++ b/src/service/migrate.ts @@ -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 { @@ -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, method: string): Promise { + 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 responses对象200不存在情况下异常处理 + * @param swagger + * @param response + */ + public static async swaggerToModelRespnse (swagger: SwaggerData, response: object): Promise { + 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 { + 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 + 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 { + 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[] + /** + * 导入请求数据 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 + 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 +} + diff --git a/src/service/mock.ts b/src/service/mock.ts new file mode 100644 index 0000000..c3c7430 --- /dev/null +++ b/src/service/mock.ts @@ -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 + ')' + } + } + } +} diff --git a/src/service/repository.ts b/src/service/repository.ts index 2b4af8d..a1a5ed8 100644 --- a/src/service/repository.ts +++ b/src/service/repository.ts @@ -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 { + public static async canUserAccessRepository( + userId: number, + repositoryId: number, + token?: string, + ): Promise { 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) } -} \ No newline at end of file + + 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), + ]) + } +} diff --git a/tsconfig.json b/tsconfig.json index b148973..26615a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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": { "*": [ diff --git a/tslint.json b/tslint.json index 8af2de9..3cdb0d0 100644 --- a/tslint.json +++ b/tslint.json @@ -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 }