mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat: enhance DBAL integration with improved error handling and in-memory storage implementations
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user