Merged PR 450: ワークフローAPI IF実装

## 概要
[Task2737: ワークフローAPI IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2737)

- Workflow関連で以下のAPIIFを実装し、OpenAPIを更新しました。
  - ワークフロー一覧取得API
  - ワークフロー追加API
  - Author一覧取得API

## レビューポイント
- パラメータは想定通りか
- バリデーションは適切か
- パスは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-09-28 10:06:51 +00:00
parent 423e5ab1e3
commit 879169c3c7
10 changed files with 530 additions and 0 deletions

View File

@ -279,6 +279,42 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/authors": {
"get": {
"operationId": "getAuthors",
"summary": "",
"description": "ログインしているユーザーのアカウント配下のAuthor一覧を取得します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/GetAuthorsResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"],
"security": [{ "bearer": [] }]
}
},
"/accounts/typists": {
"get": {
"operationId": "getTypists",
@ -2848,6 +2884,98 @@
"security": [{ "bearer": [] }]
}
},
"/workflows": {
"get": {
"operationId": "getWorkflows",
"summary": "",
"description": "アカウント内のワークフローの一覧を取得します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetWorkflowsResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["workflows"],
"security": [{ "bearer": [] }]
},
"post": {
"operationId": "createWorkflows",
"summary": "",
"description": "アカウント内にワークフローを新規作成します",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWorkflowsRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWorkflowsResponse"
}
}
}
},
"400": {
"description": "パラメータ不正エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["workflows"],
"security": [{ "bearer": [] }]
}
},
"/notification/register": {
"post": {
"operationId": "register",
@ -3036,6 +3164,24 @@
"properties": { "account": { "$ref": "#/components/schemas/Account" } },
"required": ["account"]
},
"Author": {
"type": "object",
"properties": {
"id": { "type": "number", "description": "Authorユーザーの内部ID" },
"authorId": { "type": "string", "description": "AuthorID" }
},
"required": ["id", "authorId"]
},
"GetAuthorsResponse": {
"type": "object",
"properties": {
"authors": {
"type": "array",
"items": { "$ref": "#/components/schemas/Author" }
}
},
"required": ["authors"]
},
"Typist": {
"type": "object",
"properties": {
@ -4035,6 +4181,92 @@
},
"required": ["templates"]
},
"WorkflowWorktype": {
"type": "object",
"properties": {
"id": { "type": "number", "description": "Worktypeの内部ID" },
"worktypeId": { "type": "string", "description": "WorktypeID" }
},
"required": ["id", "worktypeId"]
},
"WorkflowTemplate": {
"type": "object",
"properties": {
"id": { "type": "number", "description": "テンプレートの内部ID" },
"fileName": {
"type": "string",
"description": "テンプレートのファイル名"
}
},
"required": ["id", "fileName"]
},
"Workflow": {
"type": "object",
"properties": {
"id": { "type": "number", "description": "ワークフローの内部ID" },
"author": {
"description": "Author情報",
"allOf": [{ "$ref": "#/components/schemas/Author" }]
},
"worktype": {
"description": "Worktype情報",
"allOf": [{ "$ref": "#/components/schemas/WorkflowWorktype" }]
},
"template": {
"description": "テンプレート情報",
"allOf": [{ "$ref": "#/components/schemas/WorkflowTemplate" }]
},
"typists": {
"description": "ルーティング候補のタイピストユーザー/タイピストグループ",
"type": "array",
"items": { "$ref": "#/components/schemas/Assignee" }
}
},
"required": ["id", "author", "typists"]
},
"GetWorkflowsResponse": {
"type": "object",
"properties": {
"workflows": {
"description": "ワークフローの一覧",
"type": "array",
"items": { "$ref": "#/components/schemas/Workflow" }
}
},
"required": ["workflows"]
},
"WorkflowTypist": {
"type": "object",
"properties": {
"typistId": {
"type": "number",
"description": "タイピストユーザーの内部ID"
},
"typistGroupId": {
"type": "number",
"description": "タイピストグループの内部ID"
}
}
},
"CreateWorkflowsRequest": {
"type": "object",
"properties": {
"authorId": { "type": "number", "description": "Authornの内部ID" },
"worktypeId": { "type": "number", "description": "Worktypeの内部ID" },
"templateId": {
"type": "number",
"description": "テンプレートの内部ID"
},
"typists": {
"description": "ルーティング候補のタイピストユーザー/タイピストグループ",
"minItems": 1,
"type": "array",
"items": { "$ref": "#/components/schemas/WorkflowTypist" }
}
},
"required": ["authorId", "typists"]
},
"CreateWorkflowsResponse": { "type": "object", "properties": {} },
"RegisterRequest": {
"type": "object",
"properties": {

View File

@ -44,6 +44,9 @@ import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
import { TemplatesService } from './features/templates/templates.service';
import { WorkflowsModule } from './features/workflows/workflows.module';
import { WorkflowsController } from './features/workflows/workflows.controller';
import { WorkflowsService } from './features/workflows/workflows.service';
import { validate } from './common/validators/env.validator';
@Module({
@ -73,6 +76,7 @@ import { validate } from './common/validators/env.validator';
TemplatesModule,
SendGridModule,
LicensesModule,
WorkflowsModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
@ -113,6 +117,7 @@ import { validate } from './common/validators/env.validator';
UsersController,
LicensesController,
TemplatesController,
WorkflowsController,
],
providers: [
AuthService,
@ -123,6 +128,7 @@ import { validate } from './common/validators/env.validator';
TasksService,
LicensesService,
TemplatesService,
WorkflowsService,
],
})
export class AppModule {

View File

@ -32,6 +32,8 @@ import { LicensesService } from '../../features/licenses/licenses.service';
import { TasksService } from '../../features/tasks/tasks.service';
import { TemplatesService } from '../../features/templates/templates.service';
import { TemplatesModule } from '../../features/templates/templates.module';
import { WorkflowsService } from '../../features/workflows/workflows.service';
import { WorkflowsModule } from '../../features/workflows/workflows.module';
export const makeTestingModule = async (
datasource: DataSource,
@ -53,6 +55,7 @@ export const makeTestingModule = async (
SendGridModule,
LicensesModule,
TemplatesModule,
WorkflowsModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
@ -78,6 +81,7 @@ export const makeTestingModule = async (
TasksService,
LicensesService,
TemplatesService,
WorkflowsService,
],
})
.useMocker(async (token) => {

View File

@ -62,6 +62,7 @@ import {
UpdateAccountInfoResponse,
DeleteAccountRequest,
DeleteAccountResponse,
GetAuthorsResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -201,6 +202,40 @@ export class AccountsController {
return accountInfo;
}
@ApiResponse({
status: HttpStatus.OK,
type: GetAuthorsResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getAuthors',
description:
'ログインしているユーザーのアカウント配下のAuthor一覧を取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('authors')
async getAuthors(@Req() req: Request): Promise<GetAuthorsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
return { authors: [] };
}
@ApiResponse({
status: HttpStatus.OK,
type: GetTypistsResponse,

View File

@ -134,6 +134,18 @@ export class GetMyAccountResponse {
account: Account;
}
export class Author {
@ApiProperty({ description: 'Authorユーザーの内部ID' })
id: number;
@ApiProperty({ description: 'AuthorID' })
authorId: string;
}
export class GetAuthorsResponse {
@ApiProperty({ type: [Author] })
authors: Author[];
}
export class Typist {
@ApiProperty({
description: 'TypistのユーザーID',

View File

@ -0,0 +1,81 @@
import { ApiProperty } from '@nestjs/swagger';
import { Assignee } from '../../tasks/types/types';
import { ArrayMinSize, IsArray, IsInt, IsOptional, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { Author } from '../../accounts/types/types';
export class WorkflowWorktype {
@ApiProperty({ description: 'Worktypeの内部ID' })
id: number;
@ApiProperty({ description: 'WorktypeID' })
worktypeId: string;
}
export class WorkflowTemplate {
@ApiProperty({ description: 'テンプレートの内部ID' })
id: number;
@ApiProperty({ description: 'テンプレートのファイル名' })
fileName: string;
}
export class Workflow {
@ApiProperty({ description: 'ワークフローの内部ID' })
id: number;
@ApiProperty({ description: 'Author情報' })
author: Author;
@ApiProperty({ description: 'Worktype情報', required: false })
worktype?: WorkflowWorktype;
@ApiProperty({ description: 'テンプレート情報', required: false })
template?: WorkflowTemplate;
@ApiProperty({
description: 'ルーティング候補のタイピストユーザー/タイピストグループ',
type: [Assignee],
})
typists: Assignee[];
}
export class GetWorkflowsResponse {
@ApiProperty({
description: 'ワークフローの一覧',
type: [Workflow],
})
workflows: Workflow[];
}
export class WorkflowTypist {
@ApiProperty({ description: 'タイピストユーザーの内部ID', required: false })
typistId?: number | undefined;
@ApiProperty({ description: 'タイピストグループの内部ID', required: false })
typistGroupId?: number | undefined;
}
export class CreateWorkflowsRequest {
@ApiProperty({ description: 'Authornの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
authorId: number;
@ApiProperty({ description: 'Worktypeの内部ID', required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
worktypeId?: number | undefined;
@ApiProperty({ description: 'テンプレートの内部ID', required: false })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
templateId?: number | undefined;
@ApiProperty({
description: 'ルーティング候補のタイピストユーザー/タイピストグループ',
type: [WorkflowTypist],
minItems: 1,
})
@Type(() => WorkflowTypist)
@IsArray()
@ArrayMinSize(1)
typists: WorkflowTypist[];
}
export class CreateWorkflowsResponse {}

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { WorkflowsService } from './workflows.service';
import { WorkflowsController } from './workflows.controller';
describe('WorkflowsController', () => {
let controller: WorkflowsController;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
],
controllers: [WorkflowsController],
providers: [WorkflowsService],
})
.overrideProvider(WorkflowsService)
.useValue(mockTemplatesService)
.compile();
controller = module.get<WorkflowsController>(WorkflowsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,112 @@
import {
Body,
Controller,
Get,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import jwt from 'jsonwebtoken';
import { AccessToken } from '../../common/token';
import { ErrorResponse } from '../../common/error/types/types';
import {
GetWorkflowsResponse,
CreateWorkflowsRequest,
CreateWorkflowsResponse,
} from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES } from '../../constants';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { WorkflowsService } from './workflows.service';
@ApiTags('workflows')
@Controller('workflows')
export class WorkflowsController {
constructor(private readonly workflowsService: WorkflowsService) {}
@ApiResponse({
status: HttpStatus.OK,
type: GetWorkflowsResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getWorkflows',
description: 'アカウント内のワークフローの一覧を取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getWorkflows(@Req() req: Request): Promise<GetWorkflowsResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
return { workflows: [] };
}
@ApiResponse({
status: HttpStatus.OK,
type: CreateWorkflowsResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'パラメータ不正エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'createWorkflows',
description: 'アカウント内にワークフローを新規作成します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post()
async createWorkflows(
@Req() req: Request,
@Body() body: CreateWorkflowsRequest,
): Promise<CreateWorkflowsResponse> {
const { authorId } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
console.log(authorId);
return {};
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { WorkflowsController } from './workflows.controller';
import { WorkflowsService } from './workflows.service';
@Module({
imports: [UsersRepositoryModule],
providers: [WorkflowsService],
controllers: [WorkflowsController],
})
export class WorkflowsModule {}

View File

@ -0,0 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class WorkflowsService {
private readonly logger = new Logger(WorkflowsService.name);
constructor() {}
}