194 lines
5.2 KiB
TypeScript
194 lines
5.2 KiB
TypeScript
import { QueryResult } from "../types";
|
|
|
|
/**
|
|
* JSON Path를 사용하여 객체에서 데이터 추출
|
|
* @param obj JSON 객체
|
|
* @param path 경로 (예: "data.results", "items")
|
|
* @returns 추출된 데이터
|
|
*/
|
|
export function extractDataFromJsonPath(obj: any, path: string): any {
|
|
if (!path || path.trim() === "") {
|
|
return obj;
|
|
}
|
|
|
|
const keys = path.split(".");
|
|
let result = obj;
|
|
|
|
for (const key of keys) {
|
|
if (result === null || result === undefined) {
|
|
return null;
|
|
}
|
|
result = result[key];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* API 응답을 QueryResult 형식으로 변환
|
|
* @param data API 응답 데이터
|
|
* @param jsonPath JSON Path (선택)
|
|
* @returns QueryResult
|
|
*/
|
|
export function transformApiResponseToQueryResult(data: any, jsonPath?: string): QueryResult {
|
|
try {
|
|
// JSON Path가 있으면 데이터 추출
|
|
let extractedData = jsonPath ? extractDataFromJsonPath(data, jsonPath) : data;
|
|
|
|
// 배열이 아니면 배열로 변환
|
|
if (!Array.isArray(extractedData)) {
|
|
// 객체인 경우 키-값 쌍을 배열로 변환
|
|
if (typeof extractedData === "object" && extractedData !== null) {
|
|
extractedData = Object.entries(extractedData).map(([key, value]) => ({
|
|
key,
|
|
value,
|
|
}));
|
|
} else {
|
|
throw new Error("데이터가 배열 또는 객체 형식이 아닙니다");
|
|
}
|
|
}
|
|
|
|
if (extractedData.length === 0) {
|
|
return {
|
|
columns: [],
|
|
rows: [],
|
|
totalRows: 0,
|
|
executionTime: 0,
|
|
};
|
|
}
|
|
|
|
// 첫 번째 행에서 컬럼 추출
|
|
const firstRow = extractedData[0];
|
|
const columns = Object.keys(firstRow);
|
|
|
|
return {
|
|
columns,
|
|
rows: extractedData,
|
|
totalRows: extractedData.length,
|
|
executionTime: 0,
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`API 응답 변환 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 데이터 소스가 유효한지 검증
|
|
* @param type 데이터 소스 타입
|
|
* @param connectionType 커넥션 타입 (DB일 때)
|
|
* @param externalConnectionId 외부 커넥션 ID (외부 DB일 때)
|
|
* @param query SQL 쿼리 (DB일 때)
|
|
* @param endpoint API URL (API일 때)
|
|
* @returns 유효성 검증 결과
|
|
*/
|
|
export function validateDataSource(
|
|
type: "database" | "api",
|
|
connectionType?: "current" | "external",
|
|
externalConnectionId?: string,
|
|
query?: string,
|
|
endpoint?: string,
|
|
): { valid: boolean; message?: string } {
|
|
if (type === "database") {
|
|
// DB 검증
|
|
if (!connectionType) {
|
|
return { valid: false, message: "데이터베이스 타입을 선택하세요" };
|
|
}
|
|
|
|
if (connectionType === "external" && !externalConnectionId) {
|
|
return { valid: false, message: "외부 커넥션을 선택하세요" };
|
|
}
|
|
|
|
if (!query || query.trim() === "") {
|
|
return { valid: false, message: "SQL 쿼리를 입력하세요" };
|
|
}
|
|
|
|
// SELECT 쿼리인지 검증 (간단한 검증)
|
|
const trimmedQuery = query.trim().toLowerCase();
|
|
if (!trimmedQuery.startsWith("select")) {
|
|
return { valid: false, message: "SELECT 쿼리만 허용됩니다" };
|
|
}
|
|
|
|
// 위험한 키워드 체크
|
|
const dangerousKeywords = ["drop", "delete", "insert", "update", "truncate", "alter", "create", "exec", "execute"];
|
|
for (const keyword of dangerousKeywords) {
|
|
if (trimmedQuery.includes(keyword)) {
|
|
return {
|
|
valid: false,
|
|
message: `보안상 ${keyword.toUpperCase()} 명령은 사용할 수 없습니다`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { valid: true };
|
|
} else if (type === "api") {
|
|
// API 검증
|
|
if (!endpoint || endpoint.trim() === "") {
|
|
return { valid: false, message: "API URL을 입력하세요" };
|
|
}
|
|
|
|
// URL 형식 검증
|
|
try {
|
|
new URL(endpoint);
|
|
} catch {
|
|
return { valid: false, message: "올바른 URL 형식이 아닙니다" };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
return { valid: false, message: "알 수 없는 데이터 소스 타입입니다" };
|
|
}
|
|
|
|
/**
|
|
* 쿼리 파라미터를 URL에 추가
|
|
* @param baseUrl 기본 URL
|
|
* @param params 쿼리 파라미터 객체
|
|
* @returns 쿼리 파라미터가 추가된 URL
|
|
*/
|
|
export function buildUrlWithParams(baseUrl: string, params?: Record<string, string>): string {
|
|
if (!params || Object.keys(params).length === 0) {
|
|
return baseUrl;
|
|
}
|
|
|
|
const url = new URL(baseUrl);
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (key && value) {
|
|
url.searchParams.append(key, value);
|
|
}
|
|
});
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
/**
|
|
* 컬럼 데이터 타입 추론
|
|
* @param rows 데이터 행
|
|
* @param columnName 컬럼명
|
|
* @returns 데이터 타입 ('string' | 'number' | 'date' | 'boolean')
|
|
*/
|
|
export function inferColumnType(rows: Record<string, any>[], columnName: string): string {
|
|
if (rows.length === 0) {
|
|
return "string";
|
|
}
|
|
|
|
const sampleValue = rows[0][columnName];
|
|
|
|
if (typeof sampleValue === "number") {
|
|
return "number";
|
|
}
|
|
|
|
if (typeof sampleValue === "boolean") {
|
|
return "boolean";
|
|
}
|
|
|
|
if (typeof sampleValue === "string") {
|
|
// 날짜 형식인지 확인
|
|
if (!isNaN(Date.parse(sampleValue))) {
|
|
return "date";
|
|
}
|
|
return "string";
|
|
}
|
|
|
|
return "string";
|
|
}
|