Repository navigation
feat(dbmanager): implement enterprise dbmanager and integrate with co… #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d6005f
96fb163
9df4104
3d62984
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,8 +20,9 @@ import { | |||||||
| DATABASE_CONNECTION, | ||||||||
| type DrizzleDb, | ||||||||
| } from '@nexiom/database'; | ||||||||
| import { sql } from 'drizzle-orm'; | ||||||||
| import * as crypto from 'crypto'; | ||||||||
| import { DatabaseManager, SchemaPlan } from '@nexiom/dbmanager'; | ||||||||
| import { DB_MANAGER } from '../dbmanager/dbmanager.module'; | ||||||||
| import * as crypto from 'node:crypto'; | ||||||||
|
|
||||||||
| /** Encrypted value blob stored in app_connection.value — mirrors Activepieces BaseOAuth2ConnectionValue */ | ||||||||
| export interface ConnectionValueBlob { | ||||||||
|
|
@@ -55,6 +56,7 @@ export class ConnectorsService { | |||||||
|
|
||||||||
| constructor( | ||||||||
| @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, | ||||||||
| @Inject(DB_MANAGER) private readonly dbManager: DatabaseManager, | ||||||||
| private readonly providerRegistry: ProviderRegistryService, | ||||||||
| private readonly configService: ConfigService, | ||||||||
| ) {} | ||||||||
|
|
@@ -297,7 +299,7 @@ export class ConnectorsService { | |||||||
| } | ||||||||
|
|
||||||||
| try { | ||||||||
| await this.db.transaction(async (tx) => { | ||||||||
| const workspaceSchemaName = await this.db.transaction(async (tx) => { | ||||||||
| // 1. Insert or update the business connection metadata | ||||||||
| const [connection] = await tx | ||||||||
| .insert(appConnections) | ||||||||
|
|
@@ -340,27 +342,30 @@ export class ConnectorsService { | |||||||
| const sanitizedProvider = providerName.replaceAll(/[^a-z0-9]/g, ''); | ||||||||
| const finalProviderToken = sanitizedProvider || 'unknown'; | ||||||||
| const safeToken = finalProviderToken.substring(0, 40); | ||||||||
| const workspaceSchemaName = `ws_${safeToken}_${hashedSuffix}`; | ||||||||
| const schemaName = `ws_${safeToken}_${hashedSuffix}`; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Verify schema name length constraints. The generated 📝 Consider adding a constant for documentation+ // Max schema name: ws_ (3) + provider (40) + _ (1) + hash (16) = 60 chars (< PG 63-byte limit)
const schemaName = `ws_${safeToken}_${hashedSuffix}`;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| await tx | ||||||||
| .insert(connectionStorageRegistry) | ||||||||
| .values({ | ||||||||
| connectionId: connection.id, | ||||||||
| workspaceId: workspaceSchemaName, | ||||||||
| workspaceId: schemaName, | ||||||||
| databaseHostId: 'primary-cluster', // Can be parameterized later for regional sharding | ||||||||
| regionContext: finalRegionContext, | ||||||||
| }) | ||||||||
| .onConflictDoNothing({ | ||||||||
| target: connectionStorageRegistry.connectionId, | ||||||||
| }); // Already provisioned | ||||||||
|
|
||||||||
| // 3. Actually create the physical PostgreSQL schema on the cluster | ||||||||
| // Using sql.raw here is safe because workspaceSchemaName is strictly internally generated | ||||||||
| // from a regex-sanitized string and a UUID slice, not user input. | ||||||||
| await tx.execute( | ||||||||
| sql`CREATE SCHEMA IF NOT EXISTS "${sql.raw(workspaceSchemaName)}"`, | ||||||||
| ); | ||||||||
| return schemaName; | ||||||||
| }); | ||||||||
|
|
||||||||
| // 3. Apply the initial schema plan (NAMESPACE_ONLY) outside the transaction | ||||||||
| // so the DDL runs on its own connection and does not silently escape the | ||||||||
| // Drizzle tx scope (db.$client vs the transaction's dedicated connection). | ||||||||
| await this.dbManager.applyPlan( | ||||||||
| workspaceSchemaName, | ||||||||
| SchemaPlan.NAMESPACE_ONLY, | ||||||||
| ); | ||||||||
|
Comment on lines
+362
to
+368
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's understand the structure of the connectors.service.ts file
fd connectors.service.ts --type fRepository: pramodnarayana/nexiom Length of output: 119 🏁 Script executed: # Search for the storeOAuthConnection function and surrounding context
rg -n "storeOAuthConnection" --type=ts -A 70 | head -100Repository: pramodnarayana/nexiom Length of output: 9517 🏁 Script executed: # Search for applyPlan function definition
rg -n "applyPlan\s*\(" --type=ts -B 2 -A 10Repository: pramodnarayana/nexiom Length of output: 4011 🏁 Script executed: # Search for PENDING_PROVISIONING or similar provisioning statuses
rg -n "PENDING_PROVISIONING|ProvisioningStatus|ConnectionStatus" --type=tsRepository: pramodnarayana/nexiom Length of output: 1328 🏁 Script executed: # Check for retry mechanisms and error handling patterns
rg -n "retry|Retry|RETRY" --type=ts -A 3 | head -80Repository: pramodnarayana/nexiom Length of output: 6145 🏁 Script executed: # Search for connectionStorageRegistry definition and usage
rg -n "connectionStorageRegistry" --type=ts -B 2 -A 5 | head -100Repository: pramodnarayana/nexiom Length of output: 9413 🏁 Script executed: # Get the full storeOAuthConnection function to see complete error handling
sed -n '278,385p' apps/api/src/modules/connections/connectors.service.tsRepository: pramodnarayana/nexiom Length of output: 3523 🏁 Script executed: # Check the full AppConnectionStatus enum definition
cat packages/database/src/schema/tenant.ts | head -30Repository: pramodnarayana/nexiom Length of output: 1323 🏁 Script executed: # Search for background jobs or reconciliation mechanisms
rg -n "reconcil|background|cron|job|cleanup" --type=ts -i | grep -E "(reconcil|background|cron|cleanup)" | head -40Repository: pramodnarayana/nexiom Length of output: 4646 🏁 Script executed: # Check if there's any mechanism to handle orphaned connections
rg -n "orphan|dangling|incomplete|unprovisioned" --type=ts -iRepository: pramodnarayana/nexiom Length of output: 1251 🏁 Script executed: # Look for error recovery or compensating transaction patterns
rg -n "compensat|rollback|cleanup|recover" --type=ts -i | head -30Repository: pramodnarayana/nexiom Length of output: 2937 Fix partial failure vulnerability: If Implement one of:
|
||||||||
| } catch (error) { | ||||||||
| if (error instanceof HttpException) { | ||||||||
| throw error; | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Module, Global } from '@nestjs/common'; | ||
| import { SqlDatabaseManager } from '@nexiom/dbmanager'; | ||
| import { DbModule } from '../../db/db.module'; | ||
| import { DATABASE_CONNECTION } from '@nexiom/database'; | ||
| import type { DrizzleDb } from '@nexiom/database'; | ||
|
|
||
| export const DB_MANAGER = 'DATABASE_MANAGER'; | ||
|
|
||
| @Global() | ||
| @Module({ | ||
| imports: [DbModule], | ||
| providers: [ | ||
| { | ||
| provide: DB_MANAGER, | ||
| useFactory: (drizzleDb: DrizzleDb) => { | ||
| return new SqlDatabaseManager(drizzleDb); | ||
| }, | ||
| inject: [DATABASE_CONNECTION], | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ], | ||
| exports: [DB_MANAGER], | ||
| }) | ||
| export class DbManagerModule {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Architecture: Schema Lifecycle & Lazy Provisioning | ||
|
|
||
| This document defines the timing and triggers for database schema creation within the FluxNex 6-layer pipeline. To ensure maximum scalability, we follow a Lazy Provisioning strategy—nothing is created until there is active intent from the user. | ||
|
|
||
| 1. Provisioning Lifecycle Summary | ||
| | Lifecycle Event | Physical Action | Component Responsible | Status | | ||
| | --------------- | --------------- | ----------------------- | ------ | | ||
| | App Authentication | Create Schema Namespace (ws_{id}) | api-gateway / dbmanager | Empty Silo | | ||
| | Route Activation | Create Kernel Tables (Ingest/Logs) | dbmanager (Atlas) | Active Silo | | ||
| | Object Selection | Initialize Unified Replica (replica_entity) | dbmanager (Atlas) | Data Ready | | ||
|
|
||
| 2. Event-Driven Provisioning Details | ||
| Stage 1: Connection Handshake (The "Namespace" Event) | ||
| Trigger: User successfully completes an OAuth or API Key handshake. | ||
| Database Action: CREATE SCHEMA IF NOT EXISTS "ws_sf_101". | ||
| Registry Action: Insert row into public.connection_storage_registry. | ||
| Result: A physical "Wall" is established. The schema is empty, incurring zero index or metadata overhead in the Postgres catalog. | ||
| Stage 2: Route Activation (The "Kernel" Event) | ||
| Trigger: User creates a Route (e.g., Salesforce → QuickBooks) and clicks "Save & Activate." | ||
| Database Action: The dbmanager uses Atlas to deploy the Pipeline Kernel tables into the connection's schema. | ||
| Tables Created: * `inbound_gateway`: Stores raw ingestion data. | ||
| *(Note: `sync_log`, `sync_cursor`, and `outbound_gateway` are planned additions for future Gateway integration).* | ||
| Stage 3: Object Mapping (The "Storage Initialization" Event) | ||
| Trigger: User selects a specific object (e.g., "Accounts") in the Mapping UI. | ||
| Database Action: The system ensures the structured state store is ready. It executes the SQL to create: | ||
| replica_entity: The unified JSONB state store for ALL objects in this connection. | ||
| GIN Indexes: Applied to the data column to enable high-speed search across all fields and custom objects. | ||
| Optimization: This table is created once per connection and handles all object types (Account, Contact, etc.) through the entity_type column. | ||
|
|
||
| 3. The 6-Layer Readiness Matrix | ||
| This matrix shows which physical tables must be "ready" before a specific layer worker can process a job. | ||
| | Layer | Dependency | Physical Location | Why? | | ||
| | ----- | ---------- | ----------------- | ---- | | ||
| | L1: Source Gateway | inbound_gateway | ws_source | To log the "evidence" of the raw webhook. | | ||
| | L2: Universal Replica | replica_entity | ws_source | To store the parsed source-of-truth. | | ||
| | L3: Normalization | normalized_entity | ws_source | To store the platform-standard JSON. | | ||
| | L4: Outbound Prep | field_mapping | public | To read the rules defined by the user. | | ||
| | L5: Delivery Engine | N/A | N/A | Pure I/O. Does not write to tenant schemas. | | ||
| | L6: Dest. Gateway | outbound_gateway | ws_dest | To log the destination API response. | | ||
|
|
||
| 4. Engineering Implementation: The "Ensure" Pattern | ||
| Because workers are asynchronous, a webhook (L1) might arrive before the dbmanager has finished creating the tables. To prevent crashes, all workers follow the Ensure Pattern: | ||
| // packages/engine/src/workers/base.worker.ts | ||
| async function processLayerJob(connectionId: string, payload: any) { | ||
| // 1. Resolve Schema Name | ||
| const schema = await storageResolver.resolve(connectionId); | ||
|
|
||
| // 2. JIT Check (The "Ensure" step) | ||
| // If the tables are missing, the worker triggers a fast schema plan apply | ||
| await dbmanager.applyPlan(schema, SchemaPlan.GATEWAY_ACTIVE); | ||
|
|
||
| // 3. Perform Business Logic | ||
| await db.withSchema(schema).insert(...); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| 1. Benefits of this Timing | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix section numbering in the heading. Line 56 resets numbering to 🤖 Prompt for AI Agents |
||
| Catalog Leanliness: We avoid the "Postgres Metadata Bloat" that occurs when thousands of unused tables exist in the system. | ||
| Speed: App connection is instantaneous because it only creates a namespace. | ||
| Isolation: The architecture guarantees that no worker ever attempts to write to a "Shared" schema; it is always scoped to the connection-specific workspace. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test will fail due to missing
regionContextmock.The
ConfigServicemock at line 595 returnsundefinedfor allget()calls, causing theregionContextvalidation (lines 290-298 in the service) to throwInternalServerErrorExceptionbeforeapplyPlanis ever called. This means the test isn't actually exercising theapplyPlanfailure path.🐛 Proposed fix
{ provide: ProviderRegistryService, useValue: mockProviderRegistry }, - { provide: ConfigService, useValue: { get: vi.fn() } }, + { provide: ConfigService, useValue: { get: vi.fn().mockReturnValue('us-east-1') } }, { provide: EncryptionService, useValue: mockEncryptionService },📝 Committable suggestion
🤖 Prompt for AI Agents