## 概要 [Task2906: API実装(代行操作用トークン更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2906) - アクセストークン更新APIとテストを実装しました。 ## レビューポイント - リポジトリのアカウントチェックは適切か - テストケースは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
634 lines
20 KiB
TypeScript
634 lines
20 KiB
TypeScript
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
|
||
import {
|
||
OPTION_ITEM_NUM,
|
||
TASK_STATUS,
|
||
USER_ROLES,
|
||
} from '../../constants/index';
|
||
import { User } from '../../repositories/users/entity/user.entity';
|
||
import {
|
||
AudioFileNotFoundError,
|
||
AuthorUserNotMatchError,
|
||
TemplateFileNotFoundError,
|
||
} from './errors/types';
|
||
import {
|
||
AccountNotMatchError,
|
||
StatusNotMatchError,
|
||
TasksNotFoundError,
|
||
TypistUserNotFoundError,
|
||
} from '../../repositories/tasks/errors/types';
|
||
import { Context } from '../../common/log';
|
||
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
|
||
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
|
||
|
||
@Injectable()
|
||
export class FilesService {
|
||
private readonly logger = new Logger(FilesService.name);
|
||
constructor(
|
||
private readonly usersRepository: UsersRepositoryService,
|
||
private readonly tasksRepository: TasksRepositoryService,
|
||
private readonly tasksRepositoryService: TasksRepositoryService,
|
||
private readonly templateFilesRepository: TemplateFilesRepositoryService,
|
||
private readonly blobStorageService: BlobstorageService,
|
||
) {}
|
||
|
||
/**
|
||
* Uploads finished
|
||
* @param url アップロード先Blob Storage(ファイル名含む)
|
||
* @param authorId 自分自身(ログイン認証)したAuthorID
|
||
* @param fileName 音声ファイル名
|
||
* @param duration 音声ファイルの録音時間(ミリ秒の整数値)
|
||
* @param createdDate 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)'
|
||
* @param finishedDate 音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss)
|
||
* @param uploadedDate 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)
|
||
* @param fileSize 音声ファイルのファイルサイズ(Byte)
|
||
* @param priority 優先度 "00":Normal / "01":High
|
||
* @param audioFormat 録音形式: DSS/DS2(SP)/DS2(QP)
|
||
* @param comment コメント
|
||
* @param workType WorkType
|
||
* @param optionItemList オプションアイテム(音声メタデータ)10個固定
|
||
* @param isEncrypted 暗号化されているか
|
||
* @returns finished
|
||
*/
|
||
async uploadFinished(
|
||
context: Context,
|
||
userId: string,
|
||
url: string,
|
||
authorId: string,
|
||
fileName: string,
|
||
duration: string,
|
||
createdDate: string,
|
||
finishedDate: string,
|
||
uploadedDate: string,
|
||
fileSize: number,
|
||
priority: string,
|
||
audioFormat: string,
|
||
comment: string,
|
||
workType: string,
|
||
optionItemList: AudioOptionItem[],
|
||
isEncrypted: boolean,
|
||
): Promise<AudioUploadFinishedResponse> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.uploadFinished.name} | params: { ` +
|
||
`url: ${url}, ` +
|
||
`authorId: ${authorId}, ` +
|
||
`fileName: ${fileName}, ` +
|
||
`duration: ${duration}, ` +
|
||
`createdDate: ${createdDate}, ` +
|
||
`finishedDate: ${finishedDate}, ` +
|
||
`uploadedDate: ${uploadedDate}, ` +
|
||
`fileSize: ${fileSize}, ` +
|
||
`priority: ${priority}, ` +
|
||
`audioFormat: ${audioFormat}, ` +
|
||
`comment: ${comment}, ` +
|
||
`workType: ${workType}, ` +
|
||
`optionItemList: ${JSON.stringify(optionItemList)}, ` +
|
||
`isEncrypted: ${isEncrypted} };`,
|
||
);
|
||
|
||
const formattedCreatedDate = new Date(createdDate);
|
||
const formattedFinishedDate = new Date(finishedDate);
|
||
const formattedUploadedDate = new Date(uploadedDate);
|
||
|
||
const isInvalidCreatedDate = isNaN(formattedCreatedDate.getTime());
|
||
const isInvalidFinishedDate = isNaN(formattedFinishedDate.getTime());
|
||
const isInvalidUploadedDate = isNaN(formattedUploadedDate.getTime());
|
||
|
||
// 日付フォーマットが不正ならパラメータ不正
|
||
if (
|
||
isInvalidCreatedDate ||
|
||
isInvalidFinishedDate ||
|
||
isInvalidUploadedDate
|
||
) {
|
||
if (isInvalidCreatedDate) {
|
||
this.logger.error(
|
||
`param createdDate is invalid format:[createdDate=${createdDate}]`,
|
||
);
|
||
}
|
||
if (isInvalidFinishedDate) {
|
||
this.logger.error(
|
||
`param finishedDate is invalid format:[finishedDate=${finishedDate}]`,
|
||
);
|
||
}
|
||
if (isInvalidUploadedDate) {
|
||
this.logger.error(
|
||
`param uploadedDate is invalid format:[uploadedDate=${uploadedDate}]`,
|
||
);
|
||
}
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
|
||
);
|
||
|
||
throw new HttpException(
|
||
makeErrorResponse('E010001'),
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
}
|
||
|
||
// オプションアイテムが10個ない場合はパラメータ不正
|
||
if (optionItemList.length !== OPTION_ITEM_NUM) {
|
||
this.logger.error(
|
||
`param optionItemList expects ${OPTION_ITEM_NUM} items, but has ${optionItemList.length} items`,
|
||
);
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
|
||
);
|
||
throw new HttpException(
|
||
makeErrorResponse('E010001'),
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
}
|
||
|
||
let user: User;
|
||
try {
|
||
// ユーザー取得
|
||
user = await this.usersRepository.findUserByExternalId(userId);
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
|
||
);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
|
||
try {
|
||
// URLにSASトークンがついている場合は取り除く
|
||
const urlObj = new URL(url);
|
||
urlObj.search = '';
|
||
const fileUrl = urlObj.toString();
|
||
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
|
||
|
||
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
|
||
// 追加時に末尾のJOBナンバーにインクリメントする
|
||
const task = await this.tasksRepositoryService.create(
|
||
user.account_id,
|
||
user.id,
|
||
priority,
|
||
fileUrl,
|
||
fileName,
|
||
authorId,
|
||
workType,
|
||
formattedCreatedDate,
|
||
duration,
|
||
formattedFinishedDate,
|
||
formattedUploadedDate,
|
||
fileSize,
|
||
audioFormat,
|
||
comment,
|
||
isEncrypted,
|
||
optionItemList,
|
||
);
|
||
return { jobNumber: task.job_number };
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
} finally {
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.uploadFinished.name}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Publishs upload sas
|
||
* @param companyName
|
||
* @returns upload sas
|
||
*/
|
||
async publishUploadSas(
|
||
context: Context,
|
||
externalId: string,
|
||
): Promise<string> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.publishUploadSas.name} | params: { externalId: ${externalId} };`,
|
||
);
|
||
|
||
//DBから国情報とアカウントIDを取得する
|
||
try {
|
||
const user = await this.usersRepository.findUserByExternalId(externalId);
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError('account not found.');
|
||
}
|
||
const accountId = user.account_id;
|
||
const country = user.account.country;
|
||
|
||
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
|
||
await this.blobStorageService.containerExists(
|
||
context,
|
||
accountId,
|
||
country,
|
||
);
|
||
|
||
// SASトークン発行
|
||
const url = await this.blobStorageService.publishUploadSas(
|
||
context,
|
||
accountId,
|
||
country,
|
||
);
|
||
return url;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
} finally {
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.publishUploadSas.name}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 指定したIDの音声ファイルのダウンロードURLを取得する
|
||
* @param externalId
|
||
* @param audioFileId
|
||
* @returns audio file download sas
|
||
*/
|
||
async publishAudioFileDownloadSas(
|
||
context: Context,
|
||
externalId: string,
|
||
audioFileId: number,
|
||
): Promise<string> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
|
||
);
|
||
|
||
//DBから国情報とアカウントID,ユーザーIDを取得する
|
||
let accountId: number;
|
||
let userId: number;
|
||
let country: string;
|
||
let isTypist: boolean;
|
||
let authorId: string | undefined;
|
||
try {
|
||
const user = await this.usersRepository.findUserByExternalId(externalId);
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError('account not found.');
|
||
}
|
||
accountId = user.account.id;
|
||
userId = user.id;
|
||
country = user.account.country;
|
||
isTypist = user.role === USER_ROLES.TYPIST;
|
||
authorId = user.author_id ?? undefined;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name}`,
|
||
);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
|
||
try {
|
||
const status = isTypist
|
||
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
|
||
: Object.values(TASK_STATUS);
|
||
|
||
const task = await this.tasksRepository.getTaskAndAudioFile(
|
||
audioFileId,
|
||
accountId,
|
||
status,
|
||
);
|
||
const { file } = task;
|
||
|
||
// タスクに紐づく音声ファイルだけが消される場合がある。
|
||
// その場合はダウンロード不可なので不在エラーとして扱う
|
||
if (!file) {
|
||
throw new AudioFileNotFoundError(
|
||
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
|
||
);
|
||
}
|
||
|
||
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
|
||
if (!isTypist && file.author_id !== authorId) {
|
||
throw new AuthorUserNotMatchError(
|
||
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
|
||
);
|
||
}
|
||
|
||
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
|
||
if (isTypist && task.typist_user_id !== userId) {
|
||
throw new AuthorUserNotMatchError(
|
||
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
|
||
);
|
||
}
|
||
|
||
const filePath = `${file.file_name}`;
|
||
|
||
const isFileExist = await this.blobStorageService.fileExists(
|
||
context,
|
||
accountId,
|
||
country,
|
||
filePath,
|
||
);
|
||
|
||
if (!isFileExist) {
|
||
this.logger.log(`filePath:${filePath}`);
|
||
throw new AudioFileNotFoundError(
|
||
`Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`,
|
||
);
|
||
}
|
||
|
||
// SASトークン発行
|
||
const url = await this.blobStorageService.publishDownloadSas(
|
||
context,
|
||
accountId,
|
||
country,
|
||
filePath,
|
||
);
|
||
return url;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
if (e instanceof Error) {
|
||
switch (e.constructor) {
|
||
case TasksNotFoundError:
|
||
case AccountNotMatchError:
|
||
case StatusNotMatchError:
|
||
case AuthorUserNotMatchError:
|
||
case TypistUserNotFoundError:
|
||
throw new HttpException(
|
||
makeErrorResponse('E010603'),
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
case AudioFileNotFoundError:
|
||
throw new HttpException(
|
||
makeErrorResponse('E010701'),
|
||
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.publishAudioFileDownloadSas.name}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 指定したIDの音声ファイルに紐づいた文字起こしテンプレートファイルのダウンロードURLを取得する
|
||
* @param externalId
|
||
* @param audioFileId
|
||
* @returns template file download sas
|
||
*/
|
||
async publishTemplateFileDownloadSas(
|
||
context: Context,
|
||
externalId: string,
|
||
audioFileId: number,
|
||
): Promise<string> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
|
||
);
|
||
|
||
//DBから国情報とアカウントID,ユーザーIDを取得する
|
||
let accountId: number;
|
||
let userId: number;
|
||
let country: string;
|
||
let isTypist: boolean;
|
||
let authorId: string | undefined;
|
||
try {
|
||
const user = await this.usersRepository.findUserByExternalId(externalId);
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError('account not found.');
|
||
}
|
||
accountId = user.account_id;
|
||
userId = user.id;
|
||
country = user.account.country;
|
||
isTypist = user.role === USER_ROLES.TYPIST;
|
||
authorId = user.author_id ?? undefined;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
|
||
);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
|
||
try {
|
||
const status = isTypist
|
||
? [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
|
||
: Object.values(TASK_STATUS);
|
||
|
||
const task = await this.tasksRepository.getTaskAndAudioFile(
|
||
audioFileId,
|
||
accountId,
|
||
status,
|
||
);
|
||
const { file } = task;
|
||
|
||
// タスクに紐づく音声ファイルだけが消される場合がある。
|
||
// その場合はダウンロード不可なので不在エラーとして扱う
|
||
if (!file) {
|
||
throw new AudioFileNotFoundError(
|
||
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
|
||
);
|
||
}
|
||
|
||
const template_file = task.template_file;
|
||
|
||
// タスクに紐づくテンプレートファイルがない場合がある。
|
||
// その場合はダウンロード不可なので不在エラーとして扱う
|
||
if (!template_file) {
|
||
throw new TemplateFileNotFoundError(
|
||
`Template file is not exists in DB. audio_file_id:${audioFileId}`,
|
||
);
|
||
}
|
||
|
||
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
|
||
if (!isTypist && file.author_id !== authorId) {
|
||
throw new AuthorUserNotMatchError(
|
||
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
|
||
);
|
||
}
|
||
|
||
// ユーザーがTypistの場合、自身が担当したタスクでない場合はエラー
|
||
if (isTypist && task.typist_user_id !== userId) {
|
||
throw new AuthorUserNotMatchError(
|
||
`task typist is not match. audio_file_id:${audioFileId}, task.typist_user_id:${task.typist_user_id}, userId:${userId}`,
|
||
);
|
||
}
|
||
|
||
const filePath = `Templates/${template_file.file_name}`;
|
||
|
||
const isFileExist = await this.blobStorageService.fileExists(
|
||
context,
|
||
accountId,
|
||
country,
|
||
filePath,
|
||
);
|
||
|
||
if (!isFileExist) {
|
||
throw new TemplateFileNotFoundError(
|
||
`Template file is not exists in blob storage. audio_file_id:${audioFileId}, url:${template_file.url}, fileName:${template_file.file_name}`,
|
||
);
|
||
}
|
||
|
||
// SASトークン発行
|
||
const url = await this.blobStorageService.publishDownloadSas(
|
||
context,
|
||
accountId,
|
||
country,
|
||
filePath,
|
||
);
|
||
return url;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
if (e instanceof Error) {
|
||
switch (e.constructor) {
|
||
case TasksNotFoundError:
|
||
case AccountNotMatchError:
|
||
case StatusNotMatchError:
|
||
case AuthorUserNotMatchError:
|
||
case TypistUserNotFoundError:
|
||
throw new HttpException(
|
||
makeErrorResponse('E010603'),
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
case AudioFileNotFoundError:
|
||
case TemplateFileNotFoundError:
|
||
throw new HttpException(
|
||
makeErrorResponse('E010701'),
|
||
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.publishTemplateFileDownloadSas.name}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ログインユーザーアカウントのテンプレートファイルのアップロードURLを取得する
|
||
* @param context
|
||
* @param externalId
|
||
* @returns template file upload sas
|
||
*/
|
||
async publishTemplateFileUploadSas(
|
||
context: Context,
|
||
externalId: string,
|
||
): Promise<string> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
|
||
);
|
||
try {
|
||
const { account } = await this.usersRepository.findUserByExternalId(
|
||
externalId,
|
||
);
|
||
if (!account) {
|
||
throw new AccountNotFoundError('account not found.');
|
||
}
|
||
|
||
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
|
||
const isContainerExists = await this.blobStorageService.containerExists(
|
||
context,
|
||
account.id,
|
||
account.country,
|
||
);
|
||
if (!isContainerExists) {
|
||
throw new Error('container not found.');
|
||
}
|
||
|
||
// SASトークン発行
|
||
const url = await this.blobStorageService.publishTemplateUploadSas(
|
||
context,
|
||
account.id,
|
||
account.country,
|
||
);
|
||
|
||
return url;
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
} finally {
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* テンプレートファイルのアップロード後にDBにテンプレートファイル情報を登録する
|
||
* @param context
|
||
* @param externalId
|
||
* @param url
|
||
* @param fileName
|
||
* @returns upload finished
|
||
*/
|
||
async templateUploadFinished(
|
||
context: Context,
|
||
externalId: string,
|
||
url: string,
|
||
fileName: string,
|
||
): Promise<void> {
|
||
this.logger.log(
|
||
`[IN] [${context.trackingId}] ${this.templateUploadFinished.name} | params: { externalId: ${externalId}, url: ${url}, fileName: ${fileName} };`,
|
||
);
|
||
|
||
try {
|
||
// ユーザー取得
|
||
const { account_id: accountId } =
|
||
await this.usersRepository.findUserByExternalId(externalId);
|
||
|
||
// URLにSASトークンがついている場合は取り除く;
|
||
const urlObj = new URL(url);
|
||
urlObj.search = '';
|
||
const fileUrl = urlObj.toString();
|
||
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
|
||
|
||
// テンプレートファイル情報をDBに登録
|
||
await this.templateFilesRepository.upsertTemplateFile(
|
||
accountId,
|
||
fileName,
|
||
fileUrl,
|
||
);
|
||
} catch (e) {
|
||
this.logger.error(`error=${e}`);
|
||
throw new HttpException(
|
||
makeErrorResponse('E009999'),
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
} finally {
|
||
this.logger.log(
|
||
`[OUT] [${context.trackingId}] ${this.templateUploadFinished.name}`,
|
||
);
|
||
}
|
||
}
|
||
}
|