576 lines
15 KiB
TypeScript
576 lines
15 KiB
TypeScript
|
|
import { PrismaClient } from "@prisma/client";
|
||
|
|
import {
|
||
|
|
DataMappingConfig,
|
||
|
|
InboundMapping,
|
||
|
|
OutboundMapping,
|
||
|
|
FieldMapping,
|
||
|
|
DataMappingResult,
|
||
|
|
MappingValidationResult,
|
||
|
|
FieldTransform,
|
||
|
|
DataType,
|
||
|
|
} from "../types/dataMappingTypes";
|
||
|
|
|
||
|
|
export class DataMappingService {
|
||
|
|
private prisma: PrismaClient;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.prisma = new PrismaClient();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Inbound 데이터 매핑 처리 (외부 → 내부)
|
||
|
|
*/
|
||
|
|
async processInboundData(
|
||
|
|
externalData: any,
|
||
|
|
mapping: InboundMapping
|
||
|
|
): Promise<DataMappingResult> {
|
||
|
|
const startTime = Date.now();
|
||
|
|
const result: DataMappingResult = {
|
||
|
|
success: false,
|
||
|
|
direction: "inbound",
|
||
|
|
recordsProcessed: 0,
|
||
|
|
recordsInserted: 0,
|
||
|
|
recordsUpdated: 0,
|
||
|
|
recordsSkipped: 0,
|
||
|
|
errors: [],
|
||
|
|
executionTime: 0,
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
console.log(`📥 [DataMappingService] Inbound 매핑 시작:`, {
|
||
|
|
targetTable: mapping.targetTable,
|
||
|
|
insertMode: mapping.insertMode,
|
||
|
|
fieldMappings: mapping.fieldMappings.length,
|
||
|
|
});
|
||
|
|
|
||
|
|
// 데이터 배열로 변환
|
||
|
|
const dataArray = Array.isArray(externalData)
|
||
|
|
? externalData
|
||
|
|
: [externalData];
|
||
|
|
result.recordsProcessed = dataArray.length;
|
||
|
|
|
||
|
|
// 각 레코드 처리
|
||
|
|
for (const record of dataArray) {
|
||
|
|
try {
|
||
|
|
const mappedData = await this.mapInboundRecord(record, mapping);
|
||
|
|
|
||
|
|
if (Object.keys(mappedData).length === 0) {
|
||
|
|
result.recordsSkipped!++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터베이스에 저장
|
||
|
|
await this.saveInboundRecord(mappedData, mapping);
|
||
|
|
|
||
|
|
if (mapping.insertMode === "insert") {
|
||
|
|
result.recordsInserted!++;
|
||
|
|
} else {
|
||
|
|
result.recordsUpdated!++;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ [DataMappingService] 레코드 처리 실패:`, error);
|
||
|
|
result.errors!.push(
|
||
|
|
`레코드 처리 실패: ${error instanceof Error ? error.message : String(error)}`
|
||
|
|
);
|
||
|
|
result.recordsSkipped!++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
result.success =
|
||
|
|
result.errors!.length === 0 ||
|
||
|
|
result.recordsInserted! > 0 ||
|
||
|
|
result.recordsUpdated! > 0;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ [DataMappingService] Inbound 매핑 실패:`, error);
|
||
|
|
result.errors!.push(
|
||
|
|
`매핑 처리 실패: ${error instanceof Error ? error.message : String(error)}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
result.executionTime = Date.now() - startTime;
|
||
|
|
|
||
|
|
console.log(`✅ [DataMappingService] Inbound 매핑 완료:`, result);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Outbound 데이터 매핑 처리 (내부 → 외부)
|
||
|
|
*/
|
||
|
|
async processOutboundData(
|
||
|
|
mapping: OutboundMapping,
|
||
|
|
filter?: any
|
||
|
|
): Promise<any> {
|
||
|
|
console.log(`📤 [DataMappingService] Outbound 매핑 시작:`, {
|
||
|
|
sourceTable: mapping.sourceTable,
|
||
|
|
fieldMappings: mapping.fieldMappings.length,
|
||
|
|
filter,
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 소스 데이터 조회
|
||
|
|
const sourceData = await this.getSourceData(mapping, filter);
|
||
|
|
|
||
|
|
if (
|
||
|
|
!sourceData ||
|
||
|
|
(Array.isArray(sourceData) && sourceData.length === 0)
|
||
|
|
) {
|
||
|
|
console.log(`⚠️ [DataMappingService] 소스 데이터가 없습니다.`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 매핑
|
||
|
|
const mappedData = Array.isArray(sourceData)
|
||
|
|
? await Promise.all(
|
||
|
|
sourceData.map((record) => this.mapOutboundRecord(record, mapping))
|
||
|
|
)
|
||
|
|
: await this.mapOutboundRecord(sourceData, mapping);
|
||
|
|
|
||
|
|
console.log(`✅ [DataMappingService] Outbound 매핑 완료:`, {
|
||
|
|
recordCount: Array.isArray(mappedData) ? mappedData.length : 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
return mappedData;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ [DataMappingService] Outbound 매핑 실패:`, error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 단일 Inbound 레코드 매핑
|
||
|
|
*/
|
||
|
|
private async mapInboundRecord(
|
||
|
|
sourceRecord: any,
|
||
|
|
mapping: InboundMapping
|
||
|
|
): Promise<Record<string, any>> {
|
||
|
|
const mappedRecord: Record<string, any> = {};
|
||
|
|
|
||
|
|
for (const fieldMapping of mapping.fieldMappings) {
|
||
|
|
try {
|
||
|
|
const sourceValue = sourceRecord[fieldMapping.sourceField];
|
||
|
|
|
||
|
|
// 필수 필드 체크
|
||
|
|
if (
|
||
|
|
fieldMapping.required &&
|
||
|
|
(sourceValue === undefined || sourceValue === null)
|
||
|
|
) {
|
||
|
|
if (fieldMapping.defaultValue !== undefined) {
|
||
|
|
mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue;
|
||
|
|
} else {
|
||
|
|
throw new Error(
|
||
|
|
`필수 필드 '${fieldMapping.sourceField}'가 누락되었습니다.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 값이 없으면 기본값 사용
|
||
|
|
if (sourceValue === undefined || sourceValue === null) {
|
||
|
|
if (fieldMapping.defaultValue !== undefined) {
|
||
|
|
mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 변환 적용
|
||
|
|
const transformedValue = await this.transformFieldValue(
|
||
|
|
sourceValue,
|
||
|
|
fieldMapping.dataType,
|
||
|
|
fieldMapping.transform
|
||
|
|
);
|
||
|
|
|
||
|
|
mappedRecord[fieldMapping.targetField] = transformedValue;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(
|
||
|
|
`❌ [DataMappingService] 필드 매핑 실패 (${fieldMapping.sourceField} → ${fieldMapping.targetField}):`,
|
||
|
|
error
|
||
|
|
);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return mappedRecord;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 단일 Outbound 레코드 매핑
|
||
|
|
*/
|
||
|
|
private async mapOutboundRecord(
|
||
|
|
sourceRecord: any,
|
||
|
|
mapping: OutboundMapping
|
||
|
|
): Promise<Record<string, any>> {
|
||
|
|
const mappedRecord: Record<string, any> = {};
|
||
|
|
|
||
|
|
for (const fieldMapping of mapping.fieldMappings) {
|
||
|
|
try {
|
||
|
|
const sourceValue = sourceRecord[fieldMapping.sourceField];
|
||
|
|
|
||
|
|
// 데이터 변환 적용
|
||
|
|
const transformedValue = await this.transformFieldValue(
|
||
|
|
sourceValue,
|
||
|
|
fieldMapping.dataType,
|
||
|
|
fieldMapping.transform
|
||
|
|
);
|
||
|
|
|
||
|
|
mappedRecord[fieldMapping.targetField] = transformedValue;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(
|
||
|
|
`❌ [DataMappingService] 필드 매핑 실패 (${fieldMapping.sourceField} → ${fieldMapping.targetField}):`,
|
||
|
|
error
|
||
|
|
);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return mappedRecord;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 필드 값 변환
|
||
|
|
*/
|
||
|
|
private async transformFieldValue(
|
||
|
|
value: any,
|
||
|
|
targetDataType: DataType,
|
||
|
|
transform?: FieldTransform
|
||
|
|
): Promise<any> {
|
||
|
|
let transformedValue = value;
|
||
|
|
|
||
|
|
// 1. 변환 함수 적용
|
||
|
|
if (transform) {
|
||
|
|
switch (transform.type) {
|
||
|
|
case "constant":
|
||
|
|
transformedValue = transform.value;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "format":
|
||
|
|
if (targetDataType === "date" && transform.format) {
|
||
|
|
transformedValue = this.formatDate(value, transform.format);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "function":
|
||
|
|
if (transform.functionName) {
|
||
|
|
transformedValue = await this.applyCustomFunction(
|
||
|
|
value,
|
||
|
|
transform.functionName
|
||
|
|
);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 데이터 타입 변환
|
||
|
|
return this.convertDataType(transformedValue, targetDataType);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 데이터 타입 변환
|
||
|
|
*/
|
||
|
|
private convertDataType(value: any, targetType: DataType): any {
|
||
|
|
if (value === null || value === undefined) return value;
|
||
|
|
|
||
|
|
switch (targetType) {
|
||
|
|
case "string":
|
||
|
|
return String(value);
|
||
|
|
case "number":
|
||
|
|
const num = Number(value);
|
||
|
|
return isNaN(num) ? null : num;
|
||
|
|
case "boolean":
|
||
|
|
if (typeof value === "boolean") return value;
|
||
|
|
if (typeof value === "string") {
|
||
|
|
return (
|
||
|
|
value.toLowerCase() === "true" || value === "1" || value === "Y"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return Boolean(value);
|
||
|
|
case "date":
|
||
|
|
return new Date(value);
|
||
|
|
case "json":
|
||
|
|
return typeof value === "string" ? JSON.parse(value) : value;
|
||
|
|
default:
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 날짜 포맷 변환
|
||
|
|
*/
|
||
|
|
private formatDate(value: any, format: string): string {
|
||
|
|
const date = new Date(value);
|
||
|
|
if (isNaN(date.getTime())) return value;
|
||
|
|
|
||
|
|
// 간단한 날짜 포맷 변환
|
||
|
|
switch (format) {
|
||
|
|
case "YYYY-MM-DD":
|
||
|
|
return date.toISOString().split("T")[0];
|
||
|
|
case "YYYY-MM-DD HH:mm:ss":
|
||
|
|
return date
|
||
|
|
.toISOString()
|
||
|
|
.replace("T", " ")
|
||
|
|
.replace(/\.\d{3}Z$/, "");
|
||
|
|
default:
|
||
|
|
return date.toISOString();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 커스텀 함수 적용
|
||
|
|
*/
|
||
|
|
private async applyCustomFunction(
|
||
|
|
value: any,
|
||
|
|
functionName: string
|
||
|
|
): Promise<any> {
|
||
|
|
// 추후 확장 가능한 커스텀 함수들
|
||
|
|
switch (functionName) {
|
||
|
|
case "upperCase":
|
||
|
|
return String(value).toUpperCase();
|
||
|
|
case "lowerCase":
|
||
|
|
return String(value).toLowerCase();
|
||
|
|
case "trim":
|
||
|
|
return String(value).trim();
|
||
|
|
default:
|
||
|
|
console.warn(
|
||
|
|
`⚠️ [DataMappingService] 알 수 없는 함수: ${functionName}`
|
||
|
|
);
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Inbound 데이터 저장
|
||
|
|
*/
|
||
|
|
private async saveInboundRecord(
|
||
|
|
mappedData: Record<string, any>,
|
||
|
|
mapping: InboundMapping
|
||
|
|
): Promise<void> {
|
||
|
|
const tableName = mapping.targetTable;
|
||
|
|
|
||
|
|
try {
|
||
|
|
switch (mapping.insertMode) {
|
||
|
|
case "insert":
|
||
|
|
await this.executeInsert(tableName, mappedData);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "upsert":
|
||
|
|
await this.executeUpsert(
|
||
|
|
tableName,
|
||
|
|
mappedData,
|
||
|
|
mapping.keyFields || []
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "update":
|
||
|
|
await this.executeUpdate(
|
||
|
|
tableName,
|
||
|
|
mappedData,
|
||
|
|
mapping.keyFields || []
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(
|
||
|
|
`❌ [DataMappingService] 데이터 저장 실패 (${tableName}):`,
|
||
|
|
error
|
||
|
|
);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 소스 데이터 조회
|
||
|
|
*/
|
||
|
|
private async getSourceData(
|
||
|
|
mapping: OutboundMapping,
|
||
|
|
filter?: any
|
||
|
|
): Promise<any> {
|
||
|
|
const tableName = mapping.sourceTable;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 동적 테이블 쿼리 (Prisma의 경우 런타임에서 제한적)
|
||
|
|
// 실제 구현에서는 각 테이블별 모델을 사용하거나 Raw SQL을 사용해야 함
|
||
|
|
|
||
|
|
let whereClause = {};
|
||
|
|
if (mapping.sourceFilter) {
|
||
|
|
// 간단한 필터 파싱 (실제로는 더 정교한 파싱 필요)
|
||
|
|
console.log(
|
||
|
|
`🔍 [DataMappingService] 필터 조건: ${mapping.sourceFilter}`
|
||
|
|
);
|
||
|
|
// TODO: 필터 조건 파싱 및 적용
|
||
|
|
}
|
||
|
|
|
||
|
|
if (filter) {
|
||
|
|
whereClause = { ...whereClause, ...filter };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Raw SQL을 사용한 동적 쿼리
|
||
|
|
const query = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`;
|
||
|
|
console.log(`🔍 [DataMappingService] 쿼리 실행: ${query}`);
|
||
|
|
|
||
|
|
const result = await this.prisma.$queryRawUnsafe(query);
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(
|
||
|
|
`❌ [DataMappingService] 소스 데이터 조회 실패 (${tableName}):`,
|
||
|
|
error
|
||
|
|
);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* INSERT 실행
|
||
|
|
*/
|
||
|
|
private async executeInsert(
|
||
|
|
tableName: string,
|
||
|
|
data: Record<string, any>
|
||
|
|
): Promise<void> {
|
||
|
|
const columns = Object.keys(data);
|
||
|
|
const values = Object.values(data);
|
||
|
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||
|
|
|
||
|
|
const query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||
|
|
|
||
|
|
console.log(`📝 [DataMappingService] INSERT 실행:`, {
|
||
|
|
table: tableName,
|
||
|
|
columns,
|
||
|
|
query,
|
||
|
|
});
|
||
|
|
await this.prisma.$executeRawUnsafe(query, ...values);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* UPSERT 실행
|
||
|
|
*/
|
||
|
|
private async executeUpsert(
|
||
|
|
tableName: string,
|
||
|
|
data: Record<string, any>,
|
||
|
|
keyFields: string[]
|
||
|
|
): Promise<void> {
|
||
|
|
if (keyFields.length === 0) {
|
||
|
|
throw new Error("UPSERT 모드에서는 키 필드가 필요합니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns = Object.keys(data);
|
||
|
|
const values = Object.values(data);
|
||
|
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||
|
|
|
||
|
|
const updateClauses = columns
|
||
|
|
.filter((col) => !keyFields.includes(col))
|
||
|
|
.map((col) => `${col} = EXCLUDED.${col}`)
|
||
|
|
.join(", ");
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO ${tableName} (${columns.join(", ")})
|
||
|
|
VALUES (${placeholders})
|
||
|
|
ON CONFLICT (${keyFields.join(", ")})
|
||
|
|
DO UPDATE SET ${updateClauses}
|
||
|
|
`;
|
||
|
|
|
||
|
|
console.log(`🔄 [DataMappingService] UPSERT 실행:`, {
|
||
|
|
table: tableName,
|
||
|
|
keyFields,
|
||
|
|
query,
|
||
|
|
});
|
||
|
|
await this.prisma.$executeRawUnsafe(query, ...values);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* UPDATE 실행
|
||
|
|
*/
|
||
|
|
private async executeUpdate(
|
||
|
|
tableName: string,
|
||
|
|
data: Record<string, any>,
|
||
|
|
keyFields: string[]
|
||
|
|
): Promise<void> {
|
||
|
|
if (keyFields.length === 0) {
|
||
|
|
throw new Error("UPDATE 모드에서는 키 필드가 필요합니다.");
|
||
|
|
}
|
||
|
|
|
||
|
|
const updateColumns = Object.keys(data).filter(
|
||
|
|
(col) => !keyFields.includes(col)
|
||
|
|
);
|
||
|
|
const updateClauses = updateColumns
|
||
|
|
.map((col, i) => `${col} = $${i + 1}`)
|
||
|
|
.join(", ");
|
||
|
|
|
||
|
|
const whereConditions = keyFields
|
||
|
|
.map((field, i) => `${field} = $${updateColumns.length + i + 1}`)
|
||
|
|
.join(" AND ");
|
||
|
|
|
||
|
|
const values = [
|
||
|
|
...updateColumns.map((col) => data[col]),
|
||
|
|
...keyFields.map((field) => data[field]),
|
||
|
|
];
|
||
|
|
|
||
|
|
const query = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`;
|
||
|
|
|
||
|
|
console.log(`✏️ [DataMappingService] UPDATE 실행:`, {
|
||
|
|
table: tableName,
|
||
|
|
keyFields,
|
||
|
|
query,
|
||
|
|
});
|
||
|
|
await this.prisma.$executeRawUnsafe(query, ...values);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 매핑 설정 검증
|
||
|
|
*/
|
||
|
|
validateMappingConfig(config: DataMappingConfig): MappingValidationResult {
|
||
|
|
const result: MappingValidationResult = {
|
||
|
|
isValid: true,
|
||
|
|
errors: [],
|
||
|
|
warnings: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
if (config.direction === "none") {
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Inbound 매핑 검증
|
||
|
|
if (
|
||
|
|
(config.direction === "inbound" ||
|
||
|
|
config.direction === "bidirectional") &&
|
||
|
|
config.inboundMapping
|
||
|
|
) {
|
||
|
|
if (!config.inboundMapping.targetTable) {
|
||
|
|
result.errors.push("Inbound 매핑에 대상 테이블이 필요합니다.");
|
||
|
|
}
|
||
|
|
if (config.inboundMapping.fieldMappings.length === 0) {
|
||
|
|
result.errors.push("Inbound 매핑에 필드 매핑이 필요합니다.");
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
config.inboundMapping.insertMode !== "insert" &&
|
||
|
|
(!config.inboundMapping.keyFields ||
|
||
|
|
config.inboundMapping.keyFields.length === 0)
|
||
|
|
) {
|
||
|
|
result.errors.push("UPSERT/UPDATE 모드에서는 키 필드가 필요합니다.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Outbound 매핑 검증
|
||
|
|
if (
|
||
|
|
(config.direction === "outbound" ||
|
||
|
|
config.direction === "bidirectional") &&
|
||
|
|
config.outboundMapping
|
||
|
|
) {
|
||
|
|
if (!config.outboundMapping.sourceTable) {
|
||
|
|
result.errors.push("Outbound 매핑에 소스 테이블이 필요합니다.");
|
||
|
|
}
|
||
|
|
if (config.outboundMapping.fieldMappings.length === 0) {
|
||
|
|
result.errors.push("Outbound 매핑에 필드 매핑이 필요합니다.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
result.isValid = result.errors.length === 0;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 리소스 정리
|
||
|
|
*/
|
||
|
|
async disconnect(): Promise<void> {
|
||
|
|
await this.prisma.$disconnect();
|
||
|
|
}
|
||
|
|
}
|