UNPKG

@gftdcojp/ksqldb-orm

Version:

ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions

585 lines (471 loc) 13.1 kB
# 高レベルクエリビルダー(Supabaseライク) DatabaseClientは、Supabaseライクな直感的なAPIでksqlDBとの連携を可能にする高レベルクエリビルダーです。 ## 特徴 - 🔗 **メソッドチェーン** - 直感的なAPIデザイン - 🛡️ **型安全性** - TypeScriptによる型チェック - 🎯 **シンプル** - 複雑なSQL文を知らなくても使用可能 - 🔄 **自動変換** - ストリーム/テーブル名の自動変換 - ⚡ **最適化** - 効率的なクエリ生成 ## セットアップ ```typescript import { DatabaseClient, createDatabaseClient } from '@gftdcojp/gftd-ksqldb-orm/client'; // クライアントの作成 const dbClient = createDatabaseClient({ ksql: { url: 'http://localhost:8088', apiKey: 'your-api-key', apiSecret: 'your-api-secret' }, schemaRegistry: { url: 'http://localhost:8081' } }); await dbClient.initialize(); ``` ## データ取得(SELECT) ### 基本的な使用法 ```typescript // 全てのレコードを取得 const { data, error } = await dbClient .from('users') .select('*') .execute(); // 特定のフィールドのみ選択 const { data } = await dbClient .from('products') .select('id, name, price') .execute(); console.log(data); // 結果の配列 ``` ### WHERE条件 #### 基本的な条件 ```typescript // 等価条件 const { data } = await dbClient .from('users') .eq('status', 'active') .execute(); // 複数条件(AND) const { data } = await dbClient .from('orders') .eq('user_id', 123) .eq('status', 'completed') .execute(); ``` #### 比較演算子 ```typescript // 大なり・小なり const { data } = await dbClient .from('products') .gt('price', 100) // price > 100 .lt('stock', 50) // stock < 50 .execute(); // 以上・以下 const { data } = await dbClient .from('orders') .gte('created_at', '2024-01-01') // created_at >= '2024-01-01' .lte('total_amount', 1000) // total_amount <= 1000 .execute(); ``` #### パターンマッチング ```typescript // LIKE検索 const { data } = await dbClient .from('users') .like('email', '%@example.com') .execute(); // 複数値検索(IN) const { data } = await dbClient .from('products') .in('category_id', [1, 2, 3]) .execute(); ``` ### ソートとページネーション ```typescript // ソート(昇順) const { data } = await dbClient .from('users') .order('created_at', true) // ASC .execute(); // ソート(降順) const { data } = await dbClient .from('products') .order('price', false) // DESC .execute(); // ページネーション const { data } = await dbClient .from('products') .limit(10) .offset(20) .execute(); // 組み合わせ const { data } = await dbClient .from('orders') .eq('status', 'completed') .order('created_at', false) .limit(25) .offset(50) .execute(); ``` ### 単一レコード取得 ```typescript // 最初の1件を取得 const { data: user, error } = await dbClient .from('users') .eq('id', 123) .single(); if (error) { console.error('Error:', error); } else if (user) { console.log('User found:', user); } else { console.log('User not found'); } ``` ## データ操作(INSERT/UPDATE/DELETE) ### データ挿入 ```typescript // 単一レコード挿入 const { data, error } = await dbClient .from('users') .insert({ name: 'John Doe', email: 'john@example.com', status: 'active', created_at: new Date().toISOString() }); if (error) { console.error('Insert failed:', error); } else { console.log('Inserted data:', data); } ``` ### データ更新 ```typescript // 条件に一致するレコードを更新 const { data, error } = await dbClient .from('users') .eq('id', 123) .update({ email: 'newemail@example.com', updated_at: new Date().toISOString() }); // 複数条件での更新 const { data } = await dbClient .from('products') .eq('category_id', 1) .lt('stock', 10) .update({ status: 'low_stock', updated_at: new Date().toISOString() }); ``` ### データ削除 ```typescript // 条件に一致するレコードを削除 const { data, error } = await dbClient .from('users') .eq('status', 'inactive') .delete(); // 特定IDのレコードを削除 const { data } = await dbClient .from('temp_data') .eq('id', 456) .delete(); ``` ## 型安全性 TypeScript型を使用することで、コンパイル時の型チェックが可能です。 ```typescript // 型定義 interface User { id: number; name: string; email: string; status: 'active' | 'inactive'; created_at: string; } // 型安全なクエリ const { data } = await dbClient .from<User>('users') .eq('status', 'active') // 'active' | 'inactive' のみ受け入れ .execute(); // data は User[] 型として推論される data.forEach(user => { console.log(user.name); // 型安全 console.log(user.invalid); // コンパイルエラー }); ``` ## エラーハンドリング ```typescript // 基本的なエラーハンドリング const { data, error } = await dbClient .from('users') .eq('id', 123) .execute(); if (error) { console.error('Query failed:', error.message); console.error('Details:', error.details); console.error('Table:', error.tableName); console.error('Query type:', error.queryType); return; } // データを安全に使用 console.log('Users found:', data.length); ``` ## 高度な使用例 ### 複雑な検索クエリ ```typescript // eコマースの商品検索 const searchProducts = async (filters: { category?: number[]; priceMin?: number; priceMax?: number; inStock?: boolean; searchTerm?: string; }) => { let query = dbClient.from('products').select('*'); if (filters.category?.length) { query = query.in('category_id', filters.category); } if (filters.priceMin !== undefined) { query = query.gte('price', filters.priceMin); } if (filters.priceMax !== undefined) { query = query.lte('price', filters.priceMax); } if (filters.inStock) { query = query.gt('stock', 0); } if (filters.searchTerm) { query = query.like('name', `%${filters.searchTerm}%`); } return await query .order('name', true) .limit(50) .execute(); }; // 使用例 const { data: products } = await searchProducts({ category: [1, 2], priceMin: 10, priceMax: 100, inStock: true, searchTerm: 'laptop' }); ``` ### ページネーション実装 ```typescript const getPaginatedUsers = async (page: number, pageSize: number = 20) => { const offset = (page - 1) * pageSize; const { data, error } = await dbClient .from('users') .eq('status', 'active') .order('created_at', false) .limit(pageSize) .offset(offset) .execute(); return { data, error, pagination: { page, pageSize, offset } }; }; // 使用例 const result = await getPaginatedUsers(2, 25); // 2ページ目、25件ずつ ``` ### バッチ操作 ```typescript // 複数のデータ操作を順番に実行 const batchOperations = async () => { try { // 1. 古いデータを削除 await dbClient .from('temp_logs') .lt('created_at', '2024-01-01') .delete(); // 2. ステータスを更新 await dbClient .from('users') .eq('last_login', null) .update({ status: 'inactive' }); // 3. 新しいデータを挿入 await dbClient .from('system_events') .insert({ event_type: 'batch_cleanup', created_at: new Date().toISOString() }); console.log('Batch operations completed successfully'); } catch (error) { console.error('Batch operations failed:', error); } }; ``` ## ベストプラクティス ### 1. 型定義の活用 ```typescript // interfaces/User.ts export interface User { id: number; name: string; email: string; status: 'active' | 'inactive'; created_at: string; updated_at?: string; } // services/UserService.ts import { User } from '../interfaces/User'; export class UserService { constructor(private dbClient: DatabaseClient) {} async getActiveUsers(): Promise<User[]> { const { data, error } = await this.dbClient .from<User>('users') .eq('status', 'active') .execute(); if (error) throw new Error(`Failed to get users: ${error.message}`); return data; } async updateUserEmail(userId: number, newEmail: string): Promise<void> { const { error } = await this.dbClient .from<User>('users') .eq('id', userId) .update({ email: newEmail, updated_at: new Date().toISOString() }); if (error) throw new Error(`Failed to update user: ${error.message}`); } } ``` ### 2. エラーハンドリングの統一 ```typescript // utils/DatabaseUtils.ts export const executeWithErrorHandling = async <T>( operation: () => Promise<{ data: T[]; error?: any }> ): Promise<T[]> => { try { const { data, error } = await operation(); if (error) { console.error('Database operation failed:', error); throw new Error(error.message || 'Unknown database error'); } return data; } catch (error) { console.error('Database operation exception:', error); throw error; } }; // 使用例 const users = await executeWithErrorHandling(() => dbClient.from('users').eq('status', 'active').execute() ); ``` ### 3. クエリの再利用 ```typescript // queries/UserQueries.ts export class UserQueries { constructor(private dbClient: DatabaseClient) {} // 基本クエリ private baseUserQuery() { return this.dbClient .from<User>('users') .select('id, name, email, status, created_at'); } // アクティブユーザー取得 getActiveUsers(limit?: number) { let query = this.baseUserQuery().eq('status', 'active'); if (limit) { query = query.limit(limit); } return query.execute(); } // ユーザー検索 searchUsers(searchTerm: string) { return this.baseUserQuery() .like('name', `%${searchTerm}%`) .order('name', true) .execute(); } // 最近登録されたユーザー getRecentUsers(days: number = 7) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - days); return this.baseUserQuery() .gte('created_at', cutoffDate.toISOString()) .order('created_at', false) .execute(); } } ``` ## テーブル名の自動変換 DatabaseClientは、ksqlDBのストリーム/テーブル命名規則に自動で対応します。 ```typescript // 'users' を指定すると... await dbClient.from('users').execute(); // 実際には 'users_table' に対してクエリが実行される // 'users_stream' を指定すると... await dbClient.from('users_stream').execute(); // 'users_table' に自動変換されてクエリが実行される // 既に '_table' サフィックスがある場合 await dbClient.from('users_table').execute(); // そのまま 'users_table' に対してクエリが実行される ``` ## 接続確認とヘルスチェック ```typescript // データベース接続の確認 const healthCheck = async () => { const result = await dbClient.health(); if (result.status === 'ok') { console.log('Database connection is healthy'); } else { console.error('Database connection failed:', result.details); } return result; }; // アプリケーション起動時に実行 await healthCheck(); ``` ## 制限事項 ### 1. ksqlDBの制約 - JOIN操作は直接サポートされていません(低レベルAPIを使用) - 複雑な集約関数は使用できません - ウィンドウ関数は使用できません ### 2. ブラウザ環境での制限 - ファイル操作は利用できません - 一部のNode.js専用機能は使用できません ### 3. プルクエリの制限 - リアルタイムデータには対応していません(プッシュクエリを使用) - 大量データの取得には適していません ## トラブルシューティング ### よくあるエラー ```typescript // 1. テーブルが見つからない // Error: Table 'unknown_table' not found // → テーブル名を確認、またはスキーマを作成 // 2. 型の不一致 // Error: Cannot convert value to expected type // → フィールドの型を確認 // 3. 権限エラー // Error: Access denied // → RLSポリシーまたはAPI認証を確認 ``` ### デバッグ方法 ```typescript // 生成されるクエリを確認(内部メソッド) const query = dbClient.from('users').eq('status', 'active'); console.log('Generated query:', query.buildSelectQuery()); // エラーの詳細情報を取得 const { error } = await dbClient.from('users').execute(); if (error) { console.log('Error details:', { message: error.message, tableName: error.tableName, queryType: error.queryType, details: error.details }); } ```