@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
450 lines • 16.6 kB
JavaScript
;
/**
* 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