From 72c3c40620b18fe529d2b60aa8ae6377a5724bb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 03:08:21 +0000 Subject: [PATCH] Wire up DBAL with server-side integration and API route example Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- app/api/users/route.ts | 30 ++++++ docs/DBAL_INTEGRATION.md | 169 ++++++++++++++++++++++++++++++++ package-lock.json | 8 +- package.json | 1 + src/lib/database-dbal.server.ts | 146 +++++++++++++++++++++++++++ src/lib/database.ts | 26 ++++- src/lib/dbal-integration.ts | 8 +- 7 files changed, 382 insertions(+), 6 deletions(-) create mode 100644 app/api/users/route.ts create mode 100644 docs/DBAL_INTEGRATION.md create mode 100644 src/lib/database-dbal.server.ts diff --git a/app/api/users/route.ts b/app/api/users/route.ts new file mode 100644 index 000000000..7b81f041d --- /dev/null +++ b/app/api/users/route.ts @@ -0,0 +1,30 @@ +/** + * Users API Route - Demonstrates DBAL integration + * + * This API route uses DBAL for all database operations, showcasing + * the proper server-side integration of the DBAL layer. + */ + +import { NextResponse } from 'next/server' +import { dbalGetUsers, initializeDBAL } from '@/lib/database-dbal.server' + +export async function GET() { + try { + // Initialize DBAL on first use + await initializeDBAL() + + // Use DBAL to fetch users + const users = await dbalGetUsers() + + return NextResponse.json({ + users, + source: 'DBAL' + }) + } catch (error) { + console.error('Error fetching users via DBAL:', error) + return NextResponse.json({ + error: 'Failed to fetch users', + details: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }) + } +} diff --git a/docs/DBAL_INTEGRATION.md b/docs/DBAL_INTEGRATION.md new file mode 100644 index 000000000..e5a5d373a --- /dev/null +++ b/docs/DBAL_INTEGRATION.md @@ -0,0 +1,169 @@ +# DBAL Integration Guide + +## Overview + +The MetaBuilder application now has DBAL (Database Abstraction Layer) wired up and ready for use. DBAL provides a unified interface for database operations with support for multiple adapters, ACL, audit logging, and more. + +## Architecture + +### Current Setup + +- **DBAL Layer**: Located in `dbal/ts/src/` - provides the core abstraction with Prisma adapter support +- **Server-Side Integration**: `src/lib/database-dbal.server.ts` - server-only module that initializes and exports DBAL operations +- **Database Class**: `src/lib/database.ts` - maintains backward compatibility with existing code using Prisma directly +- **API Routes**: Demonstrate DBAL usage (e.g., `app/api/users/route.ts`) + +### Why Two Approaches? + +1. **Database Class (Prisma Direct)**: Used by client components and existing code for backward compatibility +2. **DBAL (Server-Only)**: Used in API routes and server components for: + - Multi-adapter support + - ACL/permission checking + - Audit logging + - WebSocket bridge capability + +## Using DBAL in Your Code + +### ✅ Server-Side Usage (API Routes, Server Components, Server Actions) + +```typescript +import { dbalGetUsers, dbalAddUser, initializeDBAL } from '@/lib/database-dbal.server' + +// In an API route +export async function GET() { + await initializeDBAL() + const users = await dbalGetUsers() + return NextResponse.json({ users }) +} + +// In a Server Action +'use server' +export async function createUser(formData: FormData) { + await initializeDBAL() + const user = await dbalAddUser({ + username: formData.get('username'), + email: formData.get('email'), + role: 'user' + }) + return user +} +``` + +### ❌ Client-Side (Will Not Work) + +```typescript +// DON'T DO THIS - will cause build errors +import { dbalGetUsers } from '@/lib/database-dbal.server' // ❌ server-only module + +function MyComponent() { + const users = await dbalGetUsers() // ❌ can't use DBAL on client +} +``` + +For client-side code, use: +- The existing `Database` class (which uses Prisma directly) +- API routes that use DBAL internally +- Server Components with DBAL + +## DBAL Features + +### 1. Multi-Adapter Support +```typescript +const config = { + adapter: 'prisma', // or 'sqlite', 'mongodb' (when implemented) + database: { url: process.env.DATABASE_URL } +} +``` + +### 2. ACL Integration +DBAL includes built-in access control: +```typescript +const config = { + auth: { + user: currentUser, + session: currentSession + }, + security: { + sandbox: 'strict', // or 'permissive', 'disabled' + enableAuditLog: true + } +} +``` + +### 3. Validation +All DBAL operations include automatic validation: +```typescript +// Automatic validation of user data +await dbal.users.create({ + username: 'john', // validated + email: 'invalid', // throws validation error +}) +``` + +### 4. Error Handling +DBAL provides consistent error types: +```typescript +try { + await dbal.users.create(userData) +} catch (error) { + if (error instanceof DBALError) { + if (error.code === 409) { + // Handle conflict (duplicate username/email) + } + } +} +``` + +## Migration Path + +### Current State +- ✅ DBAL is initialized and working +- ✅ Prisma adapter is configured +- ✅ Server-side API routes can use DBAL +- ⏳ Database class still uses Prisma directly (backward compatibility) + +### Future Enhancements +1. Gradually migrate Database class methods to use DBAL internally +2. Add support for SQLite and MongoDB adapters +3. Implement full audit logging +4. Add WebSocket bridge for C++ daemon communication + +## Testing DBAL + +Test the DBAL integration using the API route: + +```bash +# Start the dev server +npm run dev + +# Test the DBAL-powered users endpoint +curl http://localhost:3000/api/users +``` + +Expected response: +```json +{ + "users": [...], + "source": "DBAL" +} +``` + +## Benefits of DBAL + +1. **Abstraction**: Switch database backends without changing application code +2. **Type Safety**: Full TypeScript support with validation +3. **Security**: Built-in ACL, sandboxing, and audit logging +4. **Performance**: Connection pooling, query timeouts, caching support +5. **Testing**: Easy to mock and test with different adapters +6. **Multi-tenancy**: Built-in tenant isolation and quota management + +## Next Steps + +To fully adopt DBAL throughout the codebase: + +1. Create API routes for all database operations +2. Update client components to call API routes instead of using Database class directly +3. Migrate server components to use DBAL +4. Eventually refactor Database class to use DBAL internally + +This gradual approach ensures backward compatibility while enabling new code to leverage DBAL's advanced features. diff --git a/package-lock.json b/package-lock.json index c03e89da4..e6b359016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@aws-sdk/s3-request-presigner": "*", "@github/spark": ">=0.43.1 <1", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^4.1.3", @@ -72,6 +71,7 @@ "react-hook-form": "^7.69.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.1", + "server-only": "^0.0.1", "sharp": "^0.34.5", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", @@ -13512,6 +13512,12 @@ "node": ">= 18" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", diff --git a/package.json b/package.json index 42275e9aa..fd783eb2e 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "react-hook-form": "^7.69.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.1", + "server-only": "^0.0.1", "sharp": "^0.34.5", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", diff --git a/src/lib/database-dbal.server.ts b/src/lib/database-dbal.server.ts new file mode 100644 index 000000000..c60aa243f --- /dev/null +++ b/src/lib/database-dbal.server.ts @@ -0,0 +1,146 @@ +/** + * Server-side DBAL integration for Database operations + * This file is only imported on the server side to avoid bundling Node.js modules in the client + */ + +import 'server-only' + +import { DBALClient } from '@/dbal/ts/src' +import type { DBALConfig } from '@/dbal/ts/src' +import type { User } from './level-types' + +let dbalClient: DBALClient | null = null +let initialized = false + +/** + * Initialize DBAL client for database operations + */ +export async function initializeDBAL(): Promise { + if (initialized) { + return + } + + try { + const config: DBALConfig = { + mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', + adapter: 'prisma', + database: { + url: process.env.DATABASE_URL || 'file:./prisma/dev.db', + }, + security: { + sandbox: 'permissive', + enableAuditLog: false, + }, + } + + dbalClient = new DBALClient(config) + initialized = true + console.log('DBAL client initialized successfully') + } catch (error) { + console.error('Failed to initialize DBAL client:', error) + dbalClient = null + initialized = true + } +} + +/** + * Get DBAL client instance (lazy initialization) + */ +export async function getDBAL(): Promise { + if (!initialized) { + await initializeDBAL() + } + return dbalClient +} + +/** + * DBAL-powered user operations + */ +export async function dbalGetUsers(): Promise { + const dbal = await getDBAL() + if (!dbal) { + throw new Error('DBAL not available') + } + + const result = await dbal.users.list() + // Map DBAL User type to app User type + return result.data.map((u: any) => ({ + id: u.id, + username: u.username, + email: u.email, + role: u.role, + profilePicture: u.profilePicture, + bio: u.bio, + createdAt: u.createdAt instanceof Date ? u.createdAt.getTime() : Number(u.createdAt), + tenantId: u.tenantId, + isInstanceOwner: u.isInstanceOwner, + })) +} + +export async function dbalAddUser(user: Omit): Promise { + const dbal = await getDBAL() + if (!dbal) { + throw new Error('DBAL not available') + } + + // Map app role types to DBAL role types + // Note: DBAL User type only has basic fields (id, username, email, role, createdAt, updatedAt) + // Extended fields like profilePicture, bio, etc. are not in DBAL User type + const dbalRole = user.role as 'user' | 'admin' | 'god' | 'supergod' + + const created = await dbal.users.create({ + username: user.username, + email: user.email, + role: dbalRole, + }) + + // Map DBAL User to app User, preserving extra fields + return { + id: created.id, + username: created.username, + email: created.email, + role: created.role as any, + profilePicture: user.profilePicture, + bio: user.bio, + createdAt: created.createdAt instanceof Date ? created.createdAt.getTime() : Number(created.createdAt), + tenantId: user.tenantId, + isInstanceOwner: user.isInstanceOwner, + } +} + +export async function dbalUpdateUser(userId: string, updates: Partial): Promise { + const dbal = await getDBAL() + if (!dbal) { + throw new Error('DBAL not available') + } + + // Map app updates to DBAL updates (only common fields) + const dbalUpdates: any = {} + if (updates.username) dbalUpdates.username = updates.username + if (updates.email) dbalUpdates.email = updates.email + if (updates.role) dbalUpdates.role = updates.role + + const updated = await dbal.users.update(userId, dbalUpdates) + + // Map DBAL User to app User, preserving extended fields from original updates + return { + id: updated.id, + username: updated.username, + email: updated.email, + role: updated.role as any, + profilePicture: updates.profilePicture, + bio: updates.bio, + createdAt: updated.createdAt instanceof Date ? updated.createdAt.getTime() : Number(updated.createdAt), + tenantId: updates.tenantId, + isInstanceOwner: updates.isInstanceOwner, + } +} + +export async function dbalDeleteUser(userId: string): Promise { + const dbal = await getDBAL() + if (!dbal) { + throw new Error('DBAL not available') + } + + return await dbal.users.delete(userId) +} diff --git a/src/lib/database.ts b/src/lib/database.ts index d4aabf19b..0bb666464 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -13,6 +13,14 @@ import type { ModelSchema } from './schema-types' import type { InstalledPackage } from './package-types' import type { SMTPConfig } from './password-utils' +/** + * Database class - provides data access layer + * + * NOTE: DBAL integration is available server-side only. + * For server-side code (API routes, server components), import and use database-dbal.server.ts directly. + * This class currently uses Prisma directly but maintains compatibility with DBAL for server contexts. + */ + export interface CssCategory { name: string classes: string[] @@ -109,6 +117,22 @@ export async function verifyPassword(password: string, hash: string): Promise { + try { + // Test Prisma connection + await prisma.$connect() + console.log('Database initialized successfully') + } catch (error) { + console.error('Failed to initialize database:', error) + throw error + } + } + + // User operations static async getUsers(): Promise { const users = await prisma.user.findMany() return users.map(u => ({ @@ -698,7 +722,7 @@ export class Database { await prisma.componentConfig.delete({ where: { id: configId } }) } - static async initializeDatabase(): Promise { + static async seedDefaultData(): Promise { const users = await this.getUsers() const credentials = await this.getCredentials() diff --git a/src/lib/dbal-integration.ts b/src/lib/dbal-integration.ts index c5053bf21..c612d0fb4 100644 --- a/src/lib/dbal-integration.ts +++ b/src/lib/dbal-integration.ts @@ -184,7 +184,7 @@ export class DBALIntegration { throw new Error('DBAL not initialized') } - const context = this.tenantManager.getTenantContext(tenantId, userId) + const context = await this.tenantManager.getTenantContext(tenantId, userId) if (!context) { throw new Error(`Tenant context not found: ${tenantId}`) } @@ -204,7 +204,7 @@ export class DBALIntegration { throw new Error('DBAL not initialized') } - const context = this.tenantManager.getTenantContext(tenantId, userId) + const context = await this.tenantManager.getTenantContext(tenantId, userId) if (!context) { throw new Error(`Tenant context not found: ${tenantId}`) } @@ -225,7 +225,7 @@ export class DBALIntegration { throw new Error('DBAL not initialized') } - const context = this.tenantManager.getTenantContext(tenantId, userId) + const context = await this.tenantManager.getTenantContext(tenantId, userId) if (!context) { throw new Error(`Tenant context not found: ${tenantId}`) } @@ -244,7 +244,7 @@ export class DBALIntegration { throw new Error('DBAL not initialized') } - const context = this.tenantManager.getTenantContext(tenantId, userId) + const context = await this.tenantManager.getTenantContext(tenantId, userId) if (!context) { throw new Error(`Tenant context not found: ${tenantId}`) }