Merged PR 453: ワークフロー一覧取得API実装

## 概要
[Task2736: ワークフロー一覧取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2736)

- ワークフロー一覧取得APIとテストを実装しました

## レビューポイント
- リポジトリの取得処理は適切か(リレーションなど)
- ADB2Cからの取得処理は適切か
- サービスでのワークフローの整形処理は適切か
- テストケースは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-10-03 01:14:18 +00:00
parent 65f80b9a5b
commit 1cc7a0141d
10 changed files with 554 additions and 5 deletions

View File

@ -48,6 +48,7 @@ import { WorkflowsModule } from './features/workflows/workflows.module';
import { WorkflowsController } from './features/workflows/workflows.controller';
import { WorkflowsService } from './features/workflows/workflows.service';
import { validate } from './common/validators/env.validator';
import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.repository.module';
@Module({
imports: [
@ -86,6 +87,7 @@ import { validate } from './common/validators/env.validator';
CheckoutPermissionsRepositoryModule,
UserGroupsRepositoryModule,
TemplateFilesRepositoryModule,
WorkflowsRepositoryModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({

View File

@ -0,0 +1,61 @@
import { DataSource } from 'typeorm';
import { Workflow } from '../../../repositories/workflows/entity/workflow.entity';
import { WorkflowTypist } from '../../../repositories/workflows/entity/workflow_typists.entity';
// Workflowを作成する
export const createWorkflow = async (
datasource: DataSource,
accountId: number,
authorId: number,
worktypeId?: number | undefined,
templateId?: number | undefined,
): Promise<Workflow> => {
const { identifiers } = await datasource.getRepository(Workflow).insert({
account_id: accountId,
author_id: authorId,
worktype_id: worktypeId ?? undefined,
template_id: templateId ?? undefined,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const workflow = identifiers.pop() as Workflow;
return workflow;
};
// Workflowを取得する
export const getWorkflows = async (
datasource: DataSource,
accountId: number,
): Promise<Workflow[]> => {
return await datasource.getRepository(Workflow).find({
where: {
account_id: accountId,
},
});
};
// Workflowを作成する
export const createWorkflowTypist = async (
datasource: DataSource,
workflowId: number,
typistUserId?: number | undefined,
typistGroupId?: number | undefined,
): Promise<Workflow> => {
const { identifiers } = await datasource
.getRepository(WorkflowTypist)
.insert({
workflow_id: workflowId,
typist_id: typistUserId ?? undefined,
typist_group_id: typistGroupId ?? undefined,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const workflow = identifiers.pop() as Workflow;
return workflow;
};

View File

@ -62,9 +62,10 @@ export class WorkflowsController {
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
console.log(context.trackingId);
return { workflows: [] };
const workflows = await this.workflowsService.getWorkflows(context, userId);
return { workflows };
}
@ApiResponse({

View File

@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { WorkflowsController } from './workflows.controller';
import { WorkflowsService } from './workflows.service';
import { WorkflowsRepositoryModule } from '../../repositories/workflows/workflows.repository.module';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
@Module({
imports: [UsersRepositoryModule],
imports: [UsersRepositoryModule, WorkflowsRepositoryModule, AdB2cModule],
providers: [WorkflowsService],
controllers: [WorkflowsController],
})

View File

@ -0,0 +1,225 @@
import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules';
import { makeTestAccount, makeTestUser } from '../../common/test/utility';
import { makeContext } from '../../common/log';
import { WorkflowsService } from './workflows.service';
import { USER_ROLES } from '../../constants';
import { createTemplateFile } from '../templates/test/utility';
import { createWorktype } from '../accounts/test/utility';
import {
createWorkflow,
createWorkflowTypist,
getWorkflows,
} from './test/utility';
import { createUserGroup } from '../users/test/utility';
import { overrideAdB2cService } from '../../common/test/overrides';
import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('getWorktypes', () => {
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('アカウント内のWorkflow一覧を取得できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
external_id: 'author1',
author_id: 'AUTHOR1',
account_id: account.id,
role: USER_ROLES.AUTHOR,
});
const { id: authorId2 } = await makeTestUser(source, {
external_id: 'author2',
author_id: 'AUTHOR2',
account_id: account.id,
role: USER_ROLES.AUTHOR,
});
const { id: authorId3 } = await makeTestUser(source, {
external_id: 'author3',
author_id: 'AUTHOR3',
account_id: account.id,
role: USER_ROLES.AUTHOR,
});
const { id: typistId, external_id: typistExternalId } = await makeTestUser(
source,
{
external_id: 'typist1',
account_id: account.id,
role: USER_ROLES.TYPIST,
},
);
const { userGroupId } = await createUserGroup(
source,
account.id,
'group1',
[typistId],
);
const { id: worktypeId1 } = await createWorktype(
source,
account.id,
'worktype1',
);
const { id: templateId1 } = await createTemplateFile(
source,
account.id,
'fileName1',
'url1',
);
const workflow1 = await createWorkflow(
source,
account.id,
authorId1,
worktypeId1,
templateId1,
);
const workflow2 = await createWorkflow(
source,
account.id,
authorId2,
undefined,
templateId1,
);
const workflow3 = await createWorkflow(
source,
account.id,
authorId3,
worktypeId1,
undefined,
);
await createWorkflowTypist(source, workflow1.id, typistId, undefined);
await createWorkflowTypist(source, workflow2.id, undefined, userGroupId);
await createWorkflowTypist(source, workflow3.id, undefined, userGroupId);
const service = module.get<WorkflowsService>(WorkflowsService);
const context = makeContext(admin.external_id);
//作成したデータを確認
{
const workflows = await getWorkflows(source, account.id);
expect(workflows.length).toBe(3);
expect(workflows[0].id).toBe(workflow1.id);
expect(workflows[0].author_id).toBe(authorId1);
expect(workflows[0].worktype_id).toBe(worktypeId1);
expect(workflows[0].template_id).toBe(templateId1);
expect(workflows[1].id).toBe(workflow2.id);
expect(workflows[1].author_id).toBe(authorId2);
expect(workflows[1].worktype_id).toBe(null);
expect(workflows[1].template_id).toBe(templateId1);
expect(workflows[2].id).toBe(workflow3.id);
expect(workflows[2].author_id).toBe(authorId3);
expect(workflows[2].worktype_id).toBe(worktypeId1);
expect(workflows[2].template_id).toBe(null);
}
overrideAdB2cService(service, {
getUsers: async () => [{ id: typistExternalId, displayName: 'typist1' }],
});
const resWorkflows = await service.getWorkflows(context, admin.external_id);
//実行結果を確認
{
expect(resWorkflows.length).toBe(3);
expect(resWorkflows[0].id).toBe(workflow1.id);
expect(resWorkflows[0].author.id).toBe(authorId1);
expect(resWorkflows[0].author.authorId).toBe('AUTHOR1');
expect(resWorkflows[0].worktype.id).toBe(worktypeId1);
expect(resWorkflows[0].worktype.worktypeId).toBe('worktype1');
expect(resWorkflows[0].template.id).toBe(templateId1);
expect(resWorkflows[0].template.fileName).toBe('fileName1');
expect(resWorkflows[0].typists.length).toBe(1);
expect(resWorkflows[0].typists[0].typistUserId).toBe(typistId);
expect(resWorkflows[0].typists[0].typistName).toBe('typist1');
expect(resWorkflows[1].id).toBe(workflow2.id);
expect(resWorkflows[1].author.id).toBe(authorId2);
expect(resWorkflows[1].author.authorId).toBe('AUTHOR2');
expect(resWorkflows[1].worktype).toBe(undefined);
expect(resWorkflows[1].template.id).toBe(templateId1);
expect(resWorkflows[1].template.fileName).toBe('fileName1');
expect(resWorkflows[1].typists.length).toBe(1);
expect(resWorkflows[1].typists[0].typistGroupId).toBe(userGroupId);
expect(resWorkflows[1].typists[0].typistName).toBe('group1');
expect(resWorkflows[2].id).toBe(workflow3.id);
expect(resWorkflows[2].author.id).toBe(authorId3);
expect(resWorkflows[2].author.authorId).toBe('AUTHOR3');
expect(resWorkflows[2].worktype.id).toBe(worktypeId1);
expect(resWorkflows[2].worktype.worktypeId).toBe('worktype1');
expect(resWorkflows[2].template).toBe(undefined);
expect(resWorkflows[2].typists.length).toBe(1);
expect(resWorkflows[2].typists[0].typistGroupId).toBe(userGroupId);
expect(resWorkflows[2].typists[0].typistName).toBe('group1');
}
});
it('アカウント内のWorkflow一覧を取得できる0件', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<WorkflowsService>(WorkflowsService);
const context = makeContext(admin.external_id);
overrideAdB2cService(service, {
getUsers: async () => [],
});
const resWorkflows = await service.getWorkflows(context, admin.external_id);
//実行結果を確認
{
expect(resWorkflows.length).toBe(0);
}
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<WorkflowsService>(WorkflowsService);
const context = makeContext(admin.external_id);
//DBアクセスに失敗するようにする
const templatesService = module.get<WorkflowsRepositoryService>(
WorkflowsRepositoryService,
);
templatesService.getWorkflows = jest.fn().mockRejectedValue('DB failed');
//実行結果を確認
try {
await service.getWorkflows(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});

View File

@ -1,7 +1,109 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { Context } from '../../common/log';
import { Workflow } from './types/types';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
@Injectable()
export class WorkflowsService {
private readonly logger = new Logger(WorkflowsService.name);
constructor() {}
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly workflowsRepository: WorkflowsRepositoryService,
private readonly adB2cService: AdB2cService,
) {}
/**
*
* @param context
* @param externalId
* @returns workflows
*/
async getWorkflows(
context: Context,
externalId: string,
): Promise<Workflow[]> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getWorkflows.name} | params: { externalId: ${externalId} };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// DBからワークフロー一覧を取得
const workflowRecords = await this.workflowsRepository.getWorkflows(
accountId,
);
// ワークフロー一覧からtypistのexternalIdを取得
const externalIds = workflowRecords.flatMap((workflow) => {
const workflowTypists = workflow.workflowTypists.flatMap(
(workflowTypist) => {
const { typist } = workflowTypist;
return typist ? [typist?.external_id] : [];
},
);
return workflowTypists;
});
const distinctedExternalIds = [...new Set(externalIds)];
// ADB2Cからユーザー一覧を取得
const adb2cUsers = await this.adB2cService.getUsers(
context,
distinctedExternalIds,
);
// DBから取得したワークフロー一覧を整形
const workflows = workflowRecords.map((workflow) => {
const { id, author, worktype, template, workflowTypists } = workflow;
const authorId = { id: author.id, authorId: author.author_id };
const worktypeId = worktype
? { id: worktype.id, worktypeId: worktype.custom_worktype_id }
: undefined;
const templateId = template
? { id: template.id, fileName: template.file_name }
: undefined;
// ルーティング候補を整形
const typists = workflowTypists.map((workflowTypist) => {
const { typist, typistGroup } = workflowTypist;
// typistがユーザーの場合はADB2Cからユーザー名を取得
const typistName = typist
? adb2cUsers.find(
(adb2cUser) => adb2cUser.id === typist.external_id,
).displayName
: typistGroup.name;
return {
typistUserId: typist?.id,
typistGroupId: typistGroup?.id,
typistName,
};
});
return {
id,
author: authorId,
worktype: worktypeId,
template: templateId,
typists,
};
});
return workflows;
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getWorkflows.name}`,
);
}
}
}

View File

@ -0,0 +1,59 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
JoinColumn,
ManyToOne,
} from 'typeorm';
import { WorkflowTypist } from './workflow_typists.entity';
import { Worktype } from '../../worktypes/entity/worktype.entity';
import { TemplateFile } from '../../template_files/entity/template_file.entity';
import { User } from '../../users/entity/user.entity';
@Entity({ name: 'workflows' })
export class Workflow {
@PrimaryGeneratedColumn()
id: number;
@Column()
account_id: number;
@Column()
author_id: number;
@Column({ nullable: true })
worktype_id?: number;
@Column({ nullable: true })
template_id?: number;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'author_id' })
author?: User;
@ManyToOne(() => Worktype, (worktype) => worktype.id)
@JoinColumn({ name: 'worktype_id' })
worktype?: Worktype;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: 'template_id' })
template?: TemplateFile;
@OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow)
workflowTypists?: WorkflowTypist[];
}

View File

@ -0,0 +1,51 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Workflow } from './workflow.entity';
import { User } from '../../users/entity/user.entity';
import { UserGroup } from '../../user_groups/entity/user_group.entity';
@Entity({ name: 'workflow_typists' })
export class WorkflowTypist {
@PrimaryGeneratedColumn()
id: number;
@Column()
workflow_id: number;
@Column({ nullable: true })
typist_id?: number;
@Column({ nullable: true })
typist_group_id?: number;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Workflow, (workflow) => workflow.id)
@JoinColumn({ name: 'workflow_id' })
workflow?: Workflow;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'typist_id' })
typist?: User;
@ManyToOne(() => UserGroup, (userGroup) => userGroup.id)
@JoinColumn({ name: 'typist_group_id' })
typistGroup?: UserGroup;
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkflowTypist } from './entity/workflow_typists.entity';
import { Workflow } from './entity/workflow.entity';
import { WorkflowsRepositoryService } from './workflows.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([Workflow, WorkflowTypist])],
providers: [WorkflowsRepositoryService],
exports: [WorkflowsRepositoryService],
})
export class WorkflowsRepositoryModule {}

View File

@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Workflow } from './entity/workflow.entity';
@Injectable()
export class WorkflowsRepositoryService {
constructor(private dataSource: DataSource) {}
/**
*
* @param externalId
* @returns worktypes and active worktype id
*/
async getWorkflows(accountId: number): Promise<Workflow[]> {
return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow);
const workflows = await workflowRepo.find({
where: { account_id: accountId },
relations: {
author: true,
worktype: true,
template: true,
workflowTypists: {
typist: true,
typistGroup: true,
},
},
});
return workflows;
});
}
}