Merged PR 314: ユーザー確認API修正

## 概要
[Task2351: ユーザー確認API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2351)

- アカウント登録後のユーザー検証時に、トライアルライセンスを作成する処理を追加
- テスト修正

## レビューポイント
- トライアルライセンスの有効期限の設定はこれでよいか
- トライアルライセンスの登録内容は正しいか
- テストケースは足りているか、確認するパラメータに不足はないか

## 動作確認状況
- ローカルで確認

## 補足
- `npm run format`で複数ファイルが更新されていますが、修正を加えたのは以下のファイルです。
  - /dictation_server/src/constants/index.ts
  - /dictation_server/src/features/users配下のファイル
  - /dictation_server/src/repositories/users/users.repository.service.ts
This commit is contained in:
saito.k 2023-08-16 02:13:10 +00:00
parent 1145debabf
commit 86d11e7447
6 changed files with 286 additions and 133 deletions

View File

@ -204,3 +204,15 @@ export const USER_LICENSE_STATUS = {
ALERT: 'Alert',
RENEW: 'Renew',
};
/**
*
* @const {number}
*/
export const TRIAL_LICENSE_EXPIRATION_DAYS = 30;
/**
*
* @const {number}
*/
export const TRIAL_LICENSE_ISSUE_NUM = 100;

View File

