@gftdcojp/ksqldb-orm
Version:
ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions
585 lines (471 loc) • 13.1 kB
Markdown
# 高レベルクエリビルダー(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
});
}
```