feat: enhance DBAL integration with improved error handling and in-memory storage implementations

This commit is contained in:
2025-12-25 15:04:20 +00:00
parent 27db516098
commit 0e2d5bd032
3 changed files with 189 additions and 196 deletions

View File

@@ -1,4 +1,5 @@
import { DBALClient, type DBALUser, type DBALConfig } from '@/lib/dbal-stub'
import { DBALClient } from '@/lib/dbal-stub'
import type { DBALConfig } from '@/lib/dbal-stub'
import type { User } from './level-types'
let dbalInstance: DBALClient | null = null
@@ -40,4 +41,4 @@ export async function migrateToDBAL() {
}
export { DBALClient }
export type { DBALUser }
export type { DBALUser } from '@/lib/dbal-stub'

View File

@@ -1,30 +1,148 @@
/**
* DBAL Integration Layer
* DBAL Integration Layer (Stub Version)
*
* This module integrates the TypeScript DBAL client with the existing MetaBuilder
* database layer, providing a bridge between the legacy Database class and the
* new DBAL system.
* This module provides a simplified DBAL integration when the full DBAL
* module is not available. It provides in-memory implementations of
* tenant management, KV store, and blob storage.
*/
import { DBALClient, DBALError, DBALErrorCode } from '@/lib/dbal-stub'
import type { DBALConfig } from '@/lib/dbal-stub'
import { InMemoryTenantManager } from '@/lib/dbal-stub/core/tenant-context'
import { InMemoryKVStore } from '@/lib/dbal-stub/core/kv-store'
import { createBlobStorage } from '@/lib/dbal-stub/blob'
import { TenantAwareBlobStorage } from '@/lib/dbal-stub/blob/tenant-aware-storage'
import { DBALClient, type DBALConfig } from '@/lib/dbal-stub'
/* eslint-disable @typescript-eslint/no-explicit-any */
// Simple in-memory tenant context
interface TenantContext {
tenantId: string
userId: string
}
// In-memory tenant manager
class InMemoryTenantManager {
private tenants = new Map<string, { limits: any }>()
async createTenant(id: string, limits?: any): Promise<void> {
this.tenants.set(id, { limits: limits || {} })
}
async getTenantContext(tenantId: string, userId: string): Promise<TenantContext | null> {
if (!this.tenants.has(tenantId)) {
return null
}
return { tenantId, userId }
}
hasTenant(id: string): boolean {
return this.tenants.has(id)
}
}
// In-memory KV store
class InMemoryKVStore {
private store = new Map<string, { value: any; expiry?: number }>()
private getKey(key: string, context: TenantContext): string {
return `${context.tenantId}:${key}`
}
async set(key: string, value: any, context: TenantContext, ttl?: number): Promise<void> {
const fullKey = this.getKey(key, context)
const expiry = ttl ? Date.now() + ttl * 1000 : undefined
this.store.set(fullKey, { value, expiry })
}
async get(key: string, context: TenantContext): Promise<any | null> {
const fullKey = this.getKey(key, context)
const item = this.store.get(fullKey)
if (!item) return null
if (item.expiry && Date.now() > item.expiry) {
this.store.delete(fullKey)
return null
}
return item.value
}
async delete(key: string, context: TenantContext): Promise<boolean> {
const fullKey = this.getKey(key, context)
return this.store.delete(fullKey)
}
async listAdd(key: string, items: any[], context: TenantContext): Promise<void> {
const fullKey = this.getKey(key, context)
const existing = this.store.get(fullKey)?.value || []
this.store.set(fullKey, { value: [...existing, ...items] })
}
async listGet(key: string, context: TenantContext, start?: number, end?: number): Promise<any[]> {
const fullKey = this.getKey(key, context)
const list = this.store.get(fullKey)?.value || []
if (start !== undefined && end !== undefined) {
return list.slice(start, end)
}
return list
}
}
// In-memory blob storage
class InMemoryBlobStorage {
private blobs = new Map<string, { data: Buffer; metadata: Record<string, string> }>()
async upload(key: string, data: Buffer, metadata?: Record<string, string>): Promise<void> {
this.blobs.set(key, { data, metadata: metadata || {} })
}
async download(key: string): Promise<Buffer> {
const blob = this.blobs.get(key)
if (!blob) throw new Error(`Blob not found: ${key}`)
return blob.data
}
async delete(key: string): Promise<void> {
this.blobs.delete(key)
}
async list(options?: { prefix?: string }): Promise<{ items: { key: string }[] }> {
const items: { key: string }[] = []
for (const key of this.blobs.keys()) {
if (!options?.prefix || key.startsWith(options.prefix)) {
items.push({ key })
}
}
return { items }
}
async getMetadata(key: string): Promise<{ customMetadata?: Record<string, string> }> {
const blob = this.blobs.get(key)
return { customMetadata: blob?.metadata }
}
}
// DBAL Error types
export enum DBALErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
UNAUTHORIZED = 'UNAUTHORIZED',
INTERNAL_ERROR = 'INTERNAL_ERROR',
}
export class DBALError extends Error {
code: DBALErrorCode
constructor(message: string, code: DBALErrorCode) {
super(message)
this.code = code
this.name = 'DBALError'
}
}
/**
* DBAL Integration Manager
*
* Manages the DBAL client instance and provides helper methods for
* integrating with the existing MetaBuilder system.
*/
export class DBALIntegration {
private static instance: DBALIntegration | null = null
private client: DBALClient | null = null
private tenantManager: InMemoryTenantManager | null = null
private kvStore: InMemoryKVStore | null = null
private blobStorage: TenantAwareBlobStorage | null = null
private blobStorage: InMemoryBlobStorage | null = null
private initialized = false
private constructor() {}
@@ -48,53 +166,34 @@ export class DBALIntegration {
try {
// Initialize tenant manager
this.tenantManager = new InMemoryTenantManager()
// Create default tenant for the application
await this.tenantManager.createTenant('default', {
maxBlobStorageBytes: 1024 * 1024 * 1024, // 1GB
maxBlobCount: 10000,
maxBlobSizeBytes: 100 * 1024 * 1024, // 100MB per file
maxBlobStorageBytes: 1024 * 1024 * 1024,
maxRecords: 100000,
maxDataSizeBytes: 500 * 1024 * 1024, // 500MB
maxListLength: 10000,
})
// Initialize KV store
this.kvStore = new InMemoryKVStore()
// Initialize blob storage (in-memory for browser)
const baseStorage = createBlobStorage({
type: 'memory',
})
// Wrap with tenant-aware storage
this.blobStorage = new TenantAwareBlobStorage(
baseStorage,
this.tenantManager,
'default',
'system'
)
// Initialize blob storage
this.blobStorage = new InMemoryBlobStorage()
// Initialize DBAL client
const dbalConfig: DBALConfig = {
mode: 'development',
adapter: 'prisma', // Use Prisma adapter
adapter: 'stub',
...config,
}
this.client = new DBALClient(dbalConfig)
this.initialized = true
console.log('DBAL Integration initialized successfully')
console.log('DBAL Integration initialized successfully (stub mode)')
} catch (error) {
console.error('Failed to initialize DBAL:', error)
throw error
}
}
/**
* Get the DBAL client instance
*/
getClient(): DBALClient {
if (!this.client) {
throw new Error('DBAL not initialized. Call initialize() first.')
@@ -102,9 +201,6 @@ export class DBALIntegration {
return this.client
}
/**
* Get the tenant manager
*/
getTenantManager(): InMemoryTenantManager {
if (!this.tenantManager) {
throw new Error('DBAL not initialized. Call initialize() first.')
@@ -112,9 +208,6 @@ export class DBALIntegration {
return this.tenantManager
}
/**
* Get the KV store
*/
getKVStore(): InMemoryKVStore {
if (!this.kvStore) {
throw new Error('DBAL not initialized. Call initialize() first.')
@@ -122,26 +215,17 @@ export class DBALIntegration {
return this.kvStore
}
/**
* Get the blob storage
*/
getBlobStorage(): TenantAwareBlobStorage {
getBlobStorage(): InMemoryBlobStorage {
if (!this.blobStorage) {
throw new Error('DBAL not initialized. Call initialize() first.')
}
return this.blobStorage
}
/**
* Check if DBAL is initialized
*/
isInitialized(): boolean {
return this.initialized
}
/**
* Reset the DBAL integration (useful for testing)
*/
reset(): void {
this.client = null
this.tenantManager = null
@@ -150,194 +234,80 @@ export class DBALIntegration {
this.initialized = false
}
/**
* Store data in the KV store with automatic tenant scoping
*/
async kvSet(
key: string,
value: any,
ttl?: number,
tenantId: string = 'default',
userId: string = 'system'
): Promise<void> {
if (!this.kvStore || !this.tenantManager) {
throw new Error('DBAL not initialized')
}
// KV Store operations
async kvSet(key: string, value: any, ttl?: number, tenantId = 'default', userId = 'system'): Promise<void> {
if (!this.kvStore || !this.tenantManager) throw new Error('DBAL not initialized')
const context = await this.tenantManager.getTenantContext(tenantId, userId)
if (!context) {
throw new Error(`Tenant context not found: ${tenantId}`)
}
if (!context) throw new Error(`Tenant not found: ${tenantId}`)
await this.kvStore.set(key, value, context, ttl)
}
/**
* Get data from the KV store
*/
async kvGet<T = any>(
key: string,
tenantId: string = 'default',
userId: string = 'system'
): Promise<T | null> {
if (!this.kvStore || !this.tenantManager) {
throw new Error('DBAL not initialized')
}
async kvGet<T = any>(key: string, tenantId = 'default', userId = 'system'): Promise<T | null> {
if (!this.kvStore || !this.tenantManager) throw new Error('DBAL not initialized')
const context = await this.tenantManager.getTenantContext(tenantId, userId)
if (!context) {
throw new Error(`Tenant context not found: ${tenantId}`)
}
if (!context) throw new Error(`Tenant not found: ${tenantId}`)
return this.kvStore.get(key, context) as T | null
}
/**
* Delete data from the KV store
*/
async kvDelete(
key: string,
tenantId: string = 'default',
userId: string = 'system'
): Promise<boolean> {
if (!this.kvStore || !this.tenantManager) {
throw new Error('DBAL not initialized')
}
async kvDelete(key: string, tenantId = 'default', userId = 'system'): Promise<boolean> {
if (!this.kvStore || !this.tenantManager) throw new Error('DBAL not initialized')
const context = await this.tenantManager.getTenantContext(tenantId, userId)
if (!context) {
throw new Error(`Tenant context not found: ${tenantId}`)
}
if (!context) throw new Error(`Tenant not found: ${tenantId}`)
return this.kvStore.delete(key, context)
}
/**
* List operations for the KV store
*/
async kvListAdd(
key: string,
items: any[],
tenantId: string = 'default',
userId: string = 'system'
): Promise<void> {
if (!this.kvStore || !this.tenantManager) {
throw new Error('DBAL not initialized')
}
async kvListAdd(key: string, items: any[], tenantId = 'default', userId = 'system'): Promise<void> {
if (!this.kvStore || !this.tenantManager) throw new Error('DBAL not initialized')
const context = await this.tenantManager.getTenantContext(tenantId, userId)
if (!context) {
throw new Error(`Tenant context not found: ${tenantId}`)
}
if (!context) throw new Error(`Tenant not found: ${tenantId}`)
await this.kvStore.listAdd(key, items, context)
}
async kvListGet(
key: string,
tenantId: string = 'default',
userId: string = 'system',
start?: number,
end?: number
): Promise<any[]> {
if (!this.kvStore || !this.tenantManager) {
throw new Error('DBAL not initialized')
}
async kvListGet(key: string, tenantId = 'default', userId = 'system', start?: number, end?: number): Promise<any[]> {
if (!this.kvStore || !this.tenantManager) throw new Error('DBAL not initialized')
const context = await this.tenantManager.getTenantContext(tenantId, userId)
if (!context) {
throw new Error(`Tenant context not found: ${tenantId}`)
}
if (!context) throw new Error(`Tenant not found: ${tenantId}`)
return this.kvStore.listGet(key, context, start, end)
}
/**
* Upload blob data
*/
async blobUpload(
key: string,
data: Buffer | Uint8Array,
metadata?: Record<string, string>
): Promise<void> {
if (!this.blobStorage) {
throw new Error('DBAL not initialized')
}
// Blob operations
async blobUpload(key: string, data: Buffer | Uint8Array, metadata?: Record<string, string>): Promise<void> {
if (!this.blobStorage) throw new Error('DBAL not initialized')
await this.blobStorage.upload(key, data as Buffer, metadata)
}
/**
* Download blob data
*/
async blobDownload(key: string): Promise<Buffer> {
if (!this.blobStorage) {
throw new Error('DBAL not initialized')
}
if (!this.blobStorage) throw new Error('DBAL not initialized')
return this.blobStorage.download(key)
}
/**
* Delete blob data
*/
async blobDelete(key: string): Promise<void> {
if (!this.blobStorage) {
throw new Error('DBAL not initialized')
}
if (!this.blobStorage) throw new Error('DBAL not initialized')
await this.blobStorage.delete(key)
}
/**
* List blobs with optional prefix filter
*/
async blobList(prefix?: string): Promise<string[]> {
if (!this.blobStorage) {
throw new Error('DBAL not initialized')
}
if (!this.blobStorage) throw new Error('DBAL not initialized')
const result = await this.blobStorage.list({ prefix })
return result.items.map(item => item.key)
}
/**
* Get blob metadata
*/
async blobGetMetadata(key: string): Promise<Record<string, string>> {
if (!this.blobStorage) {
throw new Error('DBAL not initialized')
}
if (!this.blobStorage) throw new Error('DBAL not initialized')
const metadata = await this.blobStorage.getMetadata(key)
return metadata.customMetadata || {}
}
/**
* Handle DBAL errors gracefully
*/
handleError(error: unknown): { message: string; code?: DBALErrorCode } {
if (error instanceof DBALError) {
return {
message: error.message,
code: error.code,
}
return { message: error.message, code: error.code }
}
if (error instanceof Error) {
return {
message: error.message,
}
}
return {
message: 'An unknown error occurred',
return { message: error.message }
}
return { message: 'An unknown error occurred' }
}
}
// Export singleton instance
export const dbal = DBALIntegration.getInstance()
// Export for convenience
export { DBALError, DBALErrorCode }

View File

@@ -10,6 +10,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
// Error codes for DBAL operations
export enum DBALErrorCode {
UNKNOWN = 'UNKNOWN',
NOT_FOUND = 'NOT_FOUND',
VALIDATION = 'VALIDATION',
PERMISSION = 'PERMISSION',
CONNECTION = 'CONNECTION',
TIMEOUT = 'TIMEOUT',
DUPLICATE = 'DUPLICATE',
}
// Custom error class for DBAL operations
export class DBALError extends Error {
code: DBALErrorCode
constructor(message: string, code: DBALErrorCode = DBALErrorCode.UNKNOWN) {
super(message)
this.name = 'DBALError'
this.code = code
}
}
export interface DBALConfig {
mode?: 'development' | 'production'
adapter?: string