ERP-node/backend-node/src/services/multiConnectionQueryService.ts

803 lines
25 KiB
TypeScript
Raw Normal View History

/**
*
* CRUD
*
*/
import { ExternalDbConnectionService } from "./externalDbConnectionService";
import { TableManagementService } from "./tableManagementService";
import { ExternalDbConnection } from "../types/externalDbTypes";
import { ColumnTypeInfo, TableInfo } from "../types/tableManagement";
import { PrismaClient } from "@prisma/client";
import { logger } from "../utils/logger";
const prisma = new PrismaClient();
export interface ValidationResult {
isValid: boolean;
error?: string;
warnings?: string[];
}
export interface ColumnInfo {
columnName: string;
displayName: string;
dataType: string;
dbType: string;
webType: string;
isNullable: boolean;
isPrimaryKey: boolean;
defaultValue?: string;
maxLength?: number;
description?: string;
}
export interface MultiConnectionTableInfo {
tableName: string;
displayName?: string;
columnCount: number;
connectionId: number;
connectionName: string;
dbType: string;
}
export class MultiConnectionQueryService {
private tableManagementService: TableManagementService;
constructor() {
this.tableManagementService = new TableManagementService();
}
/**
*
*/
async fetchDataFromConnection(
connectionId: number,
tableName: string,
conditions?: Record<string, any>
): Promise<Record<string, any>[]> {
try {
logger.info(
`데이터 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"select",
tableName,
undefined,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 쿼리 조건 구성
let whereClause = "";
const queryParams: any[] = [];
if (conditions && Object.keys(conditions).length > 0) {
const conditionParts: string[] = [];
let paramIndex = 1;
Object.entries(conditions).forEach(([key, value]) => {
conditionParts.push(`${key} = $${paramIndex}`);
queryParams.push(value);
paramIndex++;
});
whereClause = `WHERE ${conditionParts.join(" AND ")}`;
}
const query = `SELECT * FROM ${tableName} ${whereClause}`;
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query
);
if (!result.success || !result.data) {
throw new Error(result.message || "쿼리 실행 실패");
}
logger.info(`데이터 조회 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 조회 실패: ${error}`);
throw new Error(
`데이터 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
*
*/
async insertDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
logger.info(
`데이터 삽입 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase("insert", tableName, data);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// INSERT 쿼리 구성
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
const query = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
RETURNING *
`;
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 삽입 실패");
}
logger.info(`데이터 삽입 완료`);
return result.data[0] || result.data;
} catch (error) {
logger.error(`데이터 삽입 실패: ${error}`);
throw new Error(
`데이터 삽입 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕
*/
async updateDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>,
conditions: Record<string, any>
): Promise<any> {
try {
logger.info(
`데이터 업데이트 시작: connectionId=${connectionId}, table=${tableName}`
);
// 자기 자신 테이블 작업 검증
if (connectionId === 0) {
const validationResult = await this.validateSelfTableOperation(
tableName,
"update",
[conditions]
);
if (!validationResult.isValid) {
throw new Error(
`자기 자신 테이블 업데이트 검증 실패: ${validationResult.error}`
);
}
}
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"update",
tableName,
data,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// UPDATE 쿼리 구성
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const whereClause = Object.keys(conditions)
.map(
(key, index) => `${key} = $${Object.keys(data).length + index + 1}`
)
.join(" AND ");
const query = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${whereClause}
RETURNING *
`;
const queryParams = [
...Object.values(data),
...Object.values(conditions),
];
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 업데이트 실패");
}
logger.info(`데이터 업데이트 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 업데이트 실패: ${error}`);
throw new Error(
`데이터 업데이트 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕
*/
async deleteDataFromConnection(
connectionId: number,
tableName: string,
conditions: Record<string, any>,
maxDeleteCount: number = 100
): Promise<any> {
try {
logger.info(
`데이터 삭제 시작: connectionId=${connectionId}, table=${tableName}`
);
// 자기 자신 테이블 작업 검증
if (connectionId === 0) {
const validationResult = await this.validateSelfTableOperation(
tableName,
"delete",
[conditions]
);
if (!validationResult.isValid) {
throw new Error(
`자기 자신 테이블 삭제 검증 실패: ${validationResult.error}`
);
}
}
// WHERE 조건 필수 체크
if (!conditions || Object.keys(conditions).length === 0) {
throw new Error("DELETE 작업에는 반드시 WHERE 조건이 필요합니다.");
}
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"delete",
tableName,
undefined,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 먼저 삭제 대상 개수 확인 (안전장치)
const countQuery = `
SELECT COUNT(*) as count
FROM ${tableName}
WHERE ${Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ")}
`;
const countResult = await ExternalDbConnectionService.executeQuery(
connectionId,
countQuery
);
if (!countResult.success || !countResult.data) {
throw new Error(countResult.message || "삭제 대상 개수 조회 실패");
}
const deleteCount = parseInt(countResult.data[0]?.count || "0");
if (deleteCount > maxDeleteCount) {
throw new Error(
`삭제 대상이 ${deleteCount}건으로 최대 허용 개수(${maxDeleteCount})를 초과합니다.`
);
}
// DELETE 쿼리 실행
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ")}
RETURNING *
`;
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
deleteQuery
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 삭제 실패");
}
logger.info(`데이터 삭제 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 삭제 실패: ${error}`);
throw new Error(
`데이터 삭제 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
*
*/
async getTablesFromConnection(
connectionId: number
): Promise<MultiConnectionTableInfo[]> {
try {
logger.info(`테이블 목록 조회 시작: connectionId=${connectionId}`);
// connectionId가 0이면 메인 DB의 테이블 목록 반환
if (connectionId === 0) {
const tables = await this.tableManagementService.getTableList();
return tables.map((table) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName, // 라벨이 있으면 라벨 사용, 없으면 테이블명
columnCount: table.columnCount,
connectionId: 0,
connectionName: "메인 데이터베이스",
dbType: "postgresql",
}));
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 외부 DB의 테이블 목록 조회
const tablesResult =
await ExternalDbConnectionService.getTables(connectionId);
if (!tablesResult.success || !tablesResult.data) {
throw new Error(tablesResult.message || "테이블 조회 실패");
}
const tables = tablesResult.data;
// 성능 최적화: 컬럼 개수는 실제 필요할 때만 조회하도록 변경
return tables.map((table: any) => ({
tableName: table.table_name,
displayName: table.table_comment || table.table_name, // 라벨(comment)이 있으면 라벨 사용, 없으면 테이블명
columnCount: 0, // 성능을 위해 0으로 설정, 필요시 별도 API로 조회
connectionId: connectionId,
connectionName: connection.connection_name,
dbType: connection.db_type,
}));
} catch (error) {
logger.error(`테이블 목록 조회 실패: ${error}`);
throw new Error(
`테이블 목록 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
*
*/
async getColumnsFromConnection(
connectionId: number,
tableName: string
): Promise<ColumnInfo[]> {
try {
logger.info(
`컬럼 정보 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB의 컬럼 정보 반환
if (connectionId === 0) {
console.log(`🔍 메인 DB 컬럼 정보 조회 시작: ${tableName}`);
const columnsResult = await this.tableManagementService.getColumnList(
tableName,
1,
1000
);
console.log(
`✅ 메인 DB 컬럼 조회 성공: ${columnsResult.columns.length}`
);
return columnsResult.columns.map((column) => ({
columnName: column.columnName,
displayName: column.displayName || column.columnName, // 라벨이 있으면 라벨 사용, 없으면 컬럼명
dataType: column.dataType,
dbType: column.dataType, // dataType을 dbType으로 사용
webType: column.webType || "text", // webType 사용, 기본값 text
isNullable: column.isNullable === "Y",
isPrimaryKey: column.isPrimaryKey || false,
defaultValue: column.defaultValue,
maxLength: column.maxLength,
description: column.description,
}));
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 외부 DB의 컬럼 정보 조회
console.log(
`🔍 외부 DB 컬럼 정보 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
const columnsResult = await ExternalDbConnectionService.getTableColumns(
connectionId,
tableName
);
if (!columnsResult.success || !columnsResult.data) {
console.error(`❌ 외부 DB 컬럼 조회 실패: ${columnsResult.message}`);
throw new Error(columnsResult.message || "컬럼 조회 실패");
}
const columns = columnsResult.data;
console.log(`✅ 외부 DB 컬럼 조회 성공: ${columns.length}`);
// MSSQL 컬럼 데이터 구조 디버깅
if (columns.length > 0) {
console.log(
`🔍 MSSQL 컬럼 데이터 구조 분석:`,
JSON.stringify(columns[0], null, 2)
);
console.log(`🔍 모든 컬럼 키들:`, Object.keys(columns[0]));
}
return columns.map((column: any) => {
// MSSQL과 PostgreSQL 데이터 타입 필드명이 다를 수 있음
// MSSQL: name, type, description (MSSQLConnector에서 alias로 지정)
// PostgreSQL: column_name, data_type, column_comment
const dataType =
column.type || // MSSQL (MSSQLConnector alias)
column.data_type || // PostgreSQL
column.DATA_TYPE ||
column.Type ||
column.dataType ||
column.column_type ||
column.COLUMN_TYPE ||
"unknown";
const columnName =
column.name || // MSSQL (MSSQLConnector alias)
column.column_name || // PostgreSQL
column.COLUMN_NAME ||
column.Name ||
column.columnName ||
column.COLUMN_NAME;
const columnComment =
column.description || // MSSQL (MSSQLConnector alias)
column.column_comment || // PostgreSQL
column.COLUMN_COMMENT ||
column.Description ||
column.comment;
console.log(`🔍 컬럼 매핑: ${columnName} - 타입: ${dataType}`);
return {
columnName: columnName,
displayName: columnComment || columnName, // 라벨(comment)이 있으면 라벨 사용, 없으면 컬럼명
dataType: dataType,
dbType: dataType,
webType: this.mapDataTypeToWebType(dataType),
isNullable:
column.nullable === "YES" || // MSSQL (MSSQLConnector alias)
column.is_nullable === "YES" || // PostgreSQL
column.IS_NULLABLE === "YES" ||
column.Nullable === true,
isPrimaryKey: column.is_primary_key || column.IS_PRIMARY_KEY || false,
defaultValue:
column.default_value || // MSSQL (MSSQLConnector alias)
column.column_default || // PostgreSQL
column.COLUMN_DEFAULT,
maxLength:
column.max_length || // MSSQL (MSSQLConnector alias)
column.character_maximum_length || // PostgreSQL
column.CHARACTER_MAXIMUM_LENGTH,
description: columnComment,
};
});
} catch (error) {
logger.error(`컬럼 정보 조회 실패: ${error}`);
throw new Error(
`컬럼 정보 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕
*/
async validateSelfTableOperation(
tableName: string,
operation: "update" | "delete",
conditions: any[]
): Promise<ValidationResult> {
try {
logger.info(
`자기 자신 테이블 작업 검증: table=${tableName}, operation=${operation}`
);
const warnings: string[] = [];
// 1. 기본 조건 체크
if (!conditions || conditions.length === 0) {
return {
isValid: false,
error: `자기 자신 테이블 ${operation.toUpperCase()} 작업에는 반드시 조건이 필요합니다.`,
};
}
// 2. DELETE 작업에 대한 추가 검증
if (operation === "delete") {
// 부정 조건 체크
const hasNegativeConditions = conditions.some((condition) => {
const conditionStr = JSON.stringify(condition).toLowerCase();
return (
conditionStr.includes("!=") ||
conditionStr.includes("not in") ||
conditionStr.includes("not exists")
);
});
if (hasNegativeConditions) {
return {
isValid: false,
error:
"자기 자신 테이블 삭제 시 부정 조건(!=, NOT IN, NOT EXISTS)은 위험합니다.",
};
}
// 조건 개수 체크
if (conditions.length < 2) {
warnings.push(
"자기 자신 테이블 삭제 시 WHERE 조건을 2개 이상 설정하는 것을 권장합니다."
);
}
}
// 3. UPDATE 작업에 대한 추가 검증
if (operation === "update") {
warnings.push("자기 자신 테이블 업데이트 시 무한 루프에 주의하세요.");
}
return {
isValid: true,
warnings: warnings.length > 0 ? warnings : undefined,
};
} catch (error) {
logger.error(`자기 자신 테이블 작업 검증 실패: ${error}`);
return {
isValid: false,
error: `검증 과정에서 오류가 발생했습니다: ${error instanceof Error ? error.message : error}`,
};
}
}
/**
* 🆕 DB (connectionId = 0 )
*/
async executeOnMainDatabase(
operation: "select" | "insert" | "update" | "delete",
tableName: string,
data?: Record<string, any>,
conditions?: Record<string, any>
): Promise<any> {
try {
logger.info(
`메인 DB 작업 실행: operation=${operation}, table=${tableName}`
);
switch (operation) {
case "select":
let query = `SELECT * FROM ${tableName}`;
const queryParams: any[] = [];
if (conditions && Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ");
query += ` WHERE ${whereClause}`;
queryParams.push(...Object.values(conditions));
}
return await prisma.$queryRawUnsafe(query, ...queryParams);
case "insert":
if (!data) throw new Error("INSERT 작업에는 데이터가 필요합니다.");
const insertColumns = Object.keys(data);
const insertValues = Object.values(data);
const insertPlaceholders = insertValues
.map((_, index) => `$${index + 1}`)
.join(", ");
const insertQuery = `
INSERT INTO ${tableName} (${insertColumns.join(", ")})
VALUES (${insertPlaceholders})
RETURNING *
`;
const insertResult = await prisma.$queryRawUnsafe(
insertQuery,
...insertValues
);
return Array.isArray(insertResult) ? insertResult[0] : insertResult;
case "update":
if (!data) throw new Error("UPDATE 작업에는 데이터가 필요합니다.");
if (!conditions)
throw new Error("UPDATE 작업에는 조건이 필요합니다.");
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const updateWhereClause = Object.keys(conditions)
.map(
(key, index) =>
`${key} = $${Object.keys(data).length + index + 1}`
)
.join(" AND ");
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${updateWhereClause}
RETURNING *
`;
const updateParams = [
...Object.values(data),
...Object.values(conditions),
];
return await prisma.$queryRawUnsafe(updateQuery, ...updateParams);
case "delete":
if (!conditions)
throw new Error("DELETE 작업에는 조건이 필요합니다.");
const deleteWhereClause = Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ");
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${deleteWhereClause}
RETURNING *
`;
return await prisma.$queryRawUnsafe(
deleteQuery,
...Object.values(conditions)
);
default:
throw new Error(`지원하지 않는 작업입니다: ${operation}`);
}
} catch (error) {
logger.error(`메인 DB 작업 실패: ${error}`);
throw new Error(
`메인 DB 작업 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
*
*/
private mapDataTypeToWebType(dataType: string | undefined | null): string {
// 안전한 타입 검사
if (!dataType || typeof dataType !== "string") {
console.warn(`⚠️ 잘못된 데이터 타입: ${dataType}, 기본값 'text' 사용`);
return "text";
}
const lowerType = dataType.toLowerCase();
// PostgreSQL & MSSQL 타입 매핑
if (
lowerType.includes("int") ||
lowerType.includes("serial") ||
lowerType.includes("bigint")
) {
return "number";
}
if (
lowerType.includes("decimal") ||
lowerType.includes("numeric") ||
lowerType.includes("float") ||
lowerType.includes("money") ||
lowerType.includes("real")
) {
return "decimal";
}
if (lowerType.includes("date") && !lowerType.includes("time")) {
return "date";
}
if (
lowerType.includes("timestamp") ||
lowerType.includes("datetime") ||
lowerType.includes("datetime2")
) {
return "datetime";
}
if (lowerType.includes("bool") || lowerType.includes("bit")) {
return "boolean";
}
if (
lowerType.includes("text") ||
lowerType.includes("clob") ||
lowerType.includes("ntext")
) {
return "textarea";
}
// MSSQL 특수 타입들
if (
lowerType.includes("varchar") ||
lowerType.includes("nvarchar") ||
lowerType.includes("char")
) {
return "text";
}
return "text";
}
}