Merged PR 560: ユーザー情報取得API実装
## 概要 [Task3036: ユーザー情報取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3036) - ユーザー関連情報取得APIのIFについてレスポンスを修正しています。 - AuthorIDを非必須のパラメータにしています。 - ユーザー関連情報取得APIの中身を実装しました。 ## レビューポイント - オプションアイテムのタイプを定数に置いたDictionaryで数値に変換していますが、定数の置き方として不自然ではないでしょうか。 ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
35923f84e2
commit
ee161a405f
@ -4180,7 +4180,7 @@
|
||||
"properties": {
|
||||
"authorId": {
|
||||
"type": "string",
|
||||
"description": "ログインしたユーザーのAuthorID(Authorでない場合は空文字)"
|
||||
"description": "ログインしたユーザーのAuthorID(Authorでない場合はundefined)"
|
||||
},
|
||||
"authorIdList": {
|
||||
"description": "属しているアカウントのAuthorID List(全て)",
|
||||
@ -4215,7 +4215,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authorId",
|
||||
"authorIdList",
|
||||
"workTypeList",
|
||||
"isEncrypted",
|
||||
|
||||
@ -239,6 +239,27 @@ export const OPTION_ITEM_VALUE_TYPE = {
|
||||
LAST_INPUT: 'LastInput',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* オプションアイテムのタイプ文字列と数値の対応
|
||||
**/
|
||||
export const OPTION_ITEM_VALUE_TYPE_NUMBER: {
|
||||
type: string;
|
||||
value: number;
|
||||
}[] = [
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.BLANK,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.DEFAULT,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.LAST_INPUT,
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* ADB2Cユーザのidentity.signInType
|
||||
* @const {string[]}
|
||||
@ -261,3 +282,9 @@ export const TERM_TYPE = {
|
||||
EULA: 'EULA',
|
||||
DPA: 'DPA',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 音声ファイルのフォーマット
|
||||
* @const {string}
|
||||
*/
|
||||
export const USER_AUDIO_FORMAT = 'DS2(QP)';
|
||||
|
||||
@ -144,9 +144,11 @@ export class OptionItemList {
|
||||
|
||||
export class GetRelationsResponse {
|
||||
@ApiProperty({
|
||||
description: 'ログインしたユーザーのAuthorID(Authorでない場合は空文字)',
|
||||
required: false,
|
||||
description:
|
||||
'ログインしたユーザーのAuthorID(Authorでない場合はundefined)',
|
||||
})
|
||||
authorId: string;
|
||||
authorId?: string;
|
||||
@ApiProperty({ description: '属しているアカウントのAuthorID List(全て)' })
|
||||
authorIdList: string[];
|
||||
@ApiProperty({
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
LICENSE_TYPE,
|
||||
USER_AUDIO_FORMAT,
|
||||
USER_LICENSE_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
@ -44,6 +45,8 @@ import {
|
||||
makeTestUser,
|
||||
} from '../../common/test/utility';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createOptionItems, createWorktype } from '../accounts/test/utility';
|
||||
import { createWorkflow, getWorkflows } from '../workflows/test/utility';
|
||||
|
||||
describe('UsersService.confirmUser', () => {
|
||||
let source: DataSource | null = null;
|
||||
@ -2666,3 +2669,187 @@ describe('UsersService.getUserName', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.getRelations', () => {
|
||||
let source: DataSource | null = null;
|
||||
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('ユーザー関連情報を取得できる(Author)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { id: user1, external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_1',
|
||||
encryption: true,
|
||||
encryption_password: 'password',
|
||||
prompt: true,
|
||||
});
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_2',
|
||||
});
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_3',
|
||||
email_verified: false,
|
||||
});
|
||||
|
||||
const worktype1 = await createWorktype(
|
||||
source,
|
||||
account.id,
|
||||
'worktype1',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
await createOptionItems(source, worktype1.id);
|
||||
|
||||
const worktype2 = await createWorktype(source, account.id, 'worktype2');
|
||||
await createOptionItems(source, worktype2.id);
|
||||
|
||||
await createWorkflow(source, account.id, user1, worktype1.id);
|
||||
await createWorkflow(source, account.id, user1, worktype2.id);
|
||||
await createWorkflow(source, account.id, user1);
|
||||
await createWorkflow(source, account.id, user2, worktype1.id);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const workflows = await getWorkflows(source, account.id);
|
||||
expect(workflows.length).toBe(4);
|
||||
expect(workflows[0].worktype_id).toBe(worktype1.id);
|
||||
expect(workflows[0].author_id).toBe(user1);
|
||||
expect(workflows[1].worktype_id).toBe(worktype2.id);
|
||||
expect(workflows[1].author_id).toBe(user1);
|
||||
expect(workflows[2].worktype_id).toBe(null);
|
||||
expect(workflows[2].author_id).toBe(user1);
|
||||
expect(workflows[3].worktype_id).toBe(worktype1.id);
|
||||
expect(workflows[3].author_id).toBe(user2);
|
||||
}
|
||||
|
||||
const context = makeContext(external_id);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const relations = await service.getRelations(context, external_id);
|
||||
|
||||
// レスポンスを確認
|
||||
{
|
||||
expect(relations.authorId).toBe('AUTHOR_1');
|
||||
expect(relations.authorIdList.length).toBe(2);
|
||||
expect(relations.authorIdList[0]).toBe('AUTHOR_1');
|
||||
expect(relations.authorIdList[1]).toBe('AUTHOR_2');
|
||||
|
||||
const workTypeList = relations.workTypeList;
|
||||
expect(relations.workTypeList.length).toBe(2);
|
||||
expect(workTypeList[0].workTypeId).toBe(worktype1.custom_worktype_id);
|
||||
expect(workTypeList[0].optionItemList.length).toBe(10);
|
||||
expect(workTypeList[0].optionItemList[0].label).toBe('');
|
||||
expect(workTypeList[0].optionItemList[0].initialValueType).toBe(2);
|
||||
expect(workTypeList[0].optionItemList[0].defaultValue).toBe('');
|
||||
expect(workTypeList[1].workTypeId).toBe(worktype2.custom_worktype_id);
|
||||
expect(workTypeList[1].optionItemList.length).toBe(10);
|
||||
|
||||
expect(relations.isEncrypted).toBe(true);
|
||||
expect(relations.encryptionPassword).toBe('password');
|
||||
expect(relations.activeWorktype).toBe(worktype1.custom_worktype_id);
|
||||
expect(relations.audioFormat).toBe(USER_AUDIO_FORMAT);
|
||||
expect(relations.prompt).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('ユーザー関連情報を取得できる(Author以外)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.TYPIST,
|
||||
encryption: false,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_2',
|
||||
});
|
||||
|
||||
const worktype1 = await createWorktype(source, account.id, 'worktype1');
|
||||
await createOptionItems(source, worktype1.id);
|
||||
|
||||
await createWorkflow(source, account.id, user2, worktype1.id);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const workflows = await getWorkflows(source, account.id);
|
||||
expect(workflows.length).toBe(1);
|
||||
expect(workflows[0].worktype_id).toBe(worktype1.id);
|
||||
expect(workflows[0].author_id).toBe(user2);
|
||||
}
|
||||
|
||||
const context = makeContext(external_id);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const relations = await service.getRelations(context, external_id);
|
||||
|
||||
// レスポンスを確認
|
||||
{
|
||||
expect(relations.authorId).toBe(undefined);
|
||||
expect(relations.authorIdList.length).toBe(1);
|
||||
expect(relations.authorIdList[0]).toBe('AUTHOR_2');
|
||||
expect(relations.workTypeList.length).toBe(0);
|
||||
|
||||
expect(relations.isEncrypted).toBe(false);
|
||||
expect(relations.encryptionPassword).toBe(undefined);
|
||||
expect(relations.activeWorktype).toBe('');
|
||||
expect(relations.audioFormat).toBe(USER_AUDIO_FORMAT);
|
||||
expect(relations.prompt).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('ユーザーが存在しない場合は、ユーザー未存在エラー', async () => {
|
||||
if (!source) fail();
|
||||
|
||||
try {
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
await service.getRelations(context, 'external_id');
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010204'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -36,6 +36,8 @@ import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
OPTION_ITEM_VALUE_TYPE_NUMBER,
|
||||
USER_AUDIO_FORMAT,
|
||||
USER_LICENSE_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
@ -663,133 +665,68 @@ export class UsersService {
|
||||
): Promise<GetRelationsResponse> {
|
||||
this.logger.log(`[IN] [${context.trackingId}] ${this.getRelations.name}`);
|
||||
try {
|
||||
const user = await this.usersRepository.findUserByExternalId(userId);
|
||||
const { id } = await this.usersRepository.findUserByExternalId(userId);
|
||||
|
||||
// ユーザー関連情報を取得
|
||||
const { user, authors, worktypes, activeWorktype } =
|
||||
await this.usersRepository.getUserRelations(id);
|
||||
|
||||
// AuthorIDのリストを作成
|
||||
const authorIds = authors.flatMap((author) =>
|
||||
author.author_id ? [author.author_id] : [],
|
||||
);
|
||||
|
||||
const workTypeList = worktypes?.map((worktype) => {
|
||||
return {
|
||||
workTypeId: worktype.custom_worktype_id,
|
||||
optionItemList: worktype.option_items.map((optionItem) => {
|
||||
const initialValueType = OPTION_ITEM_VALUE_TYPE_NUMBER.find(
|
||||
(x) => x.type === optionItem.default_value_type,
|
||||
)?.value;
|
||||
|
||||
if (!initialValueType) {
|
||||
throw new Error(
|
||||
`invalid default_value_type ${optionItem.default_value_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
label: optionItem.item_label,
|
||||
initialValueType,
|
||||
defaultValue: optionItem.initial_value,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: PBI2105 本実装時に修正すること
|
||||
return {
|
||||
authorId: user.author_id ?? '',
|
||||
authorIdList: [user.author_id ?? '', 'XXX'],
|
||||
isEncrypted: true,
|
||||
encryptionPassword: 'abcd@123?dcba',
|
||||
audioFormat: 'DS2(QP)',
|
||||
prompt: true,
|
||||
workTypeList: [
|
||||
{
|
||||
workTypeId: 'workType1',
|
||||
optionItemList: [
|
||||
{
|
||||
label: 'optionItem11',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default11',
|
||||
},
|
||||
{
|
||||
label: 'optionItem12',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default12',
|
||||
},
|
||||
{
|
||||
label: 'optionItem13',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default13',
|
||||
},
|
||||
{
|
||||
label: 'optionItem14',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default14',
|
||||
},
|
||||
{
|
||||
label: 'optionItem15',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default15',
|
||||
},
|
||||
{
|
||||
label: 'optionItem16',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default16',
|
||||
},
|
||||
{
|
||||
label: 'optionItem17',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default17',
|
||||
},
|
||||
{
|
||||
label: 'optionItem18',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default18',
|
||||
},
|
||||
{
|
||||
label: 'optionItem19',
|
||||
initialValueType: 1,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: 'optionItem110',
|
||||
initialValueType: 3,
|
||||
defaultValue: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
workTypeId: 'workType2',
|
||||
optionItemList: [
|
||||
{
|
||||
label: 'optionItem21',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default21',
|
||||
},
|
||||
{
|
||||
label: 'optionItem22',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default22',
|
||||
},
|
||||
{
|
||||
label: 'optionItem23',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'defaul23',
|
||||
},
|
||||
{
|
||||
label: 'optionItem24',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default24',
|
||||
},
|
||||
{
|
||||
label: 'optionItem25',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default25',
|
||||
},
|
||||
{
|
||||
label: 'optionItem26',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default26',
|
||||
},
|
||||
{
|
||||
label: 'optionItem27',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default27',
|
||||
},
|
||||
{
|
||||
label: 'optionItem28',
|
||||
initialValueType: 2,
|
||||
defaultValue: 'default28',
|
||||
},
|
||||
{
|
||||
label: 'optionItem29',
|
||||
initialValueType: 1,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: 'optionItem210',
|
||||
initialValueType: 3,
|
||||
defaultValue: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
activeWorktype: 'workType1',
|
||||
authorId: user.author_id ?? undefined,
|
||||
authorIdList: authorIds,
|
||||
workTypeList,
|
||||
isEncrypted: user.encryption,
|
||||
encryptionPassword: user.encryption_password ?? undefined,
|
||||
activeWorktype: activeWorktype?.custom_worktype_id ?? '',
|
||||
audioFormat: USER_AUDIO_FORMAT,
|
||||
prompt: user.prompt,
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case UserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
|
||||
@ -33,6 +33,8 @@ import {
|
||||
AdminUserNotFoundError,
|
||||
} from '../accounts/errors/types';
|
||||
import { Account } from '../accounts/entity/account.entity';
|
||||
import { Workflow } from '../workflows/entity/workflow.entity';
|
||||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
@ -644,4 +646,83 @@ export class UsersRepositoryService {
|
||||
return originAccount.delegation_permission;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーに紐づく各種情報を取得する
|
||||
* @param userId
|
||||
* @returns user relations
|
||||
*/
|
||||
async getUserRelations(userId: number): Promise<{
|
||||
user: User;
|
||||
authors: User[];
|
||||
worktypes: Worktype[];
|
||||
activeWorktype: Worktype | undefined;
|
||||
}> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
|
||||
const user = await userRepo.findOne({
|
||||
where: { id: userId },
|
||||
relations: { account: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(`User is Not Found. id: ${userId}`);
|
||||
}
|
||||
|
||||
// 運用上、アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!user.account) {
|
||||
throw new AccountNotFoundError(
|
||||
`Account is Not Found. user.id: ${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ユーザーの所属するアカウント内のすべてのメール認証済みAuthorユーザーを取得する
|
||||
const authors = await userRepo.find({
|
||||
where: {
|
||||
account_id: user.account_id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
email_verified: true,
|
||||
},
|
||||
});
|
||||
|
||||
// ユーザーの所属するアカウント内のアクティブワークタイプを取得する
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
let activeWorktype: Worktype | undefined = undefined;
|
||||
const activeWorktypeId = user.account.active_worktype_id;
|
||||
if (activeWorktypeId !== null) {
|
||||
activeWorktype =
|
||||
(await worktypeRepo.findOne({
|
||||
where: {
|
||||
account_id: user.account_id,
|
||||
id: activeWorktypeId,
|
||||
},
|
||||
})) ?? undefined;
|
||||
}
|
||||
|
||||
let worktypes: Worktype[] = [];
|
||||
// ユーザーのロールがAuthorの場合はルーティングルールに紐づいたワークタイプを取得する
|
||||
if (user.role === USER_ROLES.AUTHOR) {
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
const workflows = await workflowRepo.find({
|
||||
where: {
|
||||
account_id: user.account_id,
|
||||
author_id: user.id,
|
||||
worktype_id: Not(IsNull()),
|
||||
},
|
||||
relations: {
|
||||
worktype: {
|
||||
option_items: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
worktypes = workflows.flatMap((workflow) =>
|
||||
workflow.worktype ? [workflow.worktype] : [],
|
||||
);
|
||||
}
|
||||
|
||||
return { user, authors, worktypes, activeWorktype };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,10 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Worktype } from './worktype.entity';
|
||||
|
||||
@Entity({ name: 'option_items' })
|
||||
export class OptionItem {
|
||||
@ -32,4 +35,8 @@ export class OptionItem {
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date | null;
|
||||
|
||||
@ManyToOne(() => Worktype, (worktype) => worktype.id)
|
||||
@JoinColumn({ name: 'worktype_id' })
|
||||
worktype: Worktype;
|
||||
}
|
||||
|
||||
@ -5,7 +5,9 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { OptionItem } from './option_item.entity';
|
||||
|
||||
@Entity({ name: 'worktypes' })
|
||||
export class Worktype {
|
||||
@ -41,4 +43,7 @@ export class Worktype {
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => OptionItem, (optionItem) => optionItem.worktype)
|
||||
option_items: OptionItem[];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user