diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 53a4fdf..b8e5b6c 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -409,7 +409,7 @@ "post": { "operationId": "updateTypistGroup", "summary": "", - "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを編集します", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します", "parameters": [ { "name": "typistGroupId", diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index ee2e6dd..390fc48 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -363,7 +363,7 @@ export class AccountsController { @ApiOperation({ operationId: 'updateTypistGroup', description: - 'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを編集します', + 'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します', }) @ApiBearerAuth() @UseGuards(AuthGuard) @@ -374,13 +374,22 @@ export class AccountsController { @Body() body: UpdateTypistGroupRequest, @Param() param: UpdateTypistGroupRequestParam, ): Promise { + const { typistGroupName, typistIds } = body; + const { typistGroupId } = param; + // アクセストークン取得 const accessToken = retrieveAuthorizationToken(req); - const payload = jwt.decode(accessToken, { json: true }) as AccessToken; + const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; - console.log(param.typistGroupId); - console.log(body.typistGroupName); - console.log(body.typistIds); + const context = makeContext(userId); + + await this.accountService.updateTypistGroup( + context, + userId, + typistGroupId, + typistGroupName, + typistIds, + ); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index ce53585..153a529 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -18,7 +18,6 @@ import { getSortCriteria, getTypistGroup, getTypistGroupMember, - createTypistGroup, } from './test/utility'; import { DataSource } from 'typeorm'; import { makeTestingModule } from '../../common/test/modules'; @@ -2633,28 +2632,31 @@ describe('getTypistGroup', () => { // アカウントにタイピストグループを作成する const typistGroupName = 'typist-group-name'; - const { id: typistGroupId } = await createTypistGroup( - source, - account.id, + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + await service.createTypistGroup( + context, + admin.external_id, typistGroupName, userIds, ); //作成したデータを確認 + + const group = await getTypistGroup(source, account.id); { - const group = await getTypistGroup(source, account.id); expect(group.length).toBe(1); expect(group[0].name).toBe(typistGroupName); const groupUsers = await getTypistGroupMember(source, group[0].id); expect(groupUsers.length).toBe(3); expect(groupUsers.map((user) => user.user_id)).toEqual(userIds); } - const service = module.get(AccountsService); - const context = makeContext(admin.external_id); + const typistGroup = await service.getTypistGroup( context, admin.external_id, - typistGroupId, + group[0].id, ); //実行結果を確認 { @@ -2688,7 +2690,14 @@ describe('getTypistGroup', () => { // アカウントにタイピストグループを作成する const typistGroupName = 'typist-group-name'; - await createTypistGroup(source, account.id, typistGroupName, userIds); + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + userIds, + ); //作成したデータを確認 { @@ -2699,8 +2708,6 @@ describe('getTypistGroup', () => { expect(groupUsers.length).toBe(3); expect(groupUsers.map((user) => user.user_id)).toEqual(userIds); } - const service = module.get(AccountsService); - const context = makeContext(admin.external_id); try { await service.getTypistGroup(context, admin.external_id, 999); @@ -2736,25 +2743,25 @@ describe('getTypistGroup', () => { // アカウントにタイピストグループを作成する const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); - const { id: typistGroupId } = await createTypistGroup( - source, - account.id, + await service.createTypistGroup( + context, + admin.external_id, typistGroupName, userIds, ); //作成したデータを確認 + const group = await getTypistGroup(source, account.id); { - const group = await getTypistGroup(source, account.id); expect(group.length).toBe(1); expect(group[0].name).toBe(typistGroupName); const groupUsers = await getTypistGroupMember(source, group[0].id); expect(groupUsers.length).toBe(3); expect(groupUsers.map((user) => user.user_id)).toEqual(userIds); } - const service = module.get(AccountsService); - const context = makeContext(admin.external_id); //DBアクセスに失敗するようにする const typistGroupService = module.get( @@ -2765,7 +2772,349 @@ describe('getTypistGroup', () => { .mockRejectedValue('DB failed'); try { - await service.getTypistGroup(context, admin.external_id, typistGroupId); + await service.getTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + +describe('updateTypistGroup', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + it('TypistGroupを更新できる', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを3名追加する + const typiptUserExternalIds = [ + 'typist-user-external-id1', + 'typist-user-external-id2', + 'typist-user-external-id3', + ]; + const userIds: number[] = []; + for (const typiptUserExternalId of typiptUserExternalIds) { + const { id: userId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + userIds.push(userId); + } + + const service = module.get(AccountsService); + const typistUserIds = [userIds[1]]; + const context = makeContext(admin.external_id); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [userIds[0]], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + const group = await getTypistGroup(source, account.id); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(userIds[0]); + } + + const updateTypistGroupName = 'typist-group-name-update'; + + await service.updateTypistGroup( + context, + admin.external_id, + group[0].id, + updateTypistGroupName, + typistUserIds, + ); + //実行結果を確認 + { + const typistGroups = await getTypistGroup(source, account.id); + expect(typistGroups.length).toBe(1); + expect(typistGroups[0].name).toBe(updateTypistGroupName); + + const typistGroupUsers = await getTypistGroupMember( + source, + typistGroups[0].id, + ); + expect(typistGroupUsers.length).toBe(1); + expect(typistGroupUsers[0].user_id).toEqual(userIds[1]); + } + }); + it('typistIdsにRole:typist以外のユーザーが含まれていた場合、400エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを3名追加する + const typiptUserExternalIds = [ + 'typist-user-external-id1', + 'typist-user-external-id2', + 'typist-user-external-id3', + ]; + const userIds: number[] = []; + for (const typiptUserExternalId of typiptUserExternalIds) { + const { id: userId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: + typiptUserExternalId === 'typist-user-external-id3' + ? USER_ROLES.NONE + : USER_ROLES.TYPIST, //typist-user-external-id3のみRole:none + }); + userIds.push(userId); + } + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const typistUserIds = [userIds[2]]; + const context = makeContext(admin.external_id); + + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [userIds[0]], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(userIds[0]); + } + + const updateTypistGroupName = 'typist-group-name-update'; + + try { + await service.updateTypistGroup( + context, + admin.external_id, + group[0].id, + updateTypistGroupName, + typistUserIds, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010204')); + } else { + fail(); + } + } + }); + it('typistIdsに存在しないユーザーが含まれていた場合、400エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを3名追加する + const typiptUserExternalIds = [ + 'typist-user-external-id1', + 'typist-user-external-id2', + 'typist-user-external-id3', + ]; + const userIds: number[] = []; + for (const typiptUserExternalId of typiptUserExternalIds) { + const { id: userId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + userIds.push(userId); + } + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const typistUserIds = [999]; + const context = makeContext(admin.external_id); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [userIds[0]], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(userIds[0]); + } + const updateTypistGroupName = 'typist-group-name-update'; + + try { + await service.updateTypistGroup( + context, + admin.external_id, + group[0].id, + updateTypistGroupName, + typistUserIds, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010204')); + } else { + fail(); + } + } + }); + it('タイピストグループが存在しない場合、400エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + // 作成したアカウントにユーザーを3名追加する + const typiptUserExternalIds = [ + 'typist-user-external-id1', + 'typist-user-external-id2', + 'typist-user-external-id3', + ]; + const userIds: number[] = []; + for (const typiptUserExternalId of typiptUserExternalIds) { + const { id: userId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + userIds.push(userId); + } + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const typistUserIds = [userIds[1]]; + const context = makeContext(admin.external_id); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [userIds[0]], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(userIds[0]); + } + + const updateTypistGroupName = 'typist-group-name-update'; + + try { + await service.updateTypistGroup( + context, + admin.external_id, + 999, + updateTypistGroupName, + typistUserIds, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010908')); + } else { + fail(); + } + } + }); + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを3名追加する + const typiptUserExternalIds = [ + 'typist-user-external-id1', + 'typist-user-external-id2', + 'typist-user-external-id3', + ]; + const userIds: number[] = []; + for (const typiptUserExternalId of typiptUserExternalIds) { + const { id: userId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + userIds.push(userId); + } + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const typistUserIds = [userIds[1]]; + const context = makeContext(admin.external_id); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [userIds[0]], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + const group = await getTypistGroup(source, account.id); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(userIds[0]); + } + + const updateTypistGroupName = 'typist-group-name-update'; + + //DBアクセスに失敗するようにする + const typistGroupService = module.get( + UserGroupsRepositoryService, + ); + typistGroupService.updateTypistGroup = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.updateTypistGroup( + context, + admin.external_id, + group[0].id, + updateTypistGroupName, + typistUserIds, + ); } catch (e) { if (e instanceof HttpException) { expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index f649b81..f93df46 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -41,8 +41,10 @@ import { OrderNotFoundError, } from '../../repositories/licenses/errors/types'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; -import { TypistGroupNotExistError } from '../../repositories/user_groups/errors/types'; -import { TypistIdInvalidError } from '../../repositories/user_groups/errors/types'; +import { + TypistGroupNotExistError, + TypistIdInvalidError, +} from '../../repositories/user_groups/errors/types'; @Injectable() export class AccountsService { @@ -981,4 +983,70 @@ export class AccountsService { ); } } + + /** + * タイピストグループを更新する + * @param context + * @param externalId + * @param typistGroupId + * @param typistGroupName + * @param typistIds + * @returns typist group + */ + async updateTypistGroup( + context: Context, + externalId: string, + typistGroupId: number, + typistGroupName: string, + typistIds: number[], + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.updateTypistGroup.name} | params: { typistGroupId: ${typistGroupId}, typistGroupName: ${typistGroupName}, typistIds: ${typistIds} };`, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id } = await this.usersRepository.findUserByExternalId( + externalId, + ); + + // タイピストグループと所属するタイピストを更新する + await this.userGroupsRepository.updateTypistGroup( + account_id, + typistGroupId, + typistGroupName, + typistIds, + ); + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // タイピストIDが存在しない場合は400エラーを返す + case TypistIdInvalidError: + throw new HttpException( + makeErrorResponse('E010204'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループIDが存在しない場合は400エラーを返す + case TypistGroupNotExistError: + throw new HttpException( + makeErrorResponse('E010908'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.updateTypistGroup.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index af9a47d..ef8cdb6 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -108,34 +108,3 @@ export const getTypistGroupMember = async ( }, }); }; - -// タイピストグループを作成する -export const createTypistGroup = async ( - datasource: DataSource, - accountId: number, - name: string, - memberIds: number[], -): Promise => { - const { identifiers } = await datasource.getRepository(UserGroup).insert({ - account_id: accountId, - name: name, - created_by: 'test_runner', - created_at: new Date(), - updated_by: 'updater', - updated_at: new Date(), - }); - const userGroup = identifiers.pop() as UserGroup; - - for (const memberId of memberIds) { - await datasource.getRepository(UserGroupMember).insert({ - user_group_id: userGroup.id, - user_id: memberId, - created_by: 'test_runner', - created_at: new Date(), - updated_by: 'updater', - updated_at: new Date(), - }); - } - - return userGroup; -}; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 98d0b6d..0b1074d 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -174,6 +174,7 @@ export class UpdateTypistGroupRequest { @IsArray() @IsInt({ each: true }) @Min(0, { each: true }) + @IsUnique() typistIds: number[]; } export class UpdateTypistGroupRequestParam { diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index ffd9dbc..2c15584 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -2,9 +2,8 @@ import { Injectable } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; import { UserGroup } from './entity/user_group.entity'; import { UserGroupMember } from './entity/user_group_member.entity'; -import { TypistGroupNotExistError } from './errors/types'; import { User } from '../users/entity/user.entity'; -import { TypistIdInvalidError } from './errors/types'; +import { TypistGroupNotExistError, TypistIdInvalidError } from './errors/types'; import { USER_ROLES } from '../../constants'; @Injectable() @@ -130,4 +129,72 @@ export class UserGroupsRepositoryService { return userGroup; }); } + + /** + * 指定したIDのタイピストグループを更新し、そのタイピストグループとtypistIdsのユーザーを紐付ける + * @param accountId + * @param name + * @param typistIds + * @returns createdTypistGroup + */ + async updateTypistGroup( + accountId: number, + typistGroupId: number, + typistGroupName: string, + typistIds: number[], + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const userGroupRepo = entityManager.getRepository(UserGroup); + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + // typistIdsのidを持つユーザーが、account_idのアカウントに所属していて、かつ、roleがtypistであることを確認する + const userRepo = entityManager.getRepository(User); + const userRecords = await userRepo.find({ + where: { + id: In(typistIds), + account_id: accountId, + role: USER_ROLES.TYPIST, + }, + }); + if (userRecords.length !== typistIds.length) { + throw new TypistIdInvalidError( + `Typist user not exists Error. typistIds:${typistIds}; typistIds(DB):${userRecords.map( + (x) => x.id, + )}`, + ); + } + + // GroupIdが自アカウント内に存在するか確認する + const typistGroup = await userGroupRepo.findOne({ + where: { + id: typistGroupId, + account_id: accountId, + }, + }); + if (!typistGroup) { + throw new TypistGroupNotExistError( + `TypistGroup not exists Error. accountId: ${accountId}; typistGroupId: ${typistGroupId}`, + ); + } + + // 対象のタイピストグループを更新する + // ユーザーグループ名を更新する + typistGroup.name = typistGroupName; + await userGroupRepo.save(typistGroup); + + // user_group_membersテーブルから対象のタイピストグループのユーザーを削除する + await userGroupMemberRepo.delete({ + user_group_id: typistGroupId, + }); + + const typistGroupMembers = userRecords.map((typist) => { + return { + user_group_id: typistGroup.id, + user_id: typist.id, + }; + }); + await userGroupMemberRepo.save(typistGroupMembers); + + return typistGroup; + }); + } }