From cec740f65e92bb7dfa5ab16a0c255152a41c8380 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 20 Sep 2023 01:41:14 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20418:=20API=20IF=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2649: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2649) - テンプレートファイル周りで以下のAPIIFを実装し、OpenAPIを更新しました。 - テンプレートファイル一覧取得API - テンプレートファイルアップロード先取得API - テンプレートファイルアップロード完了API ## レビューポイント - 各APIのパスは適切か - パラメータは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/api/odms/openapi.json | 173 ++++++++++++++++++ dictation_server/src/app.module.ts | 6 + .../src/features/files/files.controller.ts | 85 ++++++++- .../src/features/files/types/types.ts | 13 ++ .../templates/templates.controller.spec.ts | 30 +++ .../templates/templates.controller.ts | 57 ++++++ .../features/templates/templates.module.ts | 10 + .../features/templates/templates.service.ts | 7 + .../src/features/templates/types/types.ts | 16 ++ 9 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 dictation_server/src/features/templates/templates.controller.spec.ts create mode 100644 dictation_server/src/features/templates/templates.controller.ts create mode 100644 dictation_server/src/features/templates/templates.module.ts create mode 100644 dictation_server/src/features/templates/templates.service.ts create mode 100644 dictation_server/src/features/templates/types/types.ts diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index ff4098b..7f63f9e 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1849,6 +1849,100 @@ "security": [{ "bearer": [] }] } }, + "/files/template/upload-location": { + "get": { + "operationId": "uploadTemplateLocation", + "summary": "", + "description": "ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadLocationResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/upload-finished": { + "post": { + "operationId": "uploadTemplateFinished", + "summary": "", + "description": "アップロードが完了したテンプレートファイルの情報を登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedReqponse" + } + } + } + }, + "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": ["files"], + "security": [{ "bearer": [] }] + } + }, "/tasks": { "get": { "operationId": "getTasks", @@ -2671,6 +2765,44 @@ "security": [{ "bearer": [] }] } }, + "/templates": { + "get": { + "operationId": "getTemplates", + "summary": "", + "description": "アカウント内のテンプレートファイルの一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTemplatesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, "/notification/register": { "post": { "operationId": "register", @@ -3602,6 +3734,36 @@ "properties": { "url": { "type": "string" } }, "required": ["url"] }, + "TemplateUploadLocationResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] + }, + "TemplateFile": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "テンプレートファイルのファイル名" + }, + "url": { + "type": "string", + "description": "テンプレートファイルのURL" + } + }, + "required": ["name", "url"] + }, + "TemplateUploadFinishedRequest": { + "type": "object", + "properties": { + "templateFile": { + "description": "テンプレートファイルのファイル情報", + "allOf": [{ "$ref": "#/components/schemas/TemplateFile" }] + } + }, + "required": ["templateFile"] + }, + "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, "Assignee": { "type": "object", "properties": { @@ -3808,6 +3970,17 @@ "required": ["poNumber"] }, "CancelOrderResponse": { "type": "object", "properties": {} }, + "GetTemplatesResponse": { + "type": "object", + "properties": { + "templates": { + "description": "テンプレートファイルの一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/TemplateFile" } + } + }, + "required": ["templates"] + }, "RegisterRequest": { "type": "object", "properties": { diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index a5cd069..3deb54c 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -28,6 +28,8 @@ import { NotificationModule } from './features/notification/notification.module' import { FilesModule } from './features/files/files.module'; import { FilesController } from './features/files/files.controller'; import { FilesService } from './features/files/files.service'; +import { TemplatesModule } from './features/templates/templates.module'; +import { TemplatesController } from './features/templates/templates.controller'; import { TasksService } from './features/tasks/tasks.service'; import { TasksController } from './features/tasks/tasks.controller'; import { TasksModule } from './features/tasks/tasks.module'; @@ -41,6 +43,7 @@ import { UserGroupsRepositoryModule } from './repositories/user_groups/user_grou import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; 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'; @Module({ imports: [ @@ -65,6 +68,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re FilesModule, TasksModule, UsersModule, + TemplatesModule, SendGridModule, LicensesModule, AccountsRepositoryModule, @@ -106,6 +110,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re TasksController, UsersController, LicensesController, + TemplatesController, ], providers: [ AuthService, @@ -115,6 +120,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re FilesService, TasksService, LicensesService, + TemplatesService, ], }) export class AppModule { diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index 19b1ffa..eb0d311 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -27,10 +27,13 @@ import { AudioUploadLocationResponse, TemplateDownloadLocationRequest, TemplateDownloadLocationResponse, + TemplateUploadFinishedReqponse, + TemplateUploadFinishedRequest, + TemplateUploadLocationResponse, } from './types/types'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; -import { USER_ROLES } from '../../constants'; +import { ADMIN_ROLES, USER_ROLES } from '../../constants'; import { retrieveAuthorizationToken } from '../../common/http/helper'; import { Request } from 'express'; import { makeContext } from '../../common/log'; @@ -258,4 +261,84 @@ export class FilesController { return { url }; } + + @Get('template/upload-location') + @ApiResponse({ + status: HttpStatus.OK, + type: TemplateUploadLocationResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'uploadTemplateLocation', + description: + 'ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async uploadTemplateLocation( + @Req() req: Request, + ): Promise { + const token = retrieveAuthorizationToken(req); + const accessToken = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(accessToken.userId); + + console.log(context.trackingId); + + return { url: '' }; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: TemplateUploadFinishedReqponse, + 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: 'uploadTemplateFinished', + description: 'アップロードが完了したテンプレートファイルの情報を登録します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + @Post('template/upload-finished') + async templateUploadFinished( + @Req() req: Request, + @Body() body: TemplateUploadFinishedRequest, + ): Promise { + const { templateFile } = body; + const token = retrieveAuthorizationToken(req); + const accessToken = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(accessToken.userId); + console.log(context.trackingId); + console.log(templateFile); + + return {}; + } } diff --git a/dictation_server/src/features/files/types/types.ts b/dictation_server/src/features/files/types/types.ts index 92c1144..ed38f54 100644 --- a/dictation_server/src/features/files/types/types.ts +++ b/dictation_server/src/features/files/types/types.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { TemplateFile } from '../../templates/types/types'; export class AudioUploadLocationRequest {} @@ -31,6 +32,11 @@ export class TemplateDownloadLocationResponse { url: string; } +export class TemplateUploadLocationResponse { + @ApiProperty() + url: string; +} + export class AudioOptionItem { @ApiProperty({ minLength: 1, maxLength: 16 }) optionItemLabel: string; @@ -87,3 +93,10 @@ export class AudioUploadFinishedResponse { @ApiProperty({ description: '8桁固定の数字' }) jobNumber: string; } + +export class TemplateUploadFinishedRequest { + @ApiProperty({ description: 'テンプレートファイルのファイル情報' }) + templateFile: TemplateFile; +} + +export class TemplateUploadFinishedReqponse {} diff --git a/dictation_server/src/features/templates/templates.controller.spec.ts b/dictation_server/src/features/templates/templates.controller.spec.ts new file mode 100644 index 0000000..00bd2fc --- /dev/null +++ b/dictation_server/src/features/templates/templates.controller.spec.ts @@ -0,0 +1,30 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import { TemplatesController } from './templates.controller'; +import { TemplatesService } from './templates.service'; + +describe('TemplatesController', () => { + let controller: TemplatesController; + const mockTemplatesService = {}; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: ['.env.local', '.env'], + isGlobal: true, + }), + ], + controllers: [TemplatesController], + providers: [TemplatesService], + }) + .overrideProvider(TemplatesService) + .useValue(mockTemplatesService) + .compile(); + + controller = module.get(TemplatesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts new file mode 100644 index 0000000..f850067 --- /dev/null +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, HttpStatus, 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 { GetTemplatesResponse } 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 { TemplatesService } from './templates.service'; + +@ApiTags('templates') +@Controller('templates') +export class TemplatesController { + constructor(private readonly templatesService: TemplatesService) {} + + @ApiResponse({ + status: HttpStatus.OK, + type: GetTemplatesResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getTemplates', + description: 'アカウント内のテンプレートファイルの一覧を取得します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + @Get() + async getTemplates(@Req() req: Request): Promise { + const token = retrieveAuthorizationToken(req); + const accessToken = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(accessToken.userId); + console.log(context.trackingId); + + return { templates: [] }; + } +} diff --git a/dictation_server/src/features/templates/templates.module.ts b/dictation_server/src/features/templates/templates.module.ts new file mode 100644 index 0000000..76aa447 --- /dev/null +++ b/dictation_server/src/features/templates/templates.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TemplatesController } from './templates.controller'; +import { TemplatesService } from './templates.service'; + +@Module({ + imports: [], + providers: [TemplatesService], + controllers: [TemplatesController], +}) +export class TemplatesModule {} diff --git a/dictation_server/src/features/templates/templates.service.ts b/dictation_server/src/features/templates/templates.service.ts new file mode 100644 index 0000000..cf1d193 --- /dev/null +++ b/dictation_server/src/features/templates/templates.service.ts @@ -0,0 +1,7 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class TemplatesService { + private readonly logger = new Logger(TemplatesService.name); + constructor() {} +} diff --git a/dictation_server/src/features/templates/types/types.ts b/dictation_server/src/features/templates/types/types.ts new file mode 100644 index 0000000..a331ad8 --- /dev/null +++ b/dictation_server/src/features/templates/types/types.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TemplateFile { + @ApiProperty({ description: 'テンプレートファイルのファイル名' }) + name: string; + @ApiProperty({ description: 'テンプレートファイルのURL' }) + url: string; +} + +export class GetTemplatesResponse { + @ApiProperty({ + description: 'テンプレートファイルの一覧', + type: [TemplateFile], + }) + templates: TemplateFile[]; +}