UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

450 lines 16.6 kB
"use strict"; /** * ksqlDB コネクション - REST API 経由でクエリを実行、DDL/DML を発行 */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeKsqlDbClient = initializeKsqlDbClient; exports.transformArrayRowsToObjects = transformArrayRowsToObjects; exports.executeQuery = executeQuery; exports.executePullQuery = executePullQuery; exports.executePushQuery = executePushQuery; exports.executeAnyQuery = executeAnyQuery; exports.executeDDL = executeDDL; exports.listStreams = listStreams; exports.listTables = listTables; exports.listTopics = listTopics; exports.describeStream = describeStream; exports.describeTable = describeTable; exports.getClientConfig = getClientConfig; exports.isConnected = isConnected; exports.closeClient = closeClient; const axios_1 = __importDefault(require("axios")); const logger_1 = require("./utils/logger"); let ksqlClient = null; let config = null; /** * ksqlDB クライアントを初期化 */ function initializeKsqlDbClient(ksqlConfig) { config = ksqlConfig; const headers = { 'Content-Type': 'application/vnd.ksql.v1+json; charset=utf-8', ...ksqlConfig.headers, }; // 認証情報がある場合は Authorization ヘッダーを追加 if (ksqlConfig.apiKey && ksqlConfig.apiSecret) { const credentials = btoa(`${ksqlConfig.apiKey}:${ksqlConfig.apiSecret}`); headers['Authorization'] = `Basic ${credentials}`; } ksqlClient = axios_1.default.create({ baseURL: ksqlConfig.url, headers, timeout: 30000, // 30秒のタイムアウト }); } /** * 配列レスポンスをオブジェクトに変換する関数 * @param rows - データ行の配列 * @param columnNames - カラム名の配列 * @returns オブジェクトの配列 */ function transformArrayRowsToObjects(rows, columnNames) { if (!rows || !Array.isArray(rows) || rows.length === 0) { return []; } if (!columnNames || !Array.isArray(columnNames) || columnNames.length === 0) { logger_1.log.warn('transformArrayRowsToObjects: columnNames is empty, returning original rows'); return rows; } return rows.map((row) => { const obj = {}; columnNames.forEach((columnName, index) => { obj[columnName.toLowerCase()] = row[index]; }); return obj; }); } /** * DDL/DML文を実行(CREATE, INSERT, UPDATE, DELETE, DROP など) * /ksql エンドポイントを使用 */ async function executeQuery(sql) { if (!ksqlClient) { throw new Error('ksqlDB client is not initialized. Call initializeKsqlDbClient() first.'); } try { const response = await ksqlClient.post('/ksql', { ksql: sql, streamsProperties: {}, }); return response.data; } catch (error) { if (axios_1.default.isAxiosError(error)) { throw new Error(`ksqlDB query failed: ${error.response?.data?.message || error.message}`); } throw error; } } /** * Pull Query を実行(一度だけ結果を取得するSELECT文) * /query-stream エンドポイントを使用 * @param sql - 実行するSQL文 * @param options - Pull Queryのオプション */ async function executePullQuery(sql, options = {}) { if (!ksqlClient) { throw new Error('ksqlDB client is not initialized. Call initializeKsqlDbClient() first.'); } const { format = 'object', normalizeTableName = true } = options; logger_1.log.debug(`executePullQuery - SQL: ${sql}`); logger_1.log.debug(`executePullQuery - Options:`, options); logger_1.log.debug(`executePullQuery - Config:`, config); try { const response = await ksqlClient.post('/query-stream', { sql, properties: {}, }, { headers: { 'Content-Type': 'application/vnd.ksql.v1+json', 'Accept': 'application/vnd.ksqlapi.delimited.v1', }, }); logger_1.log.debug(`executePullQuery - Response status: ${response.status}`); logger_1.log.debug(`executePullQuery - Response headers:`, response.headers); // レスポンスを解析 if (typeof response.data === 'string') { const lines = response.data.split('\n').filter((line) => line.trim()); const results = []; let header = null; logger_1.log.debug(`executePullQuery - Response lines count: ${lines.length}`); for (const line of lines) { try { const parsed = JSON.parse(line); if (!header && parsed.columnNames) { header = parsed; logger_1.log.debug(`executePullQuery - Header:`, header); } else if (Array.isArray(parsed)) { results.push(parsed); } } catch (parseError) { logger_1.log.warn(`executePullQuery - Failed to parse line: ${line}`, parseError); } } logger_1.log.debug(`executePullQuery - Results count: ${results.length}`); // 結果の形式を選択 if (format === 'object' && header?.columnNames) { const transformedData = transformArrayRowsToObjects(results, header.columnNames); return { header, data: transformedData, columnNames: header.columnNames, queryId: header.queryId }; } else { return { header, data: results, columnNames: header?.columnNames, queryId: header?.queryId }; } } logger_1.log.debug(`executePullQuery - Raw response data:`, response.data); return response.data; } catch (error) { logger_1.log.error(`executePullQuery failed:`, error); if (axios_1.default.isAxiosError(error)) { const errorDetails = { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, headers: error.response?.headers, config: { url: error.config?.url, method: error.config?.method, headers: error.config?.headers, }, sql: sql, ksqlDbConfig: config, }; logger_1.log.error(`executePullQuery - Axios error details:`, errorDetails); let errorMessage = 'Unknown error'; if (error.response?.data) { if (typeof error.response.data === 'string') { errorMessage = error.response.data; } else if (error.response.data.message) { errorMessage = error.response.data.message; } else if (error.response.data.error_code) { errorMessage = `${error.response.data.error_code}: ${error.response.data.message || 'Unknown error'}`; } else { errorMessage = JSON.stringify(error.response.data); } } else if (error.message) { errorMessage = error.message; } // ストリーム vs テーブルの問題を特定しやすくする if (errorMessage.includes('stream') || errorMessage.includes('table')) { logger_1.log.error(`executePullQuery - Possible stream/table issue. SQL: ${sql}`); logger_1.log.error(`executePullQuery - Remember: Pull queries work only on TABLES, not STREAMS`); } throw new Error(`ksqlDB pull query failed (${error.response?.status || 'unknown'}): ${errorMessage}`); } logger_1.log.error(`executePullQuery - Non-axios error:`, error); throw error; } } /** * Push Query を実行(継続的にデータを受信するSELECT文) * /query-stream エンドポイントでストリーミング */ async function executePushQuery(sql, onData, onError, onComplete) { if (!ksqlClient) { throw new Error('ksqlDB client is not initialized. Call initializeKsqlDbClient() first.'); } let queryId = null; let terminated = false; try { // ksqlDB専用のaxiosインスタンスを作成(ストリーミング用) const streamClient = axios_1.default.create({ baseURL: config?.url, headers: { 'Content-Type': 'application/vnd.ksql.v1+json', 'Accept': 'application/vnd.ksqlapi.delimited.v1', ...(config?.headers || {}), }, timeout: 0, // ストリーミングの場合はタイムアウトなし responseType: 'stream', }); // 認証情報を追加 if (config?.apiKey && config?.apiSecret) { const credentials = btoa(`${config.apiKey}:${config.apiSecret}`); streamClient.defaults.headers['Authorization'] = `Basic ${credentials}`; } const response = await streamClient.post('/query-stream', { sql, properties: {}, }); let header = null; let buffer = ''; response.data.on('data', (chunk) => { if (terminated) return; try { buffer += chunk.toString(); const lines = buffer.split('\n'); // 最後の行が不完全な可能性があるので、一つ残しておく buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim()) continue; try { const data = JSON.parse(line); // ヘッダー情報を保存 if (!header && data.columnNames) { header = data; queryId = data.queryId; } else if (Array.isArray(data)) { // データ行を処理 onData({ header, row: data }); } } catch (parseError) { // 個別行のパースエラーは無視 logger_1.log.warn('Failed to parse line:', { line, parseError }); } } } catch (error) { if (onError && !terminated) { onError(new Error(`Failed to process streaming data: ${error}`)); } } }); response.data.on('error', (error) => { if (onError && !terminated) { onError(error); } }); response.data.on('end', () => { if (onComplete && !terminated) { onComplete(); } }); // クエリ終了関数を返す return { terminate: async () => { if (terminated) return; terminated = true; // ストリームを閉じる if (response.data && response.data.destroy) { response.data.destroy(); } // ksqlDBにクエリ終了を通知 if (queryId) { try { await ksqlClient.post('/close-query', { queryId, }); } catch (error) { // クエリが既に終了している場合は正常なケースとして処理 if (axios_1.default.isAxiosError(error) && error.response?.status === 400) { // 400エラーは通常、クエリが既に存在しないことを意味するため、警告レベルで処理 logger_1.log.debug('Query already terminated or completed:', { queryId }); } else { logger_1.log.warn('Failed to terminate query:', error); } } } } }; } catch (error) { if (axios_1.default.isAxiosError(error)) { // 詳細なエラー情報を取得 const errorMessage = error.response?.data?.message || error.response?.statusText || error.message; const statusCode = error.response?.status || 'unknown'; const ksqlError = new Error(`ksqlDB push query failed (${statusCode}): ${errorMessage}`); if (onError) { onError(ksqlError); } else { throw ksqlError; } } else { if (onError) { onError(error); } else { throw error; } } return { terminate: () => { } }; } } /** * SELECT文かどうかを判定 */ function isSelectStatement(sql) { const trimmed = sql.trim().toUpperCase(); return trimmed.startsWith('SELECT') || trimmed.startsWith('PRINT'); } /** * 汎用クエリ実行(自動でエンドポイントを選択) */ async function executeAnyQuery(sql) { const trimmed = sql.trim().toUpperCase(); // SELECT文の場合は /query-stream を使用 if (isSelectStatement(sql)) { // EMIT CHANGES が含まれている場合はプッシュクエリ if (trimmed.includes('EMIT CHANGES')) { return new Promise((resolve, reject) => { const results = []; let header = null; executePushQuery(sql, (data) => { if (!header) header = data.header; results.push(data.row); }, reject, () => resolve({ header, data: results })); // 5秒後に自動終了(デモ用) setTimeout(() => { resolve({ header, data: results }); }, 5000); }); } else { // プルクエリ return executePullQuery(sql); } } else { // DDL/DML文の場合は /ksql を使用 return executeQuery(sql); } } /** * DDL文を実行(CREATE STREAM/TABLE など) */ async function executeDDL(ddl) { if (!ksqlClient) { throw new Error('ksqlDB client is not initialized. Call initializeKsqlDbClient() first.'); } try { const response = await ksqlClient.post('/ksql', { ksql: ddl, streamsProperties: {}, }); if (response.data && response.data[0] && response.data[0].errorMessage) { throw new Error(`DDL execution failed: ${response.data[0].errorMessage.message}`); } return response.data; } catch (error) { if (axios_1.default.isAxiosError(error)) { throw new Error(`ksqlDB DDL failed: ${error.response?.data?.message || error.message}`); } throw error; } } /** * ストリーム/テーブル一覧を取得 */ async function listStreams() { return executeQuery('LIST STREAMS;'); } async function listTables() { return executeQuery('LIST TABLES;'); } /** * トピック一覧を取得 */ async function listTopics() { return executeQuery('LIST TOPICS;'); } /** * スキーマ情報を取得 */ async function describeStream(streamName) { return executeQuery(`DESCRIBE ${streamName};`); } async function describeTable(tableName) { return executeQuery(`DESCRIBE ${tableName};`); } /** * クライアント設定を取得 */ function getClientConfig() { return config; } /** * 接続状態を確認 */ function isConnected() { return ksqlClient !== null && config !== null; } /** * クライアントを閉じる */ function closeClient() { ksqlClient = null; config = null; } //# sourceMappingURL=ksqldb-client.js.map