Merged PR 76: API実装(I/F)

## 概要
[Task1576: API実装(I/F)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1576)

- sas発行APIのIFを実装
  - アップロード用のSAS発行API
    - GET /blob/uploadSas
  - ダウンロード用のSAS発行API
    - GET /blob/downloadSas
- notification関連のフォーマット修正
- notification関連でlintエラーが出ていた箇所を修正
- openapi.jsonを生成するコマンドを使用できるように修正

## レビューポイント
- レスポンスとして返却する内容に不足は無いか
- URIはこれで良さそうか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-04-14 01:08:32 +00:00
parent 6a5926ab3f
commit 0a970e814f
14 changed files with 257 additions and 27 deletions

View File

@ -8,8 +8,8 @@
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"apigen": "ts-node src/api/generate.ts",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"apigen": "ts-node src/api/generate.ts && prettier --write \"src/api/odms/*.json\"",
"format": "prettier --write \"src/**/*.ts\" \"src/api/odms/*.json\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",

View File

@ -1,24 +1,12 @@
// XXX 現状うまく動作しないため運用できない
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { Test } from '@nestjs/testing';
import { AppModule } from '../app.module';
import { promises as fs } from 'fs';
import { NestFactory } from '@nestjs/core';
async function bootstrap(): Promise<void> {
const controllers = Reflect.getMetadata('controllers', AppModule);
const providers = Reflect.getMetadata('providers', AppModule);
const mockedProviders = providers.map((provider) => {
return {
provide: provider.name,
useValue: {},
};
const app = await NestFactory.create(AppModule, {
preview: true,
});
const testingModule = await Test.createTestingModule({
controllers: controllers,
providers: mockedProviders,
}).compile();
const app = testingModule.createNestApplication();
const options = new DocumentBuilder()
.setTitle('ODMSOpenAPI')
.setVersion('1.0.0')
@ -32,7 +20,7 @@ async function bootstrap(): Promise<void> {
const document = SwaggerModule.createDocument(app, options);
await fs.writeFile(
'src/api/odms/openapi.json',
JSON.stringify(document, null, 2),
JSON.stringify(document, null, 0),
);
}
bootstrap();

View File

@ -350,6 +350,88 @@
"tags": ["notification"],
"security": [{ "bearer": [] }]
}
},
"/files/audio/upload-location": {
"get": {
"operationId": "uploadLocation",
"summary": "",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AudioUploadLocationResponse"
}
}
}
},
"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/audio/download-location": {
"get": {
"operationId": "downloadLocation",
"summary": "",
"parameters": [
{
"name": "id",
"required": true,
"in": "query",
"description": "音声ファイル情報をDBから取得するためのID",
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AudioDownloadLocationResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["files"],
"security": [{ "bearer": [] }]
}
}
},
"info": {
@ -508,7 +590,17 @@
},
"required": ["pns", "handler"]
},
"RegisterResponse": { "type": "object", "properties": {} }
"RegisterResponse": { "type": "object", "properties": {} },
"AudioUploadLocationResponse": {
"type": "object",
"properties": { "url": { "type": "string" } },
"required": ["url"]
},
"AudioDownloadLocationResponse": {
"type": "object",
"properties": { "url": { "type": "string" } },
"required": ["url"]
}
}
}
}

View File

