02fda92a93
## Context This PR introduces a Global Workspace DataSource that consolidates workspace-specific database access through a single TypeORM DataSource instance with AsyncLocalStorage-based context management instead of N workspace datasources. - Created GlobalWorkspaceDataSource extending TypeORM's DataSource to manage multiple workspaces with entity metadata caching (1-hour TTL) - Implemented AsyncLocalStorage for workspace context propagation (WorkspaceContextForStorage) containing workspace ID, metadata, permissions, and feature flags - Modified query execution flow to wrap operations in workspace context via GlobalWorkspaceOrmManager.executeInWorkspaceContext() - Added schema name to entity schemas for proper multi-tenant database separation Next: - use the new global workspace datasource everywhere and deprecate workspace datasource factory - improve metadata caching using a short TTL to avoid multiple calls to redis - Leverage the new WorkspaceContextALS and put it higher in the request hierarchy to have access to permission, metadata and featureflag everywhere --- build it manually for commands --- find a way to propagate it in jobs? - Remove PG_POOL patch once we have a unique datasource and increase global datasource pool size ## Implementation Why ALS: 1. Automatic Per-Request Isolation With schema-based multi-tenancy, each workspace has its own PostgreSQL schema (e.g. workspace_20202020-1c25-4d02-bf25-6aeccf7ea419). The critical challenge is ensuring that concurrent requests from different tenants don't interfere with each other. ```typescript // Request A (Workspace 1) and Request B (Workspace 2) executing concurrently // Without ALS: Race condition — they'd share the same global state! // With ALS: Each request has isolated context ✓ ``` ALS automatically isolates context per async execution chain, so: - Request from Tenant A → ALS stores workspaceId: "tenant-a" → Queries hit workspace_tenant_a schema - Request from Tenant B → ALS stores workspaceId: "tenant-b" → Queries hit workspace_tenant_b schema ✅ No interference, even when executing simultaneously on the same Node.js event loop. 2. No Manual Context Passing Before ALS, you'd need to pass workspaceId through every function call: ```typescript // ❌ Without ALS - Context threading nightmare getRepository(workspaceId, entity) → createEntityManager(workspaceId) → getMetadata(workspaceId, target) → findInCache(workspaceId, cacheKey) ``` With ALS: ```typescript // ✅ With ALS - Clean, implicit context getRepository(entity) // Reads workspaceId from ALS → createEntityManager() // Reads workspaceId from ALS → getMetadata(target) // Reads workspaceId from ALS → findInCache(cacheKey) // Reads workspaceId from ALS ``` example ```typescript override findMetadata(target: EntityTarget<ObjectLiteral>): EntityMetadata | undefined { const context = getWorkspaceContext(); // 👈 Automatically gets the right workspace! const { workspaceId, metadataVersion } = context; const cacheKey = `${workspaceId}-${metadataVersion}`; // ... returns metadata for THIS workspace's schema } ``` 3. Async Chain Propagation Node.js operations are heavily async. ALS automatically propagates context through: - async/await chains - Promise chains - Callbacks ```typescript executeInWorkspaceContext(workspaceId, async () => { await prepareContext(); // Has context ✓ const results = await run(); // Has context ✓ await enrichResults(); // Has context ✓ // Even nested async operations maintain context! await Promise.all([ saveToCache(), // Has context ✓ emitEvent(), // Has context ✓ logMetrics(), // Has context ✓ ]); }); ``` 5. Schema-Specific Metadata Caching The implementation caches entity metadata per workspace + version: ```typescript // Cache key format: "workspaceId-metadataVersion" const cacheKey = `${workspaceId}-${metadataVersion}`; ``` Why this matters with schemas: - Each workspace has different table structures (custom fields, objects) - EntitySchema includes schema: "workspace_xxx" property - Each cached metadata points to the correct schema ALS ensures getWorkspaceContext() returns the right workspaceId, so you always get the correct schema's metadata from cache. 6. Single DataSource for All Tenants The key change here: ```typescript // ❌ Old approach: One DataSource per tenant const dataSourceTenantA = new DataSource({ schema: 'workspace_a' }); const dataSourceTenantB = new DataSource({ schema: 'workspace_b' }); // Problem: Hundreds of DB connection pools! ``` ```typescript // ✅ New approach: One shared DataSource + ALS context const globalDataSource = new GlobalWorkspaceDataSource(); // ALS determines which schema to use at runtime ``` When you call: ```typescript globalDataSource.getRepository('person'); ``` It internally does: ```typescript const context = getWorkspaceContext(); // Gets current tenant from ALS const metadata = this.findMetadata('person'); // Finds metadata for THIS tenant's schema // EntityMetadata includes: schema: "workspace_20202020-1c25..." // TypeORM automatically queries: SELECT * FROM "workspace_20202020-1c25...".person ``` 7. Request Lifecycle Example ```typescript // 1. GraphQL request arrives: "query people { ... }" // 2. Middleware extracts authContext.workspace.id = "tenant-a" // 3. Query runner wraps execution in ALS: executeInWorkspaceContext("tenant-a", async () => { // 4. Everything inside has access to workspace context: const repo = getRepository('person'); // ALS → tenant-a const metadata = getMetadata('person'); // ALS → tenant-a → cache["tenant-a-v5"] // 5. TypeORM builds query with correct schema: // SELECT * FROM "workspace_tenant_a"."person" WHERE ... // 6. Even nested calls work: await saveAuditLog(); // ALS → tenant-a → correct audit schema await emitWebhook(); // ALS → tenant-a → correct tenant webhook }); // 7. Request completes, ALS context automatically cleaned up ``` 8. Safety & Error Prevention ```typescript // If you forget to set context: const context = getWorkspaceContext(); // ❌ Throws: "Workspace context not set..." // Fails fast rather than querying wrong schema! // Can't accidentally query wrong tenant: // Context is immutable within execution scope ```