Merged PR 652: 外部連携以外のAPIバリデータ見直し(/accounts/*)

## 概要
[Task3285: 外部連携以外のAPIバリデータ見直し](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3285)

- accounts/* 配下のRequest型に不足しているバリデータを追加

## レビュー対象外
- 他features配下のRequest型のバリデータ
  - 1個のレビューで出すとレビュー負荷が高すぎそうであったため、レビューを分割します

## レビューポイント
- 追加したバリデーションが適切か(主な対応は以下)
  - 数値に型変換していなかったものに型変換を追加
  - IsInt()を追加
  - マイナス値がセットされるとDBリクエストでコケる類のものに制限を追加
  - 文字列はDBやAzure AD B2Cで受付不能な文字数を弾く

## 動作確認状況
- ビルドとテストが成功することは確認
This commit is contained in:
湯本 開 2023-12-25 05:21:35 +00:00
parent fce3214650
commit 6deaa37df7
2 changed files with 378 additions and 314 deletions

View File

@ -24,51 +24,336 @@ import { OPTION_ITEM_VALUE_TYPE } from '../../../constants';
export class CreateAccountRequest {
@ApiProperty()
@MaxLength(255)
companyName: string;
@ApiProperty({
description: '国名(ISO 3166-1 alpha-2)',
minLength: 2,
maxLength: 2,
})
@MinLength(2)
@MaxLength(2)
country: string;
@ApiProperty({ required: false })
@Type(() => Number)
@IsInt()
@IsOptional()
dealerAccountId?: number;
@ApiProperty()
@MaxLength(256) // AzureAdB2Cの仕様上、256文字まで[https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/user-profile-attributes]
adminName: string;
@ApiProperty()
@IsEmail({ blacklisted_chars: '*' })
adminMail: string;
@ApiProperty()
@IsAdminPasswordvalid()
adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
@MaxLength(255)
acceptedEulaVersion: string;
@ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' })
@MaxLength(255)
acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
@MaxLength(255)
acceptedDpaVersion: string;
@ApiProperty({ description: 'reCAPTCHA Token' })
token: string;
}
export class GetLicenseSummaryRequest {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
accountId: number;
}
export class GetTypistGroupRequest {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
typistGroupId: number;
}
export class CreateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
@IsUnique()
typistIds: number[];
}
export class UpdateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
@IsUnique()
typistIds: number[];
}
export class UpdateTypistGroupRequestParam {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
typistGroupId: number;
}
export class CreatePartnerAccountRequest {
@ApiProperty()
@MaxLength(255)
companyName: string;
@ApiProperty({
description: '国名(ISO 3166-1 alpha-2)',
minLength: 2,
maxLength: 2,
})
@MinLength(2)
@MaxLength(2)
country: string;
@ApiProperty({ required: false })
@IsInt()
@IsOptional()
dealerAccountId?: number;
@ApiProperty()
@MaxLength(256) // AzureAdB2Cの仕様上、256文字まで[https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/user-profile-attributes]
adminName: string;
@ApiProperty()
@IsEmail({ blacklisted_chars: '*' })
adminMail: string;
@ApiProperty()
@IsAdminPasswordvalid()
adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
acceptedEulaVersion: string;
@ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' })
acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
acceptedDpaVersion: string;
@ApiProperty({ description: 'reCAPTCHA Token' })
token: string;
email: string;
}
export class GetPartnerLicensesRequest {
@ApiProperty()
@IsInt()
@Type(() => Number)
@Min(0)
limit: number;
@ApiProperty()
@IsInt()
@Type(() => Number)
@Min(0)
offset: number;
@ApiProperty()
@IsInt()
@Type(() => Number)
accountId: number;
}
export class GetOrderHistoriesRequest {
@ApiProperty({ description: '取得件数' })
@IsInt()
@Min(0)
@Type(() => Number)
limit: number;
@ApiProperty({ description: '開始位置' })
@IsInt()
@Min(0)
@Type(() => Number)
offset: number;
@ApiProperty({ description: 'アカウントID' })
@IsInt()
@Type(() => Number)
accountId: number;
}
export class IssueLicenseRequest {
@ApiProperty({ description: '注文元アカウントID' })
@IsInt()
@Type(() => Number)
orderedAccountId: number;
@ApiProperty({ description: 'POナンバー' })
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class CancelIssueRequest {
@ApiProperty({ description: '注文元アカウントID' })
@IsInt()
@Type(() => Number)
orderedAccountId: number;
@ApiProperty({ description: 'POナンバー' })
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' })
@MinLength(1)
@MaxLength(255)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string;
}
export class UpdateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1)
@MaxLength(255)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string;
}
export class PostWorktypeOptionItem {
@ApiProperty({ maxLength: 16 })
@MaxLength(16)
@IsRecorderAllowed()
itemLabel: string;
@ApiProperty({
maxLength: 20,
description: `${Object.values(OPTION_ITEM_VALUE_TYPE).join(' / ')}`,
})
@MaxLength(20)
@IsIn(Object.values(OPTION_ITEM_VALUE_TYPE))
defaultValueType: string;
@ApiProperty({ maxLength: 20 })
@MaxLength(20)
@IsRecorderAllowed()
@IsInitialValue()
initialValue: string;
}
export class GetOptionItemsRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class UpdateOptionItemsRequest {
@ApiProperty({
maxItems: 10,
minItems: 10,
type: [PostWorktypeOptionItem],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => PostWorktypeOptionItem)
@ArrayMinSize(10)
@ArrayMaxSize(10)
optionItems: PostWorktypeOptionItem[];
}
export class UpdateOptionItemsRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class UpdateWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class DeleteWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class PostActiveWorktypeRequest {
@ApiProperty({
required: false,
description: 'Active WorkTypeIDにするWorktypeの内部ID',
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
id?: number;
}
export class GetPartnersRequest {
@ApiProperty({ description: '取得件数' })
@IsInt()
@Min(0)
@Type(() => Number)
limit: number;
@ApiProperty({ description: '開始位置' })
@IsInt()
@Min(0)
@Type(() => Number)
offset: number;
}
export class UpdateAccountInfoRequest {
@ApiProperty({ description: '親アカウントのID', required: false })
@Type(() => Number)
@IsInt()
@IsOptional()
parentAccountId?: number;
@ApiProperty({ description: '代行操作許可' })
@Type(() => Boolean)
delegationPermission: boolean;
@ApiProperty({ description: 'プライマリ管理者ID' })
@Type(() => Number)
@IsInt()
primaryAdminUserId: number;
@ApiProperty({ description: 'セカンダリ管理者ID', required: false })
@Type(() => Number)
@IsInt()
@IsOptional()
secondryAdminUserId?: number;
}
export class DeleteAccountRequest {
@ApiProperty({ description: 'アカウントID' })
@Type(() => Number)
@IsInt()
accountId: number;
}
export class GetAccountInfoMinimalAccessRequest {
@ApiProperty({ description: 'idトークン' })
idToken: string;
}
export class GetCompanyNameRequest {
@ApiProperty()
@IsInt()
@Type(() => Number)
accountId: number;
}
// ==============================
// RESPONSE
// ==============================
export class CreateAccountResponse {}
export class GetLicenseSummaryRequest {
@ApiProperty()
accountId: number;
}
export class LicenseSummaryInfo {
totalLicense: number;
allocatedLicense: number;
reusableLicense: number;
freeLicense: number;
expiringSoonLicense: number;
issueRequesting: number;
numberOfRequesting: number;
allocatableLicenseWithMargin: number;
}
export class GetLicenseSummaryResponse {
@ApiProperty()
totalLicense: number;
@ -104,6 +389,11 @@ export class GetLicenseSummaryResponse {
isStorageAvailable: boolean;
}
export class GetCompanyNameResponse {
@ApiProperty()
companyName: string;
}
export class Account {
@ApiProperty()
accountId: number;
@ -133,11 +423,6 @@ export class Account {
parentAccountName?: string;
}
export class GetMyAccountResponse {
@ApiProperty({ type: Account })
account: Account;
}
export class Author {
@ApiProperty({ description: 'Authorユーザーの内部ID' })
id: number;
@ -145,11 +430,6 @@ export class Author {
authorId: string;
}
export class GetAuthorsResponse {
@ApiProperty({ type: [Author] })
authors: Author[];
}
export class Typist {
@ApiProperty({
description: 'TypistのユーザーID',
@ -160,11 +440,6 @@ export class Typist {
name: string;
}
export class GetTypistsResponse {
@ApiProperty({ type: [Typist] })
typists: Typist[];
}
export class TypistGroup {
@ApiProperty({
description: 'TypistGroupのID',
@ -175,18 +450,26 @@ export class TypistGroup {
name: string;
}
export class GetMyAccountResponse {
@ApiProperty({ type: Account })
account: Account;
}
export class GetAuthorsResponse {
@ApiProperty({ type: [Author] })
authors: Author[];
}
export class GetTypistsResponse {
@ApiProperty({ type: [Typist] })
typists: Typist[];
}
export class GetTypistGroupsResponse {
@ApiProperty({ type: [TypistGroup] })
typistGroups: TypistGroup[];
}
export class GetTypistGroupRequest {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
typistGroupId: number;
}
export class GetTypistGroupResponse {
@ApiProperty()
typistGroupName: string;
@ -194,72 +477,12 @@ export class GetTypistGroupResponse {
typistIds: number[];
}
export class CreateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
@IsUnique()
typistIds: number[];
}
export class CreateTypistGroupResponse {}
export class UpdateTypistGroupRequest {
@ApiProperty({ minLength: 1, maxLength: 50 })
@MinLength(1)
@MaxLength(50)
typistGroupName: string;
@ApiProperty({ minItems: 1, isArray: true, type: 'integer' })
@ArrayMinSize(1)
@IsArray()
@IsInt({ each: true })
@Min(1, { each: true })
@IsUnique()
typistIds: number[];
}
export class UpdateTypistGroupRequestParam {
@ApiProperty()
@Type(() => Number)
@IsInt()
@Min(1)
typistGroupId: number;
}
export class UpdateTypistGroupResponse {}
export class CreatePartnerAccountRequest {
@ApiProperty()
companyName: string;
@ApiProperty({
description: '国名(ISO 3166-1 alpha-2)',
minLength: 2,
maxLength: 2,
})
country: string;
@ApiProperty()
adminName: string;
@ApiProperty()
@IsEmail({ blacklisted_chars: '*' })
email: string;
}
export class CreatePartnerAccountResponse {}
export class GetPartnerLicensesRequest {
@ApiProperty()
@IsInt()
limit: number;
@ApiProperty()
@IsInt()
offset: number;
@ApiProperty()
accountId: number;
}
export class PartnerLicenseInfo {
@ApiProperty({ description: 'アカウントID' })
@ -291,7 +514,6 @@ export class PartnerLicenseInfo {
})
issueRequesting: number;
}
export class GetPartnerLicensesResponse {
@ApiProperty()
total: number;
@ -301,30 +523,6 @@ export class GetPartnerLicensesResponse {
childrenPartnerLicenses: PartnerLicenseInfo[];
}
// 第五階層のshortage算出用
export class PartnerLicenseInfoForShortage {
expiringSoonLicense?: number;
allocatableLicenseWithMargin?: number;
}
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
export type PartnerLicenseInfoForRepository = Omit<
PartnerLicenseInfo & PartnerLicenseInfoForShortage,
'shortage'
>;
export class GetOrderHistoriesRequest {
@ApiProperty({ description: '取得件数' })
@IsInt()
@Min(0)
limit: number;
@ApiProperty({ description: '開始位置' })
@IsInt()
@Min(0)
offset: number;
@ApiProperty({ description: 'アカウントID' })
accountId: number;
}
export class LicenseOrder {
@ApiProperty({ description: '注文日付' })
@ -338,6 +536,7 @@ export class LicenseOrder {
@ApiProperty({ description: '注文状態' })
status: string;
}
export class GetOrderHistoriesResponse {
@ApiProperty({ description: '合計件数' })
total: number;
@ -345,16 +544,9 @@ export class GetOrderHistoriesResponse {
orderHistories: LicenseOrder[];
}
export class IssueLicenseRequest {
@ApiProperty({ description: '注文元アカウントID' })
orderedAccountId: number;
@ApiProperty({ description: 'POナンバー' })
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class IssueLicenseResponse {}
export class Dealer {
@ApiProperty({ description: 'アカウントID' })
id: number;
@ -363,22 +555,15 @@ export class Dealer {
@ApiProperty({ description: '国名(ISO 3166-1 alpha-2)' })
country: string;
}
export class GetDealersResponse {
@ApiProperty({ type: [Dealer] })
dealers: Dealer[];
}
export class CancelIssueRequest {
@ApiProperty({ description: '注文元アカウントID' })
orderedAccountId: number;
@ApiProperty({ description: 'POナンバー' })
@Matches(/^[A-Z0-9]+$/)
poNumber: string;
}
export class CancelIssueResponse {}
export class Worktype {
@ApiProperty({ description: 'WorktypeのID' })
id: number;
@ -399,52 +584,11 @@ export class GetWorktypesResponse {
active?: number;
}
export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' })
@MinLength(1)
@MaxLength(255)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string;
}
export class CreateWorktypeResponse {}
export class UpdateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1)
@MaxLength(255)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@MaxLength(255)
@IsOptional()
description?: string;
}
export class UpdateWorktypeResponse {}
export class PostWorktypeOptionItem {
@ApiProperty({ maxLength: 16 })
@MaxLength(16)
@IsRecorderAllowed()
itemLabel: string;
@ApiProperty({
maxLength: 20,
description: `${Object.values(OPTION_ITEM_VALUE_TYPE).join(' / ')}`,
})
@MaxLength(20)
@IsIn(Object.values(OPTION_ITEM_VALUE_TYPE))
defaultValueType: string;
@ApiProperty({ maxLength: 20 })
@MaxLength(20)
@IsRecorderAllowed()
@IsInitialValue()
initialValue: string;
}
export class GetWorktypeOptionItem extends PostWorktypeOptionItem {
@ApiProperty()
id: number;
@ -459,82 +603,12 @@ export class GetOptionItemsResponse {
optionItems: GetWorktypeOptionItem[];
}
export class GetOptionItemsRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class UpdateOptionItemsRequest {
@ApiProperty({
maxItems: 10,
minItems: 10,
type: [PostWorktypeOptionItem],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => PostWorktypeOptionItem)
@ArrayMinSize(10)
@ArrayMaxSize(10)
optionItems: PostWorktypeOptionItem[];
}
export class UpdateOptionItemsResponse {}
export class UpdateOptionItemsRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class UpdateWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class DeleteWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(1)
id: number;
}
export class DeleteWorktypeResponse {}
export class PostActiveWorktypeRequest {
@ApiProperty({
required: false,
description: 'Active WorkTypeIDにするWorktypeの内部ID',
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
id?: number;
}
export class PostActiveWorktypeResponse {}
export class GetPartnersRequest {
@ApiProperty({ description: '取得件数' })
@IsInt()
@Min(0)
@Type(() => Number)
limit: number;
@ApiProperty({ description: '開始位置' })
@IsInt()
@Min(0)
@Type(() => Number)
offset: number;
}
export class Partner {
@ApiProperty({ description: '会社名' })
@ -560,6 +634,42 @@ export class GetPartnersResponse {
partners: Partner[];
}
export class UpdateAccountInfoResponse {}
export class DeleteAccountResponse {}
export class GetAccountInfoMinimalAccessResponse {
@ApiProperty({ description: '階層' })
tier: number;
}
// ==============================
// Request/Response外の型
// TODO: Request/Response/その他の型を別ファイルに分ける
// ==============================
export class LicenseSummaryInfo {
totalLicense: number;
allocatedLicense: number;
reusableLicense: number;
freeLicense: number;
expiringSoonLicense: number;
issueRequesting: number;
numberOfRequesting: number;
allocatableLicenseWithMargin: number;
}
// 第五階層のshortage算出用
export class PartnerLicenseInfoForShortage {
expiringSoonLicense?: number;
allocatableLicenseWithMargin?: number;
}
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
export type PartnerLicenseInfoForRepository = Omit<
PartnerLicenseInfo & PartnerLicenseInfoForShortage,
'shortage'
>;
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
export type PartnerInfoFromDb = {
name: string;
@ -568,46 +678,4 @@ export type PartnerInfoFromDb = {
country: string;
primaryAccountExternalId: string;
dealerManagement: boolean;
};
export class UpdateAccountInfoRequest {
@ApiProperty({ description: '親アカウントのID', required: false })
@IsOptional()
parentAccountId?: number;
@ApiProperty({ description: '代行操作許可' })
delegationPermission: boolean;
@ApiProperty({ description: 'プライマリ管理者ID' })
primaryAdminUserId: number;
@ApiProperty({ description: 'セカンダリ管理者ID', required: false })
@IsOptional()
secondryAdminUserId?: number;
}
export class UpdateAccountInfoResponse {}
export class DeleteAccountRequest {
@ApiProperty({ description: 'アカウントID' })
accountId: number;
}
export class DeleteAccountResponse {}
export class GetAccountInfoMinimalAccessRequest {
@ApiProperty({ description: 'idトークン' })
idToken: string;
}
export class GetAccountInfoMinimalAccessResponse {
@ApiProperty({ description: '階層' })
tier: number;
}
export class GetCompanyNameRequest {
@ApiProperty()
@IsInt()
@Type(() => Number)
accountId: number;
}
export class GetCompanyNameResponse {
@ApiProperty()
companyName: string;
}
};

View File

@ -31,11 +31,6 @@ import {
LICENSE_ISSUE_STATUS,
TIERS,
} from '../../constants';
import {
LicenseSummaryInfo,
PartnerLicenseInfoForRepository,
PartnerInfoFromDb,
} from '../../features/accounts/types/types';
import {
AccountNotFoundError,
AdminUserNotFoundError,
@ -64,6 +59,7 @@ import {
deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';
import { LicenseSummaryInfo, PartnerInfoFromDb, PartnerLicenseInfoForRepository } from '../../features/accounts/types/types';
@Injectable()
export class AccountsRepositoryService {