From df84f7bae25d11564072932db12989f1da5a9be2 Mon Sep 17 00:00:00 2001 From: bigfengyu <kuailewudi@yeah.net> Date: Thu, 2 Apr 2020 10:59:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8A=A0=E5=BC=BA=E3=80=81swagger=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=8A=A0=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/export.ts | 6 +- src/routes/organization.ts | 8 +- src/routes/repository.ts | 187 ++++++----- src/routes/utils/access.ts | 60 +++- src/scripts/dev.ts | 2 +- src/service/mail.ts | 161 +++++++++- src/service/migrate.ts | 611 ++++++++++++++++++++++++++++++------ src/service/organization.ts | 6 +- src/service/repository.ts | 12 +- 9 files changed, 833 insertions(+), 220 deletions(-) diff --git a/src/routes/export.ts b/src/routes/export.ts index f8dc557..2b4d2f6 100644 --- a/src/routes/export.ts +++ b/src/routes/export.ts @@ -12,7 +12,7 @@ router.get('/export/postman', async ctx => { const repoId = +ctx.query.id if ( !(await AccessUtils.canUserAccess( - ACCESS_TYPE.REPOSITORY, + ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, repoId )) @@ -41,7 +41,7 @@ router.get('/export/markdown', async ctx => { const repoId = +ctx.query.id if ( !(await AccessUtils.canUserAccess( - ACCESS_TYPE.REPOSITORY, + ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, repoId )) @@ -59,7 +59,7 @@ router.get('/export/docx', async ctx => { const repoId = +ctx.query.id if ( !(await AccessUtils.canUserAccess( - ACCESS_TYPE.REPOSITORY, + ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, repoId )) diff --git a/src/routes/organization.ts b/src/routes/organization.ts index 59bbefc..172b0bc 100644 --- a/src/routes/organization.ts +++ b/src/routes/organization.ts @@ -113,7 +113,7 @@ router.get('/organization/joined', isLoggedIn, async (ctx) => { }) router.get('/organization/get', async (ctx) => { const organizationId = +ctx.query.id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organizationId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_GET, ctx.session.id, organizationId)) { ctx.body = COMMON_ERROR_RES.ACCESS_DENY return } @@ -144,7 +144,7 @@ router.post('/organization/create', isLoggedIn, async (ctx) => { router.post('/organization/update', isLoggedIn, async (ctx, next) => { let body = Object.assign({}, ctx.request.body) const organizationId = +body.id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organizationId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) { ctx.body = COMMON_ERROR_RES.ACCESS_DENY return } @@ -188,7 +188,7 @@ router.post('/organization/update', isLoggedIn, async (ctx, next) => { router.post('/organization/transfer', isLoggedIn, async (ctx) => { let { id, ownerId } = ctx.request.body const organizationId = +id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organizationId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) { ctx.body = COMMON_ERROR_RES.ACCESS_DENY return } @@ -201,7 +201,7 @@ router.post('/organization/transfer', isLoggedIn, async (ctx) => { router.get('/organization/remove', isLoggedIn, async (ctx, next) => { let { id } = ctx.query const organizationId = +id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organizationId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) { ctx.body = COMMON_ERROR_RES.ACCESS_DENY return } diff --git a/src/routes/repository.ts b/src/routes/repository.ts index a714d2b..07d4153 100644 --- a/src/routes/repository.ts +++ b/src/routes/repository.ts @@ -47,7 +47,7 @@ router.get('/repository/list', async (ctx) => { let { name, user, organization } = ctx.query if (+organization > 0) { - const access = await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organization) + const access = await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_GET, ctx.session.id, organization) if (access === false) { ctx.body = { @@ -74,10 +74,11 @@ router.get('/repository/list', async (ctx) => { include: [ QueryInclude.Creator, QueryInclude.Owner, - QueryInclude.Locker, - ], - } as any) - let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + QueryInclude.Locker + ] + }) + let limit = Math.min(ctx.query.limit ?? 10, 100) + let pagination = new Pagination(total, ctx.query.cursor || 1, limit) let repositories = await Repository.findAll({ where, attributes: { exclude: [] }, @@ -91,11 +92,22 @@ router.get('/repository/list', async (ctx) => { ], offset: pagination.start, limit: pagination.limit, - order: [['updatedAt', 'DESC']], - } as any) + order: [['updatedAt', 'DESC']] + }) + let repoData = await Promise.all(repositories.map(async (repo) => { + const canUserEdit = await AccessUtils.canUserAccess( + ACCESS_TYPE.REPOSITORY_SET, + ctx.session.id, + repo.id, + ) + return { + ...repo.toJSON(), + canUserEdit + } + })) ctx.body = { isOk: true, - data: repositories, + data: repoData, pagination: pagination, } }) @@ -113,8 +125,7 @@ router.get('/repository/owned', isLoggedIn, async (ctx) => { } let auth: User = await User.findByPk(ctx.query.user || ctx.session.id) - // let total = await auth.countOwnedRepositories({ where }) - // let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let repositories = await auth.$get('ownedRepositories', { where, include: [ @@ -125,12 +136,16 @@ router.get('/repository/owned', isLoggedIn, async (ctx) => { QueryInclude.Organization, QueryInclude.Collaborators, ], - // offset: pagination.start, - // limit: pagination.limit, - order: [['updatedAt', 'DESC']], + order: [['updatedAt', 'DESC']] + }) + let repoData = repositories.map(repo => { + return { + ...repo.toJSON(), + canUserEdit: true + } }) ctx.body = { - data: repositories, + data: repoData, pagination: undefined, } }) @@ -148,8 +163,6 @@ router.get('/repository/joined', isLoggedIn, async (ctx) => { } let auth = await User.findByPk(ctx.query.user || ctx.session.id) - // let total = await auth.countJoinedRepositories({ where }) - // let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) let repositories = await auth.$get('joinedRepositories', { where, attributes: { exclude: [] }, @@ -161,19 +174,23 @@ router.get('/repository/joined', isLoggedIn, async (ctx) => { QueryInclude.Organization, QueryInclude.Collaborators, ], - // offset: pagination.start, - // limit: pagination.limit, - order: [['updatedAt', 'DESC']], + order: [['updatedAt', 'DESC']] + }) + let repoData = repositories.map(repo => { + return { + ...repo.toJSON(), + canUserEdit: true, + } }) ctx.body = { - data: repositories, - pagination: undefined, + data: repoData, + pagination: undefined } }) router.get('/repository/get', async (ctx) => { const access = await AccessUtils.canUserAccess( - ACCESS_TYPE.REPOSITORY, + ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, ctx.query.id, ctx.query.token @@ -186,54 +203,52 @@ router.get('/repository/get', async (ctx) => { return } const excludeProperty = ctx.query.excludeProperty || false - const cacheKey = excludeProperty ? CACHE_KEY.REPOSITORY_GET_EXCLUDE_PROPERTY : CACHE_KEY.REPOSITORY_GET - const tryCache = await RedisService.getCache(cacheKey, ctx.query.id) - - let repository: Partial<Repository> - if (tryCache && !excludeProperty) { - repository = JSON.parse(tryCache) - } else { - // 分开查询减少 - let [repositoryOmitModules, repositoryModules] = await Promise.all([ - Repository.findByPk(ctx.query.id, { - attributes: { exclude: [] }, - include: [ - QueryInclude.Creator, - QueryInclude.Owner, - QueryInclude.Locker, - QueryInclude.Members, - QueryInclude.Organization, - QueryInclude.Collaborators, - ], - }), - Repository.findByPk(ctx.query.id, { - attributes: { exclude: [] }, - include: [ - excludeProperty - ? QueryInclude.RepositoryHierarchyExcludeProperty - : QueryInclude.RepositoryHierarchy, - ], - order: [ - [{ model: Module, as: 'modules' }, 'priority', 'asc'], - [ - { model: Module, as: 'modules' }, - { model: Interface, as: 'interfaces' }, - 'priority', - 'asc', - ], + const canUserEdit = await AccessUtils.canUserAccess( + ACCESS_TYPE.REPOSITORY_SET, + ctx.session.id, + ctx.query.id, + ctx.query.token, + ) + let repository: Partial<Repository> & { + canUserEdit: boolean + } + // 分开查询减少查询时间 + let [repositoryOmitModules, repositoryModules] = await Promise.all([ + Repository.findByPk(ctx.query.id, { + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators, + ], + }), + Repository.findByPk(ctx.query.id, { + attributes: { exclude: [] }, + include: [ + excludeProperty + ? QueryInclude.RepositoryHierarchyExcludeProperty + : QueryInclude.RepositoryHierarchy, + ], + order: [ + [{ model: Module, as: 'modules' }, 'priority', 'asc'], + [ + { model: Module, as: 'modules' }, + { model: Interface, as: 'interfaces' }, + 'priority', + 'asc', ], - }), - ]) - repository = { - ...repositoryOmitModules.toJSON(), - ...repositoryModules.toJSON() - } - await RedisService.setCache( - cacheKey, - JSON.stringify(repository), - ctx.query.id - ) + ], + }), + ]) + repository = { + ...repositoryOmitModules.toJSON(), + ...repositoryModules.toJSON(), + canUserEdit } + ctx.body = { data: repository, } @@ -281,7 +296,7 @@ router.post('/repository/create', isLoggedIn, async (ctx, next) => { router.post('/repository/update', isLoggedIn, async (ctx, next) => { const body = Object.assign({}, ctx.request.body) - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, body.id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, body.id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -354,7 +369,7 @@ router.post('/repository/update', isLoggedIn, async (ctx, next) => { router.post('/repository/transfer', isLoggedIn, async (ctx) => { let { id, ownerId, organizationId } = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, organizationId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -372,7 +387,7 @@ router.post('/repository/transfer', isLoggedIn, async (ctx) => { router.get('/repository/remove', isLoggedIn, async (ctx, next) => { const id = +ctx.query.id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -397,7 +412,7 @@ router.get('/repository/remove', isLoggedIn, async (ctx, next) => { // TOEO 锁定/解锁仓库 待测试 router.post('/repository/lock', isLoggedIn, async (ctx) => { const id = +ctx.request.body.id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -475,7 +490,7 @@ router.post('/module/create', isLoggedIn, async (ctx, next) => { router.post('/module/update', isLoggedIn, async (ctx, next) => { const { id, name, description } = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, ctx.session.id, +id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, ctx.session.id, +id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -521,7 +536,7 @@ router.post('/module/move', isLoggedIn, async ctx => { router.get('/module/remove', isLoggedIn, async (ctx, next) => { let { id } = ctx.query - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, ctx.session.id, +id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, ctx.session.id, +id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -570,7 +585,7 @@ router.get('/interface/count', async (ctx) => { router.get('/interface/list', async (ctx) => { let where: any = {} let { repositoryId, moduleId, name } = ctx.query - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, +repositoryId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, +repositoryId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -594,7 +609,7 @@ router.get('/repository/defaultVal/get/:id', async (ctx) => { router.post('/repository/defaultVal/update/:id', async (ctx) => { const repositoryId: number = ctx.params.id - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, repositoryId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, repositoryId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -643,7 +658,7 @@ router.get('/interface/get', async (ctx) => { if ( !(await AccessUtils.canUserAccess( - ACCESS_TYPE.REPOSITORY, + ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, itf.repositoryId )) @@ -699,7 +714,7 @@ router.post('/interface/create', isLoggedIn, async (ctx, next) => { router.post('/interface/update', isLoggedIn, async (ctx, next) => { let body = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, +body.id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +body.id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -744,7 +759,7 @@ router.post('/interface/move', isLoggedIn, async ctx => { router.get('/interface/remove', async (ctx, next) => { let { id } = ctx.query - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, +id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -783,7 +798,7 @@ router.post('/interface/lock', async (ctx, next) => { } let { id } = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, +id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -820,7 +835,7 @@ router.post('/interface/unlock', async (ctx) => { } let { id } = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, +id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -919,7 +934,7 @@ router.post('/properties/update', isLoggedIn, async (ctx, next) => { properties = Array.isArray(properties) ? properties : [properties] let itf = await Interface.findByPk(itfId) - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, ctx.session.id, itfId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, itfId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -1014,7 +1029,7 @@ router.post('/properties/update', isLoggedIn, async (ctx, next) => { router.get('/property/remove', isLoggedIn, async (ctx) => { let { id } = ctx.query - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.PROPERTY, ctx.session.id, id)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.PROPERTY_SET, ctx.session.id, id)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -1027,7 +1042,7 @@ router.get('/property/remove', isLoggedIn, async (ctx) => { router.post('/repository/import', isLoggedIn, async (ctx) => { const { docUrl, orgId } = ctx.request.body - if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION, ctx.session.id, orgId)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, orgId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -1044,7 +1059,7 @@ 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)) { + if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, repositoryId)) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } @@ -1063,7 +1078,7 @@ router.post('/repository/importswagger', isLoggedIn, async (ctx) => { router.post('/repository/importJSON', isLoggedIn , async ctx => { const { data } = ctx.request.body - if (!(await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, ctx.session.id, data.id))) { + if (!(await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, data.id))) { ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY return } diff --git a/src/routes/utils/access.ts b/src/routes/utils/access.ts index b13146b..c2d6b08 100644 --- a/src/routes/utils/access.ts +++ b/src/routes/utils/access.ts @@ -2,27 +2,61 @@ import OrganizationService from '../../service/organization' import RepositoryService from '../../service/repository' import { Module, Interface, Property } from '../../models' -export enum ACCESS_TYPE { ORGANIZATION, REPOSITORY, MODULE, INTERFACE, PROPERTY, USER, ADMIN } +export enum ACCESS_TYPE { + ORGANIZATION_GET, + ORGANIZATION_SET, + REPOSITORY_GET, + REPOSITORY_SET, + MODULE_GET, + MODULE_SET, + INTERFACE_GET, + INTERFACE_SET, + PROPERTY_GET, + PROPERTY_SET, + USER, + ADMIN, +} const inTestMode = process.env.TEST_MODE === 'true' export class AccessUtils { - public static async canUserAccess(accessType: ACCESS_TYPE, curUserId: number, entityId: number, token?: string): Promise<boolean> { + public static async canUserAccess( + accessType: ACCESS_TYPE, + curUserId: number, + entityId: number, + token?: string, + ): Promise<boolean> { + // 测试模式无权限 if (inTestMode) { return true } - if (accessType === ACCESS_TYPE.ORGANIZATION) { - return await OrganizationService.canUserAccessOrganization(curUserId, entityId) - } else if (accessType === ACCESS_TYPE.REPOSITORY) { - return await RepositoryService.canUserAccessRepository(curUserId, entityId, token) - } else if (accessType === ACCESS_TYPE.MODULE) { + + // 无 session 时拒绝写操作 + if (!curUserId) { + return false + } + + if ( + accessType === ACCESS_TYPE.ORGANIZATION_GET || + accessType === ACCESS_TYPE.ORGANIZATION_SET + ) { + return OrganizationService.canUserAccessOrganization(curUserId, entityId) + } else if ( + accessType === ACCESS_TYPE.REPOSITORY_GET || + accessType === ACCESS_TYPE.REPOSITORY_SET + ) { + return RepositoryService.canUserAccessRepository(curUserId, entityId, token) + } else if (accessType === ACCESS_TYPE.MODULE_GET || accessType === ACCESS_TYPE.MODULE_SET) { const mod = await Module.findByPk(entityId) - return await RepositoryService.canUserAccessRepository(curUserId, mod.repositoryId, token) - } else if (accessType === ACCESS_TYPE.INTERFACE) { + return RepositoryService.canUserAccessRepository(curUserId, mod.repositoryId, token) + } else if ( + accessType === ACCESS_TYPE.INTERFACE_GET || + accessType === ACCESS_TYPE.INTERFACE_SET + ) { const itf = await Interface.findByPk(entityId) - return await RepositoryService.canUserAccessRepository(curUserId, itf.repositoryId, token) - } else if (accessType === ACCESS_TYPE.PROPERTY) { + return RepositoryService.canUserAccessRepository(curUserId, itf.repositoryId, token) + } else if (accessType === ACCESS_TYPE.PROPERTY_GET || accessType === ACCESS_TYPE.PROPERTY_SET) { const p = await Property.findByPk(entityId) - return await RepositoryService.canUserAccessRepository(curUserId, p.repositoryId, token) + return RepositoryService.canUserAccessRepository(curUserId, p.repositoryId, token) } return false } @@ -33,4 +67,4 @@ export class AccessUtils { } return curUserId === 1 } -} \ No newline at end of file +} diff --git a/src/scripts/dev.ts b/src/scripts/dev.ts index 002621e..029c54e 100644 --- a/src/scripts/dev.ts +++ b/src/scripts/dev.ts @@ -8,7 +8,7 @@ const start = () => { let open = false console.log('----------------------------------------') app.listen(port, () => { - console.log(`rap2-dolores is running as ${url}`) + console.log(`rap2-delos is running as ${url}`) if (!open) return try { execSync(`osascript openChrome.applescript ${url}`, { cwd: __dirname, stdio: 'ignore' }) diff --git a/src/service/mail.ts b/src/service/mail.ts index 920291d..9bf1713 100644 --- a/src/service/mail.ts +++ b/src/service/mail.ts @@ -2,26 +2,158 @@ import * as nodemailer from 'nodemailer' import config from '../config' export default class MailService { + public static async sendMail(mailOptions: nodemailer.SendMailOptions) { + const transporter = nodemailer.createTransport(config.mail) - public static send(to: string, subject: string, html: string) { + return new Promise((resolve, reject) => { + transporter.verify(function(error) { + if (error) { + reject(error) + } else { + transporter.sendMail(mailOptions, function(error, info) { + if (error) { + reject(error) + } else { + resolve(info.response) + } + }) + } + }) + }) + } - nodemailer.createTestAccount((_err, _account) => { - const transporter = nodemailer.createTransport(config.mail) + public static send(to: string | string[], subject: string, html: string) { + const transporter = nodemailer.createTransport(config.mail) - // setup email data with unicode symbols - const mailOptions = { - from: `"RAP2 Notifier" <${config.mailSender}>`, // sender address - to, - subject, - html, - } + const mailOptions = { + from: `"RAP2 Notifier" <${config.mailSender}>`, + to, + subject, + html, + } - // send mail with defined transport object - transporter.sendMail(mailOptions) + return new Promise((resolve, reject) => { + transporter.verify(function(error) { + if (error) { + reject(error) + } else { + transporter.sendMail(mailOptions, function(error, info) { + if (error) { + reject(error) + } else { + resolve(info.response) + } + }) + } + }) }) } -public static mailFindpwdTemp = `<head> + public static mailNoticeTemp = `<head> + <base target="_blank" /> + </head> + <body> + <div id="content" bgcolor="#f0f0f0" style="background-color:#f0f0f0;padding:10px"> + <table width="100%" border="0" cellpadding="0" cellspacing="0"> + <tr> + <td valign="top"> + <table class="full-width" align="center" width="700" border="0" cellpadding="0" cellspacing="0" + bgcolor="#ffffff" style="background-color:#ffffff; width:700px;"> + <tr> + <td style="vertical-align:top;"> + <table class="full-width" align="center" width="700" border="0" cellpadding="0" cellspacing="0" + style="width:700px;"> + <tr> + <td valign="top"> + <table class="full-width" width="549" border="0" cellpadding="0" cellspacing="0"> + <tr> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + <td width="32" valign="middle" style="padding-top:30px; padding-bottom:32px; width:32px;"><img width="140" height="34" src="http://img.alicdn.com/tfs/TB1vtTllHH1gK0jSZFwXXc7aXXa-140-34.png"></td> + <td valign="middle"></td> + <td width="10"></td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + <tr> + <td valign="top" style="padding-top:10px ;"> + <table class="full-width" align="center" width="700" border="0" cellpadding="0" cellspacing="0" + bgcolor="#ffffff" style="background-color:#ffffff; width:700px;"> + <tr> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + <td class="mobile-headline" + style="color:#000000; font-size:24px; line-height:26px;">{=TITLE=}</td> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + </tr> + <tr> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + <td + style="color:#555555; font-size:15px; line-height:20px; padding-top:30px;"> + <ul style="margin-bottom:0px; margin-top:0px;"> + {=CONTENT=} + </ul> + </td> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + </tr> + </table> + <table class="full-width" align="center" width="700" border="0" cellpadding="0" cellspacing="0" + bgcolor="#F7F8F9" style="background-color:#F7F8F9; width:700px;"> + <tr> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + <td + style="color:#555555; font-size:15px; line-height:20px; padding-top:30px; padding-bottom:30px;"> + <strong>此邮件为系统发送,请勿直接回复</strong> + </td> + <td class="mobile-spacer" width="30" style="width:30px;"> </td> + </tr> + </table> + </td> + </tr> + <tr> + <td valign="top"> + <table class="full-width" align="center" width="700" border="0" cellpadding="0" cellspacing="0" + bgcolor="#3f51b5" style="background-color:#3f51b5; width:700px;"> + <tr> + <td width="22"> </td> + <td align="center" + style="color:#ffffff; font-size:11px; line-height:14px; padding-top:20px;text-align:center;"> + <strong><a href="http://rap2.alibaba-inc.com/" + style="color:#ffffff; font-weight:bold; text-decoration:none;">RAP2</a> | <a + href="https://github.com/thx/rap2-delos" + style="color:#ffffff; font-weight:bold; text-decoration:none;">GitHub</a></strong> + </td> + <td width="22"> </td> + </tr> + <tr> + <td width="22" colspan="3"> </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </div> + <style type="text/css"> + body { + font-size: 14px; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + line-height: 1.666; + padding: 0; + margin: 0; + overflow: auto; + white-space: normal; + word-wrap: break-word; + min-height: 100px + } + </style> + </body>` + + public static mailFindpwdTemp = `<head> <base target="_blank" /> </head> <body> @@ -84,6 +216,7 @@ public static mailFindpwdTemp = `<head> style="color:#555555; font-size:15px; line-height:20px; padding-top:30px; padding-bottom:30px;"> <strong>为什么我会收到这封邮件?</strong>您在RAP2系统使用了密码找回功能,我们发送这封邮件,以协助您重设密码。 <br><br>如果您没有使用此功能,请忽略此邮件 + <br><br>如果您不想收到我们的邮件<a href="http://rap2.taobao.org/account/unsubscribe" style="color:#1473E6; font-weight:bold; text-decoration:none;">点击退订</a> </td> <td class="mobile-spacer" width="30" style="width:30px;"> </td> </tr> @@ -130,4 +263,4 @@ public static mailFindpwdTemp = `<head> } </style> </body>` -} \ No newline at end of file +} diff --git a/src/service/migrate.ts b/src/service/migrate.ts index de569d4..8aeb98b 100644 --- a/src/service/migrate.ts +++ b/src/service/migrate.ts @@ -1,25 +1,30 @@ 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 * as querystring from 'querystring' +import * as rp from 'request-promise' +import { Op } from 'sequelize' import RedisService, { CACHE_KEY } from "./redis" +import MailService from './mail' +import * as md5 from 'md5' +const isMd5 = require('is-md5') import * as _ from 'lodash' const SWAGGER_VERSION = { 1: '2.0' } - - +/** + * swagger json结构转化的数组转化为树形结构 + * @param list + */ const arrayToTree = (list) => { const parseChildren = (list, parent) => { list.forEach((item) => { if (item.parent === parent.id) { item.depth = parent.depth + 1 + item.parentName = parent.name item.children = item.children || [] parent.children.push(item) parseChildren(list, item) @@ -36,6 +41,50 @@ const arrayToTree = (list) => { }) } +/** + * swagger json结构转化的数组转化的树形结构转化为数组 + * @param tree + */ +const treeToArray = (tree: any) => { + const parseChildren = (parent: any, result: any) => { + if (!parent.children) { return result } + parent.children.forEach((item: any) => { + result.push(item) + parseChildren(item, result) + delete item.children + }) + return result + } + return parseChildren(tree, []) +} + +/** + * 接口属性-数组结构转化为树形结构 + * @param list + */ +const arrayToTreeProperties = (list: any) => { + const parseChildren = (list: any, parent: any) => { + list.forEach((item: any) => { + if (item.parentId === parent.id) { + item.depth = parent.depth + 1 + item.children = item.children || [] + parent.children.push(item) + parseChildren(list, item) + } + }) + return parent + } + return parseChildren(list, { + id: -1, + name: 'root', + children: [], + depth: -1, + }) +} + +/** + * 参数请求类型枚举 + */ const REQUEST_TYPE_POS = { path: 2, query: 2, @@ -44,13 +93,21 @@ const REQUEST_TYPE_POS = { body: 3 } +let checkSwaggerResult = [] +let changeTip = '' // 变更信息 + /** + * Swagger JSON 参数递归处理成数组 * @param parameters 参数列表数组 - * @param parent 父级 - * @param result swagger转化为数组结果 - * @param definitions swagger $ref definitions + * @param parent 父级id + * @param parentName 父级属性name + * @param depth parameters list 中每个属性的深度 + * @param result swagger转化为数组结果 -- 对swagger参数处理结果 + * @param definitions swagger $ref definitions, 额外传递过来的swagger的definitions数据, 非计算核心算法 + * @param scope 参数类型 -- 【暂不用】用于参数校验后提示 + * @param apiInfo 接口信息 -- 【暂不用】用于参数校验后提示 */ -const parse = (parameters, parent, result, definitions) => { +const parse = (parameters, parent, parentName, depth, result, definitions, scope, apiInfo) => { for (let key = 0, len = parameters.length; key < len; key++) { const param = parameters[key] @@ -58,6 +115,8 @@ const parse = (parameters, parent, result, definitions) => { // 非对象或者数组的基础类型 result.push({ ...param, parent, + parentName, + depth, id: `${parent}-${key}` }) } else { @@ -68,17 +127,20 @@ const parse = (parameters, parent, result, definitions) => { result.push({ ...param, parent, + parentName, + depth, 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 } @@ -87,20 +149,194 @@ const parse = (parameters, parent, result, definitions) => { 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 - }) + // swagger文档中对definition定义属性又引用自身的情况处理-死循环 + if (properties[key].$ref) { + if (properties[key].$ref.split('#/definitions/')[1] === refName) { + // delete properties[key].$ref + list.push({ + name: key, + parentName: param.name, + depth: depth + 1, + ...properties[key], + $ref: null, + type: 'object', + in: param.in, + required: (ref.required || []).indexOf(key) >= 0, + description: `【递归父级属性】${properties[key].description || ''}` + }) + } else { + list.push({ + name: key, + parentName: param.name, + depth: depth + 1, + ...properties[key], + in: param.in, + required: (ref.required || []).indexOf(key) >= 0, + }) + } + } else if ((properties[key].items || {}).$ref) { + if (properties[key].items.$ref.split('#/definitions/')[1] === refName) { + // delete properties[key].items.$ref + list.push({ + name: key, + parentName: param.name, + depth: depth + 1, + ...properties[key], + type: 'array', + $ref: null, + in: param.in, + required: (ref.required || []).indexOf(key) >= 0, + description: `【递归父级属性】${properties[key].description || ''}` + }) + } else { + list.push({ + name: key, + parentName: param.name, + depth: depth + 1, + ...properties[key], + in: param.in, + required: (ref.required || []).indexOf(key) >= 0, + }) + } + + } else { + list.push({ + name: key, + parentName: param.name, + depth: depth + 1, + ...properties[key], + in: param.in, // response 无所谓,不使用但是request 使用 + required: (ref.required || []).indexOf(key) >= 0, + }) + } } - parse(list, `${parent}-${key}`, result, definitions) + parse(list, `${parent}-${key}`, param.name, depth + 1, result, definitions, scope, apiInfo) } } } } +const transformRapParams = (p) => { + let rule = '', description = '', value = p.default || '' + + // 类型转化处理 + let type = (p.type || 'string') + if (type === 'integer') type = 'number' + if (type === 'file') type = 'string' + type = type[0].toUpperCase() + type.slice(1) + + // 规则属性说明处理 + if (p.type === 'string' && p.minLength && p.maxLength ) { + rule = `${p.minLength}-${p.maxLength}` + description = `${description}|长度限制: ${p.minLength}-${p.maxLength}` + } else if (p.type === 'string' && p.minLength && !p.maxLength) { + rule = `${p.minLength}` + description = `${description}|长度限制:最小值: ${p.minLength}` + } else if (p.type === 'string' && !p.minLength && p.maxLength) { + rule = `${p.required ? '1' : '0'}-${p.maxLength}` + description = `${description}|长度限制:最大值: ${p.maxLength}` + } + if (p.type === 'string' && p.enum && p.enum.length > 0) { + description = `${description}|枚举值: ${p.enum.join()}` + } + if ((p.type === 'integer' || p.type === 'number') && p.minimum && p.maxinum) { + rule = `${p.minimum}-${p.maxinum}` + description = `${description}|数据范围: ${p.minimum}-${p.maxinum}` + } + if ((p.type === 'integer' || p.type === 'number') && p.minimum && !p.maxinum) { + rule = `${p.minimum}` + description = `${description}|数据范围: 最小值:${p.minimum}` + } + if ((p.type === 'integer' || p.type === 'number') && !p.minimum && p.maxinum) { + rule = `${p.required ? '1' : '0'}-${p.maxinum}` + description = `${description}|数据范围: 最大值:${p.maxinum}` + } + + // 默认值转化处理 + value = p.default || '' + if (!p.default && p.type === 'string') value = '@ctitle' + if (!p.default && (p.type === 'number' || p.type === 'integer')) value = '@integer(0, 100000)' + if (p.type === 'boolean') { value = (p.default === true || p.default === false) ? p.default.toString() : 'false'} + if (p.enum && (p.enum || []).length > 0) value = p.enum[0] + if (p.type === 'string' && p.format === 'date-time') value = '@datetime' + if (p.type === 'string' && p.format === 'date') value = '@date' + + 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]}]` + } + + return { + type, rule, description: description.length > 0 ? description.substring(1) : '', value + } +} + +const propertiesUpdateService = async (properties, itfId) => { + properties = Array.isArray(properties) ? properties : [properties] + let itf = await Interface.findByPk(itfId) + + let existingProperties = properties.filter((item: any) => !item.memory) + let result = await Property.destroy({ + where: { + id: { [Op.notIn]: existingProperties.map((item: any) => item.id) }, + interfaceId: itfId + } + }) + + // 更新已存在的属性 + for (let item of existingProperties) { + let affected = await Property.update(item, { + where: { id: item.id } + }) + result += affected[0] + } + // 插入新增加的属性 + let newProperties = properties.filter((item: any) => item.memory) + let memoryIdsMap: any = {} + for (let item of newProperties) { + let created = await Property.create(Object.assign({}, item, { + id: undefined, + parentId: -1, + priority: item.priority || Date.now() + })) + memoryIdsMap[item.id] = created.id + item.id = created.id + result += 1 + } + // 同步 parentId + for (let item of newProperties) { + let parentId = memoryIdsMap[item.parentId] || item.parentId + await Property.update({ parentId }, { + where: { id: item.id } + }) + } + itf = await Interface.findByPk(itfId, { + include: (QueryInclude.RepositoryHierarchy as any).include[0].include, + }) + return { + data: { + result, + properties: itf.properties, + } + } +} + +const sendMailTemplate = (changeTip) => { + let html = MailService.mailNoticeTemp + .replace('{=TITLE=}', '您相关的接口存在如下变更:(请注意代码是否要调整)') + .replace('{=CONTENT=}', (changeTip.split('<br/>') || []).map(one => { + return one ? `<li style="margin-bottom: 20px;">${one}</li>` : '' + }).join('')) + return html +} export default class MigrateService { public static async importRepoFromRAP1ProjectData(orgId: number, curUserId: number, projectData: any): Promise<boolean> { if (!projectData || !projectData.id || !projectData.name) return false @@ -160,7 +396,6 @@ export default class MigrateService { let description = [] if (p.name) description.push(p.name) if (p.remark && remarkWithoutMock) description.push(remarkWithoutMock) - const pCreated = await Property.create({ scope, name, @@ -226,12 +461,13 @@ export default class MigrateService { } /** 请求参对象->数组->标准树形对象 @param swagger @param parameters */ - public static async swaggerToModelRequest(swagger: SwaggerData, parameters: Array<any>, method: string): Promise<any> { - const { definitions } = swagger + public static async swaggerToModelRequest(swagger: SwaggerData, parameters: Array<any>, method: string, apiInfo: any): Promise<any> { + let { definitions } = swagger const result = [] + definitions = JSON.parse(JSON.stringify(definitions)) // 防止接口之间数据处理相互影响 if (method === 'get' || method === 'GET') { - parse(parameters, 'root', result, definitions) + parse(parameters, 'root', 'root', 0, result, definitions, 'request', apiInfo) } else if (method === 'post' || method === 'POST') { let list = [] // 外层处理参数数据结果 const bodyObj = parameters.find(item => item.in === 'body') // body unique @@ -263,7 +499,7 @@ export default class MigrateService { } } } - parse(list, 'root', result, definitions) + parse(list, 'root', 'root', 0, result, definitions, 'request', apiInfo) } const tree = arrayToTree(JSON.parse(JSON.stringify(result))) @@ -277,8 +513,10 @@ export default class MigrateService { * @param swagger * @param response */ - public static async swaggerToModelRespnse (swagger: SwaggerData, response: object): Promise<any> { - const { definitions } = swagger + public static async swaggerToModelRespnse (swagger: SwaggerData, response: object, apiInfo: any): Promise<any> { + let { definitions = {} } = swagger + definitions = JSON.parse(JSON.stringify(definitions)) // 防止接口之间数据处理相互影响 + const successObj = response['200'] if (!successObj) return [] @@ -295,85 +533,62 @@ export default class MigrateService { const properties = ref.properties for (const key in properties) { + // 公共返回参数描述信息设置 + let description = '' + if (!properties[key].description && key === 'errorCode') { + description = '错误码' + } + if (!properties[key].description && key === 'errorMessage') { + description = '错误描述' + } + if (!properties[key].description && key === 'success') { + description = '请求业务结果' + } + parameters.push({ name: key, ...properties[key], in: 'body', - required: (ref.required || []).indexOf(key) >= 0 + required: key === 'success' ? true : (ref.required || []).indexOf(key) >= 0, + default: key === 'success' ? true : (properties[key].default || false), + description: properties[key].description || description, }) } } const result = [] - parse(parameters, 'root', result, definitions) - + parse(parameters, 'root', 'root', 0, result, definitions, 'response', apiInfo) const tree = arrayToTree(JSON.parse(JSON.stringify(result))) return tree } - public static async importRepoFromSwaggerProjectData(repositoryId: number, curUserId: number, swagger: SwaggerData): Promise<boolean> { +public static async importRepoFromSwaggerProjectData(repositoryId: number, curUserId: number, swagger: SwaggerData): Promise<boolean> { + checkSwaggerResult = [] if (!swagger.paths || !swagger.swagger || !swagger.host) return false let mCounter = 1 // 模块优先级顺序 let iCounter = 1 // 接口优先级顺序 let pCounter = 1 // 参数优先级顺序 + /** + * 接口创建并批量创建属性,规则,默认值,说明等处理 + * @param p + * @param scope + * @param interfaceId + * @param moduleId + * @param parentId + */ 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 { rule, value, type, description } = transformRapParams(p) + const joinDescription = `${p.description || ''}${((p.description || '') && (description || '')) ? '|' : ''}${description || ''}` const pCreated = await Property.create({ scope, - name, + name: p.name, rule, value, type, required: p.required, - description: `${p.description || ''} ${description ? `|${description}` : ''}`, + description: joinDescription, priority: pCounter++, interfaceId: interfaceId, creatorId: curUserId, @@ -392,7 +607,7 @@ export default class MigrateService { let { tags = [], paths = {}, host = '' } = swagger let pathTag: SwaggerTag[] = [] - // 处理root tag中没有的情况 + // 获取所有的TAG: 处理ROOT TAG中没有的情况 for (const action in paths) { const apiObj = paths[action][Object.keys(paths[action])[0]] const index = pathTag.findIndex((it: SwaggerTag) => { @@ -402,7 +617,11 @@ export default class MigrateService { } tags = pathTag + if (checkSwaggerResult.length > 0) return false + for (const tag of tags) { + if (checkSwaggerResult.length > 0) break + let repository: Partial<Repository> let [repositoryModules] = await Promise.all([ Repository.findByPk(repositoryId, { @@ -436,12 +655,12 @@ export default class MigrateService { } 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 + const summary = apiObj.summary if (actionTags0 === tag.name) { // 判断接口是否存在在该模块中,如果不存在则创建接口,存在则更新接口信息 @@ -464,8 +683,10 @@ export default class MigrateService { ...repositoryModules.toJSON() } - const request = await this.swaggerToModelRequest(swagger, apiObj.parameters || {}, method) - const response = await this.swaggerToModelRespnse(swagger, apiObj.responses || {}) + const request = await this.swaggerToModelRequest(swagger, apiObj.parameters || {}, method, { url, summary }) + const response = await this.swaggerToModelRespnse(swagger, apiObj.responses || {}, { url, summary }) + // 处理完每个接口请求参数后,如果-遇到第一个存在接口不符合规范就全部返回 + if (checkSwaggerResult.length > 0) break // 判断对应模块是否存在该接口 const index = repository.modules.findIndex(item => { @@ -504,18 +725,177 @@ export default class MigrateService { method: method.toUpperCase() }, { where: { id: findApi.id } }) - await Property.destroy({ where: { interfaceId: findApi.id } }) + // 获取已经存在的接口的属性信息,并处理深度和parentName + let A_ExistsPropertiesOld = JSON.parse(JSON.stringify(findApi.properties)) + A_ExistsPropertiesOld = JSON.parse(JSON.stringify(treeToArray((arrayToTreeProperties(A_ExistsPropertiesOld))))) + let A_ExistsProperties = A_ExistsPropertiesOld.map(property => { + return { + ...property, + parentName: (A_ExistsPropertiesOld.find(item => item.id === property.parentId) || {}).name || 'root', + } + }) - for (const p of (request.children || [])) { - await processParam(p, SCOPES.REQUEST, findApi.id, mod.id) + const B_SwaggerProperties_Request = treeToArray(request) + const B_SwaggerProperties_Response = treeToArray(response) + let PropertyId = 0, PriorityId = 0 + + let maxDepth_Request = 0, maxDepth_Response = 0, maxDepth_A_ExistsProperties = 0 + // 计算B的最大深度-- request + B_SwaggerProperties_Request.map(item => { + if (item.depth > maxDepth_Request) { + maxDepth_Request = item.depth + } + return item + }) + + // 计算B的最大深度-- response + B_SwaggerProperties_Response.map(item => { + if (item.depth > maxDepth_Response) { + maxDepth_Response = item.depth + } + return item + }) + + // 计算A的最大深度 + A_ExistsProperties.map(item => { + if (item.depth > maxDepth_A_ExistsProperties) { + maxDepth_A_ExistsProperties = item.depth + } + return item + }) + + const properties = [] + + /** + * 批量更新接口属性名称,类型,规则,默认值等处理 + * @param BFilterByDepth + * @param depth + * @param scope + */ + const updateProperties = (BFilterByDepth, depth, scope) => { + for (const key in BFilterByDepth) { + const bValue = BFilterByDepth[key] + const index = A_ExistsProperties.findIndex(item => (item.name === bValue.name && item.depth === bValue.depth && item.parentName === bValue.parentName && item.scope === scope)) + const { type, description, rule, value} = transformRapParams(bValue) + const joinDescription = `${bValue.description || ''}${((bValue.description || '') && (description || '')) ? '|' : ''}${description || ''}` + + if (index >= 0) { + // 属性存在 ---修改:类型;是否必填;属性说明;不修改规则和默认值(前端可能正在mock) + // 如何判断有更新 + if (type !== A_ExistsProperties[index].type) { + // 类型变更了 + changeTip = `${changeTip}<br/>接口名称:${apiObj.summary} [更新属性:${A_ExistsProperties[index].name}类型由“${A_ExistsProperties[index].type}”变更为“${type}]”` + } + if (!!bValue.required !== A_ExistsProperties[index].required) { + // 是否必填变更 + changeTip = `${changeTip}<br/>接口名称:${apiObj.summary} [更新属性:${A_ExistsProperties[index].name}是否必填由“${A_ExistsProperties[index].required}”变更为“${bValue.required}]”` + } + + if (joinDescription !== A_ExistsProperties[index].description && bValue.name !== 'success' && bValue.name !== 'errorCode' && bValue.name !== 'errorMessage') { + // 描述信息变更 + changeTip = `${changeTip}<br/>接口名称:${apiObj.summary} [更新属性:${A_ExistsProperties[index].name}属性简介由“${A_ExistsProperties[index].description || '无'}”变更为“${joinDescription}”]` + } + + properties.push({ + ...A_ExistsProperties[index], + rule: (!A_ExistsProperties[index].rule && !A_ExistsProperties[index].value) ? rule : A_ExistsProperties[index].rule, + value: (!A_ExistsProperties[index].rule && !A_ExistsProperties[index].value) ? value : A_ExistsProperties[index].value, + type, + required: !!bValue.required, // 是否必填更改 + description: `${joinDescription}`, + }) + } else { + changeTip = `${changeTip}<br/>接口名称:${apiObj.summary} [属性添加:${bValue.name};类型:${type} ;简介: ${bValue.description || ''}${(bValue.description || '') ? '|' : ''}${description || ''} ]` + // 属性不存在 + if (depth === 0) { + properties.push({ + id: `memory-${++PropertyId}`, + scope, + type, + pos: REQUEST_TYPE_POS[bValue.in], + name: bValue.name, + rule, + value, + description: `${joinDescription}`, + parentId: -1 , + priority: `${++PriorityId}`, + interfaceId: findApi.id, + moduleId: mod.id, + repositoryId, + memory: true, + depth: bValue.depth, + changeType: 'add', + }) + + } else { + // 找到父级属性信息 + const parent = properties.find(it => (it.depth === bValue.depth - 1 && it.name === bValue.parentName && it.scope === scope)) + properties.push({ + id: `memory-${++PropertyId}`, + scope, + type, + pos: REQUEST_TYPE_POS[bValue.in], + name: bValue.name, + rule, + value, + description: `${joinDescription}`, + parentId: parent.id, + priority: `${++PriorityId}`, + interfaceId: findApi.id, + moduleId: mod.id, + repositoryId, + memory: true, + depth: bValue.depth, + changeType: 'add', + }) + } + } + } } - for (const p of (response.children || [])) { - await processParam(p, SCOPES.RESPONSE, findApi.id, mod.id) + + /** 删除属性计算 */ + const deleteProperties = (AFilterByDepth, scope) => { + for (const key in AFilterByDepth) { + const aValue = AFilterByDepth[key] + let index = -1 + if (scope === 'request') { + index = B_SwaggerProperties_Request.findIndex(item => (item.name === aValue.name && item.depth === aValue.depth && item.parentName === aValue.parentName)) + } else if (scope === 'response') { + index = B_SwaggerProperties_Response.findIndex(item => (item.name === aValue.name && item.depth === aValue.depth && item.parentName === aValue.parentName)) + } + + const { type, description } = transformRapParams(aValue) + if (index < 0) { + // A 存在,B不存在 + changeTip = `${changeTip} <br/> 接口名称:${apiObj.summary} [属性删除:${aValue.name};类型:${type} ;简介: ${aValue.description || ''} ${description ? `${aValue.description ? '|' : '' }${description}` : ''} ]` + } + } + } + for (let depth = 0; depth <= maxDepth_A_ExistsProperties; depth++) { + const AFilterByDepth = A_ExistsProperties.filter(item => item.depth === depth && item.scope === 'request') + deleteProperties(AFilterByDepth, 'request') } + for (let depth = 0; depth <= maxDepth_A_ExistsProperties; depth++) { + const AFilterByDepth = A_ExistsProperties.filter(item => item.depth === depth && item.scope === 'response') + deleteProperties(AFilterByDepth, 'response') + } + + for (let depth = 0; depth <= maxDepth_Request; depth++) { + const BFilterByDepth = B_SwaggerProperties_Request.filter(item => item.depth === depth) + updateProperties(BFilterByDepth, depth, 'request') + } + + for (let depth = 0; depth <= maxDepth_Response; depth++) { + const BFilterByDepth = B_SwaggerProperties_Response.filter(item => item.depth === depth) + updateProperties(BFilterByDepth, depth, 'response') + } + await propertiesUpdateService(properties, findApi.id) } } } } + + if (checkSwaggerResult.length > 0) return false return true } @@ -527,10 +907,21 @@ export default class MigrateService { if (swagger.swagger === SWAGGER_VERSION[version]) { let result + let mailRepositoryName = '', mailRepositoryId = 0, mailRepositoryMembers = [] if (mode === 'manual') { - const repos = await Repository.findByPk(repositoryId) + const repos = await Repository.findByPk(repositoryId, { + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators, + ], + }) const { creatorId, members, collaborators, ownerId, name } = repos + const body = { creatorId: creatorId, organizationId: orgId, @@ -543,7 +934,12 @@ export default class MigrateService { description: `[host=${host}]${info.title || ''}`, } result = await Repository.update(body, { where: { id: repositoryId } }) + + mailRepositoryName = name + mailRepositoryMembers = members + mailRepositoryId = repositoryId } else if (mode === 'auto') { + // 团队下直接导入功能作废,此处不用执行 result = await Repository.create({ id: 0, name: info.title || 'swagger导入仓库', @@ -562,13 +958,46 @@ export default class MigrateService { 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' } + if (!bol) { + return {result: checkSwaggerResult, code: 'checkSwagger'} + } else { + await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, result.id) + if (changeTip.length > 0) { + const to = mailRepositoryMembers.map(item => { + return `"${item.fullname}" ${item.email},` + }) + + MailService.send( + to, + `仓库:${mailRepositoryName}(${mailRepositoryId})接口更新同步`, + sendMailTemplate(changeTip) + ).then(() => {}) + .catch(() => {}) + + // 钉钉消息发送 + // const dingMsg = { + // msgtype: 'action_card', + // action_card: { + // title: `仓库:${mailRepositoryName}(${mailRepositoryId})接口更新同步`, + // markdown: "支持markdown格式的正文内容", + // single_title: "查看仓库更新", // swagger 批量导入跳转至仓库, 如果后期只要接口更新就通知相关人的话,需要设置具体接口链接 + // single_url: `https://rap2.alibaba-inc.com/repository/editor?id=${repositoryId}` + // } + // } + + // DingPushService.dingPush(mailRepositoryMembers.map(item => item.empId).join(), dingMsg) + // .catch((err) => { console.log(err) }) + } + changeTip = '' + return { result: bol, code: 'success' } + } + } } else { return { result: true, code: 'version'} } } catch (err) { + console.log(err) return { result: false, code: 'error'} } } @@ -746,6 +1175,8 @@ interface OldParameter { remark: string dataType: string parameterList: OldParameter[] + parentName: string, + depth: number } interface SwaggerParameter { diff --git a/src/service/organization.ts b/src/service/organization.ts index 33c3803..6fda7ac 100644 --- a/src/service/organization.ts +++ b/src/service/organization.ts @@ -7,7 +7,7 @@ export default class OrganizationService { SELECT COUNT(id) AS num FROM ( SELECT o.id, o.name FROM Organizations o - WHERE visibility = ${1} OR creatorId = ${userId} OR ownerId = ${userId} + WHERE ownerId = ${userId} UNION SELECT o.id, o.name FROM Organizations o @@ -31,7 +31,7 @@ export default class OrganizationService { SELECT id FROM ( SELECT o.id, o.name FROM Organizations o - WHERE visibility = ${1} OR creatorId = ${curUserId} OR ownerId = ${curUserId} + WHERE visibility = ${1} OR ownerId = ${curUserId} UNION SELECT o.id, o.name FROM Organizations o @@ -54,7 +54,7 @@ export default class OrganizationService { SELECT count(*) AS num FROM ( SELECT o.id, o.name FROM Organizations o - WHERE visibility = ${1} OR creatorId = ${curUserId} OR ownerId = ${curUserId} + WHERE visibility = ${1} OR ownerId = ${curUserId} UNION SELECT o.id, o.name FROM Organizations o diff --git a/src/service/repository.ts b/src/service/repository.ts index a1a5ed8..a494991 100644 --- a/src/service/repository.ts +++ b/src/service/repository.ts @@ -13,7 +13,7 @@ export default class RepositoryService { const repo = await Repository.findByPk(repositoryId) if (token && repo.token === token) return true if (!repo) return false - if (repo.creatorId === userId || repo.ownerId === userId) return true + if (repo.ownerId === userId) return true const memberExistsNum = await RepositoriesMembers.count({ where: { userId, @@ -31,16 +31,16 @@ export default class RepositoryService { destModuleId: number, ) { return ( - AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE, userId, itfId) && - AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY, userId, destRepoId) && - AccessUtils.canUserAccess(ACCESS_TYPE.MODULE, userId, destModuleId) + AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_GET, userId, itfId) && + AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, userId, destRepoId) && + AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, 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) + AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_GET, userId, modId) && + AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, userId, destRepoId) ) }