@ -68,6 +68,7 @@ export const createUser = async (
encryption?: boolean | undefined,
encryption_password?: string | undefined,
prompt?: boolean | undefined,
email_verified?: boolean | undefined,
): Promise<{ userId: number; externalId: string }> => {
const { identifiers } = await datasource.getRepository(User).insert({
account_id: accountId,
@ -75,7 +76,7 @@ export const createUser = async (
role: role,
accepted_terms_version: '1.0',
author_id: author_id,
email_verified: true,
email_verified: email_verified ?? true,
auto_renew: auto_renew,
license_alert: true,
notification: true,
@ -103,6 +104,18 @@ export const getUser = async (
return user;
};
export const getLicenses = async (
datasource: DataSource,
account_id: number,
): Promise<License[]> => {
const licenses = await datasource.getRepository(License).find({
where: {
account_id: account_id,
},
});
return licenses;
};
/**
*
* @param datasource

View File

@ -15,40 +15,166 @@ import {
createLicense,
createUser,
createUserGroup,
getLicenses,
getUser,
makeTestingModuleWithAdb2c,
} from './test/utility';
import { DataSource } from 'typeorm';
import { UsersService } from './users.service';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_TYPE,
TRIAL_LICENSE_EXPIRATION_DAYS,
USER_LICENSE_STATUS,
USER_ROLES,
} from '../../constants';
import { makeTestingModule } from '../../common/test/modules';
import { ExpirationThresholdDate } from '../licenses/types/types';
import { License } from '../../repositories/licenses/entity/license.entity';
describe('UsersService.confirmUser', () => {
it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendGridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
expect(await service.confirmUser(token)).toEqual(undefined);
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
await source.destroy();
source = null;
});
it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになり、トライアルライセンスが100件作成される', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
await createUser(
source,
accountId,
'externalId_user1',
USER_ROLES.NONE,
undefined,
true,
false,
undefined,
false,
false,
);
const { userId } = await createUser(
source,
accountId,
'externalId_user2',
USER_ROLES.NONE,
undefined,
true,
false,
undefined,
false,
false,
);
const service = module.get<UsersService>(UsersService);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await service.confirmUser(token);
//result
const resultUser = await getUser(source, userId);
const resultLicenses = await getLicenses(source, accountId);
const today = new Date();
// トライアルライセンスは有効期限は今日を起算日として30日後の日付が変わるまで
const expiryDate = new ExpirationThresholdDate(
today.setDate(today.getDate() + TRIAL_LICENSE_EXPIRATION_DAYS),
);
const resultLicensesCheckParam: Omit<
License,
'deleted_at' | 'created_by' | 'created_at' | 'updated_at' | 'updated_by'
> = {
id: 0,
expiry_date: resultLicenses[0].expiry_date,
account_id: resultLicenses[0].account_id,
type: resultLicenses[0].type,
status: resultLicenses[0].status,
allocated_user_id: resultLicenses[0].allocated_user_id,
order_id: resultLicenses[0].order_id,
delete_order_id: resultLicenses[0].delete_order_id,
};
expect(resultUser.email_verified).toBe(true);
expect(resultLicenses.length).toBe(100);
expect(resultLicensesCheckParam).toEqual({
id: 0,
expiry_date: expiryDate,
account_id: accountId,
type: LICENSE_TYPE.TRIAL,
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
allocated_user_id: null,
order_id: null,
delete_order_id: null,
});
});
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
const module = await makeTestingModule(source);
const token = 'invalid.id.token';
const service = module.get<UsersService>(UsersService);
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
);
});
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
await createUser(
source,
accountId,
'externalId_user1',
USER_ROLES.NONE,
undefined,
true,
false,
undefined,
false,
false,
);
await createUser(
source,
accountId,
'externalId_user2',
USER_ROLES.NONE,
undefined,
true,
false,
undefined,
false,
true, //emailを認証済みにする
);
const service = module.get<UsersService>(UsersService);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
);
});
it('ユーザーが存在しない場合は、想定外のエラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<UsersService>(UsersService);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});
describe('UsersService.confirmUserAndInitPassword', () => {
it('ユーザーが発行されたパスワードでログインできるようにする', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
@ -88,28 +214,6 @@ describe('UsersService.confirmUser', () => {
expect(await service.confirmUserAndInitPassword(token)).toEqual(undefined);
});
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const token = 'invalid.id.token';
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
);
});
it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
@ -148,32 +252,6 @@ describe('UsersService.confirmUser', () => {
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
);
});
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
usersRepositoryMockValue.updateUserVerified =
new EmailAlreadyVerifiedError();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
);
});
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
@ -216,33 +294,6 @@ describe('UsersService.confirmUser', () => {
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
);
});
it('DBネットワークエラーとなる場合、エラーとなる。', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
usersRepositoryMockValue.updateUserVerified = new Error('DB error');
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(service.confirmUser(token)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('DBネットワークエラーとなる場合、エラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
@ -286,41 +337,6 @@ describe('UsersService.confirmUser', () => {
),
);
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const name = 'test_user1';
const role = USER_ROLES.NONE;
const email = 'test1@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
expect(
await service.createUser(
token,
name,
role,
email,
autoRenew,
licenseAlert,
notification,
),
).toEqual(undefined);
});
});
describe('UsersService.createUser', () => {
@ -448,6 +464,43 @@ describe('UsersService.createUser', () => {
),
).toEqual(undefined);
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const name = 'test_user1';
const role = USER_ROLES.NONE;
const email = 'test1@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
expect(
await service.createUser(
token,
name,
role,
email,
autoRenew,
licenseAlert,
notification,
),
).toEqual(undefined);
});
it('DBネットワークエラーとなる場合、エラーとなる。', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const licensesRepositoryMockValue = null;
@ -1614,7 +1667,7 @@ describe('UsersService.updateUser', () => {
true,
);
const { userId: user2 } = await createUser(
await createUser(
source,
accountId,
'external_id2',

View File

@ -80,7 +80,9 @@ export class UsersService {
try {
// トランザクションで取得と更新をまとめる
const userId = decodedToken.userId;
await this.usersRepository.updateUserVerified(userId);
await this.usersRepository.updateUserVerifiedAndCreateTrialLicense(
userId,
);
} catch (e) {
this.logger.error(e);
if (e instanceof Error) {

View File

@ -97,6 +97,7 @@ export class LicensesRepositoryService {
entityManager.getRepository(CardLicenseIssue);
const licenses = [];
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
// ライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < count; i++) {
const license = new License();
@ -153,6 +154,7 @@ export class LicensesRepositoryService {
}
const cardLicenses = [];
//TODO タスク 2409: カードライセンスのレコード作成がbulkinsertになっていない
// カードライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < count; i++) {
const cardLicense = new CardLicense();

View File

@ -13,7 +13,15 @@ import {
InvalidRoleChangeError,
EncryptionPasswordNeedError,
} from './errors/types';
import { USER_ROLES } from '../../constants';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_TYPE,
TRIAL_LICENSE_EXPIRATION_DAYS,
TRIAL_LICENSE_ISSUE_NUM,
USER_ROLES,
} from '../../constants';
import { License } from '../licenses/entity/license.entity';
import { ExpirationThresholdDate } from '../../features/licenses/types/types';
@Injectable()
export class UsersRepositoryService {
@ -285,6 +293,69 @@ export class UsersRepositoryService {
});
}
/**
* Emailを認証済みにして
* @param id
* @returns user verified and create trial license
*/
async updateUserVerifiedAndCreateTrialLicense(id: number): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
const targetUser = await userRepo.findOne({
relations: {
account: true,
},
where: {
id: id,
},
});
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!targetUser) {
throw new UserNotFoundError();
}
if (targetUser.email_verified) {
throw new EmailAlreadyVerifiedError();
}
targetUser.email_verified = true;
await userRepo.update({ id: targetUser.id }, targetUser);
// トライアルライセンス100件を作成する
const licenseRepo = entityManager.getRepository(License);
const licenses: License[] = [];
const today = new Date();
// トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
const expiryDate = new ExpirationThresholdDate(
today.setDate(today.getDate() + TRIAL_LICENSE_EXPIRATION_DAYS),
);
for (let i = 0; i < TRIAL_LICENSE_ISSUE_NUM; i++) {
const license = new License();
license.expiry_date = expiryDate;
license.account_id = targetUser.account.id;
license.type = LICENSE_TYPE.TRIAL;
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
licenses.push(license);
}
await licenseRepo
.createQueryBuilder()
.insert()
.into(License)
.values(
licenses.map((value) => ({
expiry_date: value.expiry_date,
account_id: value.account_id,
type: value.type,
status: value.status,
})),
)
.execute();
});
}
/**
* IDを持つユーザーを探す
* @param externalId