@ -22,6 +22,7 @@ import { UsersRepositoryModule } from './repositories/users/users.repository.mod
import { NotificationhubModule } from './gateways/notificationhub/notificationhub.module';
import { NotificationhubService } from './gateways/notificationhub/notificationhub.service';
import { NotificationModule } from './features/notification/notification.module';
import { BlobModule } from './features/blob/blob.module';
@Module({
imports: [
@ -56,6 +57,7 @@ import { NotificationModule } from './features/notification/notification.module'
}),
NotificationModule,
NotificationhubModule,
BlobModule,
],
controllers: [
HealthController,

View File

@ -0,0 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BlobController } from './blob.controller';
import { BlobService } from './blob.service';
describe('BlobController', () => {
let controller: BlobController;
const mockBlobService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BlobController],
providers: [BlobService],
})
.overrideProvider(BlobService)
.useValue(mockBlobService)
.compile();
controller = module.get<BlobController>(BlobController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,79 @@
import { Controller, Get, Headers, HttpStatus, Query } from '@nestjs/common';
import {
ApiResponse,
ApiOperation,
ApiBearerAuth,
ApiTags,
} from '@nestjs/swagger';
import { ErrorResponse } from '../../common/error/types/types';
import { BlobService } from './blob.service';
import {
AudioUploadLocationResponse,
AudioUploadLocationRequest,
AudioDownloadLocationResponse,
AudioDownloadLocationRequest,
} from './types/types';
@ApiTags('files')
@Controller('files')
export class BlobController {
constructor(private readonly blobService: BlobService) {}
@Get('audio/upload-location')
@ApiResponse({
status: HttpStatus.OK,
type: AudioUploadLocationResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'uploadLocation' })
@ApiBearerAuth()
async uploadLocation(
@Headers() headers,
@Query() query: AudioUploadLocationRequest,
): Promise<AudioUploadLocationResponse> {
const {} = query;
// コンテナ作成処理の前にアクセストークンの認証を行う
// アップロード先を決定する国情報はトークンから取得する想定
return { url: '' };
}
@Get('audio/download-location')
@ApiResponse({
status: HttpStatus.OK,
type: AudioDownloadLocationResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'downloadLocation' })
@ApiBearerAuth()
async downloadLocation(
@Headers() headers,
@Query() body: AudioDownloadLocationRequest,
): Promise<AudioDownloadLocationResponse> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id } = body;
// コンテナ作成処理の前にアクセストークンの認証を行う
//
return { url: '' };
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { BlobService } from './blob.service';
import { BlobController } from './blob.controller';
@Module({
providers: [BlobService],
controllers: [BlobController],
})
export class BlobModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BlobService } from './blob.service';
describe('BlobService', () => {
let service: BlobService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BlobService],
}).compile();
service = module.get<BlobService>(BlobService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class BlobService {}

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
export class AudioUploadLocationRequest {}
export class AudioUploadLocationResponse {
@ApiProperty()
url: string;
}
export class AudioDownloadLocationRequest {
@ApiProperty({ description: '音声ファイル情報をDBから取得するためのID' })
id: string;
}
export class AudioDownloadLocationResponse {
@ApiProperty()
url: string;
}

View File

@ -6,6 +6,6 @@ import { NotificationhubModule } from '../../gateways/notificationhub/notificati
@Module({
imports: [NotificationhubModule],
providers: [NotificationService],
controllers: [NotificationController]
controllers: [NotificationController],
})
export class NotificationModule {}

View File

@ -14,7 +14,7 @@ export class NotificationService {
* @param pnsHandler
* @returns register
*/
async register(pns: string, pnsHandler: string): Promise<{}> {
async register(pns: string, pnsHandler: string): Promise<void> {
this.logger.log(`[IN] ${this.register.name}`);
try {
await this.notificationhubService.register(pns, pnsHandler);
@ -27,6 +27,5 @@ export class NotificationService {
} finally {
this.logger.log(`[OUT] ${this.register.name}`);
}
return {};
}
}

View File

@ -23,7 +23,7 @@ export class NotificationhubService {
* @param pnsHandler
* @returns register
*/
async register(pns: string, pnsHandler: string): Promise<{}> {
async register(pns: string, pnsHandler: string): Promise<void> {
this.logger.log(`[IN] ${this.register.name}`);
let reg: RegistrationDescription;
//登録情報作成
@ -59,7 +59,5 @@ export class NotificationhubService {
} finally {
this.logger.log(`[OUT] ${this.register.name}`);
}
return {};
}
}