diff --git a/dictation_server/src/api/generate.ts b/dictation_server/src/api/generate.ts index a2010e5..03b6f7b 100644 --- a/dictation_server/src/api/generate.ts +++ b/dictation_server/src/api/generate.ts @@ -14,7 +14,6 @@ async function bootstrap(): Promise { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', - in: 'header', }) .build(); const document = SwaggerModule.createDocument(app, options); diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 3d5b563..d18759c 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -37,6 +37,7 @@ import { BlobstorageModule } from './gateways/blobstorage/blobstorage.module'; import { LicensesModule } from './features/licenses/licenses.module'; import { LicensesService } from './features/licenses/licenses.service'; import { LicensesController } from './features/licenses/licenses.controller'; +import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; @Module({ imports: [ @@ -81,6 +82,7 @@ import { LicensesController } from './features/licenses/licenses.controller'; NotificationhubModule, BlobstorageModule, AuthGuardsModule, + SortCriteriaRepositoryModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/common/types/sort/index.ts b/dictation_server/src/common/types/sort/index.ts new file mode 100644 index 0000000..b505e50 --- /dev/null +++ b/dictation_server/src/common/types/sort/index.ts @@ -0,0 +1,27 @@ +import { + TASK_LIST_SORTABLE_ATTRIBUTES, + SORT_DIRECTIONS, +} from '../../../constants'; + +export type TaskListSortableAttribute = + (typeof TASK_LIST_SORTABLE_ATTRIBUTES)[number]; + +export type SortDirection = (typeof SORT_DIRECTIONS)[number]; + +export const isTaskListSortableAttribute = ( + arg: string, +): arg is TaskListSortableAttribute => { + const param = arg as TaskListSortableAttribute; + if (TASK_LIST_SORTABLE_ATTRIBUTES.includes(param)) { + return true; + } + return false; +}; + +export const isSortDirection = (arg: string): arg is SortDirection => { + const param = arg as SortDirection; + if (SORT_DIRECTIONS.includes(param)) { + return true; + } + return false; +}; diff --git a/dictation_server/src/common/types/sort/util.ts b/dictation_server/src/common/types/sort/util.ts new file mode 100644 index 0000000..2ccf33d --- /dev/null +++ b/dictation_server/src/common/types/sort/util.ts @@ -0,0 +1,11 @@ +import { SortDirection, TaskListSortableAttribute } from '.'; + +export const getDirection = (direction: SortDirection): SortDirection => { + return direction; +}; + +export const getTaskListSortableAttribute = ( + TaskListSortableAttribute: TaskListSortableAttribute, +): TaskListSortableAttribute => { + return TaskListSortableAttribute; +}; diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 93e84f7..907e75d 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -149,5 +149,5 @@ export const TASK_LIST_SORTABLE_ATTRIBUTES = [ 'TRANSCRIPTION_FINISHED_DATE', ] as const; -export type TaskListSortableAttribute = - (typeof TASK_LIST_SORTABLE_ATTRIBUTES)[number]; +// export const SORT_DIRECTIONS = { asc: 'ASC', desc: 'DESC' } as const; +export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const; diff --git a/dictation_server/src/features/notification/notification.controller.spec.ts b/dictation_server/src/features/notification/notification.controller.spec.ts index faa0f7f..fc14685 100644 --- a/dictation_server/src/features/notification/notification.controller.spec.ts +++ b/dictation_server/src/features/notification/notification.controller.spec.ts @@ -1,12 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notification.service'; +import { ConfigModule } from '@nestjs/config'; describe('NotificationController', () => { let controller: NotificationController; const mockNotificationService = {}; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: ['.env.local', '.env'], + isGlobal: true, + }), + ], controllers: [NotificationController], providers: [NotificationService], }) diff --git a/dictation_server/src/features/notification/notification.controller.ts b/dictation_server/src/features/notification/notification.controller.ts index 7697f3a..fc33b6e 100644 --- a/dictation_server/src/features/notification/notification.controller.ts +++ b/dictation_server/src/features/notification/notification.controller.ts @@ -8,7 +8,7 @@ import { import { ErrorResponse } from '../../common/error/types/types'; import { RegisterRequest, RegisterResponse } from './types/types'; import { NotificationService } from './notification.service'; -import { AuthGuard } from 'src/common/guards/auth/authguards'; +import { AuthGuard } from '../../common/guards/auth/authguards'; @ApiTags('notification') @Controller('notification') diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 53c590a..465a76f 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -10,11 +10,17 @@ import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; import { User } from '../../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../../repositories/users/users.repository.service'; import { UsersService } from '../users.service'; +import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity'; +import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service'; export type CryptoMockValue = { getPublicKey: string | Error; }; +export type SortCriteriaRepositoryMockValue = { + updateSortCriteria: SortCriteria | Error; +}; + export type UsersRepositoryMockValue = { updateUserVerified: undefined | Error; findUserById: User | Error; @@ -44,29 +50,58 @@ export type SendGridMockValue = { sendMail: undefined | Error; }; -export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { - return { - getMetaData: { - issuer: 'issuer', - }, - getSignKeySets: [ - { - kid: 'kid', - nbf: 1111111111, - use: 'sig', - kty: 'RSA', - e: 'e', - n: 'n', - }, +export const makeUsersServiceMock = async ( + cryptoMockValue: CryptoMockValue, + usersRepositoryMockValue: UsersRepositoryMockValue, + adB2cMockValue: AdB2cMockValue, + sendGridMockValue: SendGridMockValue, + configMockValue: ConfigMockValue, + sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue, +): Promise => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UsersService], + imports: [ + ConfigModule.forRoot({ + ignoreEnvFile: true, + ignoreEnvVars: true, + }), ], - changePassword: { - sub: 'TEST9999', - }, - createUser: '001', - getUser: { - displayName: 'Hanako Sato', - mail: 'hanako@sample.com', - }, + }) + .useMocker((token) => { + switch (token) { + case CryptoService: + return makeCryptoServiceMock(cryptoMockValue); + case UsersRepositoryService: + return makeUsersRepositoryMock(usersRepositoryMockValue); + case AdB2cService: + return makeAdB2cServiceMock(adB2cMockValue); + case SendGridService: + return makeSendGridMock(sendGridMockValue); + case ConfigService: + return makeConfigMock(configMockValue); + case SortCriteriaRepositoryService: + return makeSortCriteriaRepositoryMock( + sortCriteriaRepositoryMockValue, + ); + } + }) + .compile(); + + return module.get(UsersService); +}; + +export const makeSortCriteriaRepositoryMock = ( + value: SortCriteriaRepositoryMockValue, +) => { + const { updateSortCriteria } = value; + + return { + updateSortCriteria: + updateSortCriteria instanceof Error + ? jest.fn, []>().mockRejectedValue(updateSortCriteria) + : jest + .fn, []>() + .mockResolvedValue(updateSortCriteria), }; }; @@ -130,41 +165,6 @@ export type ConfigMockValue = { get: string | Error; }; -export const makeUsersServiceMock = async ( - cryptoMockValue: CryptoMockValue, - usersRepositoryMockValue: UsersRepositoryMockValue, - adB2cMockValue: AdB2cMockValue, - sendGridMockValue: SendGridMockValue, - configMockValue: ConfigMockValue, -): Promise => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], - imports: [ - ConfigModule.forRoot({ - ignoreEnvFile: true, - ignoreEnvVars: true, - }), - ], - }) - .useMocker((token) => { - switch (token) { - case CryptoService: - return makeCryptoServiceMock(cryptoMockValue); - case UsersRepositoryService: - return makeUsersRepositoryMock(usersRepositoryMockValue); - case AdB2cService: - return makeAdB2cServiceMock(adB2cMockValue); - case SendGridService: - return makeSendGridMock(sendGridMockValue); - case ConfigService: - return makeConfigMock(configMockValue); - } - }) - .compile(); - - return module.get(UsersService); -}; - export const makeCryptoServiceMock = (value: CryptoMockValue) => { const { getPublicKey } = value; @@ -292,6 +292,46 @@ export const makeDefaultConfigValue = (): ConfigMockValue => { }; }; +export const makeDefaultSortCriteriaRepositoryMockValue = + (): SortCriteriaRepositoryMockValue => { + const sortCriteria = new SortCriteria(); + { + sortCriteria.id = 1; + sortCriteria.direction = 'ASC'; + sortCriteria.parameter = 'JOB_NUMBER'; + sortCriteria.user_id = 1; + } + return { + updateSortCriteria: sortCriteria, + }; + }; + +export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { + return { + getMetaData: { + issuer: 'issuer', + }, + getSignKeySets: [ + { + kid: 'kid', + nbf: 1111111111, + use: 'sig', + kty: 'RSA', + e: 'e', + n: 'n', + }, + ], + changePassword: { + sub: 'TEST9999', + }, + createUser: '001', + getUser: { + displayName: 'Hanako Sato', + mail: 'hanako@sample.com', + }, + }; +}; + // 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する export const makeDefaultUsersRepositoryMockValue = (): UsersRepositoryMockValue => { diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index f621d00..4aa30e1 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -139,8 +139,12 @@ export class SortCriteriaRequest { @IsIn(['ASC', 'DESC'], { message: 'invalid direction' }) direction: string; - @ApiProperty({ description: `${TASK_LIST_SORTABLE_ATTRIBUTES.join('/')}` }) - @IsIn(TASK_LIST_SORTABLE_ATTRIBUTES, { message: 'invalid attributes' }) + @ApiProperty({ + description: `${TASK_LIST_SORTABLE_ATTRIBUTES.join('/')}`, + }) + @IsIn(TASK_LIST_SORTABLE_ATTRIBUTES, { + message: 'invalid attributes', + }) paramName: string; } diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index f3b7721..5cf3b08 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -5,8 +5,8 @@ import { HttpStatus, Post, Req, - UseGuards, HttpException, + UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, @@ -33,9 +33,12 @@ import { SortCriteriaResponse, } from './types/types'; import { UsersService } from './users.service'; +import jwt from 'jsonwebtoken'; import { AuthGuard } from '../../common/guards/auth/authguards'; -import { RoleGuard } from '../../common/guards/role/roleguards'; - +import { + isSortDirection, + isTaskListSortableAttribute, +} from '../../common/types/sort'; @ApiTags('users') @Controller('users') export class UsersController { @@ -284,6 +287,11 @@ export class UsersController { description: '認証エラー', type: ErrorResponse, }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '想定外のサーバーエラー', @@ -298,8 +306,27 @@ export class UsersController { @Post('sort-criteria') async updateSortCriteria( @Body() body: SortCriteriaRequest, + @Req() req: Request, ): Promise { - console.log(body); + const { direction, paramName } = body; + const accessToken = retrieveAuthorizationToken(req); + const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + + //型チェック + if ( + !isTaskListSortableAttribute(paramName) || + !isSortDirection(direction) + ) { + throw new HttpException( + makeErrorResponse('E010001'), + HttpStatus.BAD_REQUEST, + ); + } + await this.usersService.updateSortCriteria( + paramName, + direction, + decodedToken, + ); return {}; } } diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index 072c88d..0149c9a 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -6,11 +6,13 @@ import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; +import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module'; @Module({ imports: [ CryptoModule, UsersRepositoryModule, + SortCriteriaRepositoryModule, AdB2cModule, SendGridModule, ConfigModule, diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index bc1a693..2b06fda 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -8,6 +8,7 @@ import { makeDefaultConfigValue, makeDefaultCryptoMockValue, makeDefaultSendGridlValue, + makeDefaultSortCriteriaRepositoryMockValue, makeDefaultUsersRepositoryMockValue, makeUsersServiceMock, } from './test/users.service.mock'; @@ -20,12 +21,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -52,6 +56,8 @@ describe('UsersService', () => { }; const adb2cParam = makeDefaultAdB2cMockValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeUsersServiceMock( cryptoMockValue, @@ -59,6 +65,7 @@ describe('UsersService', () => { adb2cParam, sendGridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = @@ -72,12 +79,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'invalid.id.token'; await expect(service.confirmUser(token)).rejects.toEqual( @@ -106,12 +116,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'invalid.id.token'; await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual( @@ -124,6 +137,8 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError(); @@ -134,6 +149,7 @@ describe('UsersService', () => { adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -162,6 +178,8 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError(); @@ -171,6 +189,7 @@ describe('UsersService', () => { adb2cParam, sendGridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -184,6 +203,8 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.updateUserVerified = new Error('DB error'); const service = await makeUsersServiceMock( @@ -192,6 +213,7 @@ describe('UsersService', () => { adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -224,12 +246,15 @@ describe('UsersService', () => { const sendGridMockValue = makeDefaultSendGridlValue(); usersRepositoryMockValue.updateUserVerified = new Error('DB error'); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -246,12 +271,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user1'; const role = 'None'; @@ -281,12 +309,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user2'; const role = 'Author'; @@ -318,12 +349,15 @@ describe('UsersService', () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user3'; const role = 'Transcriptioninst'; @@ -355,6 +389,8 @@ it('DBネットワークエラーとなる場合、エラーとなる。', async const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.createNormalUser = new Error('DB error'); const service = await makeUsersServiceMock( cryptoMockValue, @@ -362,6 +398,7 @@ it('DBネットワークエラーとなる場合、エラーとなる。', async adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user5'; const role = 'Transcriptioninst'; @@ -394,12 +431,15 @@ it('Azure ADB2Cでネットワークエラーとなる場合、エラーとな adb2cParam.createUser = new Error(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user6'; const role = 'Transcriptioninst'; @@ -432,12 +472,15 @@ it('メールアドレスが重複している場合、エラーとなる。', a adb2cParam.createUser = { reason: 'email', message: 'ObjectConflict' }; const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user7'; const role = 'Transcriptioninst'; @@ -466,6 +509,8 @@ it('AuthorIDが重複している場合、エラーとなる。(AuthorID重複 const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.createNormalUser = new Error(); usersRepositoryMockValue.existsAuthorId = true; @@ -475,6 +520,7 @@ it('AuthorIDが重複している場合、エラーとなる。(AuthorID重複 adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user8'; const role = 'Author'; @@ -505,6 +551,8 @@ it('AuthorIDが重複している場合、エラーとなる。(insert失敗)', const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.createNormalUser = new Error(); usersRepositoryMockValue.createNormalUser.name = 'ER_DUP_ENTRY'; @@ -514,6 +562,7 @@ it('AuthorIDが重複している場合、エラーとなる。(insert失敗)', adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const name = 'test_user9'; const role = 'Author'; @@ -545,12 +594,15 @@ it('ユーザの一覧を取得する', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -589,6 +641,8 @@ it('ユーザの一覧を取得に失敗する', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); usersRepositoryMockValue.findSameAccountUsers = new Error( 'Failure to acquire', ); @@ -599,6 +653,7 @@ it('ユーザの一覧を取得に失敗する', async () => { adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = @@ -615,6 +670,8 @@ it('ユーザの一覧を0件取得する', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); // モックでDBからのユーザ取得を空にする const noDbUsers: EntityUser[] = []; @@ -626,6 +683,7 @@ it('ユーザの一覧を0件取得する', async () => { adb2cParam, sendgridMockValue, configMockValue, + sortCriteriaRepositoryMockValue, ); const token = @@ -634,3 +692,98 @@ it('ユーザの一覧を0件取得する', async () => { const emptyMergedUsers: User[] = []; expect(await service.getUsers(token)).toEqual(emptyMergedUsers); }); + +it('ソート条件を変更できる', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + sortCriteriaRepositoryMockValue, + ); + + expect( + await service.updateSortCriteria('AUTHOR_ID', 'ASC', { + role: 'none admin', + userId: 'xxxxxxxxxxxx', + }), + ).toEqual(undefined); +}); + +it('ユーザー情報が存在せず、ソート条件を変更できない', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); + + // モックでDBからのユーザ取得がエラーとなる + usersRepositoryMockValue.findUserByExternalId = new Error('user not found'); + + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + sortCriteriaRepositoryMockValue, + ); + + await expect( + service.updateSortCriteria('AUTHOR_ID', 'ASC', { + role: 'none admin', + userId: 'xxxxxxxxxxxx', + }), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); +}); + +it('ソート条件が存在せず、ソート条件を変更できない', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const sortCriteriaRepositoryMockValue = + makeDefaultSortCriteriaRepositoryMockValue(); + sortCriteriaRepositoryMockValue.updateSortCriteria = new Error( + 'sort criteria not found', + ); + + // モックでDBからのユーザ取得を空にする + + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + sortCriteriaRepositoryMockValue, + ); + + await expect( + service.updateSortCriteria('AUTHOR_ID', 'ASC', { + role: 'none admin', + userId: 'xxxxxxxxxxxx', + }), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 4194a54..f4fcf58 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -4,6 +4,10 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { isVerifyError, verify } from '../../common/jwt'; import { makePassword } from '../../common/password/password'; import { AccessToken } from '../../common/token'; +import { + TaskListSortableAttribute, + SortDirection, +} from '../../common/types/sort'; import { AdB2cService, ConflictError, @@ -11,6 +15,7 @@ import { } from '../../gateways/adb2c/adb2c.service'; import { CryptoService } from '../../gateways/crypto/crypto.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; +import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service'; import { User as EntityUser } from '../../repositories/users/entity/user.entity'; import { EmailAlreadyVerifiedError, @@ -23,6 +28,7 @@ export class UsersService { constructor( private readonly cryptoService: CryptoService, private readonly usersRepository: UsersRepositoryService, + private readonly sortCriteriaRepository: SortCriteriaRepositoryService, private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, private readonly sendgridService: SendGridService, @@ -336,4 +342,48 @@ export class UsersService { this.logger.log(`[OUT] ${this.getUsers.name}`); } } + /** + * Updates sort criteria + * @param paramName + * @param direction + * @param token + * @returns sort criteria + */ + async updateSortCriteria( + paramName: TaskListSortableAttribute, + direction: SortDirection, + token: AccessToken, + ): Promise { + this.logger.log(`[IN] ${this.updateSortCriteria.name}`); + let user: EntityUser; + try { + // ユーザー情報を取得 + const sub = token.userId; + user = await this.usersRepository.findUserByExternalId(sub); + } catch (e) { + this.logger.error(`error=${e}`); + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + try { + // ユーザーのソート条件を更新 + await this.sortCriteriaRepository.updateSortCriteria( + user.id, + paramName, + direction, + ); + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log(`[OUT] ${this.updateSortCriteria.name}`); + } + } } diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index d63a532..5595fd6 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -2,6 +2,11 @@ import { Injectable } from '@nestjs/common'; import { DataSource, UpdateResult } from 'typeorm'; import { User } from '../users/entity/user.entity'; import { Account } from './entity/account.entity'; +import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity'; +import { + getDirection, + getTaskListSortableAttribute, +} from '../../common/types/sort/util'; export class AccountNotFoundError extends Error {} @@ -111,6 +116,17 @@ export class AccountsRepositoryService { throw new Error(`invalid update. result.affected=${result.affected}`); } + // ユーザーのタスクソート条件を作成 + const sortCriteria = new SortCriteria(); + { + sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER'); + sortCriteria.direction = getDirection('ASC'); + sortCriteria.user_id = persistedUser.id; + } + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const newSortCriteria = sortCriteriaRepo.create(sortCriteria); + await sortCriteriaRepo.save(newSortCriteria); + return { newAccount: persistedAccount, adminUser: persistedUser }; }); } diff --git a/dictation_server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts b/dictation_server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts new file mode 100644 index 0000000..260c9a9 --- /dev/null +++ b/dictation_server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts @@ -0,0 +1,16 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity({ name: 'sort_criteria' }) +export class SortCriteria { + @PrimaryGeneratedColumn() + id: number; + + @Column() + user_id: number; + + @Column() + parameter: string; + + @Column() + direction: string; +} diff --git a/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.module.ts b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.module.ts new file mode 100644 index 0000000..b90eb40 --- /dev/null +++ b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SortCriteriaRepositoryService } from './sort_criteria.repository.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SortCriteria } from './entity/sort_criteria.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SortCriteria])], + providers: [SortCriteriaRepositoryService], + exports: [SortCriteriaRepositoryService], +}) +export class SortCriteriaRepositoryModule {} diff --git a/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts new file mode 100644 index 0000000..bd7225e --- /dev/null +++ b/dictation_server/src/repositories/sort_criteria/sort_criteria.repository.service.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { SortCriteria } from './entity/sort_criteria.entity'; +import { + TaskListSortableAttribute, + SortDirection, +} from '../../common/types/sort'; + +export class SortCriteriaNotFoundError extends Error {} +@Injectable() +export class SortCriteriaRepositoryService { + constructor(private dataSource: DataSource) {} + private readonly logger = new Logger(SortCriteriaRepositoryService.name); + /** + * Updates sort criteria + * @param userId + * @param parameter + * @param direction + * @returns sort criteria + */ + async updateSortCriteria( + userId: number, + parameter: TaskListSortableAttribute, + direction: SortDirection, + ): Promise { + this.logger.log( + ` ${this.updateSortCriteria.name}; parameter:${parameter}, direction:${direction}`, + ); + return await this.dataSource.transaction(async (entityManager) => { + const repo = entityManager.getRepository(SortCriteria); + const targetSortCriteria = await repo.findOne({ + where: { + user_id: userId, + }, + }); + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!targetSortCriteria) { + throw new Error('sort criteria not found '); + } + + targetSortCriteria.parameter = parameter; + targetSortCriteria.direction = direction; + + const persisted = await repo.save(targetSortCriteria); + return persisted; + }); + } +} diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index da1ad6d..1ede522 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -1,6 +1,11 @@ import { Injectable } from '@nestjs/common'; import { DataSource, UpdateResult } from 'typeorm'; import { User } from './entity/user.entity'; +import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity'; +import { + getDirection, + getTaskListSortableAttribute, +} from '../../common/types/sort/util'; // UsersRepositoryServiceで発生するエラーを定義 export class EmailAlreadyVerifiedError extends Error {} @@ -73,6 +78,18 @@ export class UsersRepositoryService { const repo = entityManager.getRepository(User); const newUser = repo.create(user); const persisted = await repo.save(newUser); + + // ユーザーのタスクソート条件を作成 + const sortCriteria = new SortCriteria(); + { + sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER'); + sortCriteria.direction = getDirection('ASC'); + sortCriteria.user_id = persisted.id; + } + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const newSortCriteria = sortCriteriaRepo.create(sortCriteria); + await sortCriteriaRepo.save(newSortCriteria); + return persisted; }, );