From 8ae3966559b009808bfd7f7663cee500299b5ab2 Mon Sep 17 00:00:00 2001 From: Pramod Narayana Date: Thu, 30 Apr 2026 20:16:14 +0530 Subject: [PATCH 1/9] feat: decouple platform and application logic via GitOps shards - Replaced legacy ApplicationExecutorService with dynamic ApplicationLoaderService - Introduced PipelineHookBrokerService as the sole interface for platform-to-shard communication - Replaced static registries in piece-framework with dynamic shard loads - Implemented webhook-triggered GitOps synchronization (GitopsSyncController, Worker) - Updated TargetBuilder, Replica, and Normalization services to use the new broker pattern - Migrated TenantDatabaseManager CLI to load domain provisioners dynamically - Added tests for GitopsWebhookGuard and updated existing pipeline specs --- .nvmrc | 2 +- apps/api/src/app/app.module.ts | 2 + .../gitops/gitops-sync.controller.spec.ts | 48 ++++++ .../modules/gitops/gitops-sync.controller.ts | 57 +++++++ .../gitops/gitops-webhook.guard.spec.ts | 62 +++++++ .../modules/gitops/gitops-webhook.guard.ts | 53 ++++++ apps/api/src/modules/gitops/gitops.module.ts | 9 + apps/worker/src/db/database-manager.ts | 25 ++- apps/worker/src/main.ts | 4 - .../pipeline/gitops-sync.worker.spec.ts | 52 ++++-- .../modules/pipeline/gitops-sync.worker.ts | 145 +++++++++------- .../pipeline/normalization.service.spec.ts | 14 +- .../modules/pipeline/normalization.service.ts | 94 +++++----- .../src/modules/pipeline/pipeline.module.ts | 4 +- .../modules/pipeline/replica.service.spec.ts | 66 +++---- .../src/modules/pipeline/replica.service.ts | 37 ++-- .../pipeline/target-builder.service.spec.ts | 64 +++---- .../pipeline/target-builder.service.ts | 44 +++-- engine/sync/platform/core/package.json | 1 + engine/sync/platform/core/src/index.ts | 6 +- .../sharding/application-executor.module.ts | 8 + .../application-executor.service.spec.ts | 151 ++++++++++++++++ .../sharding/application-executor.service.ts | 157 +++++++++++++++++ .../src/sharding/application-loader.module.ts | 9 + .../sharding/application-loader.service.ts | 107 ++++++++++++ .../src/sharding/application-shard.types.ts | 83 +++++++++ .../src/sharding/logic-resolver.module.ts | 8 - .../src/sharding/logic-resolver.service.ts | 109 ------------ .../sharding/pipeline-hook-broker.service.ts | 146 ++++++++++++++++ engine/sync/platform/core/vitest.config.ts | 8 + package.json | 10 +- .../platform/framework/src/app-hooks.ts | 13 +- packages/queue/src/constants.ts | 2 + pnpm-lock.yaml | 161 +++++++++++++++++- scripts/init-localstack.sh | 1 + 35 files changed, 1371 insertions(+), 391 deletions(-) create mode 100644 apps/api/src/modules/gitops/gitops-sync.controller.spec.ts create mode 100644 apps/api/src/modules/gitops/gitops-sync.controller.ts create mode 100644 apps/api/src/modules/gitops/gitops-webhook.guard.spec.ts create mode 100644 apps/api/src/modules/gitops/gitops-webhook.guard.ts create mode 100644 apps/api/src/modules/gitops/gitops.module.ts create mode 100644 engine/sync/platform/core/src/sharding/application-executor.module.ts create mode 100644 engine/sync/platform/core/src/sharding/application-executor.service.spec.ts create mode 100644 engine/sync/platform/core/src/sharding/application-executor.service.ts create mode 100644 engine/sync/platform/core/src/sharding/application-loader.module.ts create mode 100644 engine/sync/platform/core/src/sharding/application-loader.service.ts create mode 100644 engine/sync/platform/core/src/sharding/application-shard.types.ts delete mode 100644 engine/sync/platform/core/src/sharding/logic-resolver.module.ts delete mode 100644 engine/sync/platform/core/src/sharding/logic-resolver.service.ts create mode 100644 engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts diff --git a/.nvmrc b/.nvmrc index 209e3ef4..d4b7699d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +20.18.1 diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index d33103b0..7809f4b1 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -35,6 +35,7 @@ import { TraceModule } from '../modules/trace/trace.module.js'; import { ExceptionsModule } from '../modules/exceptions/exceptions.module.js'; import { AiModule } from '../modules/ai/ai.module.js'; import { MappingsModule } from '../modules/mappings/mappings.module.js'; +import { GitopsModule } from '../modules/gitops/gitops.module.js'; @Module({ imports: [ @@ -113,6 +114,7 @@ import { MappingsModule } from '../modules/mappings/mappings.module.js'; ExceptionsModule, AiModule, MappingsModule, + GitopsModule, ], controllers: [AppController], providers: [AppService, ShutdownService], diff --git a/apps/api/src/modules/gitops/gitops-sync.controller.spec.ts b/apps/api/src/modules/gitops/gitops-sync.controller.spec.ts new file mode 100644 index 00000000..6131c8d5 --- /dev/null +++ b/apps/api/src/modules/gitops/gitops-sync.controller.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { GitopsSyncController } from './gitops-sync.controller.js'; +import { GitopsWebhookGuard } from './gitops-webhook.guard.js'; +import { QueueService, QueueName } from '@nexiom/queue'; + +describe('GitopsSyncController', () => { + let controller: GitopsSyncController; + let queueSendSpy: ReturnType; + + beforeEach(async () => { + queueSendSpy = vi.fn().mockResolvedValue(undefined); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [GitopsSyncController], + providers: [ + { + provide: QueueService, + useValue: { send: queueSendSpy }, + }, + ], + }) + // Override the guard so tests don't require ConfigService wiring + .overrideGuard(GitopsWebhookGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(GitopsSyncController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should publish to GitopsQueue and return { accepted: true }', async () => { + const result = await controller.triggerSync(); + + expect(queueSendSpy).toHaveBeenCalledOnce(); + const [queueName, payload] = queueSendSpy.mock.calls[0] as [ + string, + { source: string; triggeredAt: string }, + ]; + expect(queueName).toBe(QueueName.GitopsQueue); + expect(payload.source).toBe('webhook'); + expect(typeof payload.triggeredAt).toBe('string'); + expect(result).toEqual({ accepted: true }); + }); +}); diff --git a/apps/api/src/modules/gitops/gitops-sync.controller.ts b/apps/api/src/modules/gitops/gitops-sync.controller.ts new file mode 100644 index 00000000..f05cdf48 --- /dev/null +++ b/apps/api/src/modules/gitops/gitops-sync.controller.ts @@ -0,0 +1,57 @@ +import { + Controller, + Post, + HttpCode, + HttpStatus, + UseGuards, + Logger, +} from '@nestjs/common'; +import { GitopsWebhookGuard } from './gitops-webhook.guard.js'; +import { QueueService, QueueName } from '@nexiom/queue'; + +/** + * GitopsSyncController + * + * Receives webhook notifications from GitHub/GitLab when application code + * is pushed to the integrations repository. Publishes a message to the + * GitopsQueue so the worker can immediately run git pull and hot-reload + * the updated application shard — without waiting for the 5-minute cron. + * + * Register this endpoint in GitHub: + * Settings → Webhooks → Payload URL: + * https://api.nexiom.io/internal/gitops/sync + * Secret: + * Content type: application/json + * Events: Just the push event + */ +@Controller('internal/gitops') +@UseGuards(GitopsWebhookGuard) +export class GitopsSyncController { + private readonly logger = new Logger(GitopsSyncController.name); + + constructor(private readonly queueService: QueueService) {} + + /** + * POST /internal/gitops/sync + * + * Accepts a push notification from GitHub/GitLab and triggers an + * immediate shard synchronization in the worker. + * + * Returns 202 Accepted immediately — the sync is performed asynchronously + * by the worker consuming the GitopsQueue. + */ + @Post('sync') + @HttpCode(HttpStatus.ACCEPTED) + async triggerSync(): Promise<{ accepted: boolean }> { + this.logger.log( + 'GitOps webhook received — publishing sync request to GitopsQueue', + ); + + await this.queueService.send(QueueName.GitopsQueue, { + triggeredAt: new Date().toISOString(), + source: 'webhook', + }); + + return { accepted: true }; + } +} diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.spec.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.spec.ts new file mode 100644 index 00000000..173d5662 --- /dev/null +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.spec.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Test } from '@nestjs/testing'; +import { UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GitopsWebhookGuard } from './gitops-webhook.guard.js'; +import type { ExecutionContext } from '@nestjs/common'; + +const SECRET = 'test-webhook-secret-value'; + +function buildContext(authHeader?: string): ExecutionContext { + return { + switchToHttp: () => ({ + getRequest: () => ({ + headers: authHeader ? { authorization: authHeader } : {}, + }), + }), + } as unknown as ExecutionContext; +} + +describe('GitopsWebhookGuard', () => { + let guard: GitopsWebhookGuard; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + GitopsWebhookGuard, + { + provide: ConfigService, + useValue: { + getOrThrow: vi.fn().mockReturnValue(SECRET), + }, + }, + ], + }).compile(); + + guard = module.get(GitopsWebhookGuard); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should return true for a valid Bearer token', () => { + const ctx = buildContext(`Bearer ${SECRET}`); + expect(guard.canActivate(ctx)).toBe(true); + }); + + it('should throw UnauthorizedException when Authorization header is absent', () => { + const ctx = buildContext(); + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); + + it('should throw UnauthorizedException for a wrong token', () => { + const ctx = buildContext('Bearer wrong-secret'); + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); + + it('should throw UnauthorizedException when header is not Bearer format', () => { + const ctx = buildContext('Basic dXNlcjpwYXNz'); + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); +}); diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts new file mode 100644 index 00000000..8660e96b --- /dev/null +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -0,0 +1,53 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { createHash, timingSafeEqual } from 'node:crypto'; +import type { Request } from 'express'; + +/** + * Guards the POST /internal/gitops/sync endpoint. + * + * Validates that the incoming request carries: + * Authorization: Bearer + * + * Uses a constant-time comparison to prevent timing-based secret extraction. + * Register GITOPS_WEBHOOK_SECRET in GitHub/GitLab as the webhook secret. + * + * Returns 401 if: + * - The Authorization header is absent or not a Bearer token. + * - The token does not match GITOPS_WEBHOOK_SECRET. + */ +@Injectable() +export class GitopsWebhookGuard implements CanActivate { + /** Pre-computed SHA-256 digest of the expected secret. Computed once at construction. */ + private readonly expectedDigest: Buffer; + + constructor(config: ConfigService) { + const secret = config.getOrThrow('GITOPS_WEBHOOK_SECRET'); + this.expectedDigest = createHash('sha256').update(secret).digest(); + } + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException( + 'Missing or invalid Authorization header', + ); + } + + const token = authHeader.slice(7); + const tokenDigest = createHash('sha256').update(token).digest(); + + if (!timingSafeEqual(tokenDigest, this.expectedDigest)) { + throw new UnauthorizedException('Invalid gitops webhook secret'); + } + + return true; + } +} diff --git a/apps/api/src/modules/gitops/gitops.module.ts b/apps/api/src/modules/gitops/gitops.module.ts new file mode 100644 index 00000000..c288468b --- /dev/null +++ b/apps/api/src/modules/gitops/gitops.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GitopsSyncController } from './gitops-sync.controller.js'; +import { GitopsWebhookGuard } from './gitops-webhook.guard.js'; + +@Module({ + controllers: [GitopsSyncController], + providers: [GitopsWebhookGuard], +}) +export class GitopsModule {} diff --git a/apps/worker/src/db/database-manager.ts b/apps/worker/src/db/database-manager.ts index bd817291..0152fcfa 100644 --- a/apps/worker/src/db/database-manager.ts +++ b/apps/worker/src/db/database-manager.ts @@ -689,8 +689,29 @@ export class DatabaseManager { try { const db = drizzle(client, { schema }); - const { getDomainProvisioner } = await import("@nexiom/piece-framework"); const { Pool } = await import("pg"); + const { ApplicationLoaderService } = await import("@nexiom/engine"); + + // Instantiate the loader directly — this is a CLI script, not in NestJS DI. + // The loader reads from SHARD_APPLICATION_PATH and caches dynamically imported modules. + const loaderInstance = new ApplicationLoaderService(); + + // Build a domainProvisionerResolver function that matches the TenantDatabaseManager interface: + // (appName: string) => ((db, schemaName) => Promise) | undefined + const domainProvisionerResolver = (appName: string) => { + return async ( + tenantDb: import("@nexiom/database").DrizzleDb, + schemaName: string, + ) => { + const shard = await loaderInstance + .load(`${appName}-${appName}`) + .catch(() => null); + if (shard?.provisionDomain) { + await shard.provisionDomain(tenantDb, schemaName); + } + }; + }; + const schemaMgr = new TenantDatabaseManager( db as unknown as import("@nexiom/database").DrizzleDb, (hostIdentifier: string) => { @@ -713,7 +734,7 @@ export class DatabaseManager { schema, }) as unknown as import("@nexiom/database").DrizzleDb; }, - getDomainProvisioner, + domainProvisionerResolver, ); const fixtures = [ diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index f7852f50..e2883839 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -1,10 +1,6 @@ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module.js"; import { Logger } from "@nestjs/common"; -// Side-effect imports — populate the in-memory extractor/normalizer registries -// before any queue message is processed. Each application package calls -// registerReplicaExtractor / registerNormalizer at module load time. -import "@nexiom/application-revenova"; async function bootstrap() { try { diff --git a/apps/worker/src/modules/pipeline/gitops-sync.worker.spec.ts b/apps/worker/src/modules/pipeline/gitops-sync.worker.spec.ts index 43a40983..64b0cbe3 100644 --- a/apps/worker/src/modules/pipeline/gitops-sync.worker.spec.ts +++ b/apps/worker/src/modules/pipeline/gitops-sync.worker.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { GitopsSyncWorker } from "./gitops-sync.worker.js"; +import { ApplicationLoaderService } from "@nexiom/engine"; +import { QueueService } from "@nexiom/queue"; import * as fs from "node:fs/promises"; import type { Dirent, Stats } from "node:fs"; import { execFile } from "node:child_process"; @@ -29,12 +31,26 @@ vi.mock("node:child_process", () => ({ describe("GitopsSyncWorker", () => { let service: GitopsSyncWorker; + let invalidateCacheSpy: ReturnType; + let queueConsumeSpy: ReturnType; beforeEach(async () => { vi.clearAllMocks(); + invalidateCacheSpy = vi.fn(); + queueConsumeSpy = vi.fn(); const module: TestingModule = await Test.createTestingModule({ - providers: [GitopsSyncWorker], + providers: [ + GitopsSyncWorker, + { + provide: ApplicationLoaderService, + useValue: { invalidateCache: invalidateCacheSpy }, + }, + { + provide: QueueService, + useValue: { consume: queueConsumeSpy, send: vi.fn() }, + }, + ], }).compile(); service = module.get(GitopsSyncWorker); @@ -47,18 +63,21 @@ describe("GitopsSyncWorker", () => { expect(service).toBeDefined(); }); + it("should subscribe to GitopsQueue on init", () => { + service.onModuleInit(); + expect(queueConsumeSpy).toHaveBeenCalledWith( + "gitops-queue", + expect.any(Function), + ); + }); + it("should handle missing base directory gracefully", async () => { - // Simulate fs.readdir failing if path suddenly not accessible vi.mocked(fs.readdir).mockRejectedValueOnce(new Error("ENOENT")); - await service.syncShardRepositories(); - - // Worker gracefully catches outer errors expect(fs.mkdir).toHaveBeenCalled(); }); it("should exit early if no valid shard directories found", async () => { - // Hidden directories or non-directories should be skipped vi.mocked(fs.readdir).mockResolvedValueOnce([ { name: ".hidden", isDirectory: () => true } as unknown as Dirent< Buffer @@ -87,8 +106,7 @@ describe("GitopsSyncWorker", () => { expect(execFile).toHaveBeenCalled(); }); - it("should correctly log if sync pulled new changes", async () => { - // Overriding mock to return pull content + it("should correctly invalidate cache when sync pulls new changes", async () => { vi.mocked(execFile).mockImplementationOnce( (_cmd: unknown, _args: unknown, _opts: unknown, cb: unknown) => { (cb as ExecFileCallback)(null, { @@ -110,17 +128,29 @@ describe("GitopsSyncWorker", () => { await service.syncShardRepositories(); expect(execFile).toHaveBeenCalled(); + + // Verify the module cache is invalidated so the new code is hot-reloaded + expect(invalidateCacheSpy).toHaveBeenCalledWith("update-shard"); }); it("should skip shards with invalid branch names", async () => { - // Override the environment branch strictly for this run const originalBranch = process.env.DEFAULT_BRANCH; try { process.env.DEFAULT_BRANCH = "invalid&&branch;name"; const testModule: TestingModule = await Test.createTestingModule({ - providers: [GitopsSyncWorker], + providers: [ + GitopsSyncWorker, + { + provide: ApplicationLoaderService, + useValue: { invalidateCache: vi.fn() }, + }, + { + provide: QueueService, + useValue: { consume: vi.fn(), send: vi.fn() }, + }, + ], }).compile(); const testService = testModule.get(GitopsSyncWorker); @@ -139,7 +169,7 @@ describe("GitopsSyncWorker", () => { // Execution shouldn't happen due to validation failure expect(execFile).not.toHaveBeenCalled(); } finally { - process.env.DEFAULT_BRANCH = originalBranch; // restore + process.env.DEFAULT_BRANCH = originalBranch; } }); diff --git a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts index aaaffa52..bb71ba14 100644 --- a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts +++ b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts @@ -1,4 +1,6 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger, Inject, OnModuleInit } from "@nestjs/common"; +import { ApplicationLoaderService } from "@nexiom/engine"; +import { QueueService, QueueName } from "@nexiom/queue"; import { Cron, CronExpression } from "@nestjs/schedule"; import * as path from "node:path"; import * as fs from "node:fs/promises"; @@ -8,7 +10,7 @@ import { promisify } from "node:util"; const execFileAsync = promisify(execFile); @Injectable() -export class GitopsSyncWorker { +export class GitopsSyncWorker implements OnModuleInit { private readonly logger = new Logger(GitopsSyncWorker.name); private readonly SHARD_BASE_PATH = process.env.SHARD_APPLICATION_PATH || @@ -16,30 +18,51 @@ export class GitopsSyncWorker { private readonly DEFAULT_BRANCH = process.env.DEFAULT_BRANCH || "main"; private isSyncRunning = false; + constructor( + @Inject(ApplicationLoaderService) + private readonly applicationLoaderService: ApplicationLoaderService, + private readonly queueService: QueueService, + ) {} + /** - * Runs every 5 minutes to synchronize tenant logic shards. - * This decoupled architecture allows application developers to push PRs, - * pass standard CI/CD, and be natively picked up by the platform - * without restarting or rebuilding the core integration monorepo. + * Subscribe to the GitopsQueue on startup. + * Messages arrive immediately when the API receives a GitHub/GitLab webhook push. + * This ensures new application code is live within seconds of a git push. + */ + onModuleInit() { + this.queueService.consume(QueueName.GitopsQueue, async () => { + this.logger.log( + "GitOps webhook event received — triggering immediate sync", + ); + await this.syncShardRepositories(); + }); + } + + /** + * Fallback safety net — runs every 5 minutes to catch any git commits that + * were missed (e.g. if the webhook was not delivered due to a network blip). + * + * The primary trigger is the webhook endpoint: + * POST /internal/gitops/sync (GitopsSyncController) + * This cron is the secondary fallback only. */ @Cron(CronExpression.EVERY_5_MINUTES) async syncShardRepositories() { // Reentrancy guard to prevent overlapping sync runs if (this.isSyncRunning) { - this.logger.warn("Skipping GitOps sync - previous run still in progress"); + this.logger.warn("Skipping GitOps sync — previous run still in progress"); return; } this.isSyncRunning = true; - this.logger.log("Starting GitOps Shard Synchronization..."); + this.logger.debug( + "Starting GitOps shard synchronization (cron fallback)...", + ); try { - // 1. Ensure the base sync directory exists + // Ensure the base sync directory exists await fs.mkdir(this.SHARD_BASE_PATH, { recursive: true }); - // 2. Discover mapped shard repositories from the central storage registry or configuration. - // For this phase of the enterprise implementation, we scan the known local directories - // and perform a git pull to maintain hot-reload parity. const entries = await fs.readdir(this.SHARD_BASE_PATH, { withFileTypes: true, }); @@ -48,64 +71,66 @@ export class GitopsSyncWorker { ); if (shardDirs.length === 0) { - this.logger.debug( - "No active sharding directories found for gitops sync.", - ); + this.logger.debug("No active shard directories found for gitops sync."); return; } for (const shard of shardDirs) { - const repoPath = path.join(this.SHARD_BASE_PATH, shard.name); + await this.syncShard(shard.name); + } + } catch (error) { + this.logger.error("GitOps shard synchronization failed", error); + } finally { + this.isSyncRunning = false; + } + } - // 3. Verify it is a valid git repository - try { - const isRepo = await fs.stat(path.join(repoPath, ".git")); - if (isRepo.isDirectory() || isRepo.isFile()) { - this.logger.debug(`Synchronizing Shard: ${shard.name}`); + /** + * Synchronizes a single shard by running git pull. + * Called both by the cron fallback and directly by GitopsSyncController + * when a webhook is received from GitHub/GitLab. + */ + async syncShard(shardName: string): Promise { + const repoPath = path.join(this.SHARD_BASE_PATH, shardName); - // Execute git pull. Because the LogicResolver dynamic import appends a - // query string cache-buster `?update=timestamp`, Node natively evaluates - // the new pulled JS without requiring a worker restart! - const branchName = this.DEFAULT_BRANCH; + try { + await fs.stat(path.join(repoPath, ".git")); - // Validate branch name to prevent command injection - if (!/^[a-zA-Z0-9/_\-.]+$/.test(branchName)) { - this.logger.warn( - `Invalid branch name format: ${branchName}. Skipping sync for ${shard.name}.`, - ); - continue; - } + // Validate branch name to prevent command injection + const branchName = this.DEFAULT_BRANCH; + if (!/^[a-zA-Z0-9/_\-.]+$/.test(branchName)) { + this.logger.warn( + `Invalid branch name format: ${branchName}. Skipping sync for ${shardName}.`, + ); + return; + } - const { stdout } = await execFileAsync( - "git", - ["pull", "origin", branchName, "--ff-only"], - { - cwd: repoPath, - env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, - timeout: 60000, // 60 second timeout - maxBuffer: 1024 * 1024 * 10, // 10MB buffer - }, - ); + this.logger.debug(`Synchronizing shard: ${shardName}`); + const { stdout } = await execFileAsync( + "git", + ["pull", "origin", branchName, "--ff-only"], + { + cwd: repoPath, + env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, + timeout: 60_000, + maxBuffer: 1024 * 1024 * 10, + }, + ); - if (stdout.includes("Already up to date.")) { - this.logger.debug(`[${shard.name}] Up to date.`); - } else { - this.logger.log( - `[${shard.name}] Successfully synced new logic: \n${stdout}`, - ); - } - } - } catch (err) { - this.logger.warn( - `Directory ${shard.name} is not a valid git repository or git operation failed. Skipping sync.`, - err instanceof Error ? err.message : String(err), - ); - } + if (stdout.includes("Already up to date.")) { + this.logger.debug(`[${shardName}] Already up to date.`); + } else { + this.logger.log( + `[${shardName}] New commits pulled — invalidating module cache:\n${stdout}`, + ); + // Invalidate the module cache so the next pipeline event imports fresh code + this.applicationLoaderService.invalidateCache(shardName); } - } catch (error) { - this.logger.error("GitOps Shard Synchronization failed", error); - } finally { - this.isSyncRunning = false; + } catch (err) { + this.logger.warn( + `[${shardName}] Not a git repository or git pull failed — skipping.`, + err instanceof Error ? err.message : String(err), + ); } } } diff --git a/apps/worker/src/modules/pipeline/normalization.service.spec.ts b/apps/worker/src/modules/pipeline/normalization.service.spec.ts index 4f522b03..5e820d68 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.spec.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.spec.ts @@ -3,7 +3,10 @@ import { Test, TestingModule } from "@nestjs/testing"; import { NormalizationService } from "./normalization.service.js"; import { QueueService, QueueName } from "@nexiom/queue"; import { DATABASE_CONNECTION } from "@nexiom/database"; -import { StorageResolverService } from "@nexiom/engine"; +import { + StorageResolverService, + PipelineHookBrokerService, +} from "@nexiom/engine"; import { PieceRegistryService } from "@nexiom/piece-registry"; import { describe, it, expect, vi, beforeEach } from "vitest"; @@ -85,6 +88,15 @@ describe("NormalizationService", () => { { provide: DATABASE_CONNECTION, useValue: db }, { provide: StorageResolverService, useValue: storageResolver }, { provide: PieceRegistryService, useValue: pieceRegistry }, + { + provide: PipelineHookBrokerService, + // Default: shard normalize returns null → falls through to piece.normalize + // and writeNormalized is a no-op (returns void). + useValue: { + normalize: vi.fn().mockResolvedValue(null), + writeNormalized: vi.fn().mockResolvedValue(undefined), + }, + }, ], }).compile(); diff --git a/apps/worker/src/modules/pipeline/normalization.service.ts b/apps/worker/src/modules/pipeline/normalization.service.ts index 6edc79bd..679c4d19 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.ts @@ -14,14 +14,16 @@ import { appConnections, } from "@nexiom/database"; import type { DrizzleDb } from "@nexiom/database"; -import { StorageResolverService } from "@nexiom/engine"; +import { + StorageResolverService, + PipelineHookBrokerService, +} from "@nexiom/engine"; import { PieceRegistryService } from "@nexiom/piece-registry"; import { sql } from "drizzle-orm"; import { sanitizeError, isValidPipelineMessage, } from "../../shared/pipeline.utils.js"; -import { getNormalizer, getNormalizedWriter } from "@nexiom/piece-framework"; @Injectable() export class NormalizationService implements OnModuleInit, OnModuleDestroy { @@ -32,6 +34,7 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, private readonly storageResolver: StorageResolverService, private readonly pieceRegistry: PieceRegistryService, + private readonly hookBroker: PipelineHookBrokerService, ) {} onModuleInit() { @@ -155,18 +158,17 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { let canonicalType = "RAW"; let canonicalData = replica.data; - // ── Normalize via Registry or Piece ───────────────────────────────── - const customNormalizer = getNormalizer(connectionAppName, appProfile); - - if (customNormalizer) { - const normalized = await customNormalizer({ + // ── Normalize via HookBroker or Piece ──────────────────────────────── + const normalizedFromShard = await this.hookBroker + .normalize(connectionAppName, appProfile, { entityType: replica.entityType, data: replica.data as Record, - }); - if (normalized) { - canonicalType = normalized.canonicalType; - canonicalData = normalized.data; - } + }) + .catch(() => null); // shard may not exist yet — fall through to piece.normalize + + if (normalizedFromShard) { + canonicalType = normalizedFromShard.canonicalType; + canonicalData = normalizedFromShard.data; } else if (piece.normalize) { const normalized = await piece.normalize( replica.entityType, @@ -230,42 +232,38 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { if (insertRes.length > 0) { // ── Step 3.5: Application canonical write hook ───────────────────── - // Call the app-registered writer to persist into typed per-entity - // tables (e.g., tms_carrier, tms_tp). The platform knows nothing - // about these tables — the application owns the schema. - const appNormalizedWriter = getNormalizedWriter( - connectionAppName, - appProfile, - ); - if (appNormalizedWriter) { - try { - // Wrap in nested savepoint so hook failures roll back only the savepoint - await tx.transaction((sp) => - appNormalizedWriter( - sp, - this.db, - schemaName, - replica.id, - replica.entityId, - traceId, - canonicalType, - safeData, - ), - ); - } catch (hookErr) { - // Log but do not fail the pipeline — the generic normalized_entity - // write already succeeded. App table write failure is observable - // via logs and can be replayed. - this.logger.warn( - { - event: "l3.normalized_writer_hook_failed", - traceId, - normalizedEntityType: canonicalType, - err: sanitizeError(hookErr), - }, - "App normalized writer hook failed — typed table write skipped", - ); - } + // Delegates to the application shard's writeNormalized function. + // The shard receives the live Drizzle savepoint and executes its own + // upserts into application-owned tables (e.g. tms_carrier, tms_tp). + // The platform knows nothing about those tables — only the shard does. + try { + await tx.transaction((sp) => + this.hookBroker.writeNormalized( + connectionAppName, + appProfile, + sp, + this.db, + schemaName, + replica.id, + replica.entityId, + traceId, + canonicalType, + safeData, + ), + ); + } catch (hookErr) { + // Log but do not fail the pipeline — the generic normalized_entity + // write already succeeded. App table write failure is observable + // via logs and can be replayed. + this.logger.warn( + { + event: "l3.normalized_writer_hook_failed", + traceId, + normalizedEntityType: canonicalType, + err: sanitizeError(hookErr), + }, + "App normalized writer hook failed — typed table write skipped", + ); } // ── Transactional outbox for L3→L4 handoff ────────────────────────── diff --git a/apps/worker/src/modules/pipeline/pipeline.module.ts b/apps/worker/src/modules/pipeline/pipeline.module.ts index 4918bcd8..743697b1 100644 --- a/apps/worker/src/modules/pipeline/pipeline.module.ts +++ b/apps/worker/src/modules/pipeline/pipeline.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { QueueModule } from "@nexiom/queue"; import { DbModule } from "../../db/db.module.js"; import { DbManagerModule } from "../dbmanager/dbmanager.module.js"; -import { StorageResolverModule } from "@nexiom/engine"; +import { StorageResolverModule, ApplicationLoaderModule } from "@nexiom/engine"; import { TokenManagerService, AesEncryptionService, @@ -29,8 +29,10 @@ import { GitopsSyncWorker } from "./gitops-sync.worker.js"; DbModule, DbManagerModule, StorageResolverModule, + ApplicationLoaderModule, PiecesModule, ], + providers: [ ReplicaService, NormalizationService, // Extended with app canonical write hook (step 3.5) diff --git a/apps/worker/src/modules/pipeline/replica.service.spec.ts b/apps/worker/src/modules/pipeline/replica.service.spec.ts index 5360efe6..b93e939d 100644 --- a/apps/worker/src/modules/pipeline/replica.service.spec.ts +++ b/apps/worker/src/modules/pipeline/replica.service.spec.ts @@ -3,46 +3,32 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ReplicaService } from "./replica.service.js"; import { QueueService, QueueName } from "@nexiom/queue"; import { DATABASE_CONNECTION } from "@nexiom/database"; -import { StorageResolverService } from "@nexiom/engine"; +import { + StorageResolverService, + PipelineHookBrokerService, +} from "@nexiom/engine"; import { describe, it, expect, vi, beforeEach } from "vitest"; -vi.mock("@nexiom/piece-framework", async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - getReplicaExtractor: vi - .fn() - .mockImplementation((appName: string, appProfile: string) => { - if (appName === "test_extraction_fail") { - return () => null; // Extractor returns null → no entityId found - } - if (appName === "test_empty_entityid") { - return () => ({ entityType: "DEFAULT", entityId: "", data: {} }); - } - if (appName === "salesforce" && appProfile === "revenova") { - // Happy-path extractor: returns a stable entityId from the request - return (payload: Record) => ({ - entityType: (payload?.objectType as string) || "sf_Account", - entityId: (payload?.extReqId as string) || "mock-entity-id", - data: payload ?? {}, - }); - } - return undefined; - }), - }; -}); describe("ReplicaService", () => { let service: ReplicaService; let queueService: any; let db: any; let storageResolver: any; + let hookBroker: { extractReplica: ReturnType }; beforeEach(async () => { queueService = { consume: vi.fn(), send: vi.fn().mockResolvedValue(undefined), }; + // Default happy-path extractor + hookBroker = { + extractReplica: vi.fn().mockResolvedValue({ + entityType: "sf_Account", + entityId: "mock-entity-id", + data: {}, + }), + }; db = { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), @@ -87,6 +73,7 @@ describe("ReplicaService", () => { { provide: QueueService, useValue: queueService }, { provide: DATABASE_CONNECTION, useValue: db }, { provide: StorageResolverService, useValue: storageResolver }, + { provide: PipelineHookBrokerService, useValue: hookBroker }, ], }).compile(); @@ -215,8 +202,10 @@ describe("ReplicaService", () => { }); it("should throw if replica extraction fails due to payload shape mismatch", async () => { - // Return a piece appName that demands extraction and has a mock returning null - db.limit.mockResolvedValueOnce([{ appName: "test_extraction_fail" }]); + hookBroker.extractReplica.mockResolvedValueOnce(null); + db.limit.mockResolvedValueOnce([ + { appName: "salesforce", metadata: { appProfile: "revenova" } }, + ]); db.transaction.mockImplementationOnce(async (cb: any) => cb({ execute: vi.fn(), @@ -230,7 +219,7 @@ describe("ReplicaService", () => { extReqId: "bar", id: "1", status: "RECEIVED", - request: { junk: "data" }, // Missing the root envelope to fail extraction + request: { junk: "data" }, }, ]), insert: vi.fn().mockReturnThis(), @@ -246,15 +235,12 @@ describe("ReplicaService", () => { }); it("should throw if extractor returns null (no stable entityId found)", async () => { - // Override connection to use an appName that always fails extraction - db.limit.mockResolvedValueOnce([ - { appName: "test_extraction_fail", metadata: { appProfile: "default" } }, - ]); + hookBroker.extractReplica.mockResolvedValueOnce(null); service.onModuleInit(); const handler = queueService.consume.mock.calls[0][1]; await expect( handler({ traceId: "123", connectionId: "456" }), - ).rejects.toThrow("extractor returned null"); + ).rejects.toThrow("shard returned null"); }); it("should log nested error when error-handler transaction also fails", async () => { @@ -319,10 +305,12 @@ describe("ReplicaService", () => { }); it("should throw if extractor returns an entity with empty entityId", async () => { - // Use a dedicated mock appName that returns a blank entityId - db.limit.mockResolvedValueOnce([ - { appName: "test_empty_entityid", metadata: { appProfile: "default" } }, - ]); + // hookBroker returns an entity with blank entityId + hookBroker.extractReplica.mockResolvedValueOnce({ + entityType: "DEFAULT", + entityId: "", + data: {}, + }); db.transaction.mockImplementationOnce(async (cb: any) => cb({ execute: vi.fn().mockResolvedValue(undefined), diff --git a/apps/worker/src/modules/pipeline/replica.service.ts b/apps/worker/src/modules/pipeline/replica.service.ts index c52c2c79..ee1ec583 100644 --- a/apps/worker/src/modules/pipeline/replica.service.ts +++ b/apps/worker/src/modules/pipeline/replica.service.ts @@ -13,9 +13,11 @@ import { appConnections, } from "@nexiom/database"; import type { DrizzleDb } from "@nexiom/database"; -import { StorageResolverService } from "@nexiom/engine"; +import { + StorageResolverService, + PipelineHookBrokerService, +} from "@nexiom/engine"; import { sql, eq, and } from "drizzle-orm"; -import { getReplicaExtractor } from "@nexiom/piece-framework"; @Injectable() export class ReplicaService implements OnModuleInit, OnModuleDestroy { @@ -25,6 +27,7 @@ export class ReplicaService implements OnModuleInit, OnModuleDestroy { private readonly queueService: QueueService, @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, private readonly storageResolver: StorageResolverService, + private readonly hookBroker: PipelineHookBrokerService, ) {} onModuleInit() { @@ -134,33 +137,15 @@ export class ReplicaService implements OnModuleInit, OnModuleDestroy { return; } - const extractor = getReplicaExtractor(appName, appProfile); - - // If the connection has an explicit appProfile (e.g. "revenova") but no - // extractor is registered for it, this is a hard misconfiguration — fail - // loudly rather than silently emitting raw data, which would be invisible - // until someone notices the replica table looks wrong. - if (!extractor && appProfile !== "default") { - throw new Error( - `No ReplicaExtractor registered for appName="${appName}" appProfile="${appProfile}". ` + - `Ensure the application package (e.g. @nexiom/application-${appProfile}) is imported in the worker entry point.`, - ); - } - - // If no extractor is available (appProfile=default), fail early with a clear error - if (!extractor) { - throw new Error( - `No ReplicaExtractor available for traceId ${traceId} (appName="${appName}", appProfile="${appProfile}"). ` + - `Cannot derive stable entityId from raw payload. ` + - `Set a valid appProfile on the connection or register a default extractor.`, - ); - } - - const extracted = extractor(inbound.request); + const extracted = await this.hookBroker.extractReplica( + appName, + appProfile, + inbound.request, + ); if (!extracted) { throw new Error( - `Replica extraction failed for traceId ${traceId}: extractor returned null. ` + + `Replica extraction failed for traceId ${traceId}: shard returned null. ` + `Likely the payload is missing the required entity ID (e.g. sf:id).`, ); } diff --git a/apps/worker/src/modules/pipeline/target-builder.service.spec.ts b/apps/worker/src/modules/pipeline/target-builder.service.spec.ts index 41f3ae5b..918e90c5 100644 --- a/apps/worker/src/modules/pipeline/target-builder.service.spec.ts +++ b/apps/worker/src/modules/pipeline/target-builder.service.spec.ts @@ -1,15 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Test, TestingModule } from "@nestjs/testing"; import { TargetBuilderService } from "./target-builder.service.js"; import { DATABASE_CONNECTION } from "@nexiom/database"; +import { PipelineHookBrokerService } from "@nexiom/engine"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import * as appHooks from "@nexiom/piece-framework"; - -vi.mock("@nexiom/piece-framework", async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, getTargetBuilder: vi.fn() }; -}); vi.mock("@nexiom/engine", async (importOriginal) => { const actual = await importOriginal(); @@ -23,7 +16,8 @@ vi.mock("@nexiom/engine", async (importOriginal) => { describe("TargetBuilderService", () => { let service: TargetBuilderService; - let db: any; + let db: Record>; + let hookBrokerBuildTarget: ReturnType; const SCHEMA = "ws_test"; const APP = "salesforce"; @@ -34,14 +28,17 @@ describe("TargetBuilderService", () => { const RULES = [{ src: "$.displayName", dest: "$.DisplayName" }]; beforeEach(async () => { - vi.mocked(appHooks.getTargetBuilder).mockReset(); - + hookBrokerBuildTarget = vi.fn().mockResolvedValue({}); db = { select: vi.fn(), transaction: vi.fn() }; const module: TestingModule = await Test.createTestingModule({ providers: [ TargetBuilderService, { provide: DATABASE_CONNECTION, useValue: db }, + { + provide: PipelineHookBrokerService, + useValue: { buildTarget: hookBrokerBuildTarget }, + }, ], }).compile(); @@ -50,8 +47,8 @@ describe("TargetBuilderService", () => { afterEach(() => vi.clearAllMocks()); - it("returns normalizedData directly when no app builder is registered", async () => { - vi.mocked(appHooks.getTargetBuilder).mockReturnValue(undefined); + it("returns normalizedData directly when hookBroker returns empty context", async () => { + hookBrokerBuildTarget.mockResolvedValue({}); const result = await service.buildPayload( SCHEMA, @@ -66,14 +63,12 @@ describe("TargetBuilderService", () => { expect(result).toEqual(NORMALIZED_DATA); }); - it("merges enrichment context from registered app builder", async () => { + it("merges enrichment context from hookBroker.buildTarget", async () => { const enrichment = { tp: { mcNumber: "MC123456" }, remitTo: { displayName: "Factor Co" }, }; - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - vi.fn().mockResolvedValue(enrichment), - ); + hookBrokerBuildTarget.mockResolvedValue(enrichment); const result = await service.buildPayload( SCHEMA, @@ -90,9 +85,7 @@ describe("TargetBuilderService", () => { it("applies field mapping rules via hydratePayload after enrichment", async () => { const { hydratePayload } = await import("@nexiom/engine"); - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - vi.fn().mockResolvedValue({ extra: "field" }), - ); + hookBrokerBuildTarget.mockResolvedValue({ extra: "field" }); await service.buildPayload( SCHEMA, @@ -110,10 +103,8 @@ describe("TargetBuilderService", () => { ); }); - it("falls back to normalizedData when app builder returns empty context", async () => { - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - vi.fn().mockResolvedValue({}), - ); + it("falls back to normalizedData when hookBroker.buildTarget returns empty context", async () => { + hookBrokerBuildTarget.mockResolvedValue({}); const result = await service.buildPayload( SCHEMA, @@ -128,13 +119,10 @@ describe("TargetBuilderService", () => { expect(result).toEqual(NORMALIZED_DATA); }); - it("falls back to normalizedData and logs warning when app builder throws", async () => { + it("falls back to normalizedData and logs warning when hookBroker.buildTarget throws", async () => { const warnSpy = vi.spyOn(service["logger"], "warn"); - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - vi.fn().mockRejectedValue(new Error("DB join failed")), - ); + hookBrokerBuildTarget.mockRejectedValue(new Error("DB join failed")); - // Should not throw — falls back gracefully const result = await service.buildPayload( SCHEMA, APP, @@ -152,12 +140,9 @@ describe("TargetBuilderService", () => { ); }); - it("falls back gracefully when app builder throws a non-Error value", async () => { + it("falls back gracefully when hookBroker.buildTarget throws a non-Error value", async () => { const warnSpy = vi.spyOn(service["logger"], "warn"); - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - // Throw a plain string — exercises the `err` (non-Error) branch on line 81 - vi.fn().mockRejectedValue("raw string error"), - ); + hookBrokerBuildTarget.mockRejectedValue("raw string error"); const result = await service.buildPayload( SCHEMA, @@ -176,10 +161,7 @@ describe("TargetBuilderService", () => { ); }); - it("skips app builder call when srcEntityId is undefined", async () => { - const builderFn = vi.fn().mockResolvedValue({ tp: {} }); - vi.mocked(appHooks.getTargetBuilder).mockReturnValue(builderFn); - + it("skips hookBroker.buildTarget call when srcEntityId is undefined", async () => { await service.buildPayload( SCHEMA, APP, @@ -190,14 +172,12 @@ describe("TargetBuilderService", () => { [], ); - expect(builderFn).not.toHaveBeenCalled(); + expect(hookBrokerBuildTarget).not.toHaveBeenCalled(); }); it("returns enrichedContext unchanged when rules array is empty", async () => { const { hydratePayload } = await import("@nexiom/engine"); - vi.mocked(appHooks.getTargetBuilder).mockReturnValue( - vi.fn().mockResolvedValue({ extra: "x" }), - ); + hookBrokerBuildTarget.mockResolvedValue({ extra: "x" }); const result = await service.buildPayload( SCHEMA, diff --git a/apps/worker/src/modules/pipeline/target-builder.service.ts b/apps/worker/src/modules/pipeline/target-builder.service.ts index ff456123..f20bb782 100644 --- a/apps/worker/src/modules/pipeline/target-builder.service.ts +++ b/apps/worker/src/modules/pipeline/target-builder.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject, Logger } from "@nestjs/common"; import { DATABASE_CONNECTION } from "@nexiom/database"; import type { DrizzleDb } from "@nexiom/database"; -import { getTargetBuilder } from "@nexiom/piece-framework"; +import { PipelineHookBrokerService } from "@nexiom/engine"; import type { Rule } from "@nexiom/engine"; import { hydratePayload } from "@nexiom/engine"; @@ -23,7 +23,10 @@ import { hydratePayload } from "@nexiom/engine"; export class TargetBuilderService { private readonly logger = new Logger(TargetBuilderService.name); - constructor(@Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb) {} + constructor( + @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, + private readonly hookBroker: PipelineHookBrokerService, + ) {} async buildPayload( schemaName: string, @@ -34,14 +37,14 @@ export class TargetBuilderService { normalizedData: Record, rules: Rule[], ): Promise> { - // ── 1. Attempt app-registered enrichment ───────────────────────────────── - const appBuilder = getTargetBuilder(appName, appProfile); - + // ── 1. Attempt app shard enrichment via PipelineHookBrokerService ───────── let enrichedContext = normalizedData; - if (appBuilder && srcEntityId) { + if (srcEntityId) { try { - const appContext = await appBuilder( + const appContext = await this.hookBroker.buildTarget( + appName, + appProfile, this.db, schemaName, normalizedEntityType, @@ -49,7 +52,6 @@ export class TargetBuilderService { ); if (Object.keys(appContext).length > 0) { - // Merge: top-level normalizedData fields + app-enriched aliases enrichedContext = { ...normalizedData, ...appContext }; this.logger.debug( { @@ -61,7 +63,7 @@ export class TargetBuilderService { (k) => appContext[k] !== null, ), }, - "Enrichment context assembled from app hook", + "Enrichment context assembled from application shard", ); } else { this.logger.debug( @@ -71,40 +73,32 @@ export class TargetBuilderService { normalizedEntityType, srcEntityId, }, - "App target builder returned empty context — using normalizedData", + "Application shard buildTarget returned empty context — using normalizedData", ); } } catch (err) { - const errPayload = - err instanceof Error - ? { message: err.message, stack: err.stack } - : err; this.logger.warn( { event: "target_builder.hook_failed", appName, normalizedEntityType, srcEntityId, - err: errPayload, + err: + err instanceof Error + ? { message: err.message, stack: err.stack } + : err, }, - "App target builder hook failed — falling back to normalizedData", + "Application shard buildTarget failed — falling back to normalizedData", ); } - } else if (appBuilder && !srcEntityId) { + } else { this.logger.debug( { event: "target_builder.missing_src_entity_id", appName, normalizedEntityType, - appProfile, - srcEntityId, }, - "App builder registered but srcEntityId is undefined — skipping enrichment", - ); - } else if (!appBuilder) { - this.logger.debug( - { event: "target_builder.no_hook", appName, appProfile }, - "No app target builder registered — applying rules to normalizedData directly", + "srcEntityId is undefined — skipping enrichment", ); } diff --git a/engine/sync/platform/core/package.json b/engine/sync/platform/core/package.json index e75b85f3..f94b698e 100644 --- a/engine/sync/platform/core/package.json +++ b/engine/sync/platform/core/package.json @@ -23,6 +23,7 @@ "@nexiom/database": "workspace:*", "@nexiom/piece-framework": "workspace:*", "ai": "^6.0.146", + "quickjs-emscripten": "^0.32.0", "zod": "^4.3.5" }, "devDependencies": { diff --git a/engine/sync/platform/core/src/index.ts b/engine/sync/platform/core/src/index.ts index 8fc4ff25..20838b33 100644 --- a/engine/sync/platform/core/src/index.ts +++ b/engine/sync/platform/core/src/index.ts @@ -10,5 +10,7 @@ export * from './storage-resolver/storage-resolver.service.js'; export * from './evaluator.js'; export * from './hydrator.js'; -export * from './sharding/logic-resolver.module.js'; -export * from './sharding/logic-resolver.service.js'; \ No newline at end of file +export * from './sharding/application-loader.module.js'; +export * from './sharding/application-loader.service.js'; +export * from './sharding/application-shard.types.js'; +export * from './sharding/pipeline-hook-broker.service.js'; \ No newline at end of file diff --git a/engine/sync/platform/core/src/sharding/application-executor.module.ts b/engine/sync/platform/core/src/sharding/application-executor.module.ts new file mode 100644 index 00000000..6f75ef52 --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-executor.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ApplicationExecutorService } from './application-executor.service.js'; + +@Module({ + providers: [ApplicationExecutorService], + exports: [ApplicationExecutorService], +}) +export class ApplicationExecutorModule {} diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts b/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts new file mode 100644 index 00000000..8a01568b --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts @@ -0,0 +1,151 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; + +// ── Module Mocking ──────────────────────────────────────────────────────────── +// We mock esbuild so tests don't need real shard files on disk +vi.mock('esbuild', () => ({ + build: vi.fn(), +})); + +vi.mock('node:fs/promises', () => ({ + access: vi.fn(), + mkdir: vi.fn(), +})); + +// We DO NOT mock quickjs-emscripten — the entire point is to test real sandbox behavior + +import * as esbuild from 'esbuild'; +import { ApplicationExecutorService } from './application-executor.service.js'; +import { InternalServerErrorException } from '@nestjs/common'; + +// A minimal, self-contained IIFE bundle that the "esbuild" mock will return +const makeShardBundle = (fnName: string, fnBody: string) => + `var CustomLogic = (function() { + function ${fnName}(payload) { ${fnBody} } + return { ${fnName}: ${fnName} }; + })();`; + +describe('ApplicationExecutorService — WebAssembly Sandbox', () => { + let service: ApplicationExecutorService; + + beforeEach(() => { + service = new ApplicationExecutorService(); + vi.clearAllMocks(); + + // Default: file access succeeds + vi.mocked(fs.access).mockResolvedValue(undefined); + }); + + // ── Path Traversal Guard ────────────────────────────────────────────────── + it('throws on path traversal attempt', async () => { + await expect( + service.executeCustomMapping('t1', '../../../etc/passwd', 'transform', {}) + ).rejects.toThrow(InternalServerErrorException); + }); + + // ── Missing Shard File ──────────────────────────────────────────────────── + it('throws when shard module file does not exist', async () => { + vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT')); + await expect( + service.executeCustomMapping('t1', 'my-shard', 'transform', {}) + ).rejects.toThrow(InternalServerErrorException); + }); + + // ── Successful Execution ────────────────────────────────────────────────── + it('executes transform and returns result through the WebAssembly boundary', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [ + { text: makeShardBundle('transform', 'return { result: payload.value * 2 };') }, + ], + } as never); + + const result = await service.executeCustomMapping( + 't1', 'my-shard', 'transform', { value: 21 } + ); + + expect(result).toEqual({ result: 42 }); + }); + + // ── Cache Hit ───────────────────────────────────────────────────────────── + it('only calls esbuild once for repeated invocations of the same shard', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [ + { text: makeShardBundle('transform', 'return { ok: true };') }, + ], + } as never); + + await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); + await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); + + expect(esbuild.build).toHaveBeenCalledTimes(1); + }); + + // ── Cache Invalidation ──────────────────────────────────────────────────── + it('re-bundles after invalidateCache is called', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [ + { text: makeShardBundle('transform', 'return { ok: true };') }, + ], + } as never); + + await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); + service.invalidateCache('my-shard'); + await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); + + // esbuild called twice — once before and once after cache invalidation + expect(esbuild.build).toHaveBeenCalledTimes(2); + }); + + // ── Unknown Function Guard ──────────────────────────────────────────────── + it('throws when the requested function is not exported by the shard', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [ + { text: makeShardBundle('transform', 'return {};') }, + ], + } as never); + + await expect( + service.executeCustomMapping('t1', 'my-shard', 'nonExistentFn', {}) + ).rejects.toThrow(InternalServerErrorException); + }); + + // ── Security Boundary: No Node.js API Access ────────────────────────────── + it('sandbox prevents access to Node.js process object', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [{ + // Attempt to read process.env — should be undefined inside the sandbox + text: makeShardBundle('exfiltrate', ` + var leaked = typeof process !== 'undefined' ? JSON.stringify(process.env) : 'BLOCKED'; + return { leaked }; + `), + }], + } as never); + + const result = await service.executeCustomMapping( + 't1', 'my-shard', 'exfiltrate', {} + ); + + // process is not defined in the QuickJS sandbox — it should be 'BLOCKED' + expect(result).toEqual({ leaked: 'BLOCKED' }); + }); + + // ── Security Boundary: Memory Limit ────────────────────────────────────── + it('sandbox enforces memory limit and throws on excessive allocation', async () => { + vi.mocked(esbuild.build).mockResolvedValue({ + outputFiles: [{ + // Attempt to allocate a 200MB string in one shot — exceeds the 128MB hard limit. + // Using a single large allocation is safer than a loop: it triggers the QuickJS + // OOM boundary instantly without exhausting Node.js host memory. + text: makeShardBundle('memBomb', ` + var bomb = 'a'.repeat(200 * 1024 * 1024); + return { size: bomb.length }; + `), + }], + } as never); + + await expect( + service.executeCustomMapping('t1', 'my-shard', 'memBomb', {}) + ).rejects.toThrow(InternalServerErrorException); + }); +}); diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.ts b/engine/sync/platform/core/src/sharding/application-executor.service.ts new file mode 100644 index 00000000..8f56bdea --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-executor.service.ts @@ -0,0 +1,157 @@ +import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import { constants as fsConstants } from 'node:fs'; +import * as esbuild from 'esbuild'; +import { getQuickJS } from 'quickjs-emscripten'; +const SHARD_APPLICATION_PATH = process.env.SHARD_APPLICATION_PATH || path.resolve(process.cwd(), '../../engine/sync/application'); + +/** + * Executes a dynamically synced first-party custom logic module. + * + * Safely resolves the tenant's logic module from the `engine/sync/application` path. + * This execution path assumes the module has already been safely synced via + * GitOps Shard Synchronization. + */ +@Injectable() +export class ApplicationExecutorService { + private readonly logger = new Logger(ApplicationExecutorService.name); + + // The base path where shard synchronization pulls all tenant-specific Git shards + private readonly SHARD_BASE_PATH = SHARD_APPLICATION_PATH; + + // Cache for bundled JS code to avoid recompiling on every invocation + private esbuildCache = new Map(); + + /** + * Invalidates the bundled cache for a specific shard. + * Intended to be called by the GitOps worker upon detecting a new commit. + */ + invalidateCache(shardName: string) { + this.esbuildCache.delete(shardName); + } + + /** + * Resolves a custom transformer module from the application directory and executes it. + * + * IMPORTANT: The caller MUST ensure shardName corresponds to tenantId before calling this method. + * This service does not validate shard ownership - tenant isolation must be enforced by the caller + * (e.g., by querying the shard registry or configuration to verify the tenant has access to the shard). + * + * @param tenantId - Tenant identifier for logging/audit purposes only + * @param shardName - Name of the shard directory (caller must validate ownership) + * @param transformFunctionName - Name of the exported function to execute + * @param payload - Data to pass to the transform function + */ + async executeCustomMapping( + tenantId: string, + shardName: string, + transformFunctionName: string, + payload: Record + ): Promise> { + // Use path.resolve to get absolute paths for reliable comparison across platforms + const resolvedBasePath = path.resolve(this.SHARD_BASE_PATH); + const modulePath = path.resolve(this.SHARD_BASE_PATH, shardName, 'index.js'); + + // Verify the resolved module path is within the base path to prevent path traversal + if (!modulePath.startsWith(resolvedBasePath + path.sep) && modulePath !== resolvedBasePath) { + throw new InternalServerErrorException(`Security Violation: Module path escapes trusted boundary.`); + } + + // Use async filesystem check instead of blocking existsSync + try { + await fs.access(modulePath, fsConstants.F_OK); + } catch { + throw new InternalServerErrorException(`Cannot find synced shard module at ${modulePath}`); + } + + try { + let code = this.esbuildCache.get(shardName); + + // 1. Bundle the application logic (includes jsonata/libraries) + if (!code) { + this.logger.debug(`Bundling application shard: ${shardName}`); + const buildResult = await esbuild.build({ + entryPoints: [modulePath], + bundle: true, + write: false, + format: 'iife', + globalName: 'CustomLogic', + platform: 'browser', // Prevent native Node.js requires + }); + code = buildResult.outputFiles[0].text; + this.esbuildCache.set(shardName, code); + } + + this.logger.debug(`Executing custom logic ${transformFunctionName} for tenant ${tenantId} via shard ${shardName} in WebAssembly Sandbox`); + + // 2. Initialize QuickJS WebAssembly Sandbox + const QuickJS = await getQuickJS(); + const vm = QuickJS.newContext(); + + // Hard memory limit: 128MB per execution + vm.runtime.setMemoryLimit(128 * 1024 * 1024); + + try { + // 3. Pass data securely across the boundary + const payloadStr = JSON.stringify(payload); + const payloadHandle = vm.newString(payloadStr); + vm.setProp(vm.global, 'payloadStr', payloadHandle); + payloadHandle.dispose(); + + const funcNameHandle = vm.newString(transformFunctionName); + vm.setProp(vm.global, 'funcName', funcNameHandle); + funcNameHandle.dispose(); + + // 4. Compile execution wrapper + const scriptCode = ` + var __sandbox_result = null; + var __sandbox_error = null; + (async () => { + try { + ${code} + const fn = CustomLogic[funcName]; + if (typeof fn !== 'function') throw new Error('Function ' + funcName + ' is not exported from the synced shard module'); + const result = await fn(JSON.parse(payloadStr)); + __sandbox_result = JSON.stringify(result); + } catch (e) { + __sandbox_error = e.message || String(e); + } + })(); + `; + + // 5. Execute Code and flush the Promise Microtask Queue + const evalResult = vm.evalCode(scriptCode); + if (evalResult.error) { + const errDump = vm.dump(evalResult.error); + evalResult.error.dispose(); + throw new Error(errDump.message || "Unknown Sandbox Evaluation Error"); + } + evalResult.value.dispose(); + + vm.runtime.executePendingJobs(); + + // Extract result or error from the global context + const errorHandle = vm.getProp(vm.global, '__sandbox_error'); + const isErrorNull = vm.dump(errorHandle) === null; + if (!isErrorNull) { + const errStr = vm.getString(errorHandle); + errorHandle.dispose(); + throw new Error(errStr); + } + errorHandle.dispose(); + + const resultHandle = vm.getProp(vm.global, '__sandbox_result'); + const resultStr = vm.getString(resultHandle); + resultHandle.dispose(); + + return JSON.parse(resultStr); + } finally { + vm.dispose(); + } + } catch (error) { + this.logger.error(`Failed to execute custom logic in shard ${shardName}`, error); + throw new InternalServerErrorException(`Custom mapping execution failed for shard ${shardName}: ${error instanceof Error ? error.message : String(error)}`); + } + } +} \ No newline at end of file diff --git a/engine/sync/platform/core/src/sharding/application-loader.module.ts b/engine/sync/platform/core/src/sharding/application-loader.module.ts new file mode 100644 index 00000000..1d524bf9 --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-loader.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ApplicationLoaderService } from './application-loader.service.js'; +import { PipelineHookBrokerService } from './pipeline-hook-broker.service.js'; + +@Module({ + providers: [ApplicationLoaderService, PipelineHookBrokerService], + exports: [ApplicationLoaderService, PipelineHookBrokerService], +}) +export class ApplicationLoaderModule {} diff --git a/engine/sync/platform/core/src/sharding/application-loader.service.ts b/engine/sync/platform/core/src/sharding/application-loader.service.ts new file mode 100644 index 00000000..93ed4bab --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-loader.service.ts @@ -0,0 +1,107 @@ +import { Injectable, Logger } from '@nestjs/common'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import { constants as fsConstants } from 'node:fs'; +import type { ApplicationShardModule } from './application-shard.types.js'; + +// --------------------------------------------------------------------------- +// ApplicationLoaderService +// +// Dynamically loads application shard modules from the filesystem using +// Node.js native import(). Caches the loaded module in memory and +// invalidates the cache when the GitopsSyncWorker detects new git commits. +// +// The SHARD_APPLICATION_PATH environment variable controls where shards are +// loaded from. In local development this points to the monorepo directory. +// In production, it points to the checked-out nexiom-integrations repository. +// --------------------------------------------------------------------------- + +@Injectable() +export class ApplicationLoaderService { + private readonly logger = new Logger(ApplicationLoaderService.name); + + private readonly SHARD_BASE_PATH = + process.env.SHARD_APPLICATION_PATH ?? + path.resolve(process.cwd(), '../../engine/sync/application'); + + /** In-memory cache: shardName → loaded module */ + private readonly cache = new Map(); + + /** + * Loads an application shard module by name. + * + * On first call (or after invalidation), dynamically imports the shard's + * index.js from disk and caches the result. Subsequent calls return the + * cached module with no filesystem I/O. + * + * The ?v=timestamp cache-buster forces Node.js to bypass its internal module + * registry and re-read from disk after a cache invalidation. Without it, + * Node.js would return the stale in-process module despite the file changing. + * + * IMPORTANT: The caller MUST validate that the shardName is authorized for + * the tenant before calling this method. This service does not enforce + * tenant isolation — that is the caller's responsibility. + */ + async load(shardName: string): Promise { + const cached = this.cache.get(shardName); + if (cached) return cached; + + const resolvedBase = path.resolve(this.SHARD_BASE_PATH); + const shardPath = path.resolve(resolvedBase, shardName, 'index.js'); + + // Path traversal guard: ensure the shard path stays within SHARD_BASE_PATH + if (!shardPath.startsWith(resolvedBase + path.sep)) { + throw new Error( + `Security violation: shard path escapes trusted boundary — shardName="${shardName}"`, + ); + } + + // Verify the file exists before attempting import + try { + await fs.access(shardPath, fsConstants.F_OK); + } catch { + throw new Error( + `Application shard not found at ${shardPath}. ` + + `Ensure the shard has been synced via GitOps before the pipeline processes events.`, + ); + } + + this.logger.log(`Loading application shard: ${shardName}`); + + // Dynamic import with timestamp cache-buster so Node.js re-reads from disk + // after invalidation. The URL query string is ignored at runtime but prevents + // Node.js from returning the cached module from a previous import() call. + const mod = (await import( + `${shardPath}?v=${Date.now()}` + )) as ApplicationShardModule; + + this.cache.set(shardName, mod); + this.logger.log(`Application shard loaded and cached: ${shardName}`); + return mod; + } + + /** + * Invalidates the cached module for a specific shard. + * + * Called by GitopsSyncWorker when a git pull detects new commits for the + * shard. The next pipeline event that calls load(shardName) will re-import + * the updated module from disk. + * + * @param shardName - The name of the shard to invalidate (e.g. "salesforce-revenova") + */ + invalidateCache(shardName: string): void { + const existed = this.cache.delete(shardName); + if (existed) { + this.logger.log(`Cache invalidated for shard: ${shardName}`); + } + } + + /** + * Invalidates all cached shards. Used on full re-sync or after a + * platform restart to ensure stale modules are not served. + */ + invalidateAll(): void { + this.cache.clear(); + this.logger.log('All shard caches invalidated'); + } +} diff --git a/engine/sync/platform/core/src/sharding/application-shard.types.ts b/engine/sync/platform/core/src/sharding/application-shard.types.ts new file mode 100644 index 00000000..0d0a7d94 --- /dev/null +++ b/engine/sync/platform/core/src/sharding/application-shard.types.ts @@ -0,0 +1,83 @@ +import type { NormalizedRecord } from '@nexiom/piece-framework'; + +// --------------------------------------------------------------------------- +// ApplicationShardModule — the contract every application shard must satisfy. +// +// This is the ONLY shared type between the platform and application code. +// It has no implementation — pure TypeScript interface, no platform imports. +// +// Every shard loaded by ApplicationLoaderService must export these named +// functions. Optional hooks (getWebhookResponse) may be omitted. +// --------------------------------------------------------------------------- + +export type WebhookResponseShape = { + status: number; + contentType: string; + body: string; +}; + +export interface ApplicationShardModule { + /** + * L2 — Extract a stable entity identity from the raw inbound webhook payload. + * Returns null if the payload is not applicable (e.g. wrong event type). + */ + extractReplica( + payload: unknown, + ): { entityType: string; entityId: string; data: Record } | null; + + /** + * L3 — Map the extracted vendor domain object into a Nexiom canonical record. + * Returns null if normalization is not applicable for this entity type. + */ + normalize( + replica: { entityType: string; data: Record }, + ): NormalizedRecord | null | Promise; + + /** + * L3.5 — Write the normalized entity into application-owned typed tables + * (e.g. tms_carrier, tms_tp). Receives the live DB transaction so it can + * participate in the platform's existing transaction boundary. + * + * tx and db are typed as unknown so the shard stays database-agnostic at the + * type level. The implementation casts to DrizzleDb/DrizzleTransaction. + */ + writeNormalized( + tx: unknown, + db: unknown, + schemaName: string, + replicaId: string, + entityId: string, + traceId: string, + normalizedEntityType: string, + data: Record, + ): Promise; + + /** + * L4 — Build the enriched context for field-mapping rule hydration. + * Executes SQL lookups on application-owned typed tables and returns a flat + * context object merged with normalizedData before rules are applied. + */ + buildTarget( + db: unknown, + schemaName: string, + normalizedEntityType: string, + srcEntityId: string, + ): Promise>; + + /** + * Provision — Idempotent DDL for application-owned domain tables. + * Called once per tenant schema when a stitch for this app is first activated. + * All DDL inside MUST use IF NOT EXISTS. + */ + provisionDomain(db: unknown, schemaName: string): Promise; + + /** + * Optional — Return a custom HTTP response for vendor-specific webhook + * acknowledgment (e.g. Salesforce Outbound Message SOAP ACK). + * Return null if this request does not require a custom response. + */ + getWebhookResponse?( + body: unknown, + headers: Record, + ): WebhookResponseShape | null; +} diff --git a/engine/sync/platform/core/src/sharding/logic-resolver.module.ts b/engine/sync/platform/core/src/sharding/logic-resolver.module.ts deleted file mode 100644 index 984cd4db..00000000 --- a/engine/sync/platform/core/src/sharding/logic-resolver.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LogicResolverService } from './logic-resolver.service.js'; - -@Module({ - providers: [LogicResolverService], - exports: [LogicResolverService], -}) -export class LogicResolverModule {} diff --git a/engine/sync/platform/core/src/sharding/logic-resolver.service.ts b/engine/sync/platform/core/src/sharding/logic-resolver.service.ts deleted file mode 100644 index 786785dc..00000000 --- a/engine/sync/platform/core/src/sharding/logic-resolver.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; -import * as path from 'node:path'; -import * as fs from 'node:fs/promises'; -import { constants as fsConstants } from 'node:fs'; -const SHARD_APPLICATION_PATH = process.env.SHARD_APPLICATION_PATH || path.resolve(process.cwd(), '../../engine/sync/application'); - -/** - * Executes a dynamically synced first-party custom logic module. - * - * Safely resolves the tenant's logic module from the `engine/sync/application` path. - * This execution path assumes the module has already been safely synced via - * GitOps Shard Synchronization. - */ -@Injectable() -export class LogicResolverService { - private readonly logger = new Logger(LogicResolverService.name); - - // The base path where shard synchronization pulls all tenant-specific Git shards - private readonly SHARD_BASE_PATH = SHARD_APPLICATION_PATH; - - /** - * Resolves a custom transformer module from the application directory and executes it. - * - * IMPORTANT: The caller MUST ensure shardName corresponds to tenantId before calling this method. - * This service does not validate shard ownership - tenant isolation must be enforced by the caller - * (e.g., by querying the shard registry or configuration to verify the tenant has access to the shard). - * - * @param tenantId - Tenant identifier for logging/audit purposes only - * @param shardName - Name of the shard directory (caller must validate ownership) - * @param transformFunctionName - Name of the exported function to execute - * @param payload - Data to pass to the transform function - */ - async executeCustomMapping( - tenantId: string, - shardName: string, - transformFunctionName: string, - payload: Record - ): Promise> { - // Use path.resolve to get absolute paths for reliable comparison across platforms - const resolvedBasePath = path.resolve(this.SHARD_BASE_PATH); - const modulePath = path.resolve(this.SHARD_BASE_PATH, shardName, 'index.js'); - - // Verify the resolved module path is within the base path to prevent path traversal - if (!modulePath.startsWith(resolvedBasePath + path.sep) && modulePath !== resolvedBasePath) { - throw new InternalServerErrorException(`Security Violation: Module path escapes trusted boundary.`); - } - - // Use async filesystem check instead of blocking existsSync - try { - await fs.access(modulePath, fsConstants.F_OK); - } catch { - throw new InternalServerErrorException(`Cannot find synced shard module at ${modulePath}`); - } - - try { - // Use dynamic import to load the module. - // To support hot-reloading without process restarts via GitOps sync (T054), - // we read the file content and import via data URL to force fresh evaluation. - const fileContent = await fs.readFile(modulePath, 'utf-8'); - const dataUrl = `data:application/javascript;base64,${Buffer.from(fileContent).toString('base64')}`; - const customModule = await import(dataUrl); - - const fn = customModule[transformFunctionName]; - if (typeof fn !== 'function') { - throw new Error(`Function ${transformFunctionName} is not exported from the synced shard module ${shardName}`); - } - - this.logger.debug(`Executing custom logic ${transformFunctionName} for tenant ${tenantId} via shard ${shardName}`); - - // Execute with timeout to prevent indefinite hangs - const result = await this.runTransformWithTimeout(fn, payload, 30000); - return result as Record; - } catch (error) { - this.logger.error(`Failed to execute custom logic in shard ${shardName}`, error); - throw new InternalServerErrorException(`Custom mapping execution failed for shard ${shardName}: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Runs a transform function with a wall-clock timeout to prevent indefinite execution. - * If the function exceeds the timeout, the execution is aborted and an error is thrown. - * - * @param fn - The transform function to execute - * @param payload - Data to pass to the function - * @param timeoutMs - Maximum execution time in milliseconds - * @returns The result from the transform function - */ - private async runTransformWithTimeout( - fn: (payload: Record) => unknown, - payload: Record, - timeoutMs: number, - ): Promise { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - reject(new Error(`Transform function execution exceeded timeout of ${timeoutMs}ms`)); - }, timeoutMs); - - Promise.resolve(fn(payload)) - .then((result) => { - clearTimeout(timeoutHandle); - resolve(result); - }) - .catch((error) => { - clearTimeout(timeoutHandle); - reject(error); - }); - }); - } -} \ No newline at end of file diff --git a/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts b/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts new file mode 100644 index 00000000..5a171e84 --- /dev/null +++ b/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts @@ -0,0 +1,146 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ApplicationLoaderService } from './application-loader.service.js'; +import type { ApplicationShardModule } from './application-shard.types.js'; + +// --------------------------------------------------------------------------- +// PipelineHookBrokerService +// +// The single point of contact between the platform pipeline services and +// application shard code. All pipeline services (ReplicaService, +// NormalizationService, TargetBuilderService, DatabaseManager) call this +// broker — they never import anything from application packages directly. +// +// The broker loads the correct shard module via ApplicationLoaderService, +// then delegates the call to the appropriate exported function. +// --------------------------------------------------------------------------- + +@Injectable() +export class PipelineHookBrokerService { + private readonly logger = new Logger(PipelineHookBrokerService.name); + + constructor(private readonly loader: ApplicationLoaderService) {} + + /** + * Derives the shard name from appName + appProfile. + * Convention: "salesforce-revenova", "salesforce-default", etc. + */ + private shardName(appName: string, appProfile: string): string { + return `${appName}-${appProfile}`; + } + + /** + * L2 — Extract replica entity from raw inbound payload. + * Returns null if the payload is not applicable for this app. + */ + async extractReplica( + appName: string, + appProfile: string, + payload: unknown, + ): Promise> { + const shard = await this.loader.load(this.shardName(appName, appProfile)); + this.logger.debug( + { event: 'hook.extractReplica', appName, appProfile }, + 'Delegating extractReplica to application shard', + ); + return shard.extractReplica(payload); + } + + /** + * L3 — Normalize extracted replica into a canonical record. + * Returns null if normalization is not applicable. + */ + async normalize( + appName: string, + appProfile: string, + replica: { entityType: string; data: Record }, + ): Promise> { + const shard = await this.loader.load(this.shardName(appName, appProfile)); + this.logger.debug( + { event: 'hook.normalize', appName, appProfile }, + 'Delegating normalize to application shard', + ); + return shard.normalize(replica); + } + + /** + * L3.5 — Write normalized entity into application-owned typed tables. + * Executes inside the platform's existing DB transaction boundary. + */ + async writeNormalized( + appName: string, + appProfile: string, + tx: unknown, + db: unknown, + schemaName: string, + replicaId: string, + entityId: string, + traceId: string, + normalizedEntityType: string, + data: Record, + ): Promise { + const shard = await this.loader.load(this.shardName(appName, appProfile)); + this.logger.debug( + { event: 'hook.writeNormalized', appName, appProfile, normalizedEntityType }, + 'Delegating writeNormalized to application shard', + ); + return shard.writeNormalized(tx, db, schemaName, replicaId, entityId, traceId, normalizedEntityType, data); + } + + /** + * L4 — Build enriched context for field-mapping rule hydration. + * Returns empty object if no enrichment is applicable. + */ + async buildTarget( + appName: string, + appProfile: string, + db: unknown, + schemaName: string, + normalizedEntityType: string, + srcEntityId: string, + ): Promise> { + const shard = await this.loader.load(this.shardName(appName, appProfile)); + this.logger.debug( + { event: 'hook.buildTarget', appName, appProfile, normalizedEntityType }, + 'Delegating buildTarget to application shard', + ); + return shard.buildTarget(db, schemaName, normalizedEntityType, srcEntityId); + } + + /** + * Provision — Idempotent DDL provisioning for application-owned tables. + * Called once per tenant schema when a stitch is first activated. + */ + async provisionDomain( + appName: string, + db: unknown, + schemaName: string, + ): Promise { + // For provisioning, the shard name is appName-appName (the domain shard) + // e.g. "salesforce-salesforce" which owns tms_carrier, tms_tp, etc. + const shard = await this.loader.load(this.shardName(appName, appName)); + this.logger.log( + { event: 'hook.provisionDomain', appName, schemaName }, + 'Delegating provisionDomain to application shard', + ); + return shard.provisionDomain(db, schemaName); + } + + /** + * Optional — Get a custom webhook response for vendor-specific ACKs. + * Returns null if no custom response is needed. + */ + async getWebhookResponse( + appName: string, + appProfile: string, + body: unknown, + headers: Record, + ): Promise> | null> { + try { + const shard = await this.loader.load(this.shardName(appName, appProfile)); + return shard.getWebhookResponse?.(body, headers) ?? null; + } catch { + // Shard may not exist for this app — webhook response is always optional + return null; + } + } +} diff --git a/engine/sync/platform/core/vitest.config.ts b/engine/sync/platform/core/vitest.config.ts index 4eede10e..68c5acda 100644 --- a/engine/sync/platform/core/vitest.config.ts +++ b/engine/sync/platform/core/vitest.config.ts @@ -5,6 +5,13 @@ export default defineConfig({ globals: true, environment: 'node', include: ['src/**/*.spec.ts'], + // Run in a single isolated fork — WebAssembly sandbox tests (e.g. memory-limit + // tests) can spike host RSS briefly. Using a dedicated fork prevents these tests + // from crashing shared Tinypool workers that other monorepo packages rely on. + pool: 'forks', + poolOptions: { + forks: { singleFork: true }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], @@ -13,3 +20,4 @@ export default defineConfig({ }, }, }); + diff --git a/package.json b/package.json index 5f9dc3d6..6fe58157 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,18 @@ }, "packageManager": "pnpm@10.27.0", "engines": { - "node": "20.x" + "node": "20.18.1" }, "pnpm": { "overrides": { "axios": "1.13.2", "esbuild": "0.25.12" - } + }, + "onlyBuiltDependencies": [ + "@nestjs/core", + "@swc/core", + "esbuild", + "sharp" + ] } } \ No newline at end of file diff --git a/packages/pieces/platform/framework/src/app-hooks.ts b/packages/pieces/platform/framework/src/app-hooks.ts index 1734078f..1d12bd3e 100644 --- a/packages/pieces/platform/framework/src/app-hooks.ts +++ b/packages/pieces/platform/framework/src/app-hooks.ts @@ -1,11 +1,16 @@ // --------------------------------------------------------------------------- // Application Hook Registries // -// Provides the extension points that application-layer packages (domain/tms, -// domain/accounting, etc.) use to plug into the platform pipeline. +// @deprecated +// These static in-memory registries are superseded by the dynamic shard +// loading architecture (ApplicationLoaderService + PipelineHookBrokerService). // -// Naming convention: "normalized" everywhere — consistent with NormalizationService, -// normalized_entity, and NormalizedRecord. +// Pipeline services (ReplicaService, NormalizationService, TargetBuilderService) +// now load application code at runtime via PipelineHookBrokerService and +// no longer call any register* / get* functions from this file. +// +// This file is retained for one release cycle to avoid breaking any external +// consumers. It will be deleted in a future cleanup pass. // --------------------------------------------------------------------------- /** diff --git a/packages/queue/src/constants.ts b/packages/queue/src/constants.ts index 184ba3f4..ebbf39bf 100644 --- a/packages/queue/src/constants.ts +++ b/packages/queue/src/constants.ts @@ -8,6 +8,7 @@ export enum QueueName { NormalizedQueue = "normalized-queue", DeliveryQueue = "delivery-queue", AiCopilotQueue = "ai-copilot-queue", + GitopsQueue = "gitops-queue", // Dead-letter queues — activated after 5 failed attempts InboundQueueDLQ = "inbound-queue-dlq", @@ -15,6 +16,7 @@ export enum QueueName { NormalizedQueueDLQ = "normalized-queue-dlq", DeliveryQueueDLQ = "delivery-queue-dlq", AiCopilotQueueDLQ = "ai-copilot-queue-dlq", + GitopsQueueDLQ = "gitops-queue-dlq", } /** Injection token — use to inject QueueService across the application. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beca3072..d8cd62fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,7 +119,7 @@ importers: version: 3.0.3 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -376,7 +376,7 @@ importers: version: 1.13.2 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -584,7 +584,7 @@ importers: version: 3.0.3 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -825,6 +825,9 @@ importers: ai: specifier: ^6.0.146 version: 6.0.146(zod@4.3.5) + quickjs-emscripten: + specifier: ^0.32.0 + version: 0.32.0 zod: specifier: ^4.3.5 version: 4.3.5 @@ -2946,6 +2949,21 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jitl/quickjs-ffi-types@0.32.0': + resolution: {integrity: sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.32.0': + resolution: {integrity: sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==} + + '@jitl/quickjs-wasmfile-debug-sync@0.32.0': + resolution: {integrity: sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==} + + '@jitl/quickjs-wasmfile-release-asyncify@0.32.0': + resolution: {integrity: sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==} + + '@jitl/quickjs-wasmfile-release-sync@0.32.0': + resolution: {integrity: sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -8977,6 +8995,13 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + quickjs-emscripten-core@0.32.0: + resolution: {integrity: sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==} + + quickjs-emscripten@0.32.0: + resolution: {integrity: sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==} + engines: {node: '>=16.0.0'} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -10263,6 +10288,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -13058,6 +13084,24 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@jitl/quickjs-ffi-types@0.32.0': {} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-debug-sync@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-release-asyncify@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-release-sync@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -16225,7 +16269,7 @@ snapshots: bcryptjs@3.0.3: {} - better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): + better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): dependencies: '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) @@ -16242,13 +16286,13 @@ snapshots: optionalDependencies: drizzle-kit: 0.31.8 drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3) - next: 16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) + next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) pg: 8.16.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) vitest: 2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) - better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): + better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): dependencies: '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) @@ -16271,6 +16315,29 @@ snapshots: react-dom: 19.2.3(react@19.2.3) vitest: 2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) + better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): + dependencies: + '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.7(zod@4.3.5) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.9 + nanostores: 1.1.0 + zod: 4.3.5 + optionalDependencies: + drizzle-kit: 0.31.8 + drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3) + next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) + pg: 8.16.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + vitest: 2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) + better-call@1.1.7(zod@4.3.5): dependencies: '@better-auth/utils': 0.3.0 @@ -19163,6 +19230,34 @@ snapshots: rxjs: 7.8.2 zod: 4.3.5 + next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): + dependencies: + '@next/env': 16.1.1 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001780 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.1 + '@next/swc-darwin-x64': 16.1.1 + '@next/swc-linux-arm64-gnu': 16.1.1 + '@next/swc-linux-arm64-musl': 16.1.1 + '@next/swc-linux-x64-gnu': 16.1.1 + '@next/swc-linux-x64-musl': 16.1.1 + '@next/swc-win32-arm64-msvc': 16.1.1 + '@next/swc-win32-x64-msvc': 16.1.1 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.58.2 + sass: 1.97.3 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + optional: true + next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): dependencies: '@next/env': 16.1.1 @@ -19191,6 +19286,34 @@ snapshots: - babel-plugin-macros optional: true + next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): + dependencies: + '@next/env': 16.1.1 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001780 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.1 + '@next/swc-darwin-x64': 16.1.1 + '@next/swc-linux-arm64-gnu': 16.1.1 + '@next/swc-linux-arm64-musl': 16.1.1 + '@next/swc-linux-x64-gnu': 16.1.1 + '@next/swc-linux-x64-musl': 16.1.1 + '@next/swc-win32-arm64-msvc': 16.1.1 + '@next/swc-win32-x64-msvc': 16.1.1 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.58.2 + sass: 1.97.3 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + optional: true + node-abort-controller@3.1.1: {} node-addon-api@7.1.1: @@ -19992,6 +20115,18 @@ snapshots: quick-lru@5.1.1: {} + quickjs-emscripten-core@0.32.0: + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + quickjs-emscripten@0.32.0: + dependencies: + '@jitl/quickjs-wasmfile-debug-asyncify': 0.32.0 + '@jitl/quickjs-wasmfile-debug-sync': 0.32.0 + '@jitl/quickjs-wasmfile-release-asyncify': 0.32.0 + '@jitl/quickjs-wasmfile-release-sync': 0.32.0 + quickjs-emscripten-core: 0.32.0 + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -20871,6 +21006,14 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optionalDependencies: + '@babel/core': 7.28.5 + optional: true + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): dependencies: client-only: 0.0.1 @@ -20879,6 +21022,12 @@ snapshots: '@babel/core': 7.29.0 optional: true + styled-jsx@5.1.6(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optional: true + stylehacks@6.1.1(postcss@8.5.6): dependencies: browserslist: 4.28.1 diff --git a/scripts/init-localstack.sh b/scripts/init-localstack.sh index 586750a6..3b5cf7f1 100755 --- a/scripts/init-localstack.sh +++ b/scripts/init-localstack.sh @@ -15,6 +15,7 @@ QUEUES=( "replica-queue" "normalized-queue" "delivery-queue" + "gitops-queue" ) # Step 1: Create all DLQs first (no redrive policy); idempotent — ignore From 7f257a42c389817302bf8b11618d0b391fb24e1b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:27:49 +0000 Subject: [PATCH 2/9] fix: apply CodeRabbit auto-fixes Fixed 9 file(s) based on 12 unresolved review comments. Co-authored-by: CodeRabbit --- .../modules/gitops/gitops-webhook.guard.ts | 110 ++++++++++++++++-- apps/worker/src/db/database-manager.ts | 24 +++- .../modules/pipeline/gitops-sync.worker.ts | 19 ++- .../pipeline/normalization.service.spec.ts | 81 +++++++++++++ .../modules/pipeline/normalization.service.ts | 16 ++- .../sharding/application-executor.service.ts | 41 ++++++- .../sharding/application-loader.service.ts | 45 ++++++- .../sharding/pipeline-hook-broker.service.ts | 39 ++++++- .../platform/framework/src/app-hooks.ts | 23 ++++ 9 files changed, 371 insertions(+), 27 deletions(-) diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index 8660e96b..4fcea9bc 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -5,43 +5,139 @@ import { UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { createHash, timingSafeEqual } from 'node:crypto'; +import { createHash, createHmac, timingSafeEqual } from 'node:crypto'; import type { Request } from 'express'; /** * Guards the POST /internal/gitops/sync endpoint. * - * Validates that the incoming request carries: - * Authorization: Bearer + * Validates that the incoming request carries one of: + * 1. X-Hub-Signature-256 (GitHub) - HMAC SHA256 of raw request body + * 2. X-Gitlab-Token (GitLab) - exact match to GITOPS_WEBHOOK_SECRET + * 3. Authorization: Bearer (fallback) * * Uses a constant-time comparison to prevent timing-based secret extraction. * Register GITOPS_WEBHOOK_SECRET in GitHub/GitLab as the webhook secret. * * Returns 401 if: - * - The Authorization header is absent or not a Bearer token. - * - The token does not match GITOPS_WEBHOOK_SECRET. + * - None of the above authentication methods are present or valid. */ @Injectable() export class GitopsWebhookGuard implements CanActivate { /** Pre-computed SHA-256 digest of the expected secret. Computed once at construction. */ private readonly expectedDigest: Buffer; + /** The raw secret for HMAC and exact-match validation. */ + private readonly secret: string; constructor(config: ConfigService) { const secret = config.getOrThrow('GITOPS_WEBHOOK_SECRET'); + + // Validate secret is non-empty and non-whitespace + const trimmedSecret = secret.trim(); + if (trimmedSecret.length === 0) { + throw new Error( + 'GITOPS_WEBHOOK_SECRET must be a non-empty, non-whitespace string', + ); + } + + this.secret = secret; this.expectedDigest = createHash('sha256').update(secret).digest(); } canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); + + // Strategy 1: GitHub X-Hub-Signature-256 (HMAC SHA256 of raw body) + const githubSig = request.headers['x-hub-signature-256'] as string | undefined; + if (githubSig) { + return this.validateGitHubSignature(request, githubSig); + } + + // Strategy 2: GitLab X-Gitlab-Token (exact match) + const gitlabToken = request.headers['x-gitlab-token'] as string | undefined; + if (gitlabToken) { + return this.validateGitLabToken(gitlabToken); + } + + // Strategy 3: Authorization Bearer (fallback) const authHeader = request.headers.authorization; + if (authHeader?.startsWith('Bearer ')) { + return this.validateBearerToken(authHeader); + } + + throw new UnauthorizedException( + 'Missing or invalid authentication header (expected X-Hub-Signature-256, X-Gitlab-Token, or Authorization: Bearer)', + ); + } + + private validateGitHubSignature(request: Request, signature: string): boolean { + // Signature format: "sha256=" + if (!signature.startsWith('sha256=')) { + throw new UnauthorizedException('Invalid X-Hub-Signature-256 format'); + } + + const providedDigest = signature.slice(7); - if (!authHeader?.startsWith('Bearer ')) { + // Get raw request body (must be available via express.raw() middleware) + const rawBody = (request as any).rawBody as Buffer | undefined; + if (!rawBody) { throw new UnauthorizedException( - 'Missing or invalid Authorization header', + 'Raw request body not available for HMAC verification', ); } + // Compute HMAC SHA256 of raw body + const expectedHmac = createHmac('sha256', this.secret) + .update(rawBody) + .digest('hex'); + + // Constant-time comparison + try { + const providedBuffer = Buffer.from(providedDigest, 'hex'); + const expectedBuffer = Buffer.from(expectedHmac, 'hex'); + + if (providedBuffer.length !== expectedBuffer.length) { + throw new UnauthorizedException('Invalid GitHub webhook signature'); + } + + if (!timingSafeEqual(providedBuffer, expectedBuffer)) { + throw new UnauthorizedException('Invalid GitHub webhook signature'); + } + + return true; + } catch (err) { + if (err instanceof UnauthorizedException) throw err; + throw new UnauthorizedException('Invalid GitHub webhook signature format'); + } + } + + private validateGitLabToken(token: string): boolean { + // Validate token is non-empty and non-whitespace + const trimmedToken = token.trim(); + if (trimmedToken.length === 0) { + throw new UnauthorizedException('X-Gitlab-Token cannot be empty or whitespace'); + } + + // GitLab uses exact match - constant-time comparison + const tokenDigest = createHash('sha256').update(token).digest(); + const secretDigest = createHash('sha256').update(this.secret).digest(); + + if (!timingSafeEqual(tokenDigest, secretDigest)) { + throw new UnauthorizedException('Invalid GitLab webhook token'); + } + + return true; + } + + private validateBearerToken(authHeader: string): boolean { const token = authHeader.slice(7); + + // Validate token is non-empty and non-whitespace + const trimmedToken = token.trim(); + if (trimmedToken.length === 0) { + throw new UnauthorizedException('Bearer token cannot be empty or whitespace'); + } + const tokenDigest = createHash('sha256').update(token).digest(); if (!timingSafeEqual(tokenDigest, this.expectedDigest)) { diff --git a/apps/worker/src/db/database-manager.ts b/apps/worker/src/db/database-manager.ts index 0152fcfa..d5c048c0 100644 --- a/apps/worker/src/db/database-manager.ts +++ b/apps/worker/src/db/database-manager.ts @@ -703,11 +703,27 @@ export class DatabaseManager { tenantDb: import("@nexiom/database").DrizzleDb, schemaName: string, ) => { - const shard = await loaderInstance - .load(`${appName}-${appName}`) - .catch(() => null); + let shard; + try { + shard = await loaderInstance.load(`${appName}-${appName}`); + } catch (loadErr) { + console.error( + `domainProvisionerResolver: Failed to load shard ${appName}-${appName} for schema ${schemaName}:`, + loadErr instanceof Error ? loadErr.message : String(loadErr), + ); + throw loadErr; + } + if (shard?.provisionDomain) { - await shard.provisionDomain(tenantDb, schemaName); + try { + await shard.provisionDomain(tenantDb, schemaName); + } catch (provisionErr) { + console.error( + `domainProvisionerResolver: shard.provisionDomain failed for appName=${appName}, schemaName=${schemaName}:`, + provisionErr instanceof Error ? provisionErr.message : String(provisionErr), + ); + throw provisionErr; + } } }; }; diff --git a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts index bb71ba14..ee658cd7 100644 --- a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts +++ b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts @@ -91,7 +91,24 @@ export class GitopsSyncWorker implements OnModuleInit { * when a webhook is received from GitHub/GitLab. */ async syncShard(shardName: string): Promise { - const repoPath = path.join(this.SHARD_BASE_PATH, shardName); + // Normalize and validate shardName to prevent path traversal + const resolvedBasePath = path.resolve(this.SHARD_BASE_PATH); + const repoPath = path.resolve(this.SHARD_BASE_PATH, shardName); + + // Ensure repoPath is contained within SHARD_BASE_PATH + const relativePath = path.relative(resolvedBasePath, repoPath); + if ( + relativePath.startsWith('..') || + path.isAbsolute(relativePath) || + !repoPath.startsWith(resolvedBasePath + path.sep) + ) { + this.logger.warn( + `Path traversal attempt detected: shardName="${shardName}" escapes SHARD_BASE_PATH. Rejecting sync.`, + ); + throw new Error( + `Security violation: shardName escapes trusted boundary — shardName="${shardName}"`, + ); + } try { await fs.stat(path.join(repoPath, ".git")); diff --git a/apps/worker/src/modules/pipeline/normalization.service.spec.ts b/apps/worker/src/modules/pipeline/normalization.service.spec.ts index 5e820d68..572b83e4 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.spec.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.spec.ts @@ -111,10 +111,37 @@ describe("NormalizationService", () => { ); const handler = queueService.consume.mock.calls[0][1]; + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NormalizationService, + { provide: QueueService, useValue: queueService }, + { provide: DATABASE_CONNECTION, useValue: db }, + { provide: StorageResolverService, useValue: storageResolver }, + { provide: PieceRegistryService, useValue: pieceRegistry }, + { + provide: PipelineHookBrokerService, + useValue: { + normalize: vi.fn().mockResolvedValue(null), + writeNormalized: vi.fn().mockResolvedValue(undefined), + }, + }, + ], + }).compile(); + const mockBroker = module.get(PipelineHookBrokerService); + await handler({ traceId: "123", connectionId: "456" }); expect(db.transaction).toHaveBeenCalled(); expect(mockTxInsert).toHaveBeenCalled(); + // Assert that broker.normalize was called + expect(mockBroker.normalize).toHaveBeenCalledWith( + "test_app", + "default", + expect.objectContaining({ + entityType: expect.any(String), + data: expect.any(Object), + }), + ); }); it("should handle errors gracefully", async () => { @@ -273,4 +300,58 @@ describe("NormalizationService", () => { // Error-handler transaction attempted (for FAIL sync_log) expect(db.transaction).toHaveBeenCalled(); }); + + it("should call broker.writeNormalized when broker.normalize returns non-null", async () => { + const mockBroker = { + normalize: vi.fn().mockResolvedValue({ + canonicalType: "BROKER_TYPE", + data: { brokered: true }, + }), + writeNormalized: vi.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NormalizationService, + { provide: QueueService, useValue: queueService }, + { provide: DATABASE_CONNECTION, useValue: db }, + { provide: StorageResolverService, useValue: storageResolver }, + { provide: PieceRegistryService, useValue: pieceRegistry }, + { provide: PipelineHookBrokerService, useValue: mockBroker }, + ], + }).compile(); + + const svc = module.get(NormalizationService); + svc.onModuleInit(); + const handler = queueService.consume.mock.calls[0][1]; + + await handler({ traceId: "123", connectionId: "456" }); + + // Assert broker.normalize was called + expect(mockBroker.normalize).toHaveBeenCalledWith( + "test_app", + "default", + expect.objectContaining({ + entityType: expect.any(String), + data: expect.any(Object), + }), + ); + + // Assert broker.writeNormalized was called + expect(mockBroker.writeNormalized).toHaveBeenCalledWith( + "test_app", + "default", + expect.anything(), // tx + expect.anything(), // db + "ws_1", // schemaName + "1", // replicaId + expect.any(String), // entityId + "123", // traceId + "BROKER_TYPE", // normalizedEntityType + expect.objectContaining({ brokered: true }), // data + ); + + // Piece.normalize should NOT have been called (broker took precedence) + expect(pieceRegistry.getPiece().normalize).not.toHaveBeenCalled(); + }); }); diff --git a/apps/worker/src/modules/pipeline/normalization.service.ts b/apps/worker/src/modules/pipeline/normalization.service.ts index 679c4d19..7db58920 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.ts @@ -164,7 +164,21 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { entityType: replica.entityType, data: replica.data as Record, }) - .catch(() => null); // shard may not exist yet — fall through to piece.normalize + .catch((err) => { + // Log the error for operational visibility, then fall through to piece.normalize + this.logger.warn( + { + event: 'l3.shard_normalize_failed', + connectionAppName, + appProfile, + entityType: replica.entityType, + err: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + }, + 'Shard normalize failed — falling back to piece.normalize', + ); + return null; + }); // shard may not exist yet — fall through to piece.normalize if (normalizedFromShard) { canonicalType = normalizedFromShard.canonicalType; diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.ts b/engine/sync/platform/core/src/sharding/application-executor.service.ts index 8f56bdea..3b12f2ac 100644 --- a/engine/sync/platform/core/src/sharding/application-executor.service.ts +++ b/engine/sync/platform/core/src/sharding/application-executor.service.ts @@ -88,10 +88,18 @@ export class ApplicationExecutorService { // 2. Initialize QuickJS WebAssembly Sandbox const QuickJS = await getQuickJS(); const vm = QuickJS.newContext(); - + // Hard memory limit: 128MB per execution vm.runtime.setMemoryLimit(128 * 1024 * 1024); + // Set interrupt handler with deadline (30 second timeout) + const TIMEOUT_MS = 30_000; + const deadline = Date.now() + TIMEOUT_MS; + const shouldInterruptAfterDeadline = () => { + return Date.now() > deadline; + }; + vm.runtime.setInterruptHandler(shouldInterruptAfterDeadline); + try { // 3. Pass data securely across the boundary const payloadStr = JSON.stringify(payload); @@ -103,11 +111,11 @@ export class ApplicationExecutorService { vm.setProp(vm.global, 'funcName', funcNameHandle); funcNameHandle.dispose(); - // 4. Compile execution wrapper + // 4. Compile execution wrapper that produces a guest Promise const scriptCode = ` var __sandbox_result = null; var __sandbox_error = null; - (async () => { + var __sandbox_promise = (async () => { try { ${code} const fn = CustomLogic[funcName]; @@ -120,7 +128,7 @@ export class ApplicationExecutorService { })(); `; - // 5. Execute Code and flush the Promise Microtask Queue + // 5. Execute Code const evalResult = vm.evalCode(scriptCode); if (evalResult.error) { const errDump = vm.dump(evalResult.error); @@ -128,9 +136,20 @@ export class ApplicationExecutorService { throw new Error(errDump.message || "Unknown Sandbox Evaluation Error"); } evalResult.value.dispose(); - + + // Check if deadline has elapsed before executing pending jobs + if (Date.now() > deadline) { + throw new Error(`Execution timeout: exceeded ${TIMEOUT_MS}ms deadline before async completion`); + } + + // Flush the Promise Microtask Queue vm.runtime.executePendingJobs(); + // Check again after job execution + if (Date.now() > deadline) { + throw new Error(`Execution timeout: exceeded ${TIMEOUT_MS}ms deadline during async execution`); + } + // Extract result or error from the global context const errorHandle = vm.getProp(vm.global, '__sandbox_error'); const isErrorNull = vm.dump(errorHandle) === null; @@ -145,7 +164,17 @@ export class ApplicationExecutorService { const resultStr = vm.getString(resultHandle); resultHandle.dispose(); - return JSON.parse(resultStr); + // Parse and validate return type + const parsed = JSON.parse(resultStr); + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error( + `Invalid shard mapping return: expected Record, got ${ + parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed + }` + ); + } + + return parsed as Record; } finally { vm.dispose(); } diff --git a/engine/sync/platform/core/src/sharding/application-loader.service.ts b/engine/sync/platform/core/src/sharding/application-loader.service.ts index 93ed4bab..7e68000c 100644 --- a/engine/sync/platform/core/src/sharding/application-loader.service.ts +++ b/engine/sync/platform/core/src/sharding/application-loader.service.ts @@ -49,14 +49,18 @@ export class ApplicationLoaderService { const resolvedBase = path.resolve(this.SHARD_BASE_PATH); const shardPath = path.resolve(resolvedBase, shardName, 'index.js'); - // Path traversal guard: ensure the shard path stays within SHARD_BASE_PATH - if (!shardPath.startsWith(resolvedBase + path.sep)) { + // Canonicalize paths to prevent symlink-based traversal attacks + let baseReal: string; + let shardReal: string; + try { + baseReal = await fs.realpath(resolvedBase); + } catch { throw new Error( - `Security violation: shard path escapes trusted boundary — shardName="${shardName}"`, + `SHARD_BASE_PATH does not exist or is not accessible: ${resolvedBase}`, ); } - // Verify the file exists before attempting import + // Verify the file exists before attempting canonicalization try { await fs.access(shardPath, fsConstants.F_OK); } catch { @@ -66,6 +70,21 @@ export class ApplicationLoaderService { ); } + try { + shardReal = await fs.realpath(shardPath); + } catch { + throw new Error( + `Failed to canonicalize shard path: ${shardPath}`, + ); + } + + // Path traversal guard: ensure the canonical shard path stays within canonical base + if (shardReal !== baseReal && !shardReal.startsWith(baseReal + path.sep)) { + throw new Error( + `Security violation: shard path escapes trusted boundary — shardName="${shardName}"`, + ); + } + this.logger.log(`Loading application shard: ${shardName}`); // Dynamic import with timestamp cache-buster so Node.js re-reads from disk @@ -75,6 +94,24 @@ export class ApplicationLoaderService { `${shardPath}?v=${Date.now()}` )) as ApplicationShardModule; + // Validate exported shape to ensure required functions are present + const requiredExports = ['extractReplica', 'normalize', 'writeNormalized', 'buildTarget', 'provisionDomain']; + const missingExports: string[] = []; + + for (const exportName of requiredExports) { + if (typeof mod[exportName as keyof ApplicationShardModule] !== 'function') { + missingExports.push(exportName); + } + } + + if (missingExports.length > 0) { + throw new Error( + `Invalid shard module at ${shardPath} (shardName="${shardName}"): ` + + `missing or invalid exports: ${missingExports.join(', ')}. ` + + `All application shards must export: ${requiredExports.join(', ')}.`, + ); + } + this.cache.set(shardName, mod); this.logger.log(`Application shard loaded and cached: ${shardName}`); return mod; diff --git a/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts b/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts index 5a171e84..d13baa07 100644 --- a/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts +++ b/engine/sync/platform/core/src/sharding/pipeline-hook-broker.service.ts @@ -135,12 +135,43 @@ export class PipelineHookBrokerService { body: unknown, headers: Record, ): Promise> | null> { + let shard: ApplicationShardModule; try { - const shard = await this.loader.load(this.shardName(appName, appProfile)); - return shard.getWebhookResponse?.(body, headers) ?? null; - } catch { - // Shard may not exist for this app — webhook response is always optional + shard = await this.loader.load(this.shardName(appName, appProfile)); + } catch (loadErr) { + // Inspect error to determine if it's a "not found" case + const errMsg = loadErr instanceof Error ? loadErr.message : String(loadErr); + if (errMsg.includes('not found') || errMsg.includes('ENOENT')) { + // Shard does not exist — return null (webhook response is optional) + return null; + } + // Loader error is NOT a "missing shard" — log and rethrow + this.logger.error( + { event: 'hook.getWebhookResponse.loader_error', appName, appProfile, err: errMsg }, + 'Failed to load shard for getWebhookResponse', + ); + throw loadErr; + } + + // Shard loaded successfully — invoke getWebhookResponse if it exists + if (!shard.getWebhookResponse) { return null; } + + try { + return await shard.getWebhookResponse(body, headers); + } catch (hookErr) { + // Runtime error in the shard's getWebhookResponse — log and rethrow + this.logger.error( + { + event: 'hook.getWebhookResponse.runtime_error', + appName, + appProfile, + err: hookErr instanceof Error ? hookErr.message : String(hookErr), + }, + 'Shard getWebhookResponse threw an error', + ); + throw hookErr; + } } } diff --git a/packages/pieces/platform/framework/src/app-hooks.ts b/packages/pieces/platform/framework/src/app-hooks.ts index 1d12bd3e..f0ac83f7 100644 --- a/packages/pieces/platform/framework/src/app-hooks.ts +++ b/packages/pieces/platform/framework/src/app-hooks.ts @@ -20,6 +20,8 @@ * * tx and db are typed as `unknown` so @nexiom/piece-framework stays * database-agnostic. Domain packages cast them to DrizzleDb. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export type AppNormalizedWriterFn = ( tx: unknown, @@ -37,6 +39,8 @@ export type AppNormalizedWriterFn = ( * application's typed normalized tables before field mapping is applied. * * db is typed as `unknown` so the framework stays database-agnostic. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export type AppTargetBuilderFn = ( db: unknown, @@ -55,6 +59,8 @@ export type AppTargetBuilderFn = ( * per-entity tables (tms_carrier, tms_tp, etc.) using IF NOT EXISTS DDL. * * db is typed as `unknown` so the framework stays database-agnostic. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export type AppDomainProvisionerFn = ( db: unknown, @@ -68,6 +74,8 @@ const domainProvisionerRegistry = new Map(); /** * Registers an application-layer normalized writer. * Called from the connector piece's index.ts at module load time. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export function registerNormalizedWriter(appName: string, appProfile: string, fn: AppNormalizedWriterFn): void { if (!normalizedWriterRegistry.has(appName)) normalizedWriterRegistry.set(appName, new Map()); @@ -82,6 +90,9 @@ export function registerNormalizedWriter(appName: string, appProfile: string, fn appMap.set(appProfile, fn); } +/** + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release + */ export function getNormalizedWriter(appName: string, appProfile: string | undefined): AppNormalizedWriterFn | undefined { if (!appProfile) return undefined; return normalizedWriterRegistry.get(appName)?.get(appProfile); @@ -90,6 +101,8 @@ export function getNormalizedWriter(appName: string, appProfile: string | undefi /** * Registers an application-layer target builder. * Called from the connector piece's index.ts at module load time. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export function registerTargetBuilder(appName: string, appProfile: string, fn: AppTargetBuilderFn): void { if (!targetBuilderRegistry.has(appName)) targetBuilderRegistry.set(appName, new Map()); @@ -104,6 +117,9 @@ export function registerTargetBuilder(appName: string, appProfile: string, fn: A appMap.set(appProfile, fn); } +/** + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release + */ export function getTargetBuilder(appName: string, appProfile: string | undefined): AppTargetBuilderFn | undefined { if (!appProfile) return undefined; return targetBuilderRegistry.get(appName)?.get(appProfile); @@ -116,6 +132,8 @@ export function getTargetBuilder(appName: string, appProfile: string | undefined * The platform's workspace activation flow calls this to create the * application's domain tables (tms_carrier, tms_tp, etc.) per tenant schema. * All DDL inside MUST be idempotent (CREATE TABLE IF NOT EXISTS). + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export function registerDomainProvisioner(appName: string, fn: AppDomainProvisionerFn): void { if (domainProvisionerRegistry.has(appName)) { @@ -128,6 +146,9 @@ export function registerDomainProvisioner(appName: string, fn: AppDomainProvisio domainProvisionerRegistry.set(appName, fn); } +/** + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release + */ export function getDomainProvisioner(appName: string): AppDomainProvisionerFn | undefined { return domainProvisionerRegistry.get(appName); } @@ -135,6 +156,8 @@ export function getDomainProvisioner(appName: string): AppDomainProvisionerFn | /** * INTERNAL TEST HELPER — Clears all app hook registries. * This function is exported for test isolation only. Do not call in production code. + * + * @deprecated Deprecated — superseded by the dynamic shard loading architecture (ApplicationLoaderService + PipelineHookBrokerService); will be removed in the next release */ export function __resetAppHookRegistries(): void { normalizedWriterRegistry.clear(); From 03b9bf003f7508d0c553084e26f2a54212e2211a Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:43:54 +0000 Subject: [PATCH 3/9] fix: apply CodeRabbit auto-fixes Fixed 6 file(s) based on 8 unresolved review comments. Co-authored-by: CodeRabbit --- .../modules/gitops/gitops-webhook.guard.ts | 4 +- apps/worker/src/db/database-manager.ts | 26 +++-------- .../modules/pipeline/gitops-sync.worker.ts | 16 ++++++- .../pipeline/normalization.service.spec.ts | 34 ++++++++------ .../modules/pipeline/normalization.service.ts | 2 +- .../sharding/application-executor.service.ts | 46 ++++++++++++++----- 6 files changed, 80 insertions(+), 48 deletions(-) diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index 4fcea9bc..16eed20f 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -40,8 +40,8 @@ export class GitopsWebhookGuard implements CanActivate { ); } - this.secret = secret; - this.expectedDigest = createHash('sha256').update(secret).digest(); + this.secret = trimmedSecret; + this.expectedDigest = createHash('sha256').update(trimmedSecret).digest(); } canActivate(context: ExecutionContext): boolean { diff --git a/apps/worker/src/db/database-manager.ts b/apps/worker/src/db/database-manager.ts index d5c048c0..03006b2b 100644 --- a/apps/worker/src/db/database-manager.ts +++ b/apps/worker/src/db/database-manager.ts @@ -690,11 +690,12 @@ export class DatabaseManager { try { const db = drizzle(client, { schema }); const { Pool } = await import("pg"); - const { ApplicationLoaderService } = await import("@nexiom/engine"); + const { ApplicationLoaderService, PipelineHookBrokerService } = await import("@nexiom/engine"); // Instantiate the loader directly — this is a CLI script, not in NestJS DI. // The loader reads from SHARD_APPLICATION_PATH and caches dynamically imported modules. const loaderInstance = new ApplicationLoaderService(); + const broker = new PipelineHookBrokerService(loaderInstance); // Build a domainProvisionerResolver function that matches the TenantDatabaseManager interface: // (appName: string) => ((db, schemaName) => Promise) | undefined @@ -703,27 +704,14 @@ export class DatabaseManager { tenantDb: import("@nexiom/database").DrizzleDb, schemaName: string, ) => { - let shard; try { - shard = await loaderInstance.load(`${appName}-${appName}`); - } catch (loadErr) { + await broker.provisionDomain(appName, tenantDb, schemaName); + } catch (provisionErr) { console.error( - `domainProvisionerResolver: Failed to load shard ${appName}-${appName} for schema ${schemaName}:`, - loadErr instanceof Error ? loadErr.message : String(loadErr), + `domainProvisionerResolver: broker.provisionDomain failed for appName=${appName}, schemaName=${schemaName}:`, + provisionErr instanceof Error ? provisionErr.message : String(provisionErr), ); - throw loadErr; - } - - if (shard?.provisionDomain) { - try { - await shard.provisionDomain(tenantDb, schemaName); - } catch (provisionErr) { - console.error( - `domainProvisionerResolver: shard.provisionDomain failed for appName=${appName}, schemaName=${schemaName}:`, - provisionErr instanceof Error ? provisionErr.message : String(provisionErr), - ); - throw provisionErr; - } + throw provisionErr; } }; }; diff --git a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts index ee658cd7..e81b6866 100644 --- a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts +++ b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts @@ -34,7 +34,19 @@ export class GitopsSyncWorker implements OnModuleInit { this.logger.log( "GitOps webhook event received — triggering immediate sync", ); - await this.syncShardRepositories(); + + // Check reentrancy guard before processing + if (this.isSyncRunning) { + this.logger.warn("Skipping GitOps sync — previous run still in progress"); + throw new Error("GitOps sync already in progress — message will be retried"); + } + + try { + await this.syncShardRepositories(); + } catch (error) { + this.logger.error("GitOps sync failed via queue message", error); + throw error; + } }); } @@ -72,6 +84,8 @@ export class GitopsSyncWorker implements OnModuleInit { if (shardDirs.length === 0) { this.logger.debug("No active shard directories found for gitops sync."); + // When called from cron, we just return (this is not an error condition) + // When called from queue, this will propagate up without throwing return; } diff --git a/apps/worker/src/modules/pipeline/normalization.service.spec.ts b/apps/worker/src/modules/pipeline/normalization.service.spec.ts index 572b83e4..cdaaba4d 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.spec.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.spec.ts @@ -68,6 +68,10 @@ describe("NormalizationService", () => { insert: mockTxInsert, update: vi.fn().mockReturnThis(), set: vi.fn().mockReturnThis(), + // Add transaction method to support nested transactions (savepoints) + transaction: vi.fn().mockImplementation(async (spCb) => { + return spCb(tx); + }), }; return cb(tx); }), @@ -104,13 +108,13 @@ describe("NormalizationService", () => { }); it("should process message normally", async () => { - service.onModuleInit(); - expect(queueService.consume).toHaveBeenCalledWith( - QueueName.ReplicaQueue, - expect.any(Function), - ); - const handler = queueService.consume.mock.calls[0][1]; + // Create a spy for the broker that was injected into the service + const mockBroker = { + normalize: vi.fn().mockResolvedValue(null), + writeNormalized: vi.fn().mockResolvedValue(undefined), + }; + // Create a new module with the spy broker const module: TestingModule = await Test.createTestingModule({ providers: [ NormalizationService, @@ -118,16 +122,18 @@ describe("NormalizationService", () => { { provide: DATABASE_CONNECTION, useValue: db }, { provide: StorageResolverService, useValue: storageResolver }, { provide: PieceRegistryService, useValue: pieceRegistry }, - { - provide: PipelineHookBrokerService, - useValue: { - normalize: vi.fn().mockResolvedValue(null), - writeNormalized: vi.fn().mockResolvedValue(undefined), - }, - }, + { provide: PipelineHookBrokerService, useValue: mockBroker }, ], }).compile(); - const mockBroker = module.get(PipelineHookBrokerService); + + const testService = module.get(NormalizationService); + testService.onModuleInit(); + + expect(queueService.consume).toHaveBeenCalledWith( + QueueName.ReplicaQueue, + expect.any(Function), + ); + const handler = queueService.consume.mock.calls[0][1]; await handler({ traceId: "123", connectionId: "456" }); diff --git a/apps/worker/src/modules/pipeline/normalization.service.ts b/apps/worker/src/modules/pipeline/normalization.service.ts index 7db58920..d3b0d6db 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.ts @@ -256,7 +256,7 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { connectionAppName, appProfile, sp, - this.db, + sp, schemaName, replica.id, replica.entityId, diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.ts b/engine/sync/platform/core/src/sharding/application-executor.service.ts index 3b12f2ac..c56074e1 100644 --- a/engine/sync/platform/core/src/sharding/application-executor.service.ts +++ b/engine/sync/platform/core/src/sharding/application-executor.service.ts @@ -49,20 +49,41 @@ export class ApplicationExecutorService { transformFunctionName: string, payload: Record ): Promise> { - // Use path.resolve to get absolute paths for reliable comparison across platforms - const resolvedBasePath = path.resolve(this.SHARD_BASE_PATH); - const modulePath = path.resolve(this.SHARD_BASE_PATH, shardName, 'index.js'); + // Resolve real filesystem paths to prevent symlink traversal attacks + let resolvedBasePath: string; + let realModulePath: string; - // Verify the resolved module path is within the base path to prevent path traversal - if (!modulePath.startsWith(resolvedBasePath + path.sep) && modulePath !== resolvedBasePath) { - throw new InternalServerErrorException(`Security Violation: Module path escapes trusted boundary.`); + try { + resolvedBasePath = await fs.realpath(this.SHARD_BASE_PATH); + } catch (err) { + this.logger.error( + `Failed to resolve SHARD_BASE_PATH: ${this.SHARD_BASE_PATH}`, + err instanceof Error ? err.stack : String(err), + ); + throw new InternalServerErrorException(`Internal server error`); } - // Use async filesystem check instead of blocking existsSync + const modulePath = path.resolve(this.SHARD_BASE_PATH, shardName, 'index.js'); + + // Use async filesystem check and resolve real path try { await fs.access(modulePath, fsConstants.F_OK); - } catch { - throw new InternalServerErrorException(`Cannot find synced shard module at ${modulePath}`); + realModulePath = await fs.realpath(modulePath); + } catch (err) { + this.logger.error( + `Cannot find or resolve shard module: ${modulePath}`, + err instanceof Error ? err.stack : String(err), + ); + throw new InternalServerErrorException(`Failed to load shard module`); + } + + // Verify the real module path is within the real base path to prevent path traversal + if (!realModulePath.startsWith(resolvedBasePath + path.sep) && realModulePath !== resolvedBasePath) { + this.logger.error( + `Security violation: realModulePath escapes trusted boundary. ` + + `realModulePath="${realModulePath}", resolvedBasePath="${resolvedBasePath}"`, + ); + throw new InternalServerErrorException(`Internal server error`); } try { @@ -179,8 +200,11 @@ export class ApplicationExecutorService { vm.dispose(); } } catch (error) { - this.logger.error(`Failed to execute custom logic in shard ${shardName}`, error); - throw new InternalServerErrorException(`Custom mapping execution failed for shard ${shardName}: ${error instanceof Error ? error.message : String(error)}`); + this.logger.error( + `Failed to execute custom logic in shard ${shardName} at modulePath ${modulePath}`, + error instanceof Error ? error.stack : String(error), + ); + throw new InternalServerErrorException(`Failed to load shard module`); } } } \ No newline at end of file From bf39bcdbacb337891f5fc3882b07e515a059c389 Mon Sep 17 00:00:00 2001 From: Pramod Narayana Date: Thu, 30 Apr 2026 23:38:09 +0530 Subject: [PATCH 4/9] platform app decouple code review --- .../modules/gitops/gitops-webhook.guard.ts | 24 +- .../webhooks/webhooks.controller.spec.ts | 74 ++++++ apps/api/src/scripts/admin-bootstrap.ts | 1 + apps/worker/src/db/database-manager.ts | 7 +- .../modules/pipeline/gitops-sync.worker.ts | 10 +- .../pipeline/normalization.service.spec.ts | 32 ++- .../modules/pipeline/normalization.service.ts | 5 +- .../sharding/application-executor.module.ts | 8 - .../application-executor.service.spec.ts | 151 ------------- .../sharding/application-executor.service.ts | 210 ------------------ 10 files changed, 130 insertions(+), 392 deletions(-) delete mode 100644 engine/sync/platform/core/src/sharding/application-executor.module.ts delete mode 100644 engine/sync/platform/core/src/sharding/application-executor.service.spec.ts delete mode 100644 engine/sync/platform/core/src/sharding/application-executor.service.ts diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index 16eed20f..cdae55e5 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -4,6 +4,7 @@ import { Injectable, UnauthorizedException, } from '@nestjs/common'; +import type { RawBodyRequest } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { createHash, createHmac, timingSafeEqual } from 'node:crypto'; import type { Request } from 'express'; @@ -48,7 +49,9 @@ export class GitopsWebhookGuard implements CanActivate { const request = context.switchToHttp().getRequest(); // Strategy 1: GitHub X-Hub-Signature-256 (HMAC SHA256 of raw body) - const githubSig = request.headers['x-hub-signature-256'] as string | undefined; + const githubSig = request.headers['x-hub-signature-256'] as + | string + | undefined; if (githubSig) { return this.validateGitHubSignature(request, githubSig); } @@ -70,7 +73,10 @@ export class GitopsWebhookGuard implements CanActivate { ); } - private validateGitHubSignature(request: Request, signature: string): boolean { + private validateGitHubSignature( + request: Request, + signature: string, + ): boolean { // Signature format: "sha256=" if (!signature.startsWith('sha256=')) { throw new UnauthorizedException('Invalid X-Hub-Signature-256 format'); @@ -79,7 +85,7 @@ export class GitopsWebhookGuard implements CanActivate { const providedDigest = signature.slice(7); // Get raw request body (must be available via express.raw() middleware) - const rawBody = (request as any).rawBody as Buffer | undefined; + const rawBody = (request as RawBodyRequest).rawBody; if (!rawBody) { throw new UnauthorizedException( 'Raw request body not available for HMAC verification', @@ -107,7 +113,9 @@ export class GitopsWebhookGuard implements CanActivate { return true; } catch (err) { if (err instanceof UnauthorizedException) throw err; - throw new UnauthorizedException('Invalid GitHub webhook signature format'); + throw new UnauthorizedException( + 'Invalid GitHub webhook signature format', + ); } } @@ -115,7 +123,9 @@ export class GitopsWebhookGuard implements CanActivate { // Validate token is non-empty and non-whitespace const trimmedToken = token.trim(); if (trimmedToken.length === 0) { - throw new UnauthorizedException('X-Gitlab-Token cannot be empty or whitespace'); + throw new UnauthorizedException( + 'X-Gitlab-Token cannot be empty or whitespace', + ); } // GitLab uses exact match - constant-time comparison @@ -135,7 +145,9 @@ export class GitopsWebhookGuard implements CanActivate { // Validate token is non-empty and non-whitespace const trimmedToken = token.trim(); if (trimmedToken.length === 0) { - throw new UnauthorizedException('Bearer token cannot be empty or whitespace'); + throw new UnauthorizedException( + 'Bearer token cannot be empty or whitespace', + ); } const tokenDigest = createHash('sha256').update(token).digest(); diff --git a/apps/api/src/modules/webhooks/webhooks.controller.spec.ts b/apps/api/src/modules/webhooks/webhooks.controller.spec.ts index 77fd83cb..e7eca088 100644 --- a/apps/api/src/modules/webhooks/webhooks.controller.spec.ts +++ b/apps/api/src/modules/webhooks/webhooks.controller.spec.ts @@ -8,6 +8,11 @@ import { WebhooksController } from './webhooks.controller.js'; import { WebhookSignatureGuard } from './webhook-signature.guard.js'; import { TenantRateLimitGuard } from '../../guards/tenant-rate-limit.guard.js'; import { StorageResolverService } from '@nexiom/engine'; +import { executeAppWebhookResponses } from '@nexiom/piece-framework'; + +vi.mock('@nexiom/piece-framework', () => ({ + executeAppWebhookResponses: vi.fn(), +})); const loggerMock = { assign: vi.fn(), @@ -125,6 +130,24 @@ describe('WebhooksController', () => { ).resolves.toBeUndefined(); }); + it('returns 202 (does not throw) when DB throws 23505 with detail containing ext_req_id', async () => { + const pgError = Object.assign(new Error('unique_violation'), { + code: '23505', + detail: 'Key (connection_id, ext_req_id)=(..., ...) already exists.', + }); + db.transaction.mockRejectedValueOnce(pgError); + + await expect( + controller.ingest( + '00000000-0000-0000-0000-000000000001', + { foo: 'bar' }, + {}, + {} as any, + {} as any, + ), + ).resolves.toBeUndefined(); + }); + it('re-throws 23505 when the violated constraint is not an idempotency constraint', async () => { const pgError = Object.assign(new Error('unique_violation'), { code: '23505', @@ -406,4 +429,55 @@ describe('WebhooksController', () => { connectionId: '00000000-0000-0000-0000-000000000001', }); }); + + it('returns app-defined synchronous response on idempotency collision', async () => { + const pgError = Object.assign(new Error('unique_violation'), { + code: '23505', + constraint: 'idx_l1_ext_id', + }); + db.transaction + .mockRejectedValueOnce(pgError) + .mockImplementationOnce(async (cb: (tx: any) => Promise) => { + const mockTx = { + execute: vi.fn().mockResolvedValue(undefined), + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi + .fn() + .mockResolvedValue([ + { traceId: 'existing-trace-id', response: null }, + ]), + update: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + }; + await cb(mockTx); + }); + + vi.mocked(executeAppWebhookResponses).mockReturnValue({ + status: 200, + contentType: 'text/xml', + body: 'ok', + }); + + const mockRes = { + status: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + send: vi.fn(), + }; + + await controller.ingest( + '00000000-0000-0000-0000-000000000001', + { foo: 'bar' }, + { 'x-webhook-id': 'sf-event-123' }, + {} as any, + mockRes as any, + ); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.set).toHaveBeenCalledWith('Content-Type', 'text/xml'); + expect(mockRes.send).toHaveBeenCalledWith('ok'); + + vi.mocked(executeAppWebhookResponses).mockReturnValue(null); // reset + }); }); diff --git a/apps/api/src/scripts/admin-bootstrap.ts b/apps/api/src/scripts/admin-bootstrap.ts index a2a4c8e1..84afb589 100644 --- a/apps/api/src/scripts/admin-bootstrap.ts +++ b/apps/api/src/scripts/admin-bootstrap.ts @@ -54,6 +54,7 @@ async function seedRbac(db: NodePgDatabase) { systemTenantId: getRequiredSystemTenantId(), }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument await seedSystemRbac(db as any, config, console); } diff --git a/apps/worker/src/db/database-manager.ts b/apps/worker/src/db/database-manager.ts index 03006b2b..6d74b897 100644 --- a/apps/worker/src/db/database-manager.ts +++ b/apps/worker/src/db/database-manager.ts @@ -690,7 +690,8 @@ export class DatabaseManager { try { const db = drizzle(client, { schema }); const { Pool } = await import("pg"); - const { ApplicationLoaderService, PipelineHookBrokerService } = await import("@nexiom/engine"); + const { ApplicationLoaderService, PipelineHookBrokerService } = + await import("@nexiom/engine"); // Instantiate the loader directly — this is a CLI script, not in NestJS DI. // The loader reads from SHARD_APPLICATION_PATH and caches dynamically imported modules. @@ -709,7 +710,9 @@ export class DatabaseManager { } catch (provisionErr) { console.error( `domainProvisionerResolver: broker.provisionDomain failed for appName=${appName}, schemaName=${schemaName}:`, - provisionErr instanceof Error ? provisionErr.message : String(provisionErr), + provisionErr instanceof Error + ? provisionErr.message + : String(provisionErr), ); throw provisionErr; } diff --git a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts index e81b6866..fc8be03f 100644 --- a/apps/worker/src/modules/pipeline/gitops-sync.worker.ts +++ b/apps/worker/src/modules/pipeline/gitops-sync.worker.ts @@ -37,8 +37,12 @@ export class GitopsSyncWorker implements OnModuleInit { // Check reentrancy guard before processing if (this.isSyncRunning) { - this.logger.warn("Skipping GitOps sync — previous run still in progress"); - throw new Error("GitOps sync already in progress — message will be retried"); + this.logger.warn( + "Skipping GitOps sync — previous run still in progress", + ); + throw new Error( + "GitOps sync already in progress — message will be retried", + ); } try { @@ -112,7 +116,7 @@ export class GitopsSyncWorker implements OnModuleInit { // Ensure repoPath is contained within SHARD_BASE_PATH const relativePath = path.relative(resolvedBasePath, repoPath); if ( - relativePath.startsWith('..') || + relativePath.startsWith("..") || path.isAbsolute(relativePath) || !repoPath.startsWith(resolvedBasePath + path.sep) ) { diff --git a/apps/worker/src/modules/pipeline/normalization.service.spec.ts b/apps/worker/src/modules/pipeline/normalization.service.spec.ts index cdaaba4d..437ca5b2 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.spec.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.spec.ts @@ -60,11 +60,16 @@ describe("NormalizationService", () => { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), - limit: vi - .fn() - .mockResolvedValue([ - { traceId: "123", data: {}, canonicalType: "RAW", id: "1" }, - ]), + limit: vi.fn().mockResolvedValue([ + { + traceId: "123", + data: {}, + canonicalType: "RAW", + id: "1", + entityType: "test_entity", + entityId: "test_entity_id", + }, + ]), insert: mockTxInsert, update: vi.fn().mockReturnThis(), set: vi.fn().mockReturnThis(), @@ -140,6 +145,7 @@ describe("NormalizationService", () => { expect(db.transaction).toHaveBeenCalled(); expect(mockTxInsert).toHaveBeenCalled(); // Assert that broker.normalize was called + console.log("SPY CALLS:", JSON.stringify(mockBroker.normalize.mock.calls)); expect(mockBroker.normalize).toHaveBeenCalledWith( "test_app", "default", @@ -200,11 +206,16 @@ describe("NormalizationService", () => { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), - limit: vi - .fn() - .mockResolvedValue([ - { traceId: "123", data: {}, canonicalType: "RAW", id: "1" }, - ]), + limit: vi.fn().mockResolvedValue([ + { + traceId: "123", + data: {}, + canonicalType: "RAW", + id: "1", + entityType: "test_entity", + entityId: "test_entity_id", + }, + ]), // Returns row from .returning() on onConflictDoUpdate insert: vi.fn().mockReturnValue({ values: vi.fn().mockReturnValue({ @@ -334,6 +345,7 @@ describe("NormalizationService", () => { await handler({ traceId: "123", connectionId: "456" }); // Assert broker.normalize was called + console.log("SPY CALLS:", JSON.stringify(mockBroker.normalize.mock.calls)); expect(mockBroker.normalize).toHaveBeenCalledWith( "test_app", "default", diff --git a/apps/worker/src/modules/pipeline/normalization.service.ts b/apps/worker/src/modules/pipeline/normalization.service.ts index d3b0d6db..5428dad3 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.ts @@ -159,6 +159,7 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { let canonicalData = replica.data; // ── Normalize via HookBroker or Piece ──────────────────────────────── + const normalizedFromShard = await this.hookBroker .normalize(connectionAppName, appProfile, { entityType: replica.entityType, @@ -168,14 +169,14 @@ export class NormalizationService implements OnModuleInit, OnModuleDestroy { // Log the error for operational visibility, then fall through to piece.normalize this.logger.warn( { - event: 'l3.shard_normalize_failed', + event: "l3.shard_normalize_failed", connectionAppName, appProfile, entityType: replica.entityType, err: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, }, - 'Shard normalize failed — falling back to piece.normalize', + "Shard normalize failed — falling back to piece.normalize", ); return null; }); // shard may not exist yet — fall through to piece.normalize diff --git a/engine/sync/platform/core/src/sharding/application-executor.module.ts b/engine/sync/platform/core/src/sharding/application-executor.module.ts deleted file mode 100644 index 6f75ef52..00000000 --- a/engine/sync/platform/core/src/sharding/application-executor.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ApplicationExecutorService } from './application-executor.service.js'; - -@Module({ - providers: [ApplicationExecutorService], - exports: [ApplicationExecutorService], -}) -export class ApplicationExecutorModule {} diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts b/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts deleted file mode 100644 index 8a01568b..00000000 --- a/engine/sync/platform/core/src/sharding/application-executor.service.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import * as path from 'node:path'; -import * as fs from 'node:fs/promises'; - -// ── Module Mocking ──────────────────────────────────────────────────────────── -// We mock esbuild so tests don't need real shard files on disk -vi.mock('esbuild', () => ({ - build: vi.fn(), -})); - -vi.mock('node:fs/promises', () => ({ - access: vi.fn(), - mkdir: vi.fn(), -})); - -// We DO NOT mock quickjs-emscripten — the entire point is to test real sandbox behavior - -import * as esbuild from 'esbuild'; -import { ApplicationExecutorService } from './application-executor.service.js'; -import { InternalServerErrorException } from '@nestjs/common'; - -// A minimal, self-contained IIFE bundle that the "esbuild" mock will return -const makeShardBundle = (fnName: string, fnBody: string) => - `var CustomLogic = (function() { - function ${fnName}(payload) { ${fnBody} } - return { ${fnName}: ${fnName} }; - })();`; - -describe('ApplicationExecutorService — WebAssembly Sandbox', () => { - let service: ApplicationExecutorService; - - beforeEach(() => { - service = new ApplicationExecutorService(); - vi.clearAllMocks(); - - // Default: file access succeeds - vi.mocked(fs.access).mockResolvedValue(undefined); - }); - - // ── Path Traversal Guard ────────────────────────────────────────────────── - it('throws on path traversal attempt', async () => { - await expect( - service.executeCustomMapping('t1', '../../../etc/passwd', 'transform', {}) - ).rejects.toThrow(InternalServerErrorException); - }); - - // ── Missing Shard File ──────────────────────────────────────────────────── - it('throws when shard module file does not exist', async () => { - vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT')); - await expect( - service.executeCustomMapping('t1', 'my-shard', 'transform', {}) - ).rejects.toThrow(InternalServerErrorException); - }); - - // ── Successful Execution ────────────────────────────────────────────────── - it('executes transform and returns result through the WebAssembly boundary', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [ - { text: makeShardBundle('transform', 'return { result: payload.value * 2 };') }, - ], - } as never); - - const result = await service.executeCustomMapping( - 't1', 'my-shard', 'transform', { value: 21 } - ); - - expect(result).toEqual({ result: 42 }); - }); - - // ── Cache Hit ───────────────────────────────────────────────────────────── - it('only calls esbuild once for repeated invocations of the same shard', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [ - { text: makeShardBundle('transform', 'return { ok: true };') }, - ], - } as never); - - await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); - await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); - - expect(esbuild.build).toHaveBeenCalledTimes(1); - }); - - // ── Cache Invalidation ──────────────────────────────────────────────────── - it('re-bundles after invalidateCache is called', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [ - { text: makeShardBundle('transform', 'return { ok: true };') }, - ], - } as never); - - await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); - service.invalidateCache('my-shard'); - await service.executeCustomMapping('t1', 'my-shard', 'transform', {}); - - // esbuild called twice — once before and once after cache invalidation - expect(esbuild.build).toHaveBeenCalledTimes(2); - }); - - // ── Unknown Function Guard ──────────────────────────────────────────────── - it('throws when the requested function is not exported by the shard', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [ - { text: makeShardBundle('transform', 'return {};') }, - ], - } as never); - - await expect( - service.executeCustomMapping('t1', 'my-shard', 'nonExistentFn', {}) - ).rejects.toThrow(InternalServerErrorException); - }); - - // ── Security Boundary: No Node.js API Access ────────────────────────────── - it('sandbox prevents access to Node.js process object', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [{ - // Attempt to read process.env — should be undefined inside the sandbox - text: makeShardBundle('exfiltrate', ` - var leaked = typeof process !== 'undefined' ? JSON.stringify(process.env) : 'BLOCKED'; - return { leaked }; - `), - }], - } as never); - - const result = await service.executeCustomMapping( - 't1', 'my-shard', 'exfiltrate', {} - ); - - // process is not defined in the QuickJS sandbox — it should be 'BLOCKED' - expect(result).toEqual({ leaked: 'BLOCKED' }); - }); - - // ── Security Boundary: Memory Limit ────────────────────────────────────── - it('sandbox enforces memory limit and throws on excessive allocation', async () => { - vi.mocked(esbuild.build).mockResolvedValue({ - outputFiles: [{ - // Attempt to allocate a 200MB string in one shot — exceeds the 128MB hard limit. - // Using a single large allocation is safer than a loop: it triggers the QuickJS - // OOM boundary instantly without exhausting Node.js host memory. - text: makeShardBundle('memBomb', ` - var bomb = 'a'.repeat(200 * 1024 * 1024); - return { size: bomb.length }; - `), - }], - } as never); - - await expect( - service.executeCustomMapping('t1', 'my-shard', 'memBomb', {}) - ).rejects.toThrow(InternalServerErrorException); - }); -}); diff --git a/engine/sync/platform/core/src/sharding/application-executor.service.ts b/engine/sync/platform/core/src/sharding/application-executor.service.ts deleted file mode 100644 index c56074e1..00000000 --- a/engine/sync/platform/core/src/sharding/application-executor.service.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; -import * as path from 'node:path'; -import * as fs from 'node:fs/promises'; -import { constants as fsConstants } from 'node:fs'; -import * as esbuild from 'esbuild'; -import { getQuickJS } from 'quickjs-emscripten'; -const SHARD_APPLICATION_PATH = process.env.SHARD_APPLICATION_PATH || path.resolve(process.cwd(), '../../engine/sync/application'); - -/** - * Executes a dynamically synced first-party custom logic module. - * - * Safely resolves the tenant's logic module from the `engine/sync/application` path. - * This execution path assumes the module has already been safely synced via - * GitOps Shard Synchronization. - */ -@Injectable() -export class ApplicationExecutorService { - private readonly logger = new Logger(ApplicationExecutorService.name); - - // The base path where shard synchronization pulls all tenant-specific Git shards - private readonly SHARD_BASE_PATH = SHARD_APPLICATION_PATH; - - // Cache for bundled JS code to avoid recompiling on every invocation - private esbuildCache = new Map(); - - /** - * Invalidates the bundled cache for a specific shard. - * Intended to be called by the GitOps worker upon detecting a new commit. - */ - invalidateCache(shardName: string) { - this.esbuildCache.delete(shardName); - } - - /** - * Resolves a custom transformer module from the application directory and executes it. - * - * IMPORTANT: The caller MUST ensure shardName corresponds to tenantId before calling this method. - * This service does not validate shard ownership - tenant isolation must be enforced by the caller - * (e.g., by querying the shard registry or configuration to verify the tenant has access to the shard). - * - * @param tenantId - Tenant identifier for logging/audit purposes only - * @param shardName - Name of the shard directory (caller must validate ownership) - * @param transformFunctionName - Name of the exported function to execute - * @param payload - Data to pass to the transform function - */ - async executeCustomMapping( - tenantId: string, - shardName: string, - transformFunctionName: string, - payload: Record - ): Promise> { - // Resolve real filesystem paths to prevent symlink traversal attacks - let resolvedBasePath: string; - let realModulePath: string; - - try { - resolvedBasePath = await fs.realpath(this.SHARD_BASE_PATH); - } catch (err) { - this.logger.error( - `Failed to resolve SHARD_BASE_PATH: ${this.SHARD_BASE_PATH}`, - err instanceof Error ? err.stack : String(err), - ); - throw new InternalServerErrorException(`Internal server error`); - } - - const modulePath = path.resolve(this.SHARD_BASE_PATH, shardName, 'index.js'); - - // Use async filesystem check and resolve real path - try { - await fs.access(modulePath, fsConstants.F_OK); - realModulePath = await fs.realpath(modulePath); - } catch (err) { - this.logger.error( - `Cannot find or resolve shard module: ${modulePath}`, - err instanceof Error ? err.stack : String(err), - ); - throw new InternalServerErrorException(`Failed to load shard module`); - } - - // Verify the real module path is within the real base path to prevent path traversal - if (!realModulePath.startsWith(resolvedBasePath + path.sep) && realModulePath !== resolvedBasePath) { - this.logger.error( - `Security violation: realModulePath escapes trusted boundary. ` + - `realModulePath="${realModulePath}", resolvedBasePath="${resolvedBasePath}"`, - ); - throw new InternalServerErrorException(`Internal server error`); - } - - try { - let code = this.esbuildCache.get(shardName); - - // 1. Bundle the application logic (includes jsonata/libraries) - if (!code) { - this.logger.debug(`Bundling application shard: ${shardName}`); - const buildResult = await esbuild.build({ - entryPoints: [modulePath], - bundle: true, - write: false, - format: 'iife', - globalName: 'CustomLogic', - platform: 'browser', // Prevent native Node.js requires - }); - code = buildResult.outputFiles[0].text; - this.esbuildCache.set(shardName, code); - } - - this.logger.debug(`Executing custom logic ${transformFunctionName} for tenant ${tenantId} via shard ${shardName} in WebAssembly Sandbox`); - - // 2. Initialize QuickJS WebAssembly Sandbox - const QuickJS = await getQuickJS(); - const vm = QuickJS.newContext(); - - // Hard memory limit: 128MB per execution - vm.runtime.setMemoryLimit(128 * 1024 * 1024); - - // Set interrupt handler with deadline (30 second timeout) - const TIMEOUT_MS = 30_000; - const deadline = Date.now() + TIMEOUT_MS; - const shouldInterruptAfterDeadline = () => { - return Date.now() > deadline; - }; - vm.runtime.setInterruptHandler(shouldInterruptAfterDeadline); - - try { - // 3. Pass data securely across the boundary - const payloadStr = JSON.stringify(payload); - const payloadHandle = vm.newString(payloadStr); - vm.setProp(vm.global, 'payloadStr', payloadHandle); - payloadHandle.dispose(); - - const funcNameHandle = vm.newString(transformFunctionName); - vm.setProp(vm.global, 'funcName', funcNameHandle); - funcNameHandle.dispose(); - - // 4. Compile execution wrapper that produces a guest Promise - const scriptCode = ` - var __sandbox_result = null; - var __sandbox_error = null; - var __sandbox_promise = (async () => { - try { - ${code} - const fn = CustomLogic[funcName]; - if (typeof fn !== 'function') throw new Error('Function ' + funcName + ' is not exported from the synced shard module'); - const result = await fn(JSON.parse(payloadStr)); - __sandbox_result = JSON.stringify(result); - } catch (e) { - __sandbox_error = e.message || String(e); - } - })(); - `; - - // 5. Execute Code - const evalResult = vm.evalCode(scriptCode); - if (evalResult.error) { - const errDump = vm.dump(evalResult.error); - evalResult.error.dispose(); - throw new Error(errDump.message || "Unknown Sandbox Evaluation Error"); - } - evalResult.value.dispose(); - - // Check if deadline has elapsed before executing pending jobs - if (Date.now() > deadline) { - throw new Error(`Execution timeout: exceeded ${TIMEOUT_MS}ms deadline before async completion`); - } - - // Flush the Promise Microtask Queue - vm.runtime.executePendingJobs(); - - // Check again after job execution - if (Date.now() > deadline) { - throw new Error(`Execution timeout: exceeded ${TIMEOUT_MS}ms deadline during async execution`); - } - - // Extract result or error from the global context - const errorHandle = vm.getProp(vm.global, '__sandbox_error'); - const isErrorNull = vm.dump(errorHandle) === null; - if (!isErrorNull) { - const errStr = vm.getString(errorHandle); - errorHandle.dispose(); - throw new Error(errStr); - } - errorHandle.dispose(); - - const resultHandle = vm.getProp(vm.global, '__sandbox_result'); - const resultStr = vm.getString(resultHandle); - resultHandle.dispose(); - - // Parse and validate return type - const parsed = JSON.parse(resultStr); - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - throw new Error( - `Invalid shard mapping return: expected Record, got ${ - parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed - }` - ); - } - - return parsed as Record; - } finally { - vm.dispose(); - } - } catch (error) { - this.logger.error( - `Failed to execute custom logic in shard ${shardName} at modulePath ${modulePath}`, - error instanceof Error ? error.stack : String(error), - ); - throw new InternalServerErrorException(`Failed to load shard module`); - } - } -} \ No newline at end of file From c807162c0a2beae65bae4537562920349f6452f9 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 05:41:20 +0000 Subject: [PATCH 5/9] fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- apps/api/src/modules/gitops/gitops-webhook.guard.ts | 3 +-- apps/api/src/scripts/admin-bootstrap.ts | 4 ++-- .../worker/src/modules/pipeline/normalization.service.spec.ts | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index cdae55e5..c39a4261 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -130,9 +130,8 @@ export class GitopsWebhookGuard implements CanActivate { // GitLab uses exact match - constant-time comparison const tokenDigest = createHash('sha256').update(token).digest(); - const secretDigest = createHash('sha256').update(this.secret).digest(); - if (!timingSafeEqual(tokenDigest, secretDigest)) { + if (!timingSafeEqual(tokenDigest, this.expectedDigest)) { throw new UnauthorizedException('Invalid GitLab webhook token'); } diff --git a/apps/api/src/scripts/admin-bootstrap.ts b/apps/api/src/scripts/admin-bootstrap.ts index 84afb589..3ffc2338 100644 --- a/apps/api/src/scripts/admin-bootstrap.ts +++ b/apps/api/src/scripts/admin-bootstrap.ts @@ -13,6 +13,7 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Client } from 'pg'; import * as schema from '../db/schema.js'; +import * as identitySchema from '@nexiom/identity/schema'; import { eq, and } from 'drizzle-orm'; import { v4 as uuidv4 } from 'uuid'; import { @@ -54,8 +55,7 @@ async function seedRbac(db: NodePgDatabase) { systemTenantId: getRequiredSystemTenantId(), }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - await seedSystemRbac(db as any, config, console); + await seedSystemRbac(db as NodePgDatabase, config, console); } // Shared helper to elevate a user to System Owner diff --git a/apps/worker/src/modules/pipeline/normalization.service.spec.ts b/apps/worker/src/modules/pipeline/normalization.service.spec.ts index 437ca5b2..2598ed73 100644 --- a/apps/worker/src/modules/pipeline/normalization.service.spec.ts +++ b/apps/worker/src/modules/pipeline/normalization.service.spec.ts @@ -145,7 +145,6 @@ describe("NormalizationService", () => { expect(db.transaction).toHaveBeenCalled(); expect(mockTxInsert).toHaveBeenCalled(); // Assert that broker.normalize was called - console.log("SPY CALLS:", JSON.stringify(mockBroker.normalize.mock.calls)); expect(mockBroker.normalize).toHaveBeenCalledWith( "test_app", "default", @@ -345,7 +344,6 @@ describe("NormalizationService", () => { await handler({ traceId: "123", connectionId: "456" }); // Assert broker.normalize was called - console.log("SPY CALLS:", JSON.stringify(mockBroker.normalize.mock.calls)); expect(mockBroker.normalize).toHaveBeenCalledWith( "test_app", "default", From b907194e3f079ffd1655a33f80d15e3f8c0d71f9 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 05:58:55 +0000 Subject: [PATCH 6/9] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- .../modules/gitops/gitops-webhook.guard.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index c39a4261..1610cbbf 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -49,15 +49,15 @@ export class GitopsWebhookGuard implements CanActivate { const request = context.switchToHttp().getRequest(); // Strategy 1: GitHub X-Hub-Signature-256 (HMAC SHA256 of raw body) - const githubSig = request.headers['x-hub-signature-256'] as - | string - | undefined; + const githubSig = this.getSingleHeader( + request.headers['x-hub-signature-256'], + ); if (githubSig) { return this.validateGitHubSignature(request, githubSig); } // Strategy 2: GitLab X-Gitlab-Token (exact match) - const gitlabToken = request.headers['x-gitlab-token'] as string | undefined; + const gitlabToken = this.getSingleHeader(request.headers['x-gitlab-token']); if (gitlabToken) { return this.validateGitLabToken(gitlabToken); } @@ -73,6 +73,19 @@ export class GitopsWebhookGuard implements CanActivate { ); } + /** + * Helper to extract a single header value from string | string[] | undefined. + * Throws UnauthorizedException if the header is an array (duplicate headers). + */ + private getSingleHeader( + value: string | string[] | undefined, + ): string | undefined { + if (Array.isArray(value)) { + throw new UnauthorizedException('Duplicate headers are not allowed'); + } + return value; + } + private validateGitHubSignature( request: Request, signature: string, @@ -129,7 +142,7 @@ export class GitopsWebhookGuard implements CanActivate { } // GitLab uses exact match - constant-time comparison - const tokenDigest = createHash('sha256').update(token).digest(); + const tokenDigest = createHash('sha256').update(trimmedToken).digest(); if (!timingSafeEqual(tokenDigest, this.expectedDigest)) { throw new UnauthorizedException('Invalid GitLab webhook token'); @@ -149,7 +162,7 @@ export class GitopsWebhookGuard implements CanActivate { ); } - const tokenDigest = createHash('sha256').update(token).digest(); + const tokenDigest = createHash('sha256').update(trimmedToken).digest(); if (!timingSafeEqual(tokenDigest, this.expectedDigest)) { throw new UnauthorizedException('Invalid gitops webhook secret'); From a15916b05182ae0ec910ed381f03406bd1e29889 Mon Sep 17 00:00:00 2001 From: Pramod Narayana Date: Mon, 4 May 2026 13:35:03 +0530 Subject: [PATCH 7/9] db per tenant migration --- apps/api/src/db/database-manager.ts | 425 ++- .../connections/connectors.service.spec.ts | 2 +- .../modules/connections/connectors.service.ts | 2 +- .../src/modules/dbmanager/dbmanager.module.ts | 4 +- .../mappings/mappings.controller.spec.ts | 24 +- .../modules/mappings/mappings.controller.ts | 34 +- .../modules/mappings/mappings.service.spec.ts | 77 +- .../src/modules/mappings/mappings.service.ts | 38 +- .../modules/mappings/mappings.validation.ts | 1 - .../pipeline/inbound-outbox.service.spec.ts | 2 +- .../pipeline/inbound-outbox.service.ts | 2 +- .../pipeline/replica-outbox.service.spec.ts | 2 +- .../pipeline/replica-outbox.service.ts | 2 +- .../trigger/trigger-executor.service.ts | 2 +- .../api/src/modules/trigger/trigger.module.ts | 2 +- .../capacity-manager.service.spec.ts | 105 + .../workspaces/capacity-manager.service.ts | 113 + .../workspaces/capacity-manager.types.ts | 13 + .../modules/workspaces/workspaces.module.ts | 6 +- .../workspaces/workspaces.service.spec.ts | 97 +- .../modules/workspaces/workspaces.service.ts | 43 +- apps/api/src/scripts/admin-bootstrap.ts | 6 +- apps/api/src/shared/db.utils.spec.ts | 65 + apps/tenant-provisioner/eslint.config.mjs | 43 + apps/tenant-provisioner/nest-cli.json | 11 + apps/tenant-provisioner/package.json | 51 + apps/tenant-provisioner/src/app.module.ts | 20 + apps/tenant-provisioner/src/main.ts | 26 + .../provisioner/provision-database.event.ts | 19 + .../modules/provisioner/provisioner.module.ts | 9 + .../tenant-provision.worker.spec.ts | 148 + .../provisioner/tenant-provision.worker.ts | 136 + apps/tenant-provisioner/tsconfig.json | 28 + apps/tenant-provisioner/vitest.config.ts | 32 + .../src/modules/dbmanager/dbmanager.module.ts | 4 +- .../pipeline/normalized-outbox.worker.spec.ts | 20 +- .../pipeline/normalized-outbox.worker.ts | 15 +- engine/ai/core/package.json | 1 + .../ai/core/src/categories/mapping.service.ts | 41 +- .../src/services/chat-persistence.service.ts | 26 +- package.json | 5 +- packages/database/drizzle.config.tenant.ts | 49 + packages/database/drizzle.config.ts | 9 +- .../drizzle/0002_add_status_enum_values.sql | 8 - .../database/drizzle/0003_add_gem_indexes.sql | 22 - .../drizzle/0004_verify_gem_indexes.sql | 67 - .../drizzle/0005_greedy_eddie_brock.sql | 14 - packages/database/drizzle/0006_quick_puck.sql | 28 - .../drizzle/0007_clammy_aaron_stack.sql | 5 - packages/database/drizzle/MIGRATION_NOTES.md | 26 - .../drizzle/global/0000_mixed_sleepwalker.sql | 158 + .../drizzle/global/0001_shocking_microbe.sql | 1 + .../0003_peaceful_beast.sql} | 337 +- .../drizzle/global/meta/0000_snapshot.json | 1283 +++++++ .../drizzle/global/meta/0001_snapshot.json | 1126 ++++++ .../meta/0003_snapshot.json} | 2289 ++++++------ .../drizzle/global/meta/_journal.json | 27 + .../database/drizzle/meta/0001_snapshot.json | 2671 -------------- .../database/drizzle/meta/0002_snapshot.json | 2671 -------------- .../database/drizzle/meta/0005_snapshot.json | 2788 --------------- .../database/drizzle/meta/0007_snapshot.json | 3055 ----------------- packages/database/drizzle/meta/_journal.json | 55 - .../drizzle/tenant/0000_oval_calypso.sql | 203 ++ .../{ => tenant}/meta/0000_snapshot.json | 2727 ++++++--------- .../drizzle/tenant/meta/_journal.json | 13 + packages/database/package.json | 9 +- packages/database/src/index.ts | 2 +- .../src/schema/global/canonical_mappings.ts | 49 - .../src/schema/global/storage_registry.ts | 3 + .../src/schema/tenant/canonical_mappings.ts | 38 + packages/database/src/schema/tenant/chat.ts | 15 +- packages/database/src/schema/tenant/tenant.ts | 8 +- .../database/src/schema/tenant/workspace.ts | 8 +- .../src/impl/sql-database-manager.ts | 20 +- packages/dbmanager/src/interfaces.ts | 2 + packages/queue/src/constants.ts | 2 + pnpm-lock.yaml | 196 +- scripts/init-localstack.sh | 1 + 78 files changed, 6656 insertions(+), 15031 deletions(-) create mode 100644 apps/api/src/modules/workspaces/capacity-manager.service.spec.ts create mode 100644 apps/api/src/modules/workspaces/capacity-manager.service.ts create mode 100644 apps/api/src/modules/workspaces/capacity-manager.types.ts create mode 100644 apps/api/src/shared/db.utils.spec.ts create mode 100644 apps/tenant-provisioner/eslint.config.mjs create mode 100644 apps/tenant-provisioner/nest-cli.json create mode 100644 apps/tenant-provisioner/package.json create mode 100644 apps/tenant-provisioner/src/app.module.ts create mode 100644 apps/tenant-provisioner/src/main.ts create mode 100644 apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts create mode 100644 apps/tenant-provisioner/src/modules/provisioner/provisioner.module.ts create mode 100644 apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts create mode 100644 apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts create mode 100644 apps/tenant-provisioner/tsconfig.json create mode 100644 apps/tenant-provisioner/vitest.config.ts create mode 100644 packages/database/drizzle.config.tenant.ts delete mode 100644 packages/database/drizzle/0002_add_status_enum_values.sql delete mode 100644 packages/database/drizzle/0003_add_gem_indexes.sql delete mode 100644 packages/database/drizzle/0004_verify_gem_indexes.sql delete mode 100644 packages/database/drizzle/0005_greedy_eddie_brock.sql delete mode 100644 packages/database/drizzle/0006_quick_puck.sql delete mode 100644 packages/database/drizzle/0007_clammy_aaron_stack.sql delete mode 100644 packages/database/drizzle/MIGRATION_NOTES.md create mode 100644 packages/database/drizzle/global/0000_mixed_sleepwalker.sql create mode 100644 packages/database/drizzle/global/0001_shocking_microbe.sql rename packages/database/drizzle/{0000_salty_stryfe.sql => global/0003_peaceful_beast.sql} (58%) create mode 100644 packages/database/drizzle/global/meta/0000_snapshot.json create mode 100644 packages/database/drizzle/global/meta/0001_snapshot.json rename packages/database/drizzle/{meta/0006_snapshot.json => global/meta/0003_snapshot.json} (97%) create mode 100644 packages/database/drizzle/global/meta/_journal.json delete mode 100644 packages/database/drizzle/meta/0001_snapshot.json delete mode 100644 packages/database/drizzle/meta/0002_snapshot.json delete mode 100644 packages/database/drizzle/meta/0005_snapshot.json delete mode 100644 packages/database/drizzle/meta/0007_snapshot.json delete mode 100644 packages/database/drizzle/meta/_journal.json create mode 100644 packages/database/drizzle/tenant/0000_oval_calypso.sql rename packages/database/drizzle/{ => tenant}/meta/0000_snapshot.json (65%) create mode 100644 packages/database/drizzle/tenant/meta/_journal.json delete mode 100644 packages/database/src/schema/global/canonical_mappings.ts create mode 100644 packages/database/src/schema/tenant/canonical_mappings.ts diff --git a/apps/api/src/db/database-manager.ts b/apps/api/src/db/database-manager.ts index 62df8d29..6ae76dcc 100644 --- a/apps/api/src/db/database-manager.ts +++ b/apps/api/src/db/database-manager.ts @@ -189,13 +189,43 @@ export class DatabaseManager { } finally { await client.end(); } + + // Drop tenant databases so fresh truly starts from scratch + await this.dropTenantDatabaseIfExists('nexiom_tenant_system'); } /** - * Run pending Drizzle migrations + * Drop a tenant database if it exists (local dev only). + */ + private async dropTenantDatabaseIfExists(dbName: string): Promise { + const { PgClient } = await this.resolvePgModule(); + const adminClient = new PgClient({ + connectionString: process.env.DATABASE_URL, + }); + await adminClient.connect(); + try { + // Terminate any existing connections first + await adminClient.query( + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, + [dbName], + ); + await adminClient.query(`DROP DATABASE IF EXISTS "${dbName}"`); + console.log(` ✓ Dropped tenant database: ${dbName}`); + } finally { + await adminClient.end(); + } + } + + /** + * Run pending global Drizzle migrations (nexiom_global DB only). + * Applies identity, registry, and pieces schema. */ migrate(): void { - console.log('🔨 Running centralized migrations...'); + this.migrateGlobal(); + } + + migrateGlobal(): void { + console.log('🔨 Running global DB migrations...'); let rootCwd: string; if (typeof __dirname !== 'undefined') { rootCwd = path.resolve(__dirname, '../../../../'); @@ -205,14 +235,88 @@ export class DatabaseManager { '../../../../', ); } - // Orchestrate migrations from the root monorepo script execSync('pnpm db:migrate', { stdio: 'inherit', cwd: rootCwd, env: { ...process.env, FORCE_COLOR: '1' }, }); + console.log(' ✓ Global migrations complete'); + } + + /** + * Create a new tenant database on the same Postgres server and run + * all tenant-schema migrations against it. + * + * @param dbName Database name to create e.g. "nexiom_tenant_abc123" + * @param hostUrl Full connection URL to the server (with credentials, without db path) + * e.g. "postgres://user:password@localhost:5432" + */ + async createTenantDatabase(dbName: string, hostUrl: string): Promise { + const { PgClient } = await this.resolvePgModule(); + + // CREATE DATABASE must run outside a transaction — connect to the global DB + const adminClient = new PgClient({ + connectionString: process.env.DATABASE_URL, + }); + await adminClient.connect(); + + try { + // Idempotent — skip if the DB already exists + const existing = await adminClient.query( + `SELECT 1 FROM pg_database WHERE datname = $1`, + [dbName], + ); + if (existing.rowCount === 0) { + // Sanitize dbName (alphanumeric + underscores only) + if (!/^[a-zA-Z0-9_]+$/.test(dbName)) { + throw new Error(`Invalid tenant database name: ${dbName}`); + } + await adminClient.query(`CREATE DATABASE "${dbName}"`); + console.log(` ✓ Created tenant database: ${dbName}`); + } else { + console.log(` ℹ️ Tenant database already exists: ${dbName}`); + } + } finally { + await adminClient.end(); + } + + // Now run tenant migrations against the new (or existing) database + await this.migrateTenant(dbName, hostUrl); + } + + /** + * Run tenant-schema Drizzle migrations against a specific tenant database. + * Uses the drizzle/tenant migration folder generated from drizzle.config.tenant.ts. + */ + async migrateTenant(dbName: string, hostUrl: string): Promise { + const { drizzle } = await import('drizzle-orm/node-postgres'); + const { migrate } = await import('drizzle-orm/node-postgres/migrator'); + const { Pool } = await import('pg'); - console.log(' ✓ Centralized migrations complete'); + const tenantUrl = `${hostUrl.replace(/\/$/, '')}/${dbName}`; + + let rootDir: string; + if (typeof __dirname !== 'undefined') { + rootDir = path.resolve(__dirname, '../../../../'); + } else { + rootDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../../', + ); + } + const migrationsFolder = path.join( + rootDir, + 'packages/database/drizzle/tenant', + ); + + const pool = new Pool({ connectionString: tenantUrl, max: 2 }); + try { + const db = drizzle(pool); + await migrate(db, { migrationsFolder }); + console.log(` ✓ Tenant migrations applied to: ${dbName}`); + } finally { + await pool.end(); + } } /** @@ -765,173 +869,190 @@ export class DatabaseManager { ); } - const { SchemaPlan } = await import('@nexiom/dbmanager'); + // ── Step 1: Derive host URL + tenant DB name ──────────────────────────── + let hostUrl: string; + const tenantDbName = 'nexiom_tenant_system'; + try { + const parsedUrl = new URL( + process.env.DATABASE_URL || + 'postgresql://user:password@localhost:5432/nexiom_global', + ); + const auth = parsedUrl.username + ? `${parsedUrl.username}${parsedUrl.password ? ':' + parsedUrl.password : ''}@` + : ''; + hostUrl = `${parsedUrl.protocol}//${auth}${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}`; + } catch { + hostUrl = 'postgresql://user:password@localhost:5432'; + } + + // ── Step 2: CREATE DATABASE nexiom_tenant_system + run tenant migrations ─ + console.log(`\n📦 Provisioning tenant database: ${tenantDbName}`); + await this.createTenantDatabase(tenantDbName, hostUrl); + + // ── Step 3: Connect to global DB to register the tenant ───────────────── + const { drizzle } = await import('drizzle-orm/node-postgres'); + const { Pool } = await import('pg'); const dbSchema = await import('./schema.js'); + const globalClient = await this.getPgClient(); + const globalDb = drizzle(globalClient, { schema: dbSchema }); + + // Register in global tenant_storage_registry + const existReg = await globalDb + .select() + .from(dbSchema.tenantStorageRegistry) + .where(eq(dbSchema.tenantStorageRegistry.tenantId, systemTenantId)) + .limit(1); + + if (!existReg[0]) { + await globalDb.insert(dbSchema.tenantStorageRegistry).values({ + tenantId: systemTenantId, + databaseName: tenantDbName, + databaseHostUrl: hostUrl, + regionContext: 'local', + }); + console.log(` ✓ Registered ${tenantDbName} in tenant_storage_registry`); + } else { + // Update host URL in case credentials changed + await globalDb + .update(dbSchema.tenantStorageRegistry) + .set({ databaseName: tenantDbName, databaseHostUrl: hostUrl }) + .where(eq(dbSchema.tenantStorageRegistry.tenantId, systemTenantId)); + console.log(` ✓ Updated tenant_storage_registry for ${tenantDbName}`); + } - await this.withSchemaMgr(async (schemaMgr) => { - const { drizzle } = await import('drizzle-orm/node-postgres'); - const client = await this.getPgClient(); - try { - const db = drizzle(client, { schema: dbSchema }); + // ── Step 4: Connect to tenant DB and write app_connection fixtures ─────── + const tenantUrl = `${hostUrl.replace(/\/$/, '')}/${tenantDbName}`; + const tenantPool = new Pool({ connectionString: tenantUrl, max: 5 }); + const tenantDb = drizzle(tenantPool, { schema: dbSchema }); - const fixtures = [ - { - id: '00000000-0000-0000-0000-000000000001', - appName: 'salesforce', - externalId: 'dev-salesforce', - displayName: 'Dev Salesforce', - metadata: this.deriveMetadata('salesforce'), - credentials: { - clientId: 'dev-sf-client-id', - clientSecret: 'dev-sf-client-secret', - accessToken: 'dev-sf-access-token', - refreshToken: 'dev-sf-refresh-token', - data: { instance_url: 'https://test.salesforce.com' }, - }, + const { SchemaPlan } = await import('@nexiom/dbmanager'); + const { TenantDatabaseManager } = await import('@nexiom/dbmanager'); + const { getDomainProvisioner } = await import('@nexiom/piece-framework'); + + const schemaMgr = new TenantDatabaseManager( + globalDb as unknown as import('@nexiom/database').DrizzleDb, + (_hostIdentifier: string) => { + const pool2 = new Pool({ connectionString: tenantUrl, max: 20 }); + return drizzle(pool2, { + schema: dbSchema, + }) as unknown as import('@nexiom/database').DrizzleDb; + }, + getDomainProvisioner, + ); + + try { + const fixtures = [ + { + id: '00000000-0000-0000-0000-000000000001', + appName: 'salesforce', + externalId: 'dev-salesforce', + displayName: 'Dev Salesforce', + metadata: this.deriveMetadata('salesforce'), + credentials: { + clientId: 'dev-sf-client-id', + clientSecret: 'dev-sf-client-secret', + accessToken: 'dev-sf-access-token', + refreshToken: 'dev-sf-refresh-token', + data: { instance_url: 'https://test.salesforce.com' }, }, - { - id: '00000000-0000-0000-0000-000000000002', - appName: 'quickbooks', - externalId: 'dev-quickbooks', - displayName: 'Dev QuickBooks', - metadata: this.deriveMetadata('quickbooks'), - credentials: { - clientId: 'dev-qb-client-id', - clientSecret: 'dev-qb-client-secret', - accessToken: 'dev-qb-access-token', - refreshToken: 'dev-qb-refresh-token', - data: { realmId: 'dev-realm-id' }, - }, + }, + { + id: '00000000-0000-0000-0000-000000000002', + appName: 'quickbooks', + externalId: 'dev-quickbooks', + displayName: 'Dev QuickBooks', + metadata: this.deriveMetadata('quickbooks'), + credentials: { + clientId: 'dev-qb-client-id', + clientSecret: 'dev-qb-client-secret', + accessToken: 'dev-qb-access-token', + refreshToken: 'dev-qb-refresh-token', + data: { realmId: 'dev-realm-id' }, }, - ] as const; + }, + ] as const; - for (const fixture of fixtures) { - const encryptedValue = this.encryptFixture( - JSON.stringify(fixture.credentials), - encryptionKey, - ); + const { getWorkspaceSchemaName } = await import('@nexiom/dbmanager'); - const [inserted] = await db - .insert(dbSchema.appConnections) - .values({ - id: fixture.id, - tenantId: systemTenantId, - appName: fixture.appName, - externalId: fixture.externalId, + for (const fixture of fixtures) { + const encryptedValue = this.encryptFixture( + JSON.stringify(fixture.credentials), + encryptionKey, + ); + + // Write app_connection into the TENANT DB (not global) + const [inserted] = await tenantDb + .insert(dbSchema.appConnections) + .values({ + id: fixture.id, + tenantId: systemTenantId, + appName: fixture.appName, + externalId: fixture.externalId, + displayName: fixture.displayName, + authType: 'OAUTH2', + value: encryptedValue, + metadata: fixture.metadata, + status: 'INACTIVE', + }) + .onConflictDoUpdate({ + target: [ + dbSchema.appConnections.tenantId, + dbSchema.appConnections.externalId, + ], + set: { + value: encryptedValue, displayName: fixture.displayName, + appName: fixture.appName, authType: 'OAUTH2', - value: encryptedValue, metadata: fixture.metadata, - status: 'INACTIVE', - }) - .onConflictDoUpdate({ - target: [ - dbSchema.appConnections.tenantId, - dbSchema.appConnections.externalId, - ], - set: { - value: encryptedValue, - displayName: fixture.displayName, - appName: fixture.appName, - authType: 'OAUTH2', - metadata: fixture.metadata, - status: sql`CASE - WHEN ${dbSchema.appConnections.status} IN ('ACTIVE', 'REVOKED') - THEN ${dbSchema.appConnections.status} - ELSE 'INACTIVE' - END`, - }, - }) - .returning(); - - if (!inserted) { - throw new Error( - `Upsert returned no row for externalId=${fixture.externalId}`, - ); - } - - const resolved = inserted; + status: sql`CASE + WHEN ${dbSchema.appConnections.status} IN ('ACTIVE', 'REVOKED') + THEN ${dbSchema.appConnections.status} + ELSE 'INACTIVE' + END`, + }, + }) + .returning(); - const { getWorkspaceSchemaName } = await import('@nexiom/dbmanager'); - const schemaName = getWorkspaceSchemaName( - resolved.id, - resolved.appName, + if (!inserted) { + throw new Error( + `Upsert returned no row for externalId=${fixture.externalId}`, ); + } - // Seed the tenant_storage_registry to map the tenant to its physical database. - // In a real environment, this is created when the tenant signs up. - // For local dev, we just map it to the current database name. - let dbName = 'nexiom_local'; - if (process.env.DATABASE_URL) { - try { - const parsedUrl = new URL(process.env.DATABASE_URL); - const pathname = parsedUrl.pathname.replace(/^\/+|\/+$/g, ''); - if (pathname) { - const segments = pathname.split('/'); - dbName = segments[segments.length - 1] || dbName; - } - // If pathname is empty, keep the default dbName instead of using parsedUrl.host - } catch { - // Fallback to safe default for invalid/Unix-socket-style URLs - dbName = 'nexiom_local'; - } - } - const existReg = await db - .select() - .from(dbSchema.tenantStorageRegistry) - .where(eq(dbSchema.tenantStorageRegistry.tenantId, systemTenantId)) - .limit(1); - - if (!existReg[0]) { - // Sanitize DATABASE_URL to remove credentials and pathname - let sanitizedHostUrl: string; - try { - const parsedUrl = new URL( - process.env.DATABASE_URL || - 'postgresql://localhost:5432/nexiom_local', - ); - // Build safe URL with protocol + hostname + optional port, no credentials or pathname - sanitizedHostUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}`; - } catch { - // Fallback to safe default if parsing fails - sanitizedHostUrl = 'postgresql://localhost:5432'; - } - - await db.insert(dbSchema.tenantStorageRegistry).values({ - tenantId: systemTenantId, - databaseName: dbName, - databaseHostUrl: sanitizedHostUrl, - regionContext: 'local', - }); - } - - await schemaMgr.applyPlan( - systemTenantId, - schemaName, - SchemaPlan.OUTBOUND_ACTIVE, - ); + const schemaName = getWorkspaceSchemaName( + inserted.id, + inserted.appName, + ); - // After successful schema provisioning, mark connection ACTIVE - await db - .update(dbSchema.appConnections) - .set({ status: 'ACTIVE' }) - .where(eq(dbSchema.appConnections.id, resolved.id)); + // Provision workspace pipeline schemas inside the tenant DB + await schemaMgr.applyPlan( + systemTenantId, + schemaName, + SchemaPlan.OUTBOUND_ACTIVE, + ); - console.log( - ` ✓ ${resolved.displayName} → ${resolved.id} (schema: ${schemaName})`, - ); - } + // Mark connection ACTIVE in the tenant DB + await tenantDb + .update(dbSchema.appConnections) + .set({ status: 'ACTIVE' }) + .where(eq(dbSchema.appConnections.id, inserted.id)); - console.log('\n✅ Local dev fixtures provisioned.'); console.log( - ' To replace credentials, use the encrypt CLI helper (e.g. pnpm db:encrypt-credential)\n' + - ' and update app_connection.value with the resulting ciphertext.\n' + - ' Do NOT edit the value column manually — it holds AES-GCM ciphertext.', + ` ✓ ${inserted.displayName} → ${inserted.id} (schema: ${schemaName})`, ); - } finally { - await client.end(); } - }); + + console.log('\n✅ Local dev fixtures provisioned.'); + console.log( + ' To replace credentials, use the encrypt CLI helper (e.g. pnpm db:encrypt-credential)\n' + + ' and update app_connection.value with the resulting ciphertext.\n' + + ' Do NOT edit the value column manually — it holds AES-GCM ciphertext.', + ); + } finally { + await tenantPool.end(); + await globalClient.end(); + } } /** diff --git a/apps/api/src/modules/connections/connectors.service.spec.ts b/apps/api/src/modules/connections/connectors.service.spec.ts index 324b5045..f426329a 100644 --- a/apps/api/src/modules/connections/connectors.service.spec.ts +++ b/apps/api/src/modules/connections/connectors.service.spec.ts @@ -5,7 +5,7 @@ import { ConnectorsService } from './connectors.service.js'; import { EncryptionService, AppCredentialError } from '@nexiom/credentials'; import { PieceRegistryService } from '@nexiom/piece-registry'; import type { Piece } from '@nexiom/piece-framework'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; import { SchemaPlan } from '@nexiom/dbmanager'; import { InternalServerErrorException, diff --git a/apps/api/src/modules/connections/connectors.service.ts b/apps/api/src/modules/connections/connectors.service.ts index 4d2bdbcb..63b5658f 100644 --- a/apps/api/src/modules/connections/connectors.service.ts +++ b/apps/api/src/modules/connections/connectors.service.ts @@ -24,7 +24,7 @@ import { import { eq, and, or, sql } from 'drizzle-orm'; import { SchemaPlan, getWorkspaceSchemaName } from '@nexiom/dbmanager'; import type { DatabaseManager } from '@nexiom/dbmanager'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; import { PieceRegistryService } from '@nexiom/piece-registry'; import { extractPgError, PG_UNIQUE_VIOLATION } from '../../shared/db.utils.js'; diff --git a/apps/api/src/modules/dbmanager/dbmanager.module.ts b/apps/api/src/modules/dbmanager/dbmanager.module.ts index 50e4d298..d2858fba 100644 --- a/apps/api/src/modules/dbmanager/dbmanager.module.ts +++ b/apps/api/src/modules/dbmanager/dbmanager.module.ts @@ -1,5 +1,5 @@ import { Module, Global } from '@nestjs/common'; -import { TenantDatabaseManager } from '@nexiom/dbmanager'; +import { TenantDatabaseManager, DB_MANAGER } from '@nexiom/dbmanager'; import { Pool } from 'pg'; import { drizzle } from 'drizzle-orm/node-postgres'; import * as schema from '../../db/schema.js'; @@ -9,8 +9,6 @@ import type { DrizzleDb } from '@nexiom/database'; import { getDomainProvisioner } from '@nexiom/piece-framework'; -export const DB_MANAGER = 'DATABASE_MANAGER'; - @Global() @Module({ imports: [DbModule], diff --git a/apps/api/src/modules/mappings/mappings.controller.spec.ts b/apps/api/src/modules/mappings/mappings.controller.spec.ts index 0b5630f4..9ad1c311 100644 --- a/apps/api/src/modules/mappings/mappings.controller.spec.ts +++ b/apps/api/src/modules/mappings/mappings.controller.spec.ts @@ -43,24 +43,24 @@ describe('MappingsController', () => { const payload = new CreateMapping(); const expected = { id: '123', ...payload }; const spy = vi.spyOn(service, 'create').mockResolvedValue(expected as any); - const result = await controller.create(payload); - expect(spy).toHaveBeenCalledWith(payload); + const result = await controller.create('tenant1', payload); + expect(spy).toHaveBeenCalledWith('tenant1', payload); expect(result).toEqual(expected); }); it('findAll() should map to service.findAll', async () => { const expected = [{ id: '1' }, { id: '2' }]; const spy = vi.spyOn(service, 'findAll').mockResolvedValue(expected as any); - const result = await controller.findAll(); - expect(spy).toHaveBeenCalled(); + const result = await controller.findAll('tenant1'); + expect(spy).toHaveBeenCalledWith('tenant1'); expect(result).toEqual(expected); }); it('findOne() should map to service.findOne', async () => { const expected = { id: 'abc', appName: 'test' }; const spy = vi.spyOn(service, 'findOne').mockResolvedValue(expected as any); - const result = await controller.findOne('abc'); - expect(spy).toHaveBeenCalledWith('abc'); + const result = await controller.findOne('tenant1', 'abc'); + expect(spy).toHaveBeenCalledWith('tenant1', 'abc'); expect(result).toEqual(expected); }); @@ -68,8 +68,8 @@ describe('MappingsController', () => { const payload = new UpdateMapping(); const expected = { id: 'abc', ...payload }; const spy = vi.spyOn(service, 'update').mockResolvedValue(expected as any); - const result = await controller.updatePartial('abc', payload); - expect(spy).toHaveBeenCalledWith('abc', payload); + const result = await controller.updatePartial('tenant1', 'abc', payload); + expect(spy).toHaveBeenCalledWith('tenant1', 'abc', payload); expect(result).toEqual(expected); }); @@ -77,16 +77,16 @@ describe('MappingsController', () => { const payload = new UpdateMapping(); const expected = { id: 'abc', ...payload }; const spy = vi.spyOn(service, 'update').mockResolvedValue(expected as any); - const result = await controller.update('abc', payload); - expect(spy).toHaveBeenCalledWith('abc', payload); + const result = await controller.update('tenant1', 'abc', payload); + expect(spy).toHaveBeenCalledWith('tenant1', 'abc', payload); expect(result).toEqual(expected); }); it('remove() should map to service.remove', async () => { const expected = { success: true }; const spy = vi.spyOn(service, 'remove').mockResolvedValue(expected as any); - const result = await controller.remove('abc'); - expect(spy).toHaveBeenCalledWith('abc'); + const result = await controller.remove('tenant1', 'abc'); + expect(spy).toHaveBeenCalledWith('tenant1', 'abc'); expect(result).toEqual(expected); }); }); diff --git a/apps/api/src/modules/mappings/mappings.controller.ts b/apps/api/src/modules/mappings/mappings.controller.ts index f64e614c..8e31ae2b 100644 --- a/apps/api/src/modules/mappings/mappings.controller.ts +++ b/apps/api/src/modules/mappings/mappings.controller.ts @@ -13,38 +13,46 @@ import { MappingsService } from './mappings.service.js'; import { CreateMapping, UpdateMapping } from './mappings.validation.js'; import { SystemAdminGuard } from '../identity/auth/system-admin.guard.js'; -@Controller('admin/mappings') +@Controller('admin/mappings/:tenantId') @UseGuards(SystemAdminGuard) export class MappingsController { constructor(private readonly mappingsService: MappingsService) {} @Post() - create(@Body() payload: CreateMapping) { - return this.mappingsService.create(payload); + create(@Param('tenantId') tenantId: string, @Body() payload: CreateMapping) { + return this.mappingsService.create(tenantId, payload); } @Get() - findAll() { - return this.mappingsService.findAll(); + findAll(@Param('tenantId') tenantId: string) { + return this.mappingsService.findAll(tenantId); } @Get(':id') - findOne(@Param('id') id: string) { - return this.mappingsService.findOne(id); + findOne(@Param('tenantId') tenantId: string, @Param('id') id: string) { + return this.mappingsService.findOne(tenantId, id); } @Patch(':id') - updatePartial(@Param('id') id: string, @Body() payload: UpdateMapping) { - return this.mappingsService.update(id, payload); + updatePartial( + @Param('tenantId') tenantId: string, + @Param('id') id: string, + @Body() payload: UpdateMapping, + ) { + return this.mappingsService.update(tenantId, id, payload); } @Put(':id') - update(@Param('id') id: string, @Body() payload: UpdateMapping) { - return this.mappingsService.update(id, payload); + update( + @Param('tenantId') tenantId: string, + @Param('id') id: string, + @Body() payload: UpdateMapping, + ) { + return this.mappingsService.update(tenantId, id, payload); } @Delete(':id') - remove(@Param('id') id: string) { - return this.mappingsService.remove(id); + remove(@Param('tenantId') tenantId: string, @Param('id') id: string) { + return this.mappingsService.remove(tenantId, id); } } diff --git a/apps/api/src/modules/mappings/mappings.service.spec.ts b/apps/api/src/modules/mappings/mappings.service.spec.ts index 2a8b4e5a..bad7eefd 100644 --- a/apps/api/src/modules/mappings/mappings.service.spec.ts +++ b/apps/api/src/modules/mappings/mappings.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { MappingsService } from './mappings.service.js'; import { PinoLogger } from 'nestjs-pino'; -import { DATABASE_CONNECTION } from '@nexiom/database'; +import { DB_MANAGER } from '@nexiom/dbmanager'; import { CreateMapping, UpdateMapping } from './mappings.validation.js'; import { NotFoundException, BadRequestException } from '@nestjs/common'; import { vi } from 'vitest'; @@ -35,7 +35,10 @@ describe('MappingsService', () => { providers: [ MappingsService, { provide: PinoLogger, useValue: mockLogger }, - { provide: DATABASE_CONNECTION, useValue: mockDb }, + { + provide: DB_MANAGER, + useValue: { getTenantDb: vi.fn().mockResolvedValue(mockDb) }, + }, ], }).compile(); @@ -55,7 +58,7 @@ describe('MappingsService', () => { mockDb.from = dbFromSpy; mockDb.orderBy = dbOrderBySpy; - const result = await service.findAll(); + const result = await service.findAll('tenant1'); expect(mockDb.select).toHaveBeenCalled(); expect(result).toEqual([{ id: '1' }]); @@ -71,7 +74,7 @@ describe('MappingsService', () => { .fn() .mockResolvedValue([{ id: '1', appName: 'salesforce' }]); - const result = await service.findOne('1'); + const result = await service.findOne('tenant1', '1'); expect(result).toEqual({ id: '1', appName: 'salesforce' }); }); @@ -81,7 +84,7 @@ describe('MappingsService', () => { mockDb.where = vi.fn().mockReturnThis(); mockDb.limit = vi.fn().mockResolvedValue([]); - await expect(service.findOne('invalid')).rejects.toThrow( + await expect(service.findOne('tenant1', 'invalid')).rejects.toThrow( NotFoundException, ); }); @@ -111,18 +114,40 @@ describe('MappingsService', () => { }), }); - const result = await service.create(dto); + const result = await service.create('tenant1', dto); expect(result.id).toBe('123'); }); + it('should default version to v1 if not provided', async () => { + const dto = new CreateMapping(); + dto.appName = 'salesforce'; + dto.category = 'tms'; + dto.entity = 'Load__c'; + dto.viewMode = 'summary'; + dto.mappingConfig = {}; + + const returningSpy = vi.fn().mockResolvedValue([{ version: 'v1' }]); + const valuesSpy = vi.fn().mockReturnValue({ returning: returningSpy }); + mockDb.insert = vi.fn().mockReturnValue({ values: valuesSpy }); + + const result = await service.create('tenant1', dto); + + expect(valuesSpy).toHaveBeenCalledWith( + expect.objectContaining({ version: 'v1' }), + ); + expect(result.version).toBe('v1'); + }); + it('should log and throw BadRequestException on insert error', async () => { const dto = new CreateMapping(); mockDb.insert = vi.fn().mockImplementation(() => { throw new Error('DB Error'); }); - await expect(service.create(dto)).rejects.toThrow(BadRequestException); + await expect(service.create('tenant1', dto)).rejects.toThrow( + BadRequestException, + ); expect(mockLogger.error).toHaveBeenCalled(); }); }); @@ -144,10 +169,28 @@ describe('MappingsService', () => { }), }); - const result = await service.update('123', dto); + const result = await service.update('tenant1', '123', dto); expect(result.appName).toBe('testapp'); }); + it('should default version to v1 if not provided on update', async () => { + const dto = new UpdateMapping(); + // @ts-expect-error test mock + vi.spyOn(service, 'findOne').mockResolvedValue({ id: '123' }); + + const returningSpy = vi.fn().mockResolvedValue([{ version: 'v1' }]); + const whereSpy = vi.fn().mockReturnValue({ returning: returningSpy }); + const setSpy = vi.fn().mockReturnValue({ where: whereSpy }); + mockDb.update = vi.fn().mockReturnValue({ set: setSpy }); + + const result = await service.update('tenant1', '123', dto); + + expect(setSpy).toHaveBeenCalledWith( + expect.objectContaining({ version: 'v1' }), + ); + expect(result.version).toBe('v1'); + }); + it('should throw BadRequestException wrapping the internal missing record on update', async () => { // @ts-expect-error test mock vi.spyOn(service, 'findOne').mockResolvedValue({ id: '123' }); @@ -159,9 +202,21 @@ describe('MappingsService', () => { }), }); - await expect(service.update('123', new UpdateMapping())).rejects.toThrow( - NotFoundException, + await expect( + service.update('tenant1', '123', new UpdateMapping()), + ).rejects.toThrow(NotFoundException); + }); + + it('should log and throw BadRequestException on update DB error', async () => { + const dto = new UpdateMapping(); + mockDb.update = vi.fn().mockImplementation(() => { + throw new Error('DB Error'); + }); + + await expect(service.update('tenant1', '123', dto)).rejects.toThrow( + BadRequestException, ); + expect(mockLogger.error).toHaveBeenCalled(); }); }); @@ -179,7 +234,7 @@ describe('MappingsService', () => { .fn() .mockReturnValue({ where: vi.fn().mockResolvedValue(true) }); - const result = await service.remove('123'); + const result = await service.remove('tenant1', '123'); expect(result.success).toBe(true); expect(mockDb.delete).toHaveBeenCalled(); }); diff --git a/apps/api/src/modules/mappings/mappings.service.ts b/apps/api/src/modules/mappings/mappings.service.ts index 31306502..99e5727a 100644 --- a/apps/api/src/modules/mappings/mappings.service.ts +++ b/apps/api/src/modules/mappings/mappings.service.ts @@ -6,8 +6,9 @@ import { HttpException, } from '@nestjs/common'; import { eq } from 'drizzle-orm'; -import { DATABASE_CONNECTION, canonicalMappings } from '@nexiom/database'; -import type { DrizzleDb } from '@nexiom/database'; +import { DB_MANAGER } from '@nexiom/dbmanager'; +import type { DatabaseManager } from '@nexiom/dbmanager'; +import { canonicalMappings } from '@nexiom/database'; import { PinoLogger } from 'nestjs-pino'; import type { CreateMapping, UpdateMapping } from './mappings.validation.js'; @@ -15,20 +16,22 @@ import type { CreateMapping, UpdateMapping } from './mappings.validation.js'; export class MappingsService { constructor( private readonly logger: PinoLogger, - @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, + @Inject(DB_MANAGER) private readonly dbManager: DatabaseManager, ) { this.logger.setContext(MappingsService.name); } - async findAll() { - return this.db + async findAll(tenantId: string) { + const tenantDb = await this.dbManager.getTenantDb(tenantId); + return tenantDb .select() .from(canonicalMappings) .orderBy(canonicalMappings.createdAt); } - async findOne(id: string) { - const records = await this.db + async findOne(tenantId: string, id: string) { + const tenantDb = await this.dbManager.getTenantDb(tenantId); + const records = await tenantDb .select() .from(canonicalMappings) .where(eq(canonicalMappings.id, id)) @@ -39,16 +42,16 @@ export class MappingsService { return records[0]; } - async create(payload: CreateMapping) { + async create(tenantId: string, payload: CreateMapping) { try { - const records = await this.db + const tenantDb = await this.dbManager.getTenantDb(tenantId); + const records = await tenantDb .insert(canonicalMappings) .values({ appName: payload.appName, category: payload.category, entity: payload.entity, viewMode: payload.viewMode, - tenantId: (payload.tenantId?.trim() || null) ?? null, version: (payload.version?.trim() || 'v1') ?? 'v1', mappingConfig: payload.mappingConfig, }) @@ -70,16 +73,16 @@ export class MappingsService { } } - async update(id: string, payload: UpdateMapping) { + async update(tenantId: string, id: string, payload: UpdateMapping) { try { - const records = await this.db + const tenantDb = await this.dbManager.getTenantDb(tenantId); + const records = await tenantDb .update(canonicalMappings) .set({ appName: payload.appName, category: payload.category, entity: payload.entity, viewMode: payload.viewMode, - tenantId: payload.tenantId?.trim() || null, version: payload.version?.trim() || 'v1', mappingConfig: payload.mappingConfig, }) @@ -110,10 +113,13 @@ export class MappingsService { } } - async remove(id: string) { - await this.findOne(id); + async remove(tenantId: string, id: string) { + const tenantDb = await this.dbManager.getTenantDb(tenantId); + await this.findOne(tenantId, id); - await this.db.delete(canonicalMappings).where(eq(canonicalMappings.id, id)); + await tenantDb + .delete(canonicalMappings) + .where(eq(canonicalMappings.id, id)); return { success: true }; } diff --git a/apps/api/src/modules/mappings/mappings.validation.ts b/apps/api/src/modules/mappings/mappings.validation.ts index ff283d21..90b3d33f 100644 --- a/apps/api/src/modules/mappings/mappings.validation.ts +++ b/apps/api/src/modules/mappings/mappings.validation.ts @@ -7,7 +7,6 @@ const BaseMappingSchema = z.object({ category: z.string().min(1, 'Category is required'), entity: z.string().min(1, 'Entity is required'), viewMode: z.string().min(1, 'View mode is required'), - tenantId: z.string().optional(), version: z.string().optional(), mappingConfig: z.record(z.string(), z.any()), }); diff --git a/apps/api/src/modules/pipeline/inbound-outbox.service.spec.ts b/apps/api/src/modules/pipeline/inbound-outbox.service.spec.ts index b9afc8f7..ffd7d53c 100644 --- a/apps/api/src/modules/pipeline/inbound-outbox.service.spec.ts +++ b/apps/api/src/modules/pipeline/inbound-outbox.service.spec.ts @@ -5,7 +5,7 @@ import { DATABASE_CONNECTION } from '@nexiom/database'; import { QueueService, QueueName } from '@nexiom/queue'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; describe('InboundOutboxService', () => { let service: InboundOutboxService; diff --git a/apps/api/src/modules/pipeline/inbound-outbox.service.ts b/apps/api/src/modules/pipeline/inbound-outbox.service.ts index c7e9d453..b260d3bd 100644 --- a/apps/api/src/modules/pipeline/inbound-outbox.service.ts +++ b/apps/api/src/modules/pipeline/inbound-outbox.service.ts @@ -12,7 +12,7 @@ import { QueueName } from '@nexiom/queue'; import { QueueService } from '@nexiom/queue'; import { getWorkspaceSchemaName } from '@nexiom/dbmanager'; import type { DatabaseManager } from '@nexiom/dbmanager'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; const BATCH_SIZE = 50; const MAX_ATTEMPTS = 6; diff --git a/apps/api/src/modules/pipeline/replica-outbox.service.spec.ts b/apps/api/src/modules/pipeline/replica-outbox.service.spec.ts index fa169347..77e3505c 100644 --- a/apps/api/src/modules/pipeline/replica-outbox.service.spec.ts +++ b/apps/api/src/modules/pipeline/replica-outbox.service.spec.ts @@ -5,7 +5,7 @@ import { DATABASE_CONNECTION } from '@nexiom/database'; import { QueueService, QueueName } from '@nexiom/queue'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; describe('ReplicaOutboxService', () => { let service: ReplicaOutboxService; diff --git a/apps/api/src/modules/pipeline/replica-outbox.service.ts b/apps/api/src/modules/pipeline/replica-outbox.service.ts index c5cf2194..47400bb3 100644 --- a/apps/api/src/modules/pipeline/replica-outbox.service.ts +++ b/apps/api/src/modules/pipeline/replica-outbox.service.ts @@ -12,7 +12,7 @@ import { QueueName } from '@nexiom/queue'; import { QueueService } from '@nexiom/queue'; import { getWorkspaceSchemaName } from '@nexiom/dbmanager'; import type { DatabaseManager } from '@nexiom/dbmanager'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; const BATCH_SIZE = 50; const MAX_ATTEMPTS = 6; diff --git a/apps/api/src/modules/trigger/trigger-executor.service.ts b/apps/api/src/modules/trigger/trigger-executor.service.ts index 6af60cc4..9fcbc798 100644 --- a/apps/api/src/modules/trigger/trigger-executor.service.ts +++ b/apps/api/src/modules/trigger/trigger-executor.service.ts @@ -10,7 +10,7 @@ import { import { sql, eq } from 'drizzle-orm'; import { SchemaPlan } from '@nexiom/dbmanager'; import type { DatabaseManager } from '@nexiom/dbmanager'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; import { StorageResolverService } from '@nexiom/engine'; import type { Redis } from 'ioredis'; import { createHash, randomUUID } from 'node:crypto'; diff --git a/apps/api/src/modules/trigger/trigger.module.ts b/apps/api/src/modules/trigger/trigger.module.ts index e8d70efe..b357726a 100644 --- a/apps/api/src/modules/trigger/trigger.module.ts +++ b/apps/api/src/modules/trigger/trigger.module.ts @@ -8,7 +8,7 @@ import { PollerService } from './poller.service.js'; import { DlqProcessorService } from './dlq-processor.service.js'; import { DATABASE_CONNECTION } from '@nexiom/database'; import type { DrizzleDb } from '@nexiom/database'; -import { DB_MANAGER } from '../dbmanager/dbmanager.module.js'; +import { DB_MANAGER } from '@nexiom/dbmanager'; import type { DatabaseManager } from '@nexiom/dbmanager'; import { StorageResolverModule, StorageResolverService } from '@nexiom/engine'; diff --git a/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts b/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts new file mode 100644 index 00000000..ff676c60 --- /dev/null +++ b/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Test } from '@nestjs/testing'; +import { CapacityManagerService } from './capacity-manager.service.js'; +import { DATABASE_CONNECTION } from '@nexiom/database'; +import { QueueService, QueueName } from '@nexiom/queue'; +import { Logger } from '@nestjs/common'; + +describe('CapacityManagerService', () => { + let service: CapacityManagerService; + let mockDb: { execute: ReturnType }; + let mockQueueService: { send: ReturnType }; + let errorSpy: ReturnType; + + beforeEach(async () => { + mockDb = { + execute: vi.fn(), + }; + + mockQueueService = { + send: vi.fn().mockResolvedValue(undefined), + }; + + const module = await Test.createTestingModule({ + providers: [ + CapacityManagerService, + { provide: DATABASE_CONNECTION, useValue: mockDb }, + { provide: QueueService, useValue: mockQueueService }, + ], + }).compile(); + + service = module.get(CapacityManagerService); + + // Silence logger during tests + vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {}); + vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => {}); + errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {}); + vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {}); + }); + + describe('replenishPool', () => { + it('does nothing if the pool is fully provisioned (count >= 5)', async () => { + // Mock countWarm to return 5 + mockDb.execute.mockResolvedValueOnce({ rows: [{ count: '5' }] }); + + await service.replenishPool(); + + // Should check the count + expect(mockDb.execute).toHaveBeenCalledTimes(1); + + // Should NOT insert or queue anything + expect(mockQueueService.send).not.toHaveBeenCalled(); + }); + + it('dispatches jobs and inserts INITIALIZING rows for the deficit', async () => { + // Mock countWarm to return 3 (deficit = 2) + mockDb.execute.mockResolvedValueOnce({ rows: [{ count: '3' }] }); + + // Mock the two INSERT executions + mockDb.execute.mockResolvedValue({ rowCount: 1 }); + + await service.replenishPool(); + + // 1 select count + 2 inserts = 3 DB calls + expect(mockDb.execute).toHaveBeenCalledTimes(3); + + // Should dispatch 2 queue messages + expect(mockQueueService.send).toHaveBeenCalledTimes(2); + expect(mockQueueService.send).toHaveBeenCalledWith( + QueueName.TenantProvisionQueue, + expect.objectContaining({ + poolSlotId: expect.any(String) as unknown as string, + hostUrl: expect.any(String) as unknown as string, + }), + ); + }); + + it('handles count string conversion correctly even if null', async () => { + // Mock countWarm to return null/undefined (fallback to '0') + mockDb.execute.mockResolvedValueOnce({ rows: [{ count: null }] }); + + // Mock the 5 INSERT executions + mockDb.execute.mockResolvedValue({ rowCount: 1 }); + + await service.replenishPool(); + + // Should dispatch 5 queue messages + expect(mockQueueService.send).toHaveBeenCalledTimes(5); + }); + + it('catches and logs errors without crashing the cron job', async () => { + // Simulate database failure + const error = new Error('Database down'); + mockDb.execute.mockRejectedValue(error); + + // Should not throw + await expect(service.replenishPool()).resolves.toBeUndefined(); + + // Ensure error was logged + expect(errorSpy).toHaveBeenCalledWith( + 'CapacityManager: pool replenishment failed', + error, + ); + }); + }); +}); diff --git a/apps/api/src/modules/workspaces/capacity-manager.service.ts b/apps/api/src/modules/workspaces/capacity-manager.service.ts new file mode 100644 index 00000000..644e6fb1 --- /dev/null +++ b/apps/api/src/modules/workspaces/capacity-manager.service.ts @@ -0,0 +1,113 @@ +import { Injectable, Inject, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { DATABASE_CONNECTION } from '@nexiom/database'; +import type { DrizzleDb } from '@nexiom/database'; +import { QueueService, QueueName } from '@nexiom/queue'; +import { sql } from 'drizzle-orm'; +import { v4 as uuidv4 } from 'uuid'; +import type { ProvisionDatabaseEvent } from './capacity-manager.types.js'; + +/** The minimum number of WARM databases we always want in the pool. */ +const WARM_POOL_TARGET = 5; + +/** + * CapacityManagerService + * + * Runs as part of the API control plane. Monitors the size of the warm + * database pool and publishes ProvisionDatabaseEvent messages to the + * TenantProvisionQueue whenever the pool dips below the target threshold. + * + * The actual DDL work (CREATE DATABASE + migrations) is performed by the + * isolated `tenant-provisioner` microservice, which is the only process + * with CREATEDB privileges. + */ +@Injectable() +export class CapacityManagerService { + private readonly logger = new Logger(CapacityManagerService.name); + + constructor( + @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, + private readonly queueService: QueueService, + ) {} + + /** + * Runs every minute to replenish the warm pool. + * Also fires once on module init (via OnModuleInit) to fill the pool + * immediately on API startup. + */ + @Cron(CronExpression.EVERY_MINUTE) + async replenishPool(): Promise { + try { + const warmCount = await this.countWarm(); + const deficit = WARM_POOL_TARGET - warmCount; + + if (deficit <= 0) { + this.logger.debug( + `Warm pool is healthy — ${warmCount}/${WARM_POOL_TARGET} slots available.`, + ); + return; + } + + this.logger.log( + `Warm pool below threshold — ${warmCount}/${WARM_POOL_TARGET}. Dispatching ${deficit} provision job(s).`, + ); + + const hostUrl = this.deriveHostUrl(); + + for (let i = 0; i < deficit; i++) { + const poolSlotId = uuidv4(); + const event: ProvisionDatabaseEvent = { poolSlotId, hostUrl }; + const dbName = `nexiom_tenant_${poolSlotId.replace(/-/g, '_')}`; + + // 1. Insert INITIALIZING slot to prevent double-queueing on next cron tick + await this.db.execute( + sql`INSERT INTO tenant_storage_registry + (tenant_id, database_name, database_host_url, region_context, status, created_at, updated_at) + VALUES ( + ${'WARM-' + poolSlotId}, + ${dbName}, + ${hostUrl}, + ${process.env.REGION_CONTEXT ?? 'local'}, + 'INITIALIZING', + NOW(), + NOW() + )`, + ); + + // 2. Dispatch the job + await this.queueService.send(QueueName.TenantProvisionQueue, event); + this.logger.debug(` → Queued provision job: poolSlotId=${poolSlotId}`); + } + } catch (err) { + this.logger.error('CapacityManager: pool replenishment failed', err); + } + } + + /** Counts how many WARM + INITIALIZING slots are currently in the registry. */ + private async countWarm(): Promise { + const result = await this.db.execute<{ count: string }>( + sql`SELECT COUNT(*)::text AS count + FROM tenant_storage_registry + WHERE status IN ('WARM', 'INITIALIZING')`, + ); + return parseInt(result.rows[0]?.count ?? '0', 10); + } + + /** + * Derives the base host URL (no database name) from DATABASE_URL. + * This is sent in the queue message so the tenant-provisioner knows + * which Postgres cluster to provision on. + */ + private deriveHostUrl(): string { + const raw = process.env.DATABASE_URL ?? ''; + try { + const parsed = new URL(raw); + const auth = parsed.username + ? `${parsed.username}${parsed.password ? ':' + parsed.password : ''}@` + : ''; + return `${parsed.protocol}//${auth}${parsed.hostname}${parsed.port ? ':' + parsed.port : ''}`; + } catch { + return 'postgresql://user:password@localhost:5432'; + } + } +} diff --git a/apps/api/src/modules/workspaces/capacity-manager.types.ts b/apps/api/src/modules/workspaces/capacity-manager.types.ts new file mode 100644 index 00000000..e6bbf98e --- /dev/null +++ b/apps/api/src/modules/workspaces/capacity-manager.types.ts @@ -0,0 +1,13 @@ +/** + * Shared type for the ProvisionDatabaseEvent published by the + * CapacityManagerService and consumed by the TenantProvisionWorker. + * + * Re-exported here so the API does not need to import from + * apps/tenant-provisioner directly. + */ +export interface ProvisionDatabaseEvent { + /** Pre-generated UUID used both as the pool slot placeholder and DB name suffix. */ + poolSlotId: string; + /** The base Postgres host URL (without the database name path). */ + hostUrl: string; +} diff --git a/apps/api/src/modules/workspaces/workspaces.module.ts b/apps/api/src/modules/workspaces/workspaces.module.ts index c18e73a4..a2472126 100644 --- a/apps/api/src/modules/workspaces/workspaces.module.ts +++ b/apps/api/src/modules/workspaces/workspaces.module.ts @@ -3,11 +3,13 @@ import { DbModule } from '../../db/db.module.js'; import { WorkspacesController } from './workspaces.controller.js'; import { WorkspacesService } from './workspaces.service.js'; import { WorkspaceConnectionsController } from './workspace-connections.controller.js'; +import { CapacityManagerService } from './capacity-manager.service.js'; +import { QueueModule } from '@nexiom/queue'; @Module({ - imports: [DbModule], + imports: [DbModule, QueueModule], controllers: [WorkspacesController, WorkspaceConnectionsController], - providers: [WorkspacesService], + providers: [WorkspacesService, CapacityManagerService], exports: [WorkspacesService], }) export class WorkspacesModule {} diff --git a/apps/api/src/modules/workspaces/workspaces.service.spec.ts b/apps/api/src/modules/workspaces/workspaces.service.spec.ts index 1f19744c..9419006e 100644 --- a/apps/api/src/modules/workspaces/workspaces.service.spec.ts +++ b/apps/api/src/modules/workspaces/workspaces.service.spec.ts @@ -14,6 +14,9 @@ function buildMockDb() { const returningUpdate = vi.fn(); const deleteReturning = vi.fn(); const selectOrderBy = vi.fn(); + const execute = vi.fn(); + + const wherePromise = vi.fn(); return { findFirst, @@ -21,7 +24,9 @@ function buildMockDb() { returningInsert, returningUpdate, deleteReturning, + wherePromise, selectOrderBy, + execute, db: { query: { uiWorkspaces: { findFirst, findMany }, @@ -39,6 +44,11 @@ function buildMockDb() { }), select: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue({ + where: vi.fn().mockImplementation(() => { + return Object.assign(Promise.resolve(wherePromise()), { + orderBy: selectOrderBy, + }); + }), leftJoin: vi.fn().mockReturnValue({ leftJoin: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ @@ -48,6 +58,7 @@ function buildMockDb() { }), }), }), + execute, }, }; } @@ -83,16 +94,55 @@ describe('WorkspacesService', () => { // ── create ──────────────────────────────────────────────────────────────── - it('creates a workspace', async () => { + it('creates a workspace and claims a WARM database if org has none', async () => { + // 1st execute: SELECT existing active tenant -> no rows + mocks.execute.mockResolvedValueOnce({ rowCount: 0 }); + // 2nd execute: UPDATE to claim WARM -> 1 row claimed + mocks.execute.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ tenant_id: ORG_ID }], + }); mocks.returningInsert.mockResolvedValue([WORKSPACE]); const result = await service.create(ORG_ID, { name: 'Logistics' }); expect(result).toEqual(WORKSPACE); + expect(mocks.execute).toHaveBeenCalledTimes(2); expect(mocks.db.insert).toHaveBeenCalled(); }); + it('skips claiming WARM database if org already has an ACTIVE one', async () => { + // 1st execute: SELECT existing active tenant -> 1 row found + mocks.execute.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ tenant_id: ORG_ID }], + }); + mocks.returningInsert.mockResolvedValue([WORKSPACE]); + + const result = await service.create(ORG_ID, { name: 'Logistics' }); + + expect(result).toEqual(WORKSPACE); + expect(mocks.execute).toHaveBeenCalledTimes(1); // the update execute is NOT called + expect(mocks.db.insert).toHaveBeenCalled(); + }); + + it('throws ServiceUnavailableException when claiming WARM database fails (pool empty)', async () => { + // 1st execute: SELECT existing active tenant -> no rows + mocks.execute.mockResolvedValueOnce({ rowCount: 0 }); + // 2nd execute: UPDATE to claim WARM -> 0 rows claimed (pool is empty) + mocks.execute.mockResolvedValueOnce({ rowCount: 0, rows: [] }); + + await expect(service.create(ORG_ID, { name: 'Logistics' })).rejects.toThrow( + 'Workspace infrastructure is being provisioned. Please try again in a few seconds.', + ); + }); + it('throws ConflictException when DB raises a unique-violation (23505)', async () => { + // 1st execute: SELECT existing active tenant -> 1 row found + mocks.execute.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ tenant_id: ORG_ID }], + }); const pgUniqueError = Object.assign(new Error('unique'), { code: '23505' }); mocks.returningInsert.mockRejectedValue(pgUniqueError); @@ -102,6 +152,11 @@ describe('WorkspacesService', () => { }); it('re-throws unexpected errors from insert', async () => { + // 1st execute: SELECT existing active tenant -> 1 row found + mocks.execute.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ tenant_id: ORG_ID }], + }); const boom = new Error('connection lost'); mocks.returningInsert.mockRejectedValue(boom); @@ -111,6 +166,11 @@ describe('WorkspacesService', () => { }); it('throws InternalServerErrorException if insert returns no row', async () => { + // 1st execute: SELECT existing active tenant -> 1 row found + mocks.execute.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ tenant_id: ORG_ID }], + }); mocks.returningInsert.mockResolvedValue([]); await expect(service.create(ORG_ID, { name: 'Logistics' })).rejects.toThrow( @@ -193,6 +253,41 @@ describe('WorkspacesService', () => { ); }); + // ── listAvailableConnections ────────────────────────────────────────────── + + describe('listAvailableConnections', () => { + it('returns available connections when some are assigned', async () => { + // Mock workspace exists + mocks.findFirst.mockResolvedValue(WORKSPACE); + // Mock assigned connections + mocks.wherePromise.mockReturnValueOnce([{ connectionId: 'conn-1' }]); + // Mock available connections + mocks.selectOrderBy.mockResolvedValue([ + { id: 'conn-2', appName: 'salesforce' }, + ]); + + const result = await service.listAvailableConnections(ORG_ID, WS_ID); + + expect(result).toEqual([{ id: 'conn-2', appName: 'salesforce' }]); + expect(mocks.db.select).toHaveBeenCalledTimes(2); + }); + + it('returns available connections when none are assigned', async () => { + // Mock workspace exists + mocks.findFirst.mockResolvedValue(WORKSPACE); + // Mock assigned connections (none) + mocks.wherePromise.mockReturnValueOnce([]); + // Mock available connections + mocks.selectOrderBy.mockResolvedValue([ + { id: 'conn-1', appName: 'salesforce' }, + ]); + + const result = await service.listAvailableConnections(ORG_ID, WS_ID); + + expect(result).toEqual([{ id: 'conn-1', appName: 'salesforce' }]); + }); + }); + // ── listConnections ─────────────────────────────────────────────────────── it('returns connections assigned to the workspace', async () => { diff --git a/apps/api/src/modules/workspaces/workspaces.service.ts b/apps/api/src/modules/workspaces/workspaces.service.ts index 9d99b6df..72bc8a2f 100644 --- a/apps/api/src/modules/workspaces/workspaces.service.ts +++ b/apps/api/src/modules/workspaces/workspaces.service.ts @@ -1,12 +1,14 @@ import { Injectable, Inject, + Logger, NotFoundException, ConflictException, BadRequestException, InternalServerErrorException, + ServiceUnavailableException, } from '@nestjs/common'; -import { eq, and, asc, notInArray } from 'drizzle-orm'; +import { eq, and, asc, notInArray, sql } from 'drizzle-orm'; import { DATABASE_CONNECTION, type DrizzleDb, @@ -23,10 +25,49 @@ import { isUniqueViolation } from '../../shared/db.utils.js'; @Injectable() export class WorkspacesService { + private readonly logger = new Logger(WorkspacesService.name); + constructor(@Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb) {} async create(orgId: string, body: CreateWorkspace) { try { + // ── Deferred Provisioning: Claim a WARM database slot ────────────── + // Only claim a new database if this org does not already have one. + // FOR UPDATE SKIP LOCKED ensures concurrent workspace creations do not + // race to claim the same slot. + const hasDb = await this.db.execute<{ tenant_id: string }>( + sql`SELECT tenant_id + FROM tenant_storage_registry + WHERE tenant_id = ${orgId} + AND status = 'ACTIVE' + LIMIT 1`, + ); + + if ((hasDb.rowCount ?? 0) === 0) { + const claimed = await this.db.execute<{ tenant_id: string }>( + sql`UPDATE tenant_storage_registry + SET tenant_id = ${orgId}, status = 'ACTIVE', updated_at = NOW() + WHERE tenant_id = ( + SELECT tenant_id FROM tenant_storage_registry + WHERE status = 'WARM' + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + RETURNING tenant_id`, + ); + + if ((claimed.rowCount ?? 0) === 0) { + // Pool is empty — the CapacityManager will replenish it shortly. + // Return 503 so the client can retry after the pool is filled. + throw new ServiceUnavailableException( + 'Workspace infrastructure is being provisioned. Please try again in a few seconds.', + ); + } + + this.logger.log(`Claimed WARM database slot for orgId=${orgId}`); + } + + // ── Create the logical Workspace record ─────────────────────────── const [workspace] = await this.db .insert(uiWorkspaces) .values({ diff --git a/apps/api/src/scripts/admin-bootstrap.ts b/apps/api/src/scripts/admin-bootstrap.ts index 3ffc2338..93330ec3 100644 --- a/apps/api/src/scripts/admin-bootstrap.ts +++ b/apps/api/src/scripts/admin-bootstrap.ts @@ -55,7 +55,11 @@ async function seedRbac(db: NodePgDatabase) { systemTenantId: getRequiredSystemTenantId(), }; - await seedSystemRbac(db as NodePgDatabase, config, console); + await seedSystemRbac( + db as unknown as NodePgDatabase, + config, + console, + ); } // Shared helper to elevate a user to System Owner diff --git a/apps/api/src/shared/db.utils.spec.ts b/apps/api/src/shared/db.utils.spec.ts new file mode 100644 index 00000000..65133e64 --- /dev/null +++ b/apps/api/src/shared/db.utils.spec.ts @@ -0,0 +1,65 @@ +import { + extractPgError, + isUniqueViolation, + PG_UNIQUE_VIOLATION, +} from './db.utils.js'; + +describe('db.utils', () => { + describe('extractPgError', () => { + it('returns null for non-objects', () => { + expect(extractPgError(null)).toBeNull(); + expect(extractPgError('string')).toBeNull(); + expect(extractPgError(123)).toBeNull(); + }); + + it('extracts code and constraint from a direct pg error', () => { + const error = { code: '23505', constraint: 'unique_email' }; + expect(extractPgError(error)).toEqual({ + code: '23505', + constraint: 'unique_email', + }); + }); + + it('extracts code and constraint from a Drizzle wrapped error (cause)', () => { + const error = { + message: 'Drizzle query failed', + cause: { code: '23505', constraint: 'unique_email' }, + }; + expect(extractPgError(error)).toEqual({ + code: '23505', + constraint: 'unique_email', + }); + }); + + it('returns null if cause is present but not an object', () => { + const error = { cause: 'string cause' }; + expect(extractPgError(error)).toBeNull(); + }); + + it('returns null if cause is present but has no code string', () => { + const error = { cause: { code: 123 } }; + expect(extractPgError(error)).toBeNull(); + }); + }); + + describe('isUniqueViolation', () => { + it('returns true for a direct unique violation error', () => { + const error = { code: PG_UNIQUE_VIOLATION }; + expect(isUniqueViolation(error)).toBe(true); + }); + + it('returns true for a Drizzle wrapped unique violation error', () => { + const error = { cause: { code: PG_UNIQUE_VIOLATION } }; + expect(isUniqueViolation(error)).toBe(true); + }); + + it('returns false for other errors', () => { + const error = { code: '12345' }; + expect(isUniqueViolation(error)).toBe(false); + }); + + it('returns false for non-pg errors', () => { + expect(isUniqueViolation(new Error('Normal error'))).toBe(false); + }); + }); +}); diff --git a/apps/tenant-provisioner/eslint.config.mjs b/apps/tenant-provisioner/eslint.config.mjs new file mode 100644 index 00000000..8d907942 --- /dev/null +++ b/apps/tenant-provisioner/eslint.config.mjs @@ -0,0 +1,43 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/apps/tenant-provisioner/nest-cli.json b/apps/tenant-provisioner/nest-cli.json new file mode 100644 index 00000000..27a481e1 --- /dev/null +++ b/apps/tenant-provisioner/nest-cli.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "entryFile": "main", + "compilerOptions": { + "deleteOutDir": true, + "builder": "swc", + "typeCheck": true + } +} diff --git a/apps/tenant-provisioner/package.json b/apps/tenant-provisioner/package.json new file mode 100644 index 00000000..45166d5f --- /dev/null +++ b/apps/tenant-provisioner/package.json @@ -0,0 +1,51 @@ +{ + "name": "tenant-provisioner", + "version": "0.0.1", + "type": "module", + "description": "Nexiom Tenant Provisioner — isolates DDL-level database provisioning from the API control plane.", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "start": "nest start", + "dev": "nest start --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:cov": "tsc --noEmit && vitest run --coverage --passWithNoTests" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.1", + "@nexiom/database": "workspace:*", + "@nexiom/queue": "workspace:*", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.45.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@swc/core": "^1.15.11", + "@types/node": "^22.10.7", + "@types/pg": "^8.16.0", + "@types/uuid": "^11.0.0", + "@vitest/coverage-v8": "^2.1.9", + "eslint": "^9.18.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0", + "unplugin-swc": "^1.5.1", + "vitest": "^2.1.9" + } +} diff --git a/apps/tenant-provisioner/src/app.module.ts b/apps/tenant-provisioner/src/app.module.ts new file mode 100644 index 00000000..feaa6c1f --- /dev/null +++ b/apps/tenant-provisioner/src/app.module.ts @@ -0,0 +1,20 @@ +import { Module } from "@nestjs/common"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { QueueModule, createQueueModuleOptions } from "@nexiom/queue"; +import { ProvisionerModule } from "./modules/provisioner/provisioner.module.js"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: [".env.local", ".env", "../../.env"], + }), + QueueModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: createQueueModuleOptions, + }), + ProvisionerModule, + ], +}) +export class AppModule {} diff --git a/apps/tenant-provisioner/src/main.ts b/apps/tenant-provisioner/src/main.ts new file mode 100644 index 00000000..f3403bae --- /dev/null +++ b/apps/tenant-provisioner/src/main.ts @@ -0,0 +1,26 @@ +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module.js"; +import { Logger } from "@nestjs/common"; + +async function bootstrap() { + try { + const app = await NestFactory.createApplicationContext(AppModule, { + logger: ["error", "warn", "log", "debug", "verbose"], + }); + + app.enableShutdownHooks(); + + const logger = new Logger("TenantProvisionerBootstrap"); + logger.log( + "Nexiom Tenant Provisioner started — listening on TenantProvisionQueue...", + ); + } catch (err: unknown) { + const logger = new Logger("TenantProvisionerBootstrap"); + logger.error( + "Failed to bootstrap Tenant Provisioner", + err instanceof Error ? err.stack : err, + ); + process.exit(1); + } +} +void bootstrap(); diff --git a/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts b/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts new file mode 100644 index 00000000..dc1a678e --- /dev/null +++ b/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts @@ -0,0 +1,19 @@ +/** + * Describes the shape of a message on the TenantProvisionQueue. + * + * The Capacity Manager publishes these messages; the TenantProvisionWorker + * consumes them and creates a physically isolated Postgres database. + */ +export interface ProvisionDatabaseEvent { + /** + * A pre-generated UUID used both as the ephemeral tenantId placeholder + * (e.g. WARM-) and the database name suffix. + */ + poolSlotId: string; + + /** + * The target Postgres host URL (without the database path). + * e.g. 'postgresql://user:password@localhost:5432' + */ + hostUrl: string; +} diff --git a/apps/tenant-provisioner/src/modules/provisioner/provisioner.module.ts b/apps/tenant-provisioner/src/modules/provisioner/provisioner.module.ts new file mode 100644 index 00000000..127c9dc0 --- /dev/null +++ b/apps/tenant-provisioner/src/modules/provisioner/provisioner.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { QueueModule } from "@nexiom/queue"; +import { TenantProvisionWorker } from "./tenant-provision.worker.js"; + +@Module({ + imports: [QueueModule], + providers: [TenantProvisionWorker], +}) +export class ProvisionerModule {} diff --git a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts new file mode 100644 index 00000000..bf8b1902 --- /dev/null +++ b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { TenantProvisionWorker } from "./tenant-provision.worker.js"; +import { QueueName } from "@nexiom/queue"; +import type { QueueService } from "@nexiom/queue"; +import type { ProvisionDatabaseEvent } from "./provision-database.event.js"; + +// --- Mocks --- +const mockQuery = vi.fn(); +const mockConnect = vi.fn(); +const mockEnd = vi.fn(); +const mockPoolEnd = vi.fn(); + +vi.mock("pg", () => { + return { + Client: vi.fn().mockImplementation(() => ({ + connect: mockConnect, + query: mockQuery, + end: mockEnd, + })), + Pool: vi.fn().mockImplementation(() => ({ + end: mockPoolEnd, + })), + }; +}); + +vi.mock("drizzle-orm/node-postgres", () => { + return { + drizzle: vi.fn().mockReturnValue({}), + }; +}); + +vi.mock("drizzle-orm/node-postgres/migrator", () => { + return { + migrate: vi.fn().mockResolvedValue(undefined), + }; +}); + +describe("TenantProvisionWorker", () => { + let worker: TenantProvisionWorker; + let queueServiceMock: { consume: ReturnType }; + + beforeEach(() => { + vi.clearAllMocks(); + + queueServiceMock = { + consume: vi.fn(), + }; + + // Manually instantiate to avoid SWC decorator metadata issues in vitest + worker = new TenantProvisionWorker( + queueServiceMock as unknown as QueueService, + ); + }); + + it("registers a consumer for TenantProvisionQueue on init", () => { + worker.onModuleInit(); + expect(queueServiceMock.consume).toHaveBeenCalledWith( + QueueName.TenantProvisionQueue, + expect.any(Function), + ); + }); + + describe("provisioning workflow", () => { + let handler: (event: ProvisionDatabaseEvent) => Promise; + + beforeEach(() => { + worker.onModuleInit(); + // Extract the callback that was passed to queueService.consume + handler = queueServiceMock.consume.mock.calls[0][1] as ( + event: ProvisionDatabaseEvent, + ) => Promise; + }); + + it("creates database, runs migrations, and registers slot as WARM", async () => { + // Mock PG to simulate the database does NOT exist initially + mockQuery.mockResolvedValueOnce({ rowCount: 0 }); // SELECT 1 FROM pg_database ... + mockQuery.mockResolvedValueOnce({ rowCount: 0 }); // CREATE DATABASE + mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE tenant_storage_registry + + await handler({ + poolSlotId: "1234-abcd", + hostUrl: "postgresql://localhost", + }); + + // 1. Connection lifecycle checks + // 2 connections total: one for DB creation, one for registry update + expect(mockConnect).toHaveBeenCalledTimes(2); + expect(mockEnd).toHaveBeenCalledTimes(2); + + // 2. Query checks + // First query checks if DB exists + expect(mockQuery).toHaveBeenNthCalledWith( + 1, + expect.stringContaining("SELECT 1 FROM pg_database"), + ["nexiom_tenant_1234_abcd"], + ); + + // Second query creates the DB + expect(mockQuery).toHaveBeenNthCalledWith( + 2, + 'CREATE DATABASE "nexiom_tenant_1234_abcd"', + ); + + // Third query updates the registry + expect(mockQuery).toHaveBeenNthCalledWith( + 3, + expect.stringContaining("UPDATE tenant_storage_registry"), + ["WARM-1234-abcd"], + ); + + // 3. Pool cleanup check + expect(mockPoolEnd).toHaveBeenCalledTimes(1); + }); + + it("skips database creation if it already exists, but still runs migrations and updates registry", async () => { + // Mock PG to simulate the database DOES exist + mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // SELECT 1 FROM pg_database ... + mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE tenant_storage_registry + + await handler({ + poolSlotId: "1234-abcd", + hostUrl: "postgresql://localhost", + }); + + // Create database query should NOT be called + expect(mockQuery).not.toHaveBeenCalledWith( + 'CREATE DATABASE "nexiom_tenant_1234_abcd"', + ); + + // Should still update registry + expect(mockQuery).toHaveBeenCalledWith( + expect.stringContaining("UPDATE tenant_storage_registry"), + ["WARM-1234-abcd"], + ); + }); + + it("throws error for invalid poolSlotId (SQL injection prevention)", async () => { + await expect( + handler({ + poolSlotId: "1234; DROP TABLE users;", + hostUrl: "postgresql://localhost", + }), + ).rejects.toThrow( + 'Invalid tenant database name: "nexiom_tenant_1234; DROP TABLE users;"', + ); + }); + }); +}); diff --git a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts new file mode 100644 index 00000000..554dd0ec --- /dev/null +++ b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts @@ -0,0 +1,136 @@ +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; +import { QueueService, QueueName } from "@nexiom/queue"; +import { Client as PgClient, Pool } from "pg"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { ProvisionDatabaseEvent } from "./provision-database.event.js"; + +/** + * TenantProvisionWorker + * + * Consumes messages from the TenantProvisionQueue and physically provisions + * isolated Postgres databases for each tenant slot in the warm pool. + * + * Separation of concerns: + * - This worker is the ONLY process with CREATEDB privileges. + * - The API control plane (CapacityManagerService) dispatches messages but + * never directly executes DDL. + * + * A single failed provision is retried by SQS up to maxReceiveCount times + * before routing to the DLQ for operator alerting. + */ +@Injectable() +export class TenantProvisionWorker implements OnModuleInit { + private readonly logger = new Logger(TenantProvisionWorker.name); + + constructor(private readonly queueService: QueueService) {} + + onModuleInit() { + this.queueService.consume( + QueueName.TenantProvisionQueue, + async (event: ProvisionDatabaseEvent) => { + this.logger.log( + `Received ProvisionDatabaseEvent — poolSlotId=${event.poolSlotId}`, + ); + await this.provision(event); + }, + ); + } + + /** + * Provisions a new database in the warm pool: + * 1. CREATE DATABASE nexiom_tenant_ + * 2. Run all tenant-schema Drizzle migrations + * 3. Register it in tenant_storage_registry with status=WARM + */ + private async provision(event: ProvisionDatabaseEvent): Promise { + const { poolSlotId, hostUrl } = event; + const dbName = `nexiom_tenant_${poolSlotId.replace(/-/g, "_")}`; + + // ── Step 1: CREATE DATABASE ──────────────────────────────────────────── + await this.createDatabase(dbName); + + // ── Step 2: Run tenant-schema migrations ────────────────────────────── + await this.runMigrations(dbName, hostUrl); + + // ── Step 3: Register as WARM in the global registry ─────────────────── + await this.registerWarmSlot(poolSlotId); + + this.logger.log( + `✓ Warm database ready — poolSlotId=${poolSlotId}, dbName=${dbName}`, + ); + } + + private async createDatabase(dbName: string): Promise { + // Sanitize: only alphanumeric + underscores to prevent SQL injection + if (!/^[a-zA-Z0-9_]+$/.test(dbName)) { + throw new Error(`Invalid tenant database name: "${dbName}"`); + } + + const adminClient = new PgClient({ + connectionString: process.env.DATABASE_URL, + }); + await adminClient.connect(); + + try { + const existing = await adminClient.query( + `SELECT 1 FROM pg_database WHERE datname = $1`, + [dbName], + ); + if ((existing.rowCount ?? 0) === 0) { + await adminClient.query(`CREATE DATABASE "${dbName}"`); + this.logger.log(` ✓ Created database: ${dbName}`); + } else { + this.logger.warn(` ⚠️ Database already exists: ${dbName}`); + } + } finally { + await adminClient.end(); + } + } + + private async runMigrations(dbName: string, hostUrl: string): Promise { + const tenantUrl = `${hostUrl.replace(/\/$/, "")}/${dbName}`; + + const rootDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../../", + ); + const migrationsFolder = path.join( + rootDir, + "packages/database/drizzle/tenant", + ); + + const pool = new Pool({ connectionString: tenantUrl, max: 2 }); + try { + const db = drizzle(pool); + await migrate(db, { migrationsFolder }); + this.logger.log(` ✓ Migrations applied to: ${dbName}`); + } finally { + await pool.end(); + } + } + + private async registerWarmSlot(poolSlotId: string): Promise { + const adminClient = new PgClient({ + connectionString: process.env.DATABASE_URL, + }); + await adminClient.connect(); + + try { + // Update the INITIALIZING slot to WARM + await adminClient.query( + `UPDATE tenant_storage_registry + SET status = 'WARM', updated_at = NOW() + WHERE tenant_id = $1`, + [`WARM-${poolSlotId}`], + ); + this.logger.log( + ` ✓ Registered WARM slot in registry: WARM-${poolSlotId}`, + ); + } finally { + await adminClient.end(); + } + } +} diff --git a/apps/tenant-provisioner/tsconfig.json b/apps/tenant-provisioner/tsconfig.json new file mode 100644 index 00000000..38723f38 --- /dev/null +++ b/apps/tenant-provisioner/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "types": ["vitest/globals"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true + }, + "exclude": ["vitest.config.mts"], + "include": ["src/**/*"] +} diff --git a/apps/tenant-provisioner/vitest.config.ts b/apps/tenant-provisioner/vitest.config.ts new file mode 100644 index 00000000..15212413 --- /dev/null +++ b/apps/tenant-provisioner/vitest.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'vitest/config'; +import swc from 'unplugin-swc'; + +export default defineConfig({ + test: { + globals: true, + root: './', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'src/main.ts', + 'src/**/*.module.ts', + 'src/**/*.event.ts', + 'src/**/*.dto.ts', + 'src/**/*.interface.ts', + '**/*.spec.ts', + 'vitest.config.ts', + 'node_modules/**', + 'dist/**' + ], + all: true, + }, + }, + plugins: [ + // This is required to build the test files with SWC + swc.vite({ + // Explicitly set the module type to avoid conflicts with nestjs/cli + module: { type: 'es6' }, + }), + ], +}); diff --git a/apps/worker/src/modules/dbmanager/dbmanager.module.ts b/apps/worker/src/modules/dbmanager/dbmanager.module.ts index 31f908f9..5eebcd47 100644 --- a/apps/worker/src/modules/dbmanager/dbmanager.module.ts +++ b/apps/worker/src/modules/dbmanager/dbmanager.module.ts @@ -1,5 +1,5 @@ import { Module, Global } from "@nestjs/common"; -import { TenantDatabaseManager } from "@nexiom/dbmanager"; +import { TenantDatabaseManager, DB_MANAGER } from "@nexiom/dbmanager"; import { Pool } from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "../../db/schema.js"; @@ -7,8 +7,6 @@ import { DbModule } from "../../db/db.module.js"; import { DATABASE_CONNECTION } from "@nexiom/database"; import type { DrizzleDb } from "@nexiom/database"; -export const DB_MANAGER = "DATABASE_MANAGER"; - @Global() @Module({ imports: [DbModule], diff --git a/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts b/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts index 09bfc95f..ceb738f1 100644 --- a/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts +++ b/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { NormalizedOutboxWorker } from "./normalized-outbox.worker.js"; import { QueueService, QueueName } from "@nexiom/queue"; import { DATABASE_CONNECTION } from "@nexiom/database"; -import { DB_MANAGER } from "../dbmanager/dbmanager.module.js"; +import { DB_MANAGER } from "@nexiom/dbmanager"; import { describe, it, expect, vi, beforeEach } from "vitest"; const MAX_ATTEMPTS = 6; // mirrors the constant in the worker @@ -48,21 +48,20 @@ describe("NormalizedOutboxWorker", () => { { id: "out_1", traceId: "trace_1", connectionId: "conn_1", attempts: 1 }, ]); + const mockTenants: any = [{ tenantId: "tenant_1" }]; + mockTenants.where = vi + .fn() + .mockResolvedValue([{ id: "conn_1", appName: "salesforce" }]); + globalDb = { select: vi.fn().mockReturnThis(), - from: vi.fn().mockResolvedValue([{ tenantId: "tenant_1" }]), + from: vi.fn().mockReturnValue(mockTenants), }; dbManager = { getTenantDb: vi.fn().mockResolvedValue(tenantDb), }; - tenantDb.select = vi.fn().mockReturnThis(); - tenantDb.from = vi.fn().mockReturnThis(); - tenantDb.where = vi - .fn() - .mockResolvedValue([{ id: "conn_1", appName: "salesforce" }]); - const module: TestingModule = await Test.createTestingModule({ providers: [ NormalizedOutboxWorker, @@ -128,11 +127,6 @@ describe("NormalizedOutboxWorker", () => { attempts: MAX_ATTEMPTS, }, ]); - tenantDb.select = vi.fn().mockReturnThis(); - tenantDb.from = vi.fn().mockReturnThis(); - tenantDb.where = vi - .fn() - .mockResolvedValue([{ id: "conn_1", appName: "salesforce" }]); dbManager.getTenantDb.mockResolvedValue(tenantDb); const module = await Test.createTestingModule({ diff --git a/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts b/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts index 95349e06..63f868d3 100644 --- a/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts +++ b/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, Logger } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; -import { eq, sql } from "drizzle-orm"; +import { eq, sql, and } from "drizzle-orm"; import { DATABASE_CONNECTION, type DrizzleDb, @@ -12,7 +12,7 @@ import { QueueName } from "@nexiom/queue"; import { QueueService } from "@nexiom/queue"; import { getWorkspaceSchemaName } from "@nexiom/dbmanager"; import type { DatabaseManager } from "@nexiom/dbmanager"; -import { DB_MANAGER } from "../dbmanager/dbmanager.module.js"; +import { DB_MANAGER } from "@nexiom/dbmanager"; import { processInChunks } from "./outbox.utils.js"; const BATCH_SIZE = 50; @@ -47,11 +47,16 @@ export class NormalizedOutboxWorker { // 2. Use TenantDatabaseManager to connect to the specific physical tenant DB. const tenantDb = await this.dbManager.getTenantDb(tenant.tenantId); - // 3. Query app_connection inside each tenant DB to find all ACTIVE connections. - const connections = await tenantDb + // 3. Query app_connection inside the Global DB to find all ACTIVE connections for this tenant. + const connections = await this.globalDb .select({ id: appConnections.id, appName: appConnections.appName }) .from(appConnections) - .where(eq(appConnections.status, "ACTIVE")); + .where( + and( + eq(appConnections.status, "ACTIVE"), + eq(appConnections.tenantId, tenant.tenantId), + ), + ); // 4. Run drainWorkspaceOutbox on each schema derived from the connection. for (const connection of connections) { diff --git a/engine/ai/core/package.json b/engine/ai/core/package.json index 668718ac..ef427473 100644 --- a/engine/ai/core/package.json +++ b/engine/ai/core/package.json @@ -26,6 +26,7 @@ "@nestjs/common": "^11.0.1", "@nexiom/credentials": "workspace:*", "@nexiom/database": "workspace:*", + "@nexiom/dbmanager": "workspace:*", "@nexiom/piece-framework": "workspace:*", "@nexiom/piece-registry": "workspace:*", "@nexiom/cache": "workspace:*", diff --git a/engine/ai/core/src/categories/mapping.service.ts b/engine/ai/core/src/categories/mapping.service.ts index 6f8b6d2b..b9c363f9 100644 --- a/engine/ai/core/src/categories/mapping.service.ts +++ b/engine/ai/core/src/categories/mapping.service.ts @@ -1,14 +1,14 @@ import { Injectable, Inject } from '@nestjs/common'; -import { eq, and, isNull } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; import { PinoLogger } from 'nestjs-pino'; -import { DATABASE_CONNECTION, canonicalMappings } from '@nexiom/database'; -import type { DrizzleDb } from '@nexiom/database'; +import { canonicalMappings } from '@nexiom/database'; +import { DB_MANAGER, type DatabaseManager } from '@nexiom/dbmanager'; @Injectable() export class MappingService { constructor( private readonly logger: PinoLogger, - @Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb, + @Inject(DB_MANAGER) private readonly dbManager: DatabaseManager, ) { this.logger.setContext(MappingService.name); } @@ -26,9 +26,10 @@ export class MappingService { tenantId: string, version: string = 'v1' ): Promise | null> { + const tenantDb = await this.dbManager.getTenantDb(tenantId); - // First: Look for tenant-specific override - const tenantOverride = await this.db.select({ config: canonicalMappings.mappingConfig }) + // Look for mapping config in the tenant's dedicated database + const mappingRecord = await tenantDb.select({ config: canonicalMappings.mappingConfig }) .from(canonicalMappings) .where( and( @@ -36,34 +37,14 @@ export class MappingService { eq(canonicalMappings.category, category), eq(canonicalMappings.entity, entity), eq(canonicalMappings.viewMode, viewMode), - eq(canonicalMappings.version, version), - eq(canonicalMappings.tenantId, tenantId) + eq(canonicalMappings.version, version) ) ) .limit(1); - if (tenantOverride.length > 0) { - this.logger.debug({ appName, category, entity, viewMode }, 'Loaded Tenant-Specific Mapping Override'); - return tenantOverride[0].config as Record; - } - - // Second: Fallback to global default - const globalDefault = await this.db.select({ config: canonicalMappings.mappingConfig }) - .from(canonicalMappings) - .where( - and( - eq(canonicalMappings.appName, appName), - eq(canonicalMappings.category, category), - eq(canonicalMappings.entity, entity), - eq(canonicalMappings.viewMode, viewMode), - eq(canonicalMappings.version, version), - isNull(canonicalMappings.tenantId) - ) - ) - .limit(1); - - if (globalDefault.length > 0) { - return globalDefault[0].config as Record; + if (mappingRecord.length > 0) { + this.logger.debug({ appName, category, entity, viewMode }, 'Loaded Tenant Mapping Configuration'); + return mappingRecord[0].config as Record; } // --- TEMPORARY MOCK MAPPING FOR PHASE 1 TESTING --- diff --git a/engine/ai/core/src/services/chat-persistence.service.ts b/engine/ai/core/src/services/chat-persistence.service.ts index ed7327fb..62b3baa9 100644 --- a/engine/ai/core/src/services/chat-persistence.service.ts +++ b/engine/ai/core/src/services/chat-persistence.service.ts @@ -1,13 +1,13 @@ import { Injectable, Logger, Inject, NotFoundException } from '@nestjs/common'; -import { DATABASE_CONNECTION, aiConversations, aiMessages } from '@nexiom/database'; +import { aiConversations, aiMessages } from '@nexiom/database'; +import { DB_MANAGER, type DatabaseManager } from '@nexiom/dbmanager'; import { eq, and, desc } from 'drizzle-orm'; -import type { DrizzleDb } from '@nexiom/database'; @Injectable() export class ChatPersistenceService { private readonly logger = new Logger(ChatPersistenceService.name); - constructor(@Inject(DATABASE_CONNECTION) private readonly db: DrizzleDb) {} + constructor(@Inject(DB_MANAGER) private readonly dbManager: DatabaseManager) {} /** * Initializes or retrieves an existing conversation. @@ -18,8 +18,10 @@ export class ChatPersistenceService { conversationId?: string, initialTitle?: string ) { + const tenantDb = await this.dbManager.getTenantDb(tenantId); + if (conversationId) { - const existing = await this.db.select().from(aiConversations) + const existing = await tenantDb.select().from(aiConversations) .where( and( eq(aiConversations.id, conversationId), @@ -34,7 +36,7 @@ export class ChatPersistenceService { return existing[0]; } - const inserted = await this.db.insert(aiConversations).values({ + const inserted = await tenantDb.insert(aiConversations).values({ tenantId: tenantId, title: initialTitle || 'New Query', }).returning(); @@ -58,7 +60,9 @@ export class ChatPersistenceService { content: string; status?: 'pending' | 'completed' | 'failed'; }) { - const inserted = await this.db.insert(aiMessages).values({ + const tenantDb = await this.dbManager.getTenantDb(tenantId); + + const inserted = await tenantDb.insert(aiMessages).values({ tenantId, conversationId, role, @@ -73,7 +77,8 @@ export class ChatPersistenceService { * Fetch complete message lineage for a conversation. */ async getLineage(tenantId: string, conversationId: string) { - return this.db.select().from(aiMessages) + const tenantDb = await this.dbManager.getTenantDb(tenantId); + return tenantDb.select().from(aiMessages) .where( and( eq(aiMessages.conversationId, conversationId), @@ -87,7 +92,8 @@ export class ChatPersistenceService { * Fetch all conversations for a given tenant, ordered by newest first. */ async listConversations(tenantId: string) { - return this.db.select().from(aiConversations) + const tenantDb = await this.dbManager.getTenantDb(tenantId); + return tenantDb.select().from(aiConversations) .where(eq(aiConversations.tenantId, tenantId)) .orderBy(desc(aiConversations.createdAt)); } @@ -96,7 +102,9 @@ export class ChatPersistenceService { * Update the title of a conversation. */ async updateConversationTitle(tenantId: string, conversationId: string, title: string) { - const updated = await this.db.update(aiConversations) + const tenantDb = await this.dbManager.getTenantDb(tenantId); + + const updated = await tenantDb.update(aiConversations) .set({ title }) .where( and( diff --git a/package.json b/package.json index 6fe58157..05e68423 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "scripts": { - "dev": "dotenv -e .env -- turbo dev", + "dev": "dotenv -e .env -- turbo dev --concurrency=20", "dev:light": "QUEUE_ENABLED=false dotenv -e .env -- pnpm --parallel --filter=api --filter=web --filter=@nexiom/ai-engine --filter=@nexiom/piece-quickbooks --filter=@nexiom/piece-salesforce --filter=@nexiom/piece-registry --filter=@nexiom/application-revenova dev", "build": "turbo build", "lint": "turbo lint", @@ -14,6 +14,7 @@ "db:migrate": "pnpm --filter @nexiom/database db:migrate", "db:generate": "pnpm --filter @nexiom/database db:generate", "db:studio": "pnpm --filter @nexiom/database db:studio", + "db:studio:tenant": "pnpm --filter @nexiom/database db:studio:tenant", "db:seed": "pnpm --filter api db:seed", "check:env": "{ [ -e .env ] || { echo '\n❌ ERROR: .env file is missing at the root! Please create it or fetch it from your secrets manager.\n'; exit 1; }; }", "infra:reset": "pnpm check:env && docker compose down -v && pnpm setup:local", @@ -51,7 +52,7 @@ }, "packageManager": "pnpm@10.27.0", "engines": { - "node": "20.18.1" + "node": ">=20.18.1" }, "pnpm": { "overrides": { diff --git a/packages/database/drizzle.config.tenant.ts b/packages/database/drizzle.config.tenant.ts new file mode 100644 index 00000000..e6f05f39 --- /dev/null +++ b/packages/database/drizzle.config.tenant.ts @@ -0,0 +1,49 @@ +import type { Config } from 'drizzle-kit'; +import { config } from 'dotenv'; +import { resolve, dirname } from 'node:path'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const thisDir = dirname(fileURLToPath(import.meta.url)); +const monorepoRoot = resolve(thisDir, '../..'); + +const apiEnv = resolve(monorepoRoot, 'apps/api/.env'); +const rootEnv = resolve(monorepoRoot, '.env'); + +let eitherFound = false; +if (existsSync(apiEnv)) { + config({ path: apiEnv, override: false }); + eitherFound = true; +} +if (existsSync(rootEnv)) { + config({ path: rootEnv, override: false }); + eitherFound = true; +} +if (!eitherFound) { + console.warn( + `[drizzle.config.tenant] Neither ${apiEnv} nor ${rootEnv} was found. ` + + `Falling back to process.env.TENANT_DATABASE_URL.`, + ); +} + +// Tenant migrations run against a specific tenant DB. +// TENANT_DATABASE_URL must be set to the target tenant's connection string. +// For local dev and drizzle-kit generate, point at any accessible DB. +const tenantDbUrl = process.env.TENANT_DATABASE_URL || process.env.DATABASE_URL; +if (!tenantDbUrl) { + throw new Error( + '[drizzle.config.tenant] Neither TENANT_DATABASE_URL nor DATABASE_URL is set.\n' + + ' Set TENANT_DATABASE_URL to the tenant DB connection string.', + ); +} + +// Tenant-specific schema ONLY — connections, stitches, workspaces, cursors, etc. +// Global tables (identity, registry, pieces) are in drizzle.config.ts. +export default { + schema: './dist/schema/tenant/**/*.js', + out: './drizzle/tenant', + dialect: 'postgresql', + dbCredentials: { + url: tenantDbUrl, + }, +} satisfies Config; diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 4fb2059c..246c45e9 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -42,12 +42,11 @@ if (!process.env.DATABASE_URL) { ); } -// Schema points to the compiled JavaScript source files. -// This bypasses jiti's difficulty in resolving explicit .js extensions -// inside TypeScript files when using type: "module" (ESM resolution). +// Global DB schema ONLY — identity, registry, pieces. +// Tenant-specific tables are managed by drizzle.config.tenant.ts. export default { - schema: './dist/schema/*.js', - out: './drizzle', + schema: ['./dist/schema/global/**/*.js', './dist/schema/tenant/**/*.js'], + out: './drizzle/global', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL, diff --git a/packages/database/drizzle/0002_add_status_enum_values.sql b/packages/database/drizzle/0002_add_status_enum_values.sql deleted file mode 100644 index c03faab8..00000000 --- a/packages/database/drizzle/0002_add_status_enum_values.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TYPE "public"."connection_status_enum" ADD VALUE 'PROVISIONING'; -ALTER TYPE "public"."connection_status_enum" ADD VALUE 'FAILED'; - -ALTER TABLE "global_entity_map" DROP CONSTRAINT "global_entity_map_source_app_id_app_connection_id_fk"; -ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_source_app_id_app_connection_id_fk" FOREIGN KEY ("source_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action; - -ALTER TABLE "global_entity_map" DROP CONSTRAINT "global_entity_map_dest_app_id_app_connection_id_fk"; -ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_dest_app_id_app_connection_id_fk" FOREIGN KEY ("dest_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action; \ No newline at end of file diff --git a/packages/database/drizzle/0003_add_gem_indexes.sql b/packages/database/drizzle/0003_add_gem_indexes.sql deleted file mode 100644 index bc068dda..00000000 --- a/packages/database/drizzle/0003_add_gem_indexes.sql +++ /dev/null @@ -1,22 +0,0 @@ --- ============================================================ --- OUT-OF-BAND ONLINE MIGRATION — DO NOT APPLY VIA db:migrate --- ============================================================ --- Plain CREATE INDEX acquires a SHARE lock on global_entity_map --- for the full index build duration, blocking all concurrent writes. --- CREATE INDEX CONCURRENTLY avoids the write lock but cannot run --- inside the implicit transaction that Drizzle's migration runner uses. --- --- Apply these two statements manually in a separate psql session --- (or via an online-migration tool) OUTSIDE of any transaction block: --- --- CREATE INDEX CONCURRENTLY IF NOT EXISTS gem_source_app_idx --- ON global_entity_map (source_app_id); --- --- CREATE INDEX CONCURRENTLY IF NOT EXISTS gem_dest_app_idx --- ON global_entity_map (dest_app_id); --- --- Track completion in your deployment runbook before marking this --- migration done in production. --- ============================================================ -SELECT 1; -- Placeholder to satisfy migration runner safely. --- (Indexes must be built OUTSIDE transactions using CONCURRENTLY as noted above) \ No newline at end of file diff --git a/packages/database/drizzle/0004_verify_gem_indexes.sql b/packages/database/drizzle/0004_verify_gem_indexes.sql deleted file mode 100644 index 209782ab..00000000 --- a/packages/database/drizzle/0004_verify_gem_indexes.sql +++ /dev/null @@ -1,67 +0,0 @@ --- ============================================================ --- GATE MIGRATION: Verify out-of-band GEM indexes are present and usable --- ============================================================ --- This migration intentionally fails unless both gem_source_app_idx --- and gem_dest_app_idx have been applied via CREATE INDEX CONCURRENTLY --- (see 0003_add_gem_indexes.sql for instructions) AND are fully valid --- (indisvalid = true, indisready = true). --- --- A stub left by an interrupted CONCURRENTLY run will have indisvalid=false --- or indisready=false and must be dropped and rebuilt before this gate passes. --- --- If this migration fails during deployment it means the out-of-band --- index step was skipped or failed midway. Run the CONCURRENTLY commands --- from 0003 first (dropping any invalid stubs with DROP INDEX CONCURRENTLY), --- then re-run db:migrate. --- ============================================================ -DO $$ -DECLARE - missing TEXT := ''; -BEGIN - -- Check gem_source_app_idx exists, is valid, and is ready. - -- Join pg_namespace for BOTH the index class and the table class so the - -- predicate is scoped to the current schema and cannot match identically - -- named objects in other schemas sharing the same cluster. - IF NOT EXISTS ( - SELECT 1 - FROM pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace - JOIN pg_index i ON i.indexrelid = c.oid - JOIN pg_class t ON t.oid = i.indrelid - JOIN pg_namespace tn ON tn.oid = t.relnamespace - WHERE c.relname = 'gem_source_app_idx' - AND n.nspname = current_schema() - AND t.relname = 'global_entity_map' - AND tn.nspname = current_schema() - AND i.indisvalid = true - AND i.indisready = true - ) THEN - missing := missing || ' gem_source_app_idx'; - END IF; - - -- Check gem_dest_app_idx exists, is valid, and is ready. - IF NOT EXISTS ( - SELECT 1 - FROM pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace - JOIN pg_index i ON i.indexrelid = c.oid - JOIN pg_class t ON t.oid = i.indrelid - JOIN pg_namespace tn ON tn.oid = t.relnamespace - WHERE c.relname = 'gem_dest_app_idx' - AND n.nspname = current_schema() - AND t.relname = 'global_entity_map' - AND tn.nspname = current_schema() - AND i.indisvalid = true - AND i.indisready = true - ) THEN - missing := missing || ' gem_dest_app_idx'; - END IF; - - IF missing <> '' THEN - RAISE EXCEPTION - 'Deployment blocked: missing or unusable index(es) on global_entity_map: [%]. ' - 'Ensure CREATE INDEX CONCURRENTLY completed successfully for each ' - '(drop any invalid stubs first, then re-run). See 0003_add_gem_indexes.sql.', - TRIM(missing); - END IF; -END $$; diff --git a/packages/database/drizzle/0005_greedy_eddie_brock.sql b/packages/database/drizzle/0005_greedy_eddie_brock.sql deleted file mode 100644 index 30e89eda..00000000 --- a/packages/database/drizzle/0005_greedy_eddie_brock.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE "canonical_mappings" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "app_name" varchar(100) NOT NULL, - "category" varchar(50) NOT NULL, - "entity" varchar(100) NOT NULL, - "view_mode" varchar(50) NOT NULL, - "tenant_id" varchar(100), - "version" varchar(50) DEFAULT 'v1' NOT NULL, - "mapping_config" jsonb NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX "canonical_mapping_unique_idx" ON "canonical_mappings" USING btree ("app_name","category","entity","view_mode","version"); \ No newline at end of file diff --git a/packages/database/drizzle/0006_quick_puck.sql b/packages/database/drizzle/0006_quick_puck.sql deleted file mode 100644 index 4163cdd9..00000000 --- a/packages/database/drizzle/0006_quick_puck.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE "ai_conversations" ( - "id" uuid DEFAULT gen_random_uuid() NOT NULL, - "tenant_id" text NOT NULL, - "title" varchar(255) DEFAULT 'New Conversation' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "ai_conversations_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") -); ---> statement-breakpoint -CREATE TABLE "ai_messages" ( - "id" uuid DEFAULT gen_random_uuid() NOT NULL, - "tenant_id" text NOT NULL, - "conversation_id" uuid NOT NULL, - "role" varchar(50) NOT NULL, - "content" text NOT NULL, - "status" varchar(50) DEFAULT 'completed' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "ai_messages_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") -); ---> statement-breakpoint ---> statement-breakpoint -ALTER TABLE "ai_messages" ADD CONSTRAINT "ai_messages_conversation_fk" FOREIGN KEY ("tenant_id","conversation_id") REFERENCES "ai_conversations"("tenant_id","id") ON DELETE CASCADE ON UPDATE NO ACTION;--> statement-breakpoint -CREATE INDEX "ai_conv_tenant_idx" ON "ai_conversations" USING btree ("tenant_id");--> statement-breakpoint -CREATE INDEX "ai_conv_tenant_created_idx" ON "ai_conversations" USING btree ("tenant_id","created_at");--> statement-breakpoint -CREATE INDEX "ai_msg_conv_idx" ON "ai_messages" USING btree ("conversation_id");--> statement-breakpoint -CREATE INDEX "ai_msg_tenant_idx" ON "ai_messages" USING btree ("tenant_id");--> statement-breakpoint -CREATE INDEX "ai_msg_tenant_created_idx" ON "ai_messages" USING btree ("tenant_id","created_at");--> statement-breakpoint -CREATE INDEX "ai_msg_timeline_idx" ON "ai_messages" USING btree ("conversation_id","created_at"); \ No newline at end of file diff --git a/packages/database/drizzle/0007_clammy_aaron_stack.sql b/packages/database/drizzle/0007_clammy_aaron_stack.sql deleted file mode 100644 index 969fb846..00000000 --- a/packages/database/drizzle/0007_clammy_aaron_stack.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX "canonical_mapping_unique_idx";--> statement-breakpoint -ALTER TABLE "connection_storage_registry" ALTER COLUMN "database_host_id" SET DEFAULT 'aurora-prod';--> statement-breakpoint -ALTER TABLE "ai_messages" ADD CONSTRAINT "ai_messages_conversation_fk" FOREIGN KEY ("tenant_id","conversation_id") REFERENCES "public"."ai_conversations"("tenant_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE UNIQUE INDEX "canonical_mapping_tenant_idx" ON "canonical_mappings" USING btree ("tenant_id","app_name","category","entity","view_mode","version") WHERE "canonical_mappings"."tenant_id" IS NOT NULL;--> statement-breakpoint -CREATE UNIQUE INDEX "canonical_mapping_global_idx" ON "canonical_mappings" USING btree ("app_name","category","entity","view_mode","version") WHERE "canonical_mappings"."tenant_id" IS NULL; \ No newline at end of file diff --git a/packages/database/drizzle/MIGRATION_NOTES.md b/packages/database/drizzle/MIGRATION_NOTES.md deleted file mode 100644 index 78f6fc76..00000000 --- a/packages/database/drizzle/MIGRATION_NOTES.md +++ /dev/null @@ -1,26 +0,0 @@ -# Migration Baseline Strategy - -**Date:** 2026-03-27 -**Baseline Tag:** `0000_salty_stryfe` (configured in `_journal.json`) - -## Context - -As part of the database centralization initiative, all schemas and migrations were consolidated into `@nexiom/database`. The migration baseline tag has been updated to `0000_salty_stryfe`. - -## Strategy - -Because this modifies the baseline tag, operators of existing deployments will need to reconcile the local `__drizzle_migrations` table to prevent Drizzle from attempting to re-apply the baseline schema. - -### Reconciling Existing Environments - -If applying to an existing deployment that has already run migrations under the old `apps/api` baseline, follow these steps before normal rollout: - -1. **Verify Existing Schema:** Ensure the environment's database has the baseline tables. -2. **Backfill Migration Record:** Insert a fake log marking the new tag as applied to prevent conflicts: - - ```sql - INSERT INTO drizzle.__drizzle_migrations (id, hash, created_at) - VALUES (0, '0000_salty_stryfe', extract(epoch from now()) * 1000); - ``` - -3. **Rollout:** Run the standard `pnpm db:migrate` going forward. diff --git a/packages/database/drizzle/global/0000_mixed_sleepwalker.sql b/packages/database/drizzle/global/0000_mixed_sleepwalker.sql new file mode 100644 index 00000000..66c7312a --- /dev/null +++ b/packages/database/drizzle/global/0000_mixed_sleepwalker.sql @@ -0,0 +1,158 @@ +CREATE TYPE "public"."organization_status" AS ENUM('active', 'disabled', 'suspended');--> statement-breakpoint +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "accountId" text NOT NULL, + "providerId" text NOT NULL, + "userId" text NOT NULL, + "accessToken" text, + "refreshToken" text, + "idToken" text, + "accessTokenExpiresAt" timestamp with time zone, + "refreshTokenExpiresAt" timestamp with time zone, + "scope" text, + "password" text, + "createdAt" timestamp with time zone NOT NULL, + "updatedAt" timestamp with time zone NOT NULL, + CONSTRAINT "account_user_provider_unique" UNIQUE("userId","providerId"), + CONSTRAINT "account_provider_account_unique" UNIQUE("providerId","accountId") +); +--> statement-breakpoint +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY NOT NULL, + "organizationId" text NOT NULL, + "email" text NOT NULL, + "role" text, + "status" text NOT NULL, + "expiresAt" timestamp with time zone NOT NULL, + "inviterId" text NOT NULL, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY NOT NULL, + "organizationId" text NOT NULL, + "userId" text NOT NULL, + "role" text NOT NULL, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "slug" text, + "logo" text, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp with time zone DEFAULT now() NOT NULL, + "metadata" text, + "status" "organization_status" DEFAULT 'active' NOT NULL, + "isSystem" boolean DEFAULT false NOT NULL, + "deletedAt" timestamp with time zone, + CONSTRAINT "organization_id_not_sentinel" CHECK ("organization"."id" <> '__NULL__') +); +--> statement-breakpoint +CREATE TABLE "permission" ( + "id" text PRIMARY KEY NOT NULL, + "resource" text NOT NULL, + "action" text NOT NULL, + "description" text, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "permission_resource_action_unique" UNIQUE("resource","action") +); +--> statement-breakpoint +CREATE TABLE "role" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "isSystem" boolean DEFAULT false NOT NULL, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "role_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "role_permission" ( + "id" text PRIMARY KEY NOT NULL, + "roleId" text NOT NULL, + "permissionId" text NOT NULL, + "organizationId" text, + "conditions" jsonb +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expiresAt" timestamp with time zone NOT NULL, + "token" text NOT NULL, + "createdAt" timestamp with time zone NOT NULL, + "updatedAt" timestamp with time zone NOT NULL, + "ipAddress" text, + "userAgent" text, + "userId" text NOT NULL, + "impersonatedBy" text, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "email" text NOT NULL, + "emailVerified" boolean NOT NULL, + "image" text, + "createdAt" timestamp with time zone NOT NULL, + "updatedAt" timestamp with time zone NOT NULL, + "role" text DEFAULT 'member', + "banned" boolean, + "banReason" text, + "banExpires" timestamp with time zone, + "deletedAt" timestamp with time zone, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expiresAt" timestamp with time zone NOT NULL, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "pieces" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar(100) NOT NULL, + "display_name" varchar(255) NOT NULL, + "logo_url" varchar(1024), + "package_name" varchar(255) NOT NULL, + "version" varchar(50) NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pieces_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "tenant_storage_registry" ( + "tenant_id" text PRIMARY KEY NOT NULL, + "database_name" varchar(128) NOT NULL, + "database_host_url" varchar(255) NOT NULL, + "region_context" varchar(50) NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_role_role_id_fk" FOREIGN KEY ("role") REFERENCES "public"."role"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviterId_user_id_fk" FOREIGN KEY ("inviterId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_role_role_id_fk" FOREIGN KEY ("role") REFERENCES "public"."role"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_roleId_role_id_fk" FOREIGN KEY ("roleId") REFERENCES "public"."role"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_permissionId_permission_id_fk" FOREIGN KEY ("permissionId") REFERENCES "public"."permission"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_impersonatedBy_user_id_fk" FOREIGN KEY ("impersonatedBy") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "member_user_org_unique" ON "member" USING btree ("userId","organizationId") WHERE "member"."deletedAt" IS NULL;--> statement-breakpoint +CREATE INDEX "member_org_idx" ON "member" USING btree ("organizationId");--> statement-breakpoint +CREATE UNIQUE INDEX "organization_slug_unique_idx" ON "organization" USING btree ("slug") WHERE "deletedAt" IS NULL;--> statement-breakpoint +CREATE INDEX "idx_role_permission_org_id" ON "role_permission" USING btree ("organizationId");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_role_permission_unique" ON "role_permission" USING btree ("roleId","permissionId",COALESCE("organizationId", '__NULL__'));--> statement-breakpoint +CREATE UNIQUE INDEX "registry_dbname_unique_idx" ON "tenant_storage_registry" USING btree ("database_name");--> statement-breakpoint +CREATE INDEX "registry_region_idx" ON "tenant_storage_registry" USING btree ("region_context"); \ No newline at end of file diff --git a/packages/database/drizzle/global/0001_shocking_microbe.sql b/packages/database/drizzle/global/0001_shocking_microbe.sql new file mode 100644 index 00000000..588815b7 --- /dev/null +++ b/packages/database/drizzle/global/0001_shocking_microbe.sql @@ -0,0 +1 @@ +ALTER TABLE "tenant_storage_registry" ADD COLUMN "status" varchar(20) DEFAULT 'ACTIVE' NOT NULL; \ No newline at end of file diff --git a/packages/database/drizzle/0000_salty_stryfe.sql b/packages/database/drizzle/global/0003_peaceful_beast.sql similarity index 58% rename from packages/database/drizzle/0000_salty_stryfe.sql rename to packages/database/drizzle/global/0003_peaceful_beast.sql index 495407c7..f29bc0f2 100644 --- a/packages/database/drizzle/0000_salty_stryfe.sql +++ b/packages/database/drizzle/global/0003_peaceful_beast.sql @@ -1,152 +1,41 @@ -CREATE TYPE "public"."organization_status" AS ENUM('active', 'disabled', 'suspended');--> statement-breakpoint -CREATE TYPE "public"."auth_type_enum" AS ENUM('OAUTH2', 'API_KEY', 'BASIC');--> statement-breakpoint -CREATE TYPE "public"."connection_status_enum" AS ENUM('ACTIVE', 'INACTIVE', 'REVOKED', 'EXPIRED');--> statement-breakpoint -CREATE TYPE "public"."env_type_enum" AS ENUM('PRODUCTION', 'SANDBOX');--> statement-breakpoint +CREATE TYPE "public"."pipeline_layer_enum" AS ENUM('L1', 'L2', 'L3', 'L4', 'L5', 'L6');--> statement-breakpoint +CREATE TYPE "public"."pipeline_status_enum" AS ENUM('RECEIVED', 'PROCESSING', 'REPLICATED', 'NORMALIZED', 'SKIPPED', 'PENDING', 'SUCCESS', 'FAIL', 'RETRY', 'DISMISSED');--> statement-breakpoint CREATE TYPE "public"."scheduler_outbox_action_enum" AS ENUM('created', 'updated', 'deleted');--> statement-breakpoint CREATE TYPE "public"."scheduler_outbox_status_enum" AS ENUM('pending', 'processing', 'succeeded', 'failed');--> statement-breakpoint CREATE TYPE "public"."stitch_status_enum" AS ENUM('ACTIVE', 'PAUSED', 'ARCHIVED');--> statement-breakpoint -CREATE TYPE "public"."pipeline_layer_enum" AS ENUM('L1', 'L2', 'L3', 'L4', 'L5', 'L6');--> statement-breakpoint -CREATE TYPE "public"."pipeline_status_enum" AS ENUM('RECEIVED', 'PROCESSING', 'REPLICATED', 'NORMALIZED', 'SKIPPED', 'PENDING', 'SUCCESS', 'FAIL', 'RETRY');--> statement-breakpoint -CREATE TABLE "account" ( - "id" text PRIMARY KEY NOT NULL, - "accountId" text NOT NULL, - "providerId" text NOT NULL, - "userId" text NOT NULL, - "accessToken" text, - "refreshToken" text, - "idToken" text, - "accessTokenExpiresAt" timestamp with time zone, - "refreshTokenExpiresAt" timestamp with time zone, - "scope" text, - "password" text, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - CONSTRAINT "account_user_provider_unique" UNIQUE("userId","providerId"), - CONSTRAINT "account_provider_account_unique" UNIQUE("providerId","accountId") -); ---> statement-breakpoint -CREATE TABLE "invitation" ( - "id" text PRIMARY KEY NOT NULL, - "organizationId" text NOT NULL, - "email" text NOT NULL, - "role" text, - "status" text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - "inviterId" text NOT NULL, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "member" ( - "id" text PRIMARY KEY NOT NULL, - "organizationId" text NOT NULL, - "userId" text NOT NULL, - "role" text NOT NULL, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, - "deletedAt" timestamp with time zone -); ---> statement-breakpoint -CREATE TABLE "organization" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "slug" text, - "logo" text, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, - "updatedAt" timestamp with time zone DEFAULT now() NOT NULL, - "metadata" text, - "status" "organization_status" DEFAULT 'active' NOT NULL, - "isSystem" boolean DEFAULT false NOT NULL, - "deletedAt" timestamp with time zone, - CONSTRAINT "organization_id_not_sentinel" CHECK ("organization"."id" <> '__NULL__') -); ---> statement-breakpoint -CREATE TABLE "permission" ( - "id" text PRIMARY KEY NOT NULL, - "resource" text NOT NULL, - "action" text NOT NULL, - "description" text, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "permission_resource_action_unique" UNIQUE("resource","action") -); ---> statement-breakpoint -CREATE TABLE "role" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "description" text, - "isSystem" boolean DEFAULT false NOT NULL, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "role_name_unique" UNIQUE("name") -); ---> statement-breakpoint -CREATE TABLE "role_permission" ( - "id" text PRIMARY KEY NOT NULL, - "roleId" text NOT NULL, - "permissionId" text NOT NULL, - "organizationId" text, - "conditions" jsonb -); ---> statement-breakpoint -CREATE TABLE "session" ( - "id" text PRIMARY KEY NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - "token" text NOT NULL, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "ipAddress" text, - "userAgent" text, - "userId" text NOT NULL, - "impersonatedBy" text, - CONSTRAINT "session_token_unique" UNIQUE("token") -); ---> statement-breakpoint -CREATE TABLE "user" ( - "id" text PRIMARY KEY NOT NULL, - "name" text, - "email" text NOT NULL, - "emailVerified" boolean NOT NULL, - "image" text, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "role" text DEFAULT 'member', - "banned" boolean, - "banReason" text, - "banExpires" timestamp with time zone, - "deletedAt" timestamp with time zone, - CONSTRAINT "user_email_unique" UNIQUE("email") -); ---> statement-breakpoint -CREATE TABLE "verification" ( - "id" text PRIMARY KEY NOT NULL, - "identifier" text NOT NULL, - "value" text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - "createdAt" timestamp with time zone, - "updatedAt" timestamp with time zone -); ---> statement-breakpoint -CREATE TABLE "app_connection" ( +CREATE TYPE "public"."auth_type_enum" AS ENUM('OAUTH2', 'API_KEY', 'BASIC');--> statement-breakpoint +CREATE TYPE "public"."connection_status_enum" AS ENUM('ACTIVE', 'INACTIVE', 'REVOKED', 'EXPIRED', 'PROVISIONING', 'FAILED');--> statement-breakpoint +CREATE TYPE "public"."env_type_enum" AS ENUM('PRODUCTION', 'SANDBOX');--> statement-breakpoint +CREATE TABLE "canonical_mappings" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "tenant_id" text NOT NULL, "app_name" varchar(100) NOT NULL, - "external_id" varchar(255) NOT NULL, - "display_name" varchar(255) NOT NULL, - "auth_type" "auth_type_enum" NOT NULL, - "value" text NOT NULL, - "expires_at" timestamp with time zone, - "status" "connection_status_enum" DEFAULT 'ACTIVE' NOT NULL, - "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, - "metadata" jsonb DEFAULT '{}'::jsonb, + "category" varchar(50) NOT NULL, + "entity" varchar(100) NOT NULL, + "view_mode" varchar(50) NOT NULL, + "version" varchar(50) DEFAULT 'v1' NOT NULL, + "mapping_config" jsonb NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE TABLE "connection_storage_registry" ( - "connection_id" uuid PRIMARY KEY NOT NULL, - "data_namespace" varchar(128) NOT NULL, - "schema_plan" varchar(64) DEFAULT 'NAMESPACE_ONLY' NOT NULL, - "database_host_id" varchar(255) DEFAULT 'primary-cluster' NOT NULL, - "region_context" varchar(50) NOT NULL, +CREATE TABLE "ai_conversations" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "title" varchar(255) DEFAULT 'New Conversation' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ai_conversations_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") +); +--> statement-breakpoint +CREATE TABLE "ai_messages" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "conversation_id" uuid NOT NULL, + "role" varchar(50) NOT NULL, + "content" text NOT NULL, + "status" varchar(50) DEFAULT 'completed' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ai_messages_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") ); --> statement-breakpoint CREATE TABLE "connector_object_profiles" ( @@ -158,34 +47,27 @@ CREATE TABLE "connector_object_profiles" ( CONSTRAINT "connector_object_profiles_connection_id_object_name_pk" PRIMARY KEY("connection_id","object_name") ); --> statement-breakpoint -CREATE TABLE "pieces" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "name" varchar(100) NOT NULL, - "display_name" varchar(255) NOT NULL, - "logo_url" varchar(1024), - "package_name" varchar(255) NOT NULL, - "version" varchar(50) NOT NULL, - "enabled" boolean DEFAULT true NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "pieces_name_unique" UNIQUE("name") -); ---> statement-breakpoint -CREATE TABLE "ui_workspace_connection" ( - "workspace_id" uuid NOT NULL, - "connection_id" uuid NOT NULL, - "assigned_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "ui_workspace_connection_workspace_id_connection_id_pk" PRIMARY KEY("workspace_id","connection_id") -); ---> statement-breakpoint -CREATE TABLE "ui_workspace" ( +CREATE TABLE "global_entity_map" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "org_id" text NOT NULL, - "name" varchar(255) NOT NULL, - "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "ui_workspace_id_org_unique_idx" UNIQUE("id","org_id") + "stitch_id" uuid NOT NULL, + "source_app_name" varchar(100) NOT NULL, + "source_app_id" uuid NOT NULL, + "source_org_id" varchar(255) NOT NULL, + "source_org_name" varchar(255), + "source_entity_type" varchar(100) NOT NULL, + "source_entity_id" varchar(255) NOT NULL, + "source_ref_layer" varchar(10) NOT NULL, + "source_trace_id" uuid NOT NULL, + "dest_app_name" varchar(100) NOT NULL, + "dest_app_id" uuid NOT NULL, + "dest_org_id" varchar(255) NOT NULL, + "dest_org_name" varchar(255), + "dest_entity_type" varchar(100) NOT NULL, + "dest_entity_id" varchar(255) NOT NULL, + "dest_ref_layer" varchar(10) NOT NULL, + "dest_trace_id" uuid NOT NULL, + "last_synced_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "field_mapping" ( @@ -237,74 +119,68 @@ CREATE TABLE "sync_cursors" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE TABLE "global_entity_map" ( +CREATE TABLE "app_connection" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "stitch_id" uuid NOT NULL, - "source_app_name" varchar(100) NOT NULL, - "source_app_id" uuid NOT NULL, - "source_org_id" varchar(255) NOT NULL, - "source_org_name" varchar(255), - "source_entity_type" varchar(100) NOT NULL, - "source_entity_id" varchar(255) NOT NULL, - "source_ref_layer" varchar(10) NOT NULL, - "source_trace_id" uuid NOT NULL, - "dest_app_name" varchar(100) NOT NULL, - "dest_app_id" uuid NOT NULL, - "dest_org_id" varchar(255) NOT NULL, - "dest_org_name" varchar(255), - "dest_entity_type" varchar(100) NOT NULL, - "dest_entity_id" varchar(255) NOT NULL, - "dest_ref_layer" varchar(10) NOT NULL, - "dest_trace_id" uuid NOT NULL, - "last_synced_at" timestamp with time zone DEFAULT now() NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL + "tenant_id" text NOT NULL, + "app_name" varchar(100) NOT NULL, + "external_id" varchar(255) NOT NULL, + "display_name" varchar(255) NOT NULL, + "auth_type" "auth_type_enum" NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp with time zone, + "status" "connection_status_enum" DEFAULT 'ACTIVE' NOT NULL, + "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb, + "schema_plan" varchar(64) DEFAULT 'NAMESPACE_ONLY' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ui_workspace_connection" ( + "workspace_id" uuid NOT NULL, + "connection_id" uuid NOT NULL, + "assigned_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ui_workspace_connection_workspace_id_connection_id_pk" PRIMARY KEY("workspace_id","connection_id") +); +--> statement-breakpoint +CREATE TABLE "ui_workspace" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "name" varchar(255) NOT NULL, + "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ui_workspace_id_org_unique_idx" UNIQUE("id","org_id") ); --> statement-breakpoint -ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "invitation" ADD CONSTRAINT "invitation_role_role_id_fk" FOREIGN KEY ("role") REFERENCES "public"."role"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviterId_user_id_fk" FOREIGN KEY ("inviterId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "member" ADD CONSTRAINT "member_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "member" ADD CONSTRAINT "member_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "member" ADD CONSTRAINT "member_role_role_id_fk" FOREIGN KEY ("role") REFERENCES "public"."role"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_roleId_role_id_fk" FOREIGN KEY ("roleId") REFERENCES "public"."role"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_permissionId_permission_id_fk" FOREIGN KEY ("permissionId") REFERENCES "public"."permission"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "session" ADD CONSTRAINT "session_impersonatedBy_user_id_fk" FOREIGN KEY ("impersonatedBy") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "app_connection" ADD CONSTRAINT "app_connection_tenant_id_organization_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "connection_storage_registry" ADD CONSTRAINT "connection_storage_registry_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_messages" ADD CONSTRAINT "ai_messages_conversation_fk" FOREIGN KEY ("tenant_id","conversation_id") REFERENCES "public"."ai_conversations"("tenant_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "connector_object_profiles" ADD CONSTRAINT "connector_object_profiles_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_workspace_id_ui_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."ui_workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ui_workspace" ADD CONSTRAINT "ui_workspace_org_id_organization_id_fk" FOREIGN KEY ("org_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_stitch_id_integration_stitch_id_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_source_app_id_app_connection_id_fk" FOREIGN KEY ("source_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_dest_app_id_app_connection_id_fk" FOREIGN KEY ("dest_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint ALTER TABLE "field_mapping" ADD CONSTRAINT "field_mapping_stitch_id_integration_stitch_id_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_workspace_org_fk" FOREIGN KEY ("workspace_id","org_id") REFERENCES "public"."ui_workspace"("id","org_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_src_connection_fk" FOREIGN KEY ("src_connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_dest_connection_fk" FOREIGN KEY ("dest_connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "scheduler_outbox" ADD CONSTRAINT "scheduler_outbox_stitch_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "sync_cursors" ADD CONSTRAINT "sync_cursors_stitch_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_stitch_id_integration_stitch_id_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_source_app_id_app_connection_id_fk" FOREIGN KEY ("source_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_dest_app_id_app_connection_id_fk" FOREIGN KEY ("dest_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint -CREATE UNIQUE INDEX "member_user_org_unique" ON "member" USING btree ("userId","organizationId") WHERE "member"."deletedAt" IS NULL;--> statement-breakpoint -CREATE INDEX "member_org_idx" ON "member" USING btree ("organizationId");--> statement-breakpoint -CREATE UNIQUE INDEX "organization_slug_unique_idx" ON "organization" USING btree ("slug") WHERE "deletedAt" IS NULL;--> statement-breakpoint -CREATE INDEX "idx_role_permission_org_id" ON "role_permission" USING btree ("organizationId");--> statement-breakpoint -CREATE UNIQUE INDEX "idx_role_permission_unique" ON "role_permission" USING btree ("roleId","permissionId",COALESCE("organizationId", '__NULL__'));--> statement-breakpoint -CREATE INDEX "app_name_idx" ON "app_connection" USING btree ("app_name");--> statement-breakpoint -CREATE INDEX "tenant_status_idx" ON "app_connection" USING btree ("tenant_id","status");--> statement-breakpoint -CREATE INDEX "connection_expires_at_idx" ON "app_connection" USING btree ("expires_at") WHERE "app_connection"."expires_at" IS NOT NULL;--> statement-breakpoint -CREATE UNIQUE INDEX "tenant_external_id_unique_idx" ON "app_connection" USING btree ("tenant_id","external_id");--> statement-breakpoint -CREATE UNIQUE INDEX "tenant_app_display_name_lower_idx" ON "app_connection" USING btree ("tenant_id","app_name",lower("display_name"));--> statement-breakpoint -CREATE UNIQUE INDEX "registry_namespace_unique_idx" ON "connection_storage_registry" USING btree ("data_namespace");--> statement-breakpoint -CREATE INDEX "registry_host_idx" ON "connection_storage_registry" USING btree ("database_host_id");--> statement-breakpoint -CREATE INDEX "registry_region_idx" ON "connection_storage_registry" USING btree ("region_context");--> statement-breakpoint +ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_workspace_id_ui_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."ui_workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "canonical_mapping_unique_idx" ON "canonical_mappings" USING btree ("app_name","category","entity","view_mode","version");--> statement-breakpoint +CREATE INDEX "ai_conv_tenant_idx" ON "ai_conversations" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "ai_conv_created_idx" ON "ai_conversations" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "ai_msg_conv_idx" ON "ai_messages" USING btree ("conversation_id");--> statement-breakpoint +CREATE INDEX "ai_msg_tenant_idx" ON "ai_messages" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "ai_msg_timeline_idx" ON "ai_messages" USING btree ("conversation_id","created_at");--> statement-breakpoint CREATE INDEX "cop_connection_idx" ON "connector_object_profiles" USING btree ("connection_id");--> statement-breakpoint -CREATE INDEX "workspace_connection_conn_idx" ON "ui_workspace_connection" USING btree ("connection_id");--> statement-breakpoint -CREATE UNIQUE INDEX "ui_workspace_org_name_lower_unique_idx" ON "ui_workspace" USING btree ("org_id","env_type",lower("name"));--> statement-breakpoint -CREATE INDEX "ui_workspace_org_idx" ON "ui_workspace" USING btree ("org_id");--> statement-breakpoint -CREATE INDEX "ui_workspace_env_idx" ON "ui_workspace" USING btree ("org_id","env_type");--> statement-breakpoint +CREATE UNIQUE INDEX "gem_unique_mapping_idx" ON "global_entity_map" USING btree ("stitch_id","source_app_id","source_entity_id","dest_app_id","dest_entity_type");--> statement-breakpoint +CREATE INDEX "gem_src_lookup_idx" ON "global_entity_map" USING btree ("source_entity_id","source_app_id");--> statement-breakpoint +CREATE INDEX "gem_dest_lookup_idx" ON "global_entity_map" USING btree ("dest_entity_id","dest_app_id");--> statement-breakpoint +CREATE INDEX "gem_source_app_idx" ON "global_entity_map" USING btree ("source_app_id");--> statement-breakpoint +CREATE INDEX "gem_dest_app_idx" ON "global_entity_map" USING btree ("dest_app_id");--> statement-breakpoint +CREATE INDEX "gem_src_trace_idx" ON "global_entity_map" USING btree ("source_trace_id");--> statement-breakpoint +CREATE INDEX "gem_dest_trace_idx" ON "global_entity_map" USING btree ("dest_trace_id");--> statement-breakpoint +CREATE INDEX "gem_stitch_idx" ON "global_entity_map" USING btree ("stitch_id");--> statement-breakpoint CREATE UNIQUE INDEX "field_mapping_stitch_canonical_unique_idx" ON "field_mapping" USING btree ("stitch_id","source_canonical");--> statement-breakpoint CREATE UNIQUE INDEX "stitch_name_workspace_unique_idx" ON "integration_stitch" USING btree ("workspace_id",lower("name"));--> statement-breakpoint CREATE INDEX "stitch_workspace_idx" ON "integration_stitch" USING btree ("workspace_id");--> statement-breakpoint @@ -316,9 +192,12 @@ CREATE INDEX "scheduler_outbox_poll_idx" ON "scheduler_outbox" USING btree ("nex CREATE INDEX "scheduler_outbox_stitch_idx" ON "scheduler_outbox" USING btree ("stitch_id");--> statement-breakpoint CREATE UNIQUE INDEX "sync_cursors_stitch_stream_unique_idx" ON "sync_cursors" USING btree ("stitch_id","stream_name");--> statement-breakpoint CREATE INDEX "sync_cursors_stitch_idx" ON "sync_cursors" USING btree ("stitch_id");--> statement-breakpoint -CREATE UNIQUE INDEX "gem_unique_mapping_idx" ON "global_entity_map" USING btree ("stitch_id","source_app_id","source_entity_id","dest_app_id","dest_entity_type");--> statement-breakpoint -CREATE INDEX "gem_src_lookup_idx" ON "global_entity_map" USING btree ("source_entity_id","source_app_id");--> statement-breakpoint -CREATE INDEX "gem_dest_lookup_idx" ON "global_entity_map" USING btree ("dest_entity_id","dest_app_id");--> statement-breakpoint -CREATE INDEX "gem_src_trace_idx" ON "global_entity_map" USING btree ("source_trace_id");--> statement-breakpoint -CREATE INDEX "gem_dest_trace_idx" ON "global_entity_map" USING btree ("dest_trace_id");--> statement-breakpoint -CREATE INDEX "gem_stitch_idx" ON "global_entity_map" USING btree ("stitch_id"); \ No newline at end of file +CREATE INDEX "app_name_idx" ON "app_connection" USING btree ("app_name");--> statement-breakpoint +CREATE INDEX "tenant_status_idx" ON "app_connection" USING btree ("tenant_id","status");--> statement-breakpoint +CREATE INDEX "connection_expires_at_idx" ON "app_connection" USING btree ("expires_at") WHERE "app_connection"."expires_at" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "tenant_external_id_unique_idx" ON "app_connection" USING btree ("tenant_id","external_id");--> statement-breakpoint +CREATE UNIQUE INDEX "tenant_app_display_name_lower_idx" ON "app_connection" USING btree ("tenant_id","app_name",lower("display_name"));--> statement-breakpoint +CREATE INDEX "workspace_connection_conn_idx" ON "ui_workspace_connection" USING btree ("connection_id");--> statement-breakpoint +CREATE UNIQUE INDEX "ui_workspace_org_name_lower_unique_idx" ON "ui_workspace" USING btree ("org_id","env_type",lower("name"));--> statement-breakpoint +CREATE INDEX "ui_workspace_org_idx" ON "ui_workspace" USING btree ("org_id");--> statement-breakpoint +CREATE INDEX "ui_workspace_env_idx" ON "ui_workspace" USING btree ("org_id","env_type"); \ No newline at end of file diff --git a/packages/database/drizzle/global/meta/0000_snapshot.json b/packages/database/drizzle/global/meta/0000_snapshot.json new file mode 100644 index 00000000..9d4724fe --- /dev/null +++ b/packages/database/drizzle/global/meta/0000_snapshot.json @@ -0,0 +1,1283 @@ +{ + "id": "355b554c-5c2a-4f2e-a30b-884581cd30e5", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.canonical_mappings": { + "name": "canonical_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "app_name": { + "name": "app_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "view_mode": { + "name": "view_mode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'v1'" + }, + "mapping_config": { + "name": "mapping_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "canonical_mapping_tenant_idx": { + "name": "canonical_mapping_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "app_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "view_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"canonical_mappings\".\"tenant_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "canonical_mapping_global_idx": { + "name": "canonical_mapping_global_idx", + "columns": [ + { + "expression": "app_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "view_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"canonical_mappings\".\"tenant_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "userId", + "providerId" + ] + }, + "account_provider_account_unique": { + "name": "account_provider_account_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId", + "accountId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviterId": { + "name": "inviterId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organizationId_organization_id_fk": { + "name": "invitation_organizationId_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_role_role_id_fk": { + "name": "invitation_role_role_id_fk", + "tableFrom": "invitation", + "tableTo": "role", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "invitation_inviterId_user_id_fk": { + "name": "invitation_inviterId_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "member_user_org_unique": { + "name": "member_user_org_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"member\".\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_org_idx": { + "name": "member_org_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organizationId_organization_id_fk": { + "name": "member_organizationId_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_userId_user_id_fk": { + "name": "member_userId_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_role_role_id_fk": { + "name": "member_role_role_id_fk", + "tableFrom": "member", + "tableTo": "role", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_unique_idx": { + "name": "organization_slug_unique_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_id_not_sentinel": { + "name": "organization_id_not_sentinel", + "value": "\"organization\".\"id\" <> '__NULL__'" + } + }, + "isRLSEnabled": false + }, + "public.permission": { + "name": "permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "permission_resource_action_unique": { + "name": "permission_resource_action_unique", + "nullsNotDistinct": false, + "columns": [ + "resource", + "action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "role_name_unique": { + "name": "role_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissionId": { + "name": "permissionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_role_permission_org_id": { + "name": "idx_role_permission_org_id", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_role_permission_unique": { + "name": "idx_role_permission_unique", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permissionId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"organizationId\", '__NULL__')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_roleId_role_id_fk": { + "name": "role_permission_roleId_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permission_permissionId_permission_id_fk": { + "name": "role_permission_permissionId_permission_id_fk", + "tableFrom": "role_permission", + "tableTo": "permission", + "columnsFrom": [ + "permissionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permission_organizationId_organization_id_fk": { + "name": "role_permission_organizationId_organization_id_fk", + "tableFrom": "role_permission", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonatedBy": { + "name": "impersonatedBy", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_impersonatedBy_user_id_fk": { + "name": "session_impersonatedBy_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "impersonatedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "banReason": { + "name": "banReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banExpires": { + "name": "banExpires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pieces": { + "name": "pieces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pieces_name_unique": { + "name": "pieces_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant_storage_registry": { + "name": "tenant_storage_registry", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "database_host_url": { + "name": "database_host_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "region_context": { + "name": "region_context", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registry_dbname_unique_idx": { + "name": "registry_dbname_unique_idx", + "columns": [ + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registry_region_idx": { + "name": "registry_region_idx", + "columns": [ + { + "expression": "region_context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled", + "suspended" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/global/meta/0001_snapshot.json b/packages/database/drizzle/global/meta/0001_snapshot.json new file mode 100644 index 00000000..fcbcebf4 --- /dev/null +++ b/packages/database/drizzle/global/meta/0001_snapshot.json @@ -0,0 +1,1126 @@ +{ + "id": "279b691e-a4c8-472b-aa1c-b83fa02be78f", + "prevId": "355b554c-5c2a-4f2e-a30b-884581cd30e5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "userId", + "providerId" + ] + }, + "account_provider_account_unique": { + "name": "account_provider_account_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId", + "accountId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviterId": { + "name": "inviterId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organizationId_organization_id_fk": { + "name": "invitation_organizationId_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_role_role_id_fk": { + "name": "invitation_role_role_id_fk", + "tableFrom": "invitation", + "tableTo": "role", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "invitation_inviterId_user_id_fk": { + "name": "invitation_inviterId_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "member_user_org_unique": { + "name": "member_user_org_unique", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"member\".\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_org_idx": { + "name": "member_org_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organizationId_organization_id_fk": { + "name": "member_organizationId_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_userId_user_id_fk": { + "name": "member_userId_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_role_role_id_fk": { + "name": "member_role_role_id_fk", + "tableFrom": "member", + "tableTo": "role", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_unique_idx": { + "name": "organization_slug_unique_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_id_not_sentinel": { + "name": "organization_id_not_sentinel", + "value": "\"organization\".\"id\" <> '__NULL__'" + } + }, + "isRLSEnabled": false + }, + "public.permission": { + "name": "permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "permission_resource_action_unique": { + "name": "permission_resource_action_unique", + "nullsNotDistinct": false, + "columns": [ + "resource", + "action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "role_name_unique": { + "name": "role_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissionId": { + "name": "permissionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_role_permission_org_id": { + "name": "idx_role_permission_org_id", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_role_permission_unique": { + "name": "idx_role_permission_unique", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permissionId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"organizationId\", '__NULL__')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_roleId_role_id_fk": { + "name": "role_permission_roleId_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permission_permissionId_permission_id_fk": { + "name": "role_permission_permissionId_permission_id_fk", + "tableFrom": "role_permission", + "tableTo": "permission", + "columnsFrom": [ + "permissionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permission_organizationId_organization_id_fk": { + "name": "role_permission_organizationId_organization_id_fk", + "tableFrom": "role_permission", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonatedBy": { + "name": "impersonatedBy", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_impersonatedBy_user_id_fk": { + "name": "session_impersonatedBy_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "impersonatedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "banReason": { + "name": "banReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banExpires": { + "name": "banExpires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pieces": { + "name": "pieces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pieces_name_unique": { + "name": "pieces_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant_storage_registry": { + "name": "tenant_storage_registry", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "database_host_url": { + "name": "database_host_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "region_context": { + "name": "region_context", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registry_dbname_unique_idx": { + "name": "registry_dbname_unique_idx", + "columns": [ + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registry_region_idx": { + "name": "registry_region_idx", + "columns": [ + { + "expression": "region_context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled", + "suspended" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0006_snapshot.json b/packages/database/drizzle/global/meta/0003_snapshot.json similarity index 97% rename from packages/database/drizzle/meta/0006_snapshot.json rename to packages/database/drizzle/global/meta/0003_snapshot.json index 9bac425f..33319d0f 100644 --- a/packages/database/drizzle/meta/0006_snapshot.json +++ b/packages/database/drizzle/global/meta/0003_snapshot.json @@ -1,6 +1,6 @@ { - "id": "fdcdfcc7-0d42-4646-95e6-2184912d2fd4", - "prevId": "7ee90d03-640f-4b72-8a4c-8df0254a51ca", + "id": "11355518-0b43-4f42-a050-a6ae06260230", + "prevId": "279b691e-a4c8-472b-aa1c-b83fa02be78f", "version": "7", "dialect": "postgresql", "tables": { @@ -39,12 +39,6 @@ "primaryKey": false, "notNull": true }, - "tenant_id": { - "name": "tenant_id", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, "version": { "name": "version", "type": "varchar(50)", @@ -121,254 +115,300 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.ai_conversations": { - "name": "ai_conversations", + "public.account": { + "name": "account", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "text", "primaryKey": false, - "notNull": true, - "default": "gen_random_uuid()" + "notNull": true }, - "tenant_id": { - "name": "tenant_id", + "providerId": { + "name": "providerId", "type": "text", "primaryKey": false, "notNull": true }, - "title": { - "name": "title", - "type": "varchar(255)", + "userId": { + "name": "userId", + "type": "text", "primaryKey": false, - "notNull": true, - "default": "'New Conversation'" + "notNull": true }, - "created_at": { - "name": "created_at", + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": false }, - "updated_at": { - "name": "updated_at", + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true } }, - "indexes": { - "ai_conv_tenant_idx": { - "name": "ai_conv_tenant_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ai_conv_created_idx": { - "name": "ai_conv_created_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "columnsTo": [ + "id" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "onDelete": "cascade", + "onUpdate": "no action" } }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "ai_conversations_tenant_id_id_pk": { - "name": "ai_conversations_tenant_id_id_pk", + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "nullsNotDistinct": false, "columns": [ - "tenant_id", - "id" + "userId", + "providerId" + ] + }, + "account_provider_account_unique": { + "name": "account_provider_account_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId", + "accountId" ] } }, - "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.ai_messages": { - "name": "ai_messages", + "public.invitation": { + "name": "invitation", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", - "primaryKey": false, - "notNull": true, - "default": "gen_random_uuid()" + "type": "text", + "primaryKey": true, + "notNull": true }, - "tenant_id": { - "name": "tenant_id", + "organizationId": { + "name": "organizationId", "type": "text", "primaryKey": false, "notNull": true }, - "conversation_id": { - "name": "conversation_id", - "type": "uuid", + "email": { + "name": "email", + "type": "text", "primaryKey": false, "notNull": true }, "role": { "name": "role", - "type": "varchar(50)", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, - "content": { - "name": "content", + "status": { + "name": "status", "type": "text", "primaryKey": false, "notNull": true }, - "status": { - "name": "status", - "type": "varchar(50)", + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "'completed'" + "notNull": true }, - "created_at": { - "name": "created_at", + "inviterId": { + "name": "inviterId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" } }, - "indexes": { - "ai_msg_conv_idx": { - "name": "ai_msg_conv_idx", - "columns": [ - { - "expression": "conversation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "indexes": {}, + "foreignKeys": { + "invitation_organizationId_organization_id_fk": { + "name": "invitation_organizationId_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" }, - "ai_msg_tenant_idx": { - "name": "ai_msg_tenant_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "invitation_role_role_id_fk": { + "name": "invitation_role_role_id_fk", + "tableFrom": "invitation", + "tableTo": "role", + "columnsFrom": [ + "role" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" }, - "ai_msg_timeline_idx": { - "name": "ai_msg_timeline_idx", - "columns": [ - { - "expression": "conversation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "invitation_inviterId_user_id_fk": { + "name": "invitation_inviterId_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviterId" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "ai_messages_tenant_id_id_pk": { - "name": "ai_messages_tenant_id_id_pk", - "columns": [ - "tenant_id", + "columnsTo": [ "id" - ] + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, + "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.connector_object_profiles": { - "name": "connector_object_profiles", + "public.member": { + "name": "member", "schema": "", "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", "primaryKey": false, "notNull": true }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", + "userId": { + "name": "userId", + "type": "text", "primaryKey": false, "notNull": true }, - "profile": { - "name": "profile", - "type": "jsonb", + "role": { + "name": "role", + "type": "text", "primaryKey": false, - "notNull": true, - "default": "'{}'" + "notNull": true }, - "created_at": { - "name": "created_at", + "createdAt": { + "name": "createdAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" }, - "updated_at": { - "name": "updated_at", + "deletedAt": { + "name": "deletedAt", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": false } }, "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", + "member_user_org_unique": { + "name": "member_user_org_unique", "columns": [ { - "expression": "connection_id", + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"member\".\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_org_idx": { + "name": "member_org_idx", + "columns": [ + { + "expression": "organizationId", "isExpression": false, "asc": true, "nulls": "last" @@ -381,213 +421,299 @@ } }, "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "tableTo": "app_connection", + "member_organizationId_organization_id_fk": { + "name": "member_organizationId_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", "columnsFrom": [ - "connection_id" + "organizationId" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" + }, + "member_userId_user_id_fk": { + "name": "member_userId_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_role_role_id_fk": { + "name": "member_role_role_id_fk", + "tableFrom": "member", + "tableTo": "role", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" } }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, + "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.global_entity_map": { - "name": "global_entity_map", + "public.organization": { + "name": "organization", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", + "type": "text", "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, "notNull": true }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", + "name": { + "name": "name", + "type": "text", "primaryKey": false, "notNull": true }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", + "slug": { + "name": "slug", + "type": "text", "primaryKey": false, "notNull": false }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", + "logo": { + "name": "logo", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", + "metadata": { + "name": "metadata", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "'active'" }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", + "isSystem": { + "name": "isSystem", + "type": "boolean", "primaryKey": false, - "notNull": true + "notNull": true, + "default": false }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", "primaryKey": false, "notNull": false - }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", - "primaryKey": false, + } + }, + "indexes": { + "organization_slug_unique_idx": { + "name": "organization_slug_unique_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"deletedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_id_not_sentinel": { + "name": "organization_id_not_sentinel", + "value": "\"organization\".\"id\" <> '__NULL__'" + } + }, + "isRLSEnabled": false + }, + "public.permission": { + "name": "permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, "notNull": true }, - "dest_entity_id": { - "name": "dest_entity_id", - "type": "varchar(255)", + "resource": { + "name": "resource", + "type": "text", "primaryKey": false, "notNull": true }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", + "action": { + "name": "action", + "type": "text", "primaryKey": false, "notNull": true }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", + "description": { + "name": "description", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, - "last_synced_at": { - "name": "last_synced_at", + "createdAt": { + "name": "createdAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "permission_resource_action_unique": { + "name": "permission_resource_action_unique", + "nullsNotDistinct": false, + "columns": [ + "resource", + "action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true }, - "created_at": { - "name": "created_at", + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isSystem": { + "name": "isSystem", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" } }, - "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "role_name_unique": { + "name": "role_name_unique", + "nullsNotDistinct": false, "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", + "roleId": { + "name": "roleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissionId": { + "name": "permissionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_role_permission_org_id": { + "name": "idx_role_permission_org_id", "columns": [ { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", + "expression": "organizationId", "isExpression": false, "asc": true, "nulls": "last" @@ -598,110 +724,54 @@ "method": "btree", "with": {} }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", + "idx_role_permission_unique": { + "name": "idx_role_permission_unique", "columns": [ { - "expression": "dest_entity_id", + "expression": "roleId", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "dest_app_id", + "expression": "permissionId", "isExpression": false, "asc": true, "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_source_app_idx": { - "name": "gem_source_app_idx", - "columns": [ + }, { - "expression": "source_app_id", - "isExpression": false, + "expression": "COALESCE(\"organizationId\", '__NULL__')", "asc": true, + "isExpression": true, "nulls": "last" } ], - "isUnique": false, + "isUnique": true, "concurrently": false, "method": "btree", "with": {} + } + }, + "foreignKeys": { + "role_permission_roleId_role_id_fk": { + "name": "role_permission_roleId_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" }, - "gem_dest_app_idx": { - "name": "gem_dest_app_idx", - "columns": [ - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "integration_stitch", + "role_permission_permissionId_permission_id_fk": { + "name": "role_permission_permissionId_permission_id_fk", + "tableFrom": "role_permission", + "tableTo": "permission", "columnsFrom": [ - "stitch_id" + "permissionId" ], "columnsTo": [ "id" @@ -709,30 +779,17 @@ "onDelete": "cascade", "onUpdate": "no action" }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "source_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", + "role_permission_organizationId_organization_id_fk": { + "name": "role_permission_organizationId_organization_id_fk", + "tableFrom": "role_permission", + "tableTo": "organization", "columnsFrom": [ - "dest_app_id" + "organizationId" ], "columnsTo": [ "id" ], - "onDelete": "restrict", + "onDelete": "cascade", "onUpdate": "no action" } }, @@ -742,8 +799,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.account": { - "name": "account", + "public.session": { + "name": "session", "schema": "", "columns": { "id": { @@ -752,84 +809,60 @@ "primaryKey": true, "notNull": true }, - "accountId": { - "name": "accountId", - "type": "text", + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true }, - "providerId": { - "name": "providerId", + "token": { + "name": "token", "type": "text", "primaryKey": false, "notNull": true }, - "userId": { - "name": "userId", - "type": "text", + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true }, - "accessToken": { - "name": "accessToken", - "type": "text", + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true }, - "refreshToken": { - "name": "refreshToken", + "ipAddress": { + "name": "ipAddress", "type": "text", "primaryKey": false, "notNull": false }, - "idToken": { - "name": "idToken", + "userAgent": { + "name": "userAgent", "type": "text", "primaryKey": false, "notNull": false }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", + "userId": { + "name": "userId", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true }, - "password": { - "name": "password", + "impersonatedBy": { + "name": "impersonatedBy", "type": "text", "primaryKey": false, "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true } }, "indexes": {}, "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", "tableTo": "user", "columnsFrom": [ "userId" @@ -839,24 +872,28 @@ ], "onDelete": "cascade", "onUpdate": "no action" + }, + "session_impersonatedBy_user_id_fk": { + "name": "session_impersonatedBy_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "impersonatedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "nullsNotDistinct": false, - "columns": [ - "userId", - "providerId" - ] - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", + "session_token_unique": { + "name": "session_token_unique", "nullsNotDistinct": false, "columns": [ - "providerId", - "accountId" + "token" ] } }, @@ -864,8 +901,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.invitation": { - "name": "invitation", + "public.user": { + "name": "user", "schema": "", "columns": { "id": { @@ -874,11 +911,11 @@ "primaryKey": true, "notNull": true }, - "organizationId": { - "name": "organizationId", + "name": { + "name": "name", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, "email": { "name": "email", @@ -886,88 +923,80 @@ "primaryKey": false, "notNull": true }, - "role": { - "name": "role", + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", "type": "text", "primaryKey": false, "notNull": false }, - "status": { - "name": "status", - "type": "text", + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true }, - "expiresAt": { - "name": "expiresAt", + "updatedAt": { + "name": "updatedAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": true }, - "inviterId": { - "name": "inviterId", + "role": { + "name": "role", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false, + "default": "'member'" }, - "createdAt": { - "name": "createdAt", + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "banReason": { + "name": "banReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banExpires": { + "name": "banExpires", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false } }, "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": [ - "inviterId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.member": { - "name": "member", + "public.verification": { + "name": "verification", "schema": "", "columns": { "id": { @@ -976,21 +1005,21 @@ "primaryKey": true, "notNull": true }, - "organizationId": { - "name": "organizationId", + "identifier": { + "name": "identifier", "type": "text", "primaryKey": false, "notNull": true }, - "userId": { - "name": "userId", + "value": { + "name": "value", "type": "text", "primaryKey": false, "notNull": true }, - "role": { - "name": "role", - "type": "text", + "expiresAt": { + "name": "expiresAt", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true }, @@ -998,349 +1027,333 @@ "name": "createdAt", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": false }, - "deletedAt": { - "name": "deletedAt", + "updatedAt": { + "name": "updatedAt", "type": "timestamp with time zone", "primaryKey": false, "notNull": false } }, - "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"member\".\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_org_idx": { - "name": "member_org_idx", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, + "indexes": {}, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.organization": { - "name": "organization", + "public.pieces": { + "name": "pieces", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, "name": { "name": "name", - "type": "text", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "slug": { - "name": "slug", - "type": "text", + "display_name": { + "name": "display_name", + "type": "varchar(255)", "primaryKey": false, - "notNull": false + "notNull": true }, - "logo": { - "name": "logo", - "type": "text", + "logo_url": { + "name": "logo_url", + "type": "varchar(1024)", "primaryKey": false, "notNull": false }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", + "package_name": { + "name": "package_name", + "type": "varchar(255)", "primaryKey": false, - "notNull": true, - "default": "now()" + "notNull": true }, - "metadata": { - "name": "metadata", - "type": "text", + "version": { + "name": "version", + "type": "varchar(50)", "primaryKey": false, - "notNull": false + "notNull": true }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", + "enabled": { + "name": "enabled", + "type": "boolean", "primaryKey": false, "notNull": true, - "default": "'active'" + "default": true }, - "isSystem": { - "name": "isSystem", - "type": "boolean", + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true, - "default": false + "default": "now()" }, - "deletedAt": { - "name": "deletedAt", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} + "notNull": true, + "default": "now()" } }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" + "uniqueConstraints": { + "pieces_name_unique": { + "name": "pieces_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] } }, + "policies": {}, + "checkConstraints": {}, "isRLSEnabled": false }, - "public.permission": { - "name": "permission", + "public.tenant_storage_registry": { + "name": "tenant_storage_registry", "schema": "", "columns": { - "id": { - "name": "id", + "tenant_id": { + "name": "tenant_id", "type": "text", "primaryKey": true, "notNull": true }, - "resource": { - "name": "resource", - "type": "text", + "database_name": { + "name": "database_name", + "type": "varchar(128)", "primaryKey": false, "notNull": true }, - "action": { - "name": "action", - "type": "text", + "database_host_url": { + "name": "database_host_url", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "description": { - "name": "description", - "type": "text", + "region_context": { + "name": "region_context", + "type": "varchar(50)", "primaryKey": false, - "notNull": false + "notNull": true }, - "createdAt": { - "name": "createdAt", + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" } }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "nullsNotDistinct": false, + "indexes": { + "registry_dbname_unique_idx": { + "name": "registry_dbname_unique_idx", "columns": [ - "resource", - "action" - ] + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registry_region_idx": { + "name": "registry_region_idx", + "columns": [ + { + "expression": "region_context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.role": { - "name": "role", + "public.ai_conversations": { + "name": "ai_conversations", "schema": "", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" }, - "name": { - "name": "name", + "tenant_id": { + "name": "tenant_id", "type": "text", "primaryKey": false, "notNull": true }, - "description": { - "name": "description", - "type": "text", + "title": { + "name": "title", + "type": "varchar(255)", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'New Conversation'" }, - "isSystem": { - "name": "isSystem", - "type": "boolean", + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", "primaryKey": false, "notNull": true, - "default": false + "default": "now()" }, - "createdAt": { - "name": "createdAt", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" } }, - "indexes": {}, + "indexes": { + "ai_conv_tenant_idx": { + "name": "ai_conv_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_conv_created_idx": { + "name": "ai_conv_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "nullsNotDistinct": false, + "compositePrimaryKeys": { + "ai_conversations_tenant_id_id_pk": { + "name": "ai_conversations_tenant_id_id_pk", "columns": [ - "name" + "tenant_id", + "id" ] } }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.role_permission": { - "name": "role_permission", + "public.ai_messages": { + "name": "ai_messages", "schema": "", "columns": { "id": { "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", "type": "text", - "primaryKey": true, + "primaryKey": false, "notNull": true }, - "roleId": { - "name": "roleId", - "type": "text", + "conversation_id": { + "name": "conversation_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "permissionId": { - "name": "permissionId", - "type": "text", + "role": { + "name": "role", + "type": "varchar(50)", "primaryKey": false, "notNull": true }, - "organizationId": { - "name": "organizationId", + "content": { + "name": "content", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true }, - "conditions": { - "name": "conditions", - "type": "jsonb", + "status": { + "name": "status", + "type": "varchar(50)", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'completed'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" } }, "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", + "ai_msg_conv_idx": { + "name": "ai_msg_conv_idx", "columns": [ { - "expression": "organizationId", + "expression": "conversation_id", "isExpression": false, "asc": true, "nulls": "last" @@ -1351,161 +1364,136 @@ "method": "btree", "with": {} }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", + "ai_msg_tenant_idx": { + "name": "ai_msg_tenant_idx", "columns": [ { - "expression": "roleId", + "expression": "tenant_id", "isExpression": false, "asc": true, "nulls": "last" - }, + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_msg_timeline_idx": { + "name": "ai_msg_timeline_idx", + "columns": [ { - "expression": "permissionId", + "expression": "conversation_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "COALESCE(\"organizationId\", '__NULL__')", + "expression": "created_at", + "isExpression": false, "asc": true, - "isExpression": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} } }, "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "tableTo": "role", + "ai_messages_conversation_fk": { + "name": "ai_messages_conversation_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "tableTo": "permission", - "columnsFrom": [ - "permissionId" + "tenant_id", + "conversation_id" ], "columnsTo": [ + "tenant_id", "id" ], "onDelete": "cascade", "onUpdate": "no action" - }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ + } + }, + "compositePrimaryKeys": { + "ai_messages_tenant_id_id_pk": { + "name": "ai_messages_tenant_id_id_pk", + "columns": [ + "tenant_id", "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" + ] } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.session": { - "name": "session", + "public.connector_object_profiles": { + "name": "connector_object_profiles", "schema": "", "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", + "connection_id": { + "name": "connection_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "token": { - "name": "token", - "type": "text", + "object_name": { + "name": "object_name", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "profile": { + "name": "profile", + "type": "jsonb", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "'{}'" }, - "updatedAt": { - "name": "updatedAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" } }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" + "indexes": { + "cop_connection_idx": { + "name": "cop_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "tableTo": "user", + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connector_object_profiles_connection_id_app_connection_id_fk": { + "name": "connector_object_profiles_connection_id_app_connection_id_fk", + "tableFrom": "connector_object_profiles", + "tableTo": "app_connection", "columnsFrom": [ - "impersonatedBy" + "connection_id" ], "columnsTo": [ "id" @@ -1514,238 +1502,349 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, + "compositePrimaryKeys": { + "connector_object_profiles_connection_id_object_name_pk": { + "name": "connector_object_profiles_connection_id_object_name_pk", "columns": [ - "token" + "connection_id", + "object_name" ] } }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.user": { - "name": "user", + "public.global_entity_map": { + "name": "global_entity_map", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "stitch_id": { + "name": "stitch_id", + "type": "uuid", + "primaryKey": false, "notNull": true }, - "name": { - "name": "name", - "type": "text", + "source_app_name": { + "name": "source_app_name", + "type": "varchar(100)", "primaryKey": false, - "notNull": false + "notNull": true }, - "email": { - "name": "email", - "type": "text", + "source_app_id": { + "name": "source_app_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", + "source_org_id": { + "name": "source_org_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "image": { - "name": "image", - "type": "text", + "source_org_name": { + "name": "source_org_name", + "type": "varchar(255)", "primaryKey": false, "notNull": false }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "source_entity_type": { + "name": "source_entity_type", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", + "source_entity_id": { + "name": "source_entity_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "role": { - "name": "role", - "type": "text", + "source_ref_layer": { + "name": "source_ref_layer", + "type": "varchar(10)", "primaryKey": false, - "notNull": false, - "default": "'member'" + "notNull": true }, - "banned": { - "name": "banned", - "type": "boolean", + "source_trace_id": { + "name": "source_trace_id", + "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, - "banReason": { - "name": "banReason", - "type": "text", + "dest_app_name": { + "name": "dest_app_name", + "type": "varchar(100)", "primaryKey": false, - "notNull": false + "notNull": true }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", + "dest_app_id": { + "name": "dest_app_id", + "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", + "dest_org_id": { + "name": "dest_org_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "dest_org_name": { + "name": "dest_org_name", + "type": "varchar(255)", "primaryKey": false, "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, + }, + "dest_entity_type": { + "name": "dest_entity_type", + "type": "varchar(100)", + "primaryKey": false, "notNull": true }, - "identifier": { - "name": "identifier", - "type": "text", + "dest_entity_id": { + "name": "dest_entity_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "value": { - "name": "value", - "type": "text", + "dest_ref_layer": { + "name": "dest_ref_layer", + "type": "varchar(10)", "primaryKey": false, "notNull": true }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", + "dest_trace_id": { + "name": "dest_trace_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "createdAt": { - "name": "createdAt", + "last_synced_at": { + "name": "last_synced_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" }, - "updatedAt": { - "name": "updatedAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" } }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pieces": { - "name": "pieces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" + "indexes": { + "gem_unique_mapping_idx": { + "name": "gem_unique_mapping_idx", + "columns": [ + { + "expression": "stitch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dest_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true + "gem_src_lookup_idx": { + "name": "gem_src_lookup_idx", + "columns": [ + { + "expression": "source_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true + "gem_dest_lookup_idx": { + "name": "gem_dest_lookup_idx", + "columns": [ + { + "expression": "dest_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", - "primaryKey": false, - "notNull": false + "gem_source_app_idx": { + "name": "gem_source_app_idx", + "columns": [ + { + "expression": "source_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_dest_app_idx": { + "name": "gem_dest_app_idx", + "columns": [ + { + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true + "gem_src_trace_idx": { + "name": "gem_src_trace_idx", + "columns": [ + { + "expression": "source_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true + "gem_dest_trace_idx": { + "name": "gem_dest_trace_idx", + "columns": [ + { + "expression": "dest_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true + "gem_stitch_idx": { + "name": "gem_stitch_idx", + "columns": [ + { + "expression": "stitch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "global_entity_map_stitch_id_integration_stitch_id_fk": { + "name": "global_entity_map_stitch_id_integration_stitch_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "integration_stitch", + "columnsFrom": [ + "stitch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" + "global_entity_map_source_app_id_app_connection_id_fk": { + "name": "global_entity_map_source_app_id_app_connection_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "app_connection", + "columnsFrom": [ + "source_app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" + "global_entity_map_dest_app_id_app_connection_id_fk": { + "name": "global_entity_map_dest_app_id_app_connection_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "app_connection", + "columnsFrom": [ + "dest_app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" } }, - "indexes": {}, - "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false @@ -2324,125 +2423,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", - "primaryKey": false, - "notNull": true - }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" - }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'primary-cluster'" - }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_host_idx": { - "name": "registry_host_idx", - "columns": [ - { - "expression": "database_host_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_region_idx": { - "name": "registry_region_idx", - "columns": [ - { - "expression": "region_context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.app_connection": { "name": "app_connection", "schema": "", @@ -2520,6 +2500,13 @@ "notNull": false, "default": "'{}'::jsonb" }, + "schema_plan": { + "name": "schema_plan", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'NAMESPACE_ONLY'" + }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -2637,21 +2624,7 @@ "with": {} } }, - "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "tableTo": "organization", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, @@ -2852,21 +2825,7 @@ "with": {} } }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "tableTo": "organization", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": { "ui_workspace_id_org_unique_idx": { diff --git a/packages/database/drizzle/global/meta/_journal.json b/packages/database/drizzle/global/meta/_journal.json new file mode 100644 index 00000000..d956e8db --- /dev/null +++ b/packages/database/drizzle/global/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1777718670146, + "tag": "0000_mixed_sleepwalker", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1777800241851, + "tag": "0001_shocking_microbe", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1777879392091, + "tag": "0003_peaceful_beast", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0001_snapshot.json b/packages/database/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 40285bcb..00000000 --- a/packages/database/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,2671 +0,0 @@ -{ - "id": "dafc6985-5598-4cf8-887b-64825b6f9987", - "prevId": "e18b4e93-c53c-4b94-9117-da765192c10e", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.connector_object_profiles": { - "name": "connector_object_profiles", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "profile": { - "name": "profile", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.global_entity_map": { - "name": "global_entity_map", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_entity_id": { - "name": "dest_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "last_synced_at": { - "name": "last_synced_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", - "columns": [ - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", - "columns": [ - { - "expression": "dest_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_source_app_idx": { - "name": "gem_source_app_idx", - "columns": [ - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_app_idx": { - "name": "gem_dest_app_idx", - "columns": [ - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "source_app_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "dest_app_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "accountId": { - "name": "accountId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "accessToken": { - "name": "accessToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refreshToken": { - "name": "refreshToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "idToken": { - "name": "idToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "columns": [ - "userId", - "providerId" - ], - "nullsNotDistinct": false - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", - "columns": [ - "providerId", - "accountId" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "inviterId": { - "name": "inviterId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "role" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "inviterId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "where": "\"member\".\"deletedAt\" IS NULL", - "concurrently": false - }, - "member_org_idx": { - "name": "member_org_idx", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "role" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organization": { - "name": "organization", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "where": "\"deletedAt\" IS NULL", - "concurrently": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" - } - }, - "isRLSEnabled": false - }, - "public.permission": { - "name": "permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "resource": { - "name": "resource", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "columns": [ - "resource", - "action" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role": { - "name": "role", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "columns": [ - "name" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permission": { - "name": "role_permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permissionId": { - "name": "permissionId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "conditions": { - "name": "conditions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permissionId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "COALESCE(\"organizationId\", '__NULL__')", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "roleId" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "permissionId" - ], - "tableTo": "permission", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "columnsFrom": [ - "impersonatedBy" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "columns": [ - "token" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'member'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "banReason": { - "name": "banReason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pieces": { - "name": "pieces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", - "primaryKey": false, - "notNull": false - }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "columns": [ - "name" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.field_mapping": { - "name": "field_mapping", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_canonical": { - "name": "source_canonical", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "mapping_rules": { - "name": "mapping_rules", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "field_mapping_stitch_canonical_unique_idx": { - "name": "field_mapping_stitch_canonical_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_canonical", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "field_mapping_stitch_id_integration_stitch_id_fk": { - "name": "field_mapping_stitch_id_integration_stitch_id_fk", - "tableFrom": "field_mapping", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.integration_stitch": { - "name": "integration_stitch", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "src_connection_id": { - "name": "src_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_connection_id": { - "name": "dest_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_object": { - "name": "source_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "target_object": { - "name": "target_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "sync_condition": { - "name": "sync_condition", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "status": { - "name": "status", - "type": "stitch_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 30 - }, - "schedule_enabled": { - "name": "schedule_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_scheduled_at": { - "name": "last_scheduled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "stitch_name_workspace_unique_idx": { - "name": "stitch_name_workspace_unique_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_workspace_idx": { - "name": "stitch_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_org_idx": { - "name": "stitch_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_src_conn_idx": { - "name": "stitch_src_conn_idx", - "columns": [ - { - "expression": "src_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_dest_conn_idx": { - "name": "stitch_dest_conn_idx", - "columns": [ - { - "expression": "dest_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_status_idx": { - "name": "stitch_status_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "stitch_workspace_org_fk": { - "name": "stitch_workspace_org_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "workspace_id", - "org_id" - ], - "tableTo": "ui_workspace", - "columnsTo": [ - "id", - "org_id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "stitch_src_connection_fk": { - "name": "stitch_src_connection_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "src_connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "stitch_dest_connection_fk": { - "name": "stitch_dest_connection_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "dest_connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "sync_interval_minutes_positive": { - "name": "sync_interval_minutes_positive", - "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" - } - }, - "isRLSEnabled": false - }, - "public.scheduler_outbox": { - "name": "scheduler_outbox", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "scheduler_outbox_action_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "scheduler_outbox_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "next_retry_at": { - "name": "next_retry_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "scheduler_outbox_poll_idx": { - "name": "scheduler_outbox_poll_idx", - "columns": [ - { - "expression": "next_retry_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "where": "status = 'pending'", - "concurrently": false - }, - "scheduler_outbox_stitch_idx": { - "name": "scheduler_outbox_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "scheduler_outbox_stitch_fk": { - "name": "scheduler_outbox_stitch_fk", - "tableFrom": "scheduler_outbox", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sync_cursors": { - "name": "sync_cursors", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "stream_name": { - "name": "stream_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "state_document": { - "name": "state_document", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sync_cursors_stitch_stream_unique_idx": { - "name": "sync_cursors_stitch_stream_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stream_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "sync_cursors_stitch_idx": { - "name": "sync_cursors_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "sync_cursors_stitch_fk": { - "name": "sync_cursors_stitch_fk", - "tableFrom": "sync_cursors", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", - "primaryKey": false, - "notNull": true - }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" - }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'primary-cluster'" - }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "registry_host_idx": { - "name": "registry_host_idx", - "columns": [ - { - "expression": "database_host_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "registry_region_idx": { - "name": "registry_region_idx", - "columns": [ - { - "expression": "region_context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_connection": { - "name": "app_connection", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "external_id": { - "name": "external_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "auth_type": { - "name": "auth_type", - "type": "auth_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "connection_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "app_name_idx": { - "name": "app_name_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "tenant_status_idx": { - "name": "tenant_status_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "connection_expires_at_idx": { - "name": "connection_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "where": "\"app_connection\".\"expires_at\" IS NOT NULL", - "concurrently": false - }, - "tenant_external_id_unique_idx": { - "name": "tenant_external_id_unique_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "external_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "tenant_app_display_name_lower_idx": { - "name": "tenant_app_display_name_lower_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"display_name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "columnsFrom": [ - "tenant_id" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace_connection": { - "name": "ui_workspace_connection", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_connection_conn_idx": { - "name": "workspace_connection_conn_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { - "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", - "tableFrom": "ui_workspace_connection", - "columnsFrom": [ - "workspace_id" - ], - "tableTo": "ui_workspace", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "ui_workspace_connection_connection_id_app_connection_id_fk": { - "name": "ui_workspace_connection_connection_id_app_connection_id_fk", - "tableFrom": "ui_workspace_connection", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "ui_workspace_connection_workspace_id_connection_id_pk": { - "name": "ui_workspace_connection_workspace_id_connection_id_pk", - "columns": [ - "workspace_id", - "connection_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace": { - "name": "ui_workspace", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ui_workspace_org_name_lower_unique_idx": { - "name": "ui_workspace_org_name_lower_unique_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "ui_workspace_org_idx": { - "name": "ui_workspace_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "ui_workspace_env_idx": { - "name": "ui_workspace_env_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "columnsFrom": [ - "org_id" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ui_workspace_id_org_unique_idx": { - "name": "ui_workspace_id_org_unique_idx", - "columns": [ - "id", - "org_id" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.organization_status": { - "name": "organization_status", - "schema": "public", - "values": [ - "active", - "disabled", - "suspended" - ] - }, - "public.pipeline_layer_enum": { - "name": "pipeline_layer_enum", - "schema": "public", - "values": [ - "L1", - "L2", - "L3", - "L4", - "L5", - "L6" - ] - }, - "public.pipeline_status_enum": { - "name": "pipeline_status_enum", - "schema": "public", - "values": [ - "RECEIVED", - "PROCESSING", - "REPLICATED", - "NORMALIZED", - "SKIPPED", - "PENDING", - "SUCCESS", - "FAIL", - "RETRY", - "DISMISSED" - ] - }, - "public.scheduler_outbox_action_enum": { - "name": "scheduler_outbox_action_enum", - "schema": "public", - "values": [ - "created", - "updated", - "deleted" - ] - }, - "public.scheduler_outbox_status_enum": { - "name": "scheduler_outbox_status_enum", - "schema": "public", - "values": [ - "pending", - "processing", - "succeeded", - "failed" - ] - }, - "public.stitch_status_enum": { - "name": "stitch_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "PAUSED", - "ARCHIVED" - ] - }, - "public.auth_type_enum": { - "name": "auth_type_enum", - "schema": "public", - "values": [ - "OAUTH2", - "API_KEY", - "BASIC" - ] - }, - "public.connection_status_enum": { - "name": "connection_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "INACTIVE", - "REVOKED", - "EXPIRED", - "PROVISIONING", - "FAILED" - ] - }, - "public.env_type_enum": { - "name": "env_type_enum", - "schema": "public", - "values": [ - "PRODUCTION", - "SANDBOX" - ] - } - }, - "schemas": {}, - "views": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0002_snapshot.json b/packages/database/drizzle/meta/0002_snapshot.json deleted file mode 100644 index aad151d7..00000000 --- a/packages/database/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,2671 +0,0 @@ -{ - "id": "38b72cd1-429b-47d0-a5a8-ae2885d8eb91", - "prevId": "dafc6985-5598-4cf8-887b-64825b6f9987", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.connector_object_profiles": { - "name": "connector_object_profiles", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "profile": { - "name": "profile", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.global_entity_map": { - "name": "global_entity_map", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_entity_id": { - "name": "dest_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "last_synced_at": { - "name": "last_synced_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", - "columns": [ - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", - "columns": [ - { - "expression": "dest_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_source_app_idx": { - "name": "gem_source_app_idx", - "columns": [ - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_app_idx": { - "name": "gem_dest_app_idx", - "columns": [ - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "source_app_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "columnsFrom": [ - "dest_app_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "accountId": { - "name": "accountId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "accessToken": { - "name": "accessToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refreshToken": { - "name": "refreshToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "idToken": { - "name": "idToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "columns": [ - "userId", - "providerId" - ], - "nullsNotDistinct": false - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", - "columns": [ - "providerId", - "accountId" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "inviterId": { - "name": "inviterId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "role" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "columnsFrom": [ - "inviterId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "where": "\"member\".\"deletedAt\" IS NULL", - "concurrently": false - }, - "member_org_idx": { - "name": "member_org_idx", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "columnsFrom": [ - "role" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "restrict" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organization": { - "name": "organization", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "where": "\"deletedAt\" IS NULL", - "concurrently": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" - } - }, - "isRLSEnabled": false - }, - "public.permission": { - "name": "permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "resource": { - "name": "resource", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "columns": [ - "resource", - "action" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role": { - "name": "role", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "columns": [ - "name" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permission": { - "name": "role_permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permissionId": { - "name": "permissionId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "conditions": { - "name": "conditions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permissionId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "COALESCE(\"organizationId\", '__NULL__')", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "roleId" - ], - "tableTo": "role", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "permissionId" - ], - "tableTo": "permission", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "columnsFrom": [ - "organizationId" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "columnsFrom": [ - "userId" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "columnsFrom": [ - "impersonatedBy" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "columns": [ - "token" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'member'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "banReason": { - "name": "banReason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pieces": { - "name": "pieces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", - "primaryKey": false, - "notNull": false - }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "columns": [ - "name" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.field_mapping": { - "name": "field_mapping", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_canonical": { - "name": "source_canonical", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "mapping_rules": { - "name": "mapping_rules", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "field_mapping_stitch_canonical_unique_idx": { - "name": "field_mapping_stitch_canonical_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_canonical", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "field_mapping_stitch_id_integration_stitch_id_fk": { - "name": "field_mapping_stitch_id_integration_stitch_id_fk", - "tableFrom": "field_mapping", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.integration_stitch": { - "name": "integration_stitch", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "src_connection_id": { - "name": "src_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_connection_id": { - "name": "dest_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_object": { - "name": "source_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "target_object": { - "name": "target_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "sync_condition": { - "name": "sync_condition", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "status": { - "name": "status", - "type": "stitch_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 30 - }, - "schedule_enabled": { - "name": "schedule_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_scheduled_at": { - "name": "last_scheduled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "stitch_name_workspace_unique_idx": { - "name": "stitch_name_workspace_unique_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_workspace_idx": { - "name": "stitch_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_org_idx": { - "name": "stitch_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_src_conn_idx": { - "name": "stitch_src_conn_idx", - "columns": [ - { - "expression": "src_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_dest_conn_idx": { - "name": "stitch_dest_conn_idx", - "columns": [ - { - "expression": "dest_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "stitch_status_idx": { - "name": "stitch_status_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "stitch_workspace_org_fk": { - "name": "stitch_workspace_org_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "workspace_id", - "org_id" - ], - "tableTo": "ui_workspace", - "columnsTo": [ - "id", - "org_id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "stitch_src_connection_fk": { - "name": "stitch_src_connection_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "src_connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "stitch_dest_connection_fk": { - "name": "stitch_dest_connection_fk", - "tableFrom": "integration_stitch", - "columnsFrom": [ - "dest_connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "sync_interval_minutes_positive": { - "name": "sync_interval_minutes_positive", - "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" - } - }, - "isRLSEnabled": false - }, - "public.scheduler_outbox": { - "name": "scheduler_outbox", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "scheduler_outbox_action_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "scheduler_outbox_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "next_retry_at": { - "name": "next_retry_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "scheduler_outbox_poll_idx": { - "name": "scheduler_outbox_poll_idx", - "columns": [ - { - "expression": "next_retry_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "where": "status = 'pending'", - "concurrently": false - }, - "scheduler_outbox_stitch_idx": { - "name": "scheduler_outbox_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "scheduler_outbox_stitch_fk": { - "name": "scheduler_outbox_stitch_fk", - "tableFrom": "scheduler_outbox", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sync_cursors": { - "name": "sync_cursors", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "stream_name": { - "name": "stream_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "state_document": { - "name": "state_document", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sync_cursors_stitch_stream_unique_idx": { - "name": "sync_cursors_stitch_stream_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stream_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "sync_cursors_stitch_idx": { - "name": "sync_cursors_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "sync_cursors_stitch_fk": { - "name": "sync_cursors_stitch_fk", - "tableFrom": "sync_cursors", - "columnsFrom": [ - "stitch_id" - ], - "tableTo": "integration_stitch", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", - "primaryKey": false, - "notNull": true - }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" - }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'primary-cluster'" - }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "registry_host_idx": { - "name": "registry_host_idx", - "columns": [ - { - "expression": "database_host_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "registry_region_idx": { - "name": "registry_region_idx", - "columns": [ - { - "expression": "region_context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_connection": { - "name": "app_connection", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "external_id": { - "name": "external_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "auth_type": { - "name": "auth_type", - "type": "auth_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "connection_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "app_name_idx": { - "name": "app_name_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "tenant_status_idx": { - "name": "tenant_status_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "connection_expires_at_idx": { - "name": "connection_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "where": "\"app_connection\".\"expires_at\" IS NOT NULL", - "concurrently": false - }, - "tenant_external_id_unique_idx": { - "name": "tenant_external_id_unique_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "external_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "tenant_app_display_name_lower_idx": { - "name": "tenant_app_display_name_lower_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"display_name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "columnsFrom": [ - "tenant_id" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace_connection": { - "name": "ui_workspace_connection", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_connection_conn_idx": { - "name": "workspace_connection_conn_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { - "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", - "tableFrom": "ui_workspace_connection", - "columnsFrom": [ - "workspace_id" - ], - "tableTo": "ui_workspace", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "ui_workspace_connection_connection_id_app_connection_id_fk": { - "name": "ui_workspace_connection_connection_id_app_connection_id_fk", - "tableFrom": "ui_workspace_connection", - "columnsFrom": [ - "connection_id" - ], - "tableTo": "app_connection", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": { - "ui_workspace_connection_workspace_id_connection_id_pk": { - "name": "ui_workspace_connection_workspace_id_connection_id_pk", - "columns": [ - "workspace_id", - "connection_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace": { - "name": "ui_workspace", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ui_workspace_org_name_lower_unique_idx": { - "name": "ui_workspace_org_name_lower_unique_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "isExpression": true, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "with": {}, - "method": "btree", - "concurrently": false - }, - "ui_workspace_org_idx": { - "name": "ui_workspace_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - }, - "ui_workspace_env_idx": { - "name": "ui_workspace_env_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "columnsFrom": [ - "org_id" - ], - "tableTo": "organization", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ui_workspace_id_org_unique_idx": { - "name": "ui_workspace_id_org_unique_idx", - "columns": [ - "id", - "org_id" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.organization_status": { - "name": "organization_status", - "schema": "public", - "values": [ - "active", - "disabled", - "suspended" - ] - }, - "public.pipeline_layer_enum": { - "name": "pipeline_layer_enum", - "schema": "public", - "values": [ - "L1", - "L2", - "L3", - "L4", - "L5", - "L6" - ] - }, - "public.pipeline_status_enum": { - "name": "pipeline_status_enum", - "schema": "public", - "values": [ - "RECEIVED", - "PROCESSING", - "REPLICATED", - "NORMALIZED", - "SKIPPED", - "PENDING", - "SUCCESS", - "FAIL", - "RETRY", - "DISMISSED" - ] - }, - "public.scheduler_outbox_action_enum": { - "name": "scheduler_outbox_action_enum", - "schema": "public", - "values": [ - "created", - "updated", - "deleted" - ] - }, - "public.scheduler_outbox_status_enum": { - "name": "scheduler_outbox_status_enum", - "schema": "public", - "values": [ - "pending", - "processing", - "succeeded", - "failed" - ] - }, - "public.stitch_status_enum": { - "name": "stitch_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "PAUSED", - "ARCHIVED" - ] - }, - "public.auth_type_enum": { - "name": "auth_type_enum", - "schema": "public", - "values": [ - "OAUTH2", - "API_KEY", - "BASIC" - ] - }, - "public.connection_status_enum": { - "name": "connection_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "INACTIVE", - "REVOKED", - "EXPIRED", - "PROVISIONING", - "FAILED" - ] - }, - "public.env_type_enum": { - "name": "env_type_enum", - "schema": "public", - "values": [ - "PRODUCTION", - "SANDBOX" - ] - } - }, - "schemas": {}, - "views": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0005_snapshot.json b/packages/database/drizzle/meta/0005_snapshot.json deleted file mode 100644 index 2b1111ef..00000000 --- a/packages/database/drizzle/meta/0005_snapshot.json +++ /dev/null @@ -1,2788 +0,0 @@ -{ - "id": "7ee90d03-640f-4b72-8a4c-8df0254a51ca", - "prevId": "38b72cd1-429b-47d0-a5a8-ae2885d8eb91", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.canonical_mappings": { - "name": "canonical_mappings", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "category": { - "name": "category", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "entity": { - "name": "entity", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "view_mode": { - "name": "view_mode", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "tenant_id": { - "name": "tenant_id", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "default": "'v1'" - }, - "mapping_config": { - "name": "mapping_config", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "canonical_mapping_unique_idx": { - "name": "canonical_mapping_unique_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "category", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "view_mode", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connector_object_profiles": { - "name": "connector_object_profiles", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "profile": { - "name": "profile", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.global_entity_map": { - "name": "global_entity_map", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_entity_id": { - "name": "dest_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "last_synced_at": { - "name": "last_synced_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", - "columns": [ - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", - "columns": [ - { - "expression": "dest_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_source_app_idx": { - "name": "gem_source_app_idx", - "columns": [ - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_app_idx": { - "name": "gem_dest_app_idx", - "columns": [ - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "source_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "accountId": { - "name": "accountId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "accessToken": { - "name": "accessToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refreshToken": { - "name": "refreshToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "idToken": { - "name": "idToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "nullsNotDistinct": false, - "columns": [ - "userId", - "providerId" - ] - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", - "nullsNotDistinct": false, - "columns": [ - "providerId", - "accountId" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "inviterId": { - "name": "inviterId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": [ - "inviterId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"member\".\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_org_idx": { - "name": "member_org_idx", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organization": { - "name": "organization", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" - } - }, - "isRLSEnabled": false - }, - "public.permission": { - "name": "permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "resource": { - "name": "resource", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "nullsNotDistinct": false, - "columns": [ - "resource", - "action" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role": { - "name": "role", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permission": { - "name": "role_permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permissionId": { - "name": "permissionId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "conditions": { - "name": "conditions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permissionId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "COALESCE(\"organizationId\", '__NULL__')", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "tableTo": "role", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "tableTo": "permission", - "columnsFrom": [ - "permissionId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "impersonatedBy" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'member'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "banReason": { - "name": "banReason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pieces": { - "name": "pieces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", - "primaryKey": false, - "notNull": false - }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.field_mapping": { - "name": "field_mapping", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_canonical": { - "name": "source_canonical", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "mapping_rules": { - "name": "mapping_rules", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "field_mapping_stitch_canonical_unique_idx": { - "name": "field_mapping_stitch_canonical_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_canonical", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "field_mapping_stitch_id_integration_stitch_id_fk": { - "name": "field_mapping_stitch_id_integration_stitch_id_fk", - "tableFrom": "field_mapping", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.integration_stitch": { - "name": "integration_stitch", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "src_connection_id": { - "name": "src_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_connection_id": { - "name": "dest_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_object": { - "name": "source_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "target_object": { - "name": "target_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "sync_condition": { - "name": "sync_condition", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "status": { - "name": "status", - "type": "stitch_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 30 - }, - "schedule_enabled": { - "name": "schedule_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_scheduled_at": { - "name": "last_scheduled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "stitch_name_workspace_unique_idx": { - "name": "stitch_name_workspace_unique_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_workspace_idx": { - "name": "stitch_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_org_idx": { - "name": "stitch_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_src_conn_idx": { - "name": "stitch_src_conn_idx", - "columns": [ - { - "expression": "src_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_dest_conn_idx": { - "name": "stitch_dest_conn_idx", - "columns": [ - { - "expression": "dest_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_status_idx": { - "name": "stitch_status_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "stitch_workspace_org_fk": { - "name": "stitch_workspace_org_fk", - "tableFrom": "integration_stitch", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id", - "org_id" - ], - "columnsTo": [ - "id", - "org_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_src_connection_fk": { - "name": "stitch_src_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "src_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_dest_connection_fk": { - "name": "stitch_dest_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "sync_interval_minutes_positive": { - "name": "sync_interval_minutes_positive", - "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" - } - }, - "isRLSEnabled": false - }, - "public.scheduler_outbox": { - "name": "scheduler_outbox", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "scheduler_outbox_action_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "scheduler_outbox_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "next_retry_at": { - "name": "next_retry_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "scheduler_outbox_poll_idx": { - "name": "scheduler_outbox_poll_idx", - "columns": [ - { - "expression": "next_retry_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'pending'", - "concurrently": false, - "method": "btree", - "with": {} - }, - "scheduler_outbox_stitch_idx": { - "name": "scheduler_outbox_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "scheduler_outbox_stitch_fk": { - "name": "scheduler_outbox_stitch_fk", - "tableFrom": "scheduler_outbox", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sync_cursors": { - "name": "sync_cursors", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "stream_name": { - "name": "stream_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "state_document": { - "name": "state_document", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sync_cursors_stitch_stream_unique_idx": { - "name": "sync_cursors_stitch_stream_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stream_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sync_cursors_stitch_idx": { - "name": "sync_cursors_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sync_cursors_stitch_fk": { - "name": "sync_cursors_stitch_fk", - "tableFrom": "sync_cursors", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", - "primaryKey": false, - "notNull": true - }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" - }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'primary-cluster'" - }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_host_idx": { - "name": "registry_host_idx", - "columns": [ - { - "expression": "database_host_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_region_idx": { - "name": "registry_region_idx", - "columns": [ - { - "expression": "region_context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_connection": { - "name": "app_connection", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "external_id": { - "name": "external_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "auth_type": { - "name": "auth_type", - "type": "auth_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "connection_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "app_name_idx": { - "name": "app_name_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_status_idx": { - "name": "tenant_status_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "connection_expires_at_idx": { - "name": "connection_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"app_connection\".\"expires_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_external_id_unique_idx": { - "name": "tenant_external_id_unique_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "external_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_app_display_name_lower_idx": { - "name": "tenant_app_display_name_lower_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"display_name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "tableTo": "organization", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace_connection": { - "name": "ui_workspace_connection", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_connection_conn_idx": { - "name": "workspace_connection_conn_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { - "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ui_workspace_connection_connection_id_app_connection_id_fk": { - "name": "ui_workspace_connection_connection_id_app_connection_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "ui_workspace_connection_workspace_id_connection_id_pk": { - "name": "ui_workspace_connection_workspace_id_connection_id_pk", - "columns": [ - "workspace_id", - "connection_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace": { - "name": "ui_workspace", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ui_workspace_org_name_lower_unique_idx": { - "name": "ui_workspace_org_name_lower_unique_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ui_workspace_org_idx": { - "name": "ui_workspace_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ui_workspace_env_idx": { - "name": "ui_workspace_env_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "tableTo": "organization", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ui_workspace_id_org_unique_idx": { - "name": "ui_workspace_id_org_unique_idx", - "nullsNotDistinct": false, - "columns": [ - "id", - "org_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.organization_status": { - "name": "organization_status", - "schema": "public", - "values": [ - "active", - "disabled", - "suspended" - ] - }, - "public.pipeline_layer_enum": { - "name": "pipeline_layer_enum", - "schema": "public", - "values": [ - "L1", - "L2", - "L3", - "L4", - "L5", - "L6" - ] - }, - "public.pipeline_status_enum": { - "name": "pipeline_status_enum", - "schema": "public", - "values": [ - "RECEIVED", - "PROCESSING", - "REPLICATED", - "NORMALIZED", - "SKIPPED", - "PENDING", - "SUCCESS", - "FAIL", - "RETRY", - "DISMISSED" - ] - }, - "public.scheduler_outbox_action_enum": { - "name": "scheduler_outbox_action_enum", - "schema": "public", - "values": [ - "created", - "updated", - "deleted" - ] - }, - "public.scheduler_outbox_status_enum": { - "name": "scheduler_outbox_status_enum", - "schema": "public", - "values": [ - "pending", - "processing", - "succeeded", - "failed" - ] - }, - "public.stitch_status_enum": { - "name": "stitch_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "PAUSED", - "ARCHIVED" - ] - }, - "public.auth_type_enum": { - "name": "auth_type_enum", - "schema": "public", - "values": [ - "OAUTH2", - "API_KEY", - "BASIC" - ] - }, - "public.connection_status_enum": { - "name": "connection_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "INACTIVE", - "REVOKED", - "EXPIRED", - "PROVISIONING", - "FAILED" - ] - }, - "public.env_type_enum": { - "name": "env_type_enum", - "schema": "public", - "values": [ - "PRODUCTION", - "SANDBOX" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0007_snapshot.json b/packages/database/drizzle/meta/0007_snapshot.json deleted file mode 100644 index 08528802..00000000 --- a/packages/database/drizzle/meta/0007_snapshot.json +++ /dev/null @@ -1,3055 +0,0 @@ -{ - "id": "5033178c-3837-4f31-940c-f57750dc2cbd", - "prevId": "fdcdfcc7-0d42-4646-95e6-2184912d2fd4", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.canonical_mappings": { - "name": "canonical_mappings", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "category": { - "name": "category", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "entity": { - "name": "entity", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "view_mode": { - "name": "view_mode", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "tenant_id": { - "name": "tenant_id", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "default": "'v1'" - }, - "mapping_config": { - "name": "mapping_config", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "canonical_mapping_tenant_idx": { - "name": "canonical_mapping_tenant_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "category", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "view_mode", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"canonical_mappings\".\"tenant_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "canonical_mapping_global_idx": { - "name": "canonical_mapping_global_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "category", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "view_mode", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"canonical_mappings\".\"tenant_id\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ai_conversations": { - "name": "ai_conversations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": false, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'New Conversation'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ai_conv_tenant_idx": { - "name": "ai_conv_tenant_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ai_conv_created_idx": { - "name": "ai_conv_created_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": { - "ai_conversations_tenant_id_id_pk": { - "name": "ai_conversations_tenant_id_id_pk", - "columns": [ - "tenant_id", - "id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ai_messages": { - "name": "ai_messages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": false, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "conversation_id": { - "name": "conversation_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "default": "'completed'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ai_msg_conv_idx": { - "name": "ai_msg_conv_idx", - "columns": [ - { - "expression": "conversation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ai_msg_tenant_idx": { - "name": "ai_msg_tenant_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ai_msg_timeline_idx": { - "name": "ai_msg_timeline_idx", - "columns": [ - { - "expression": "conversation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_messages_conversation_fk": { - "name": "ai_messages_conversation_fk", - "tableFrom": "ai_messages", - "tableTo": "ai_conversations", - "columnsFrom": [ - "tenant_id", - "conversation_id" - ], - "columnsTo": [ - "tenant_id", - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "ai_messages_tenant_id_id_pk": { - "name": "ai_messages_tenant_id_id_pk", - "columns": [ - "tenant_id", - "id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connector_object_profiles": { - "name": "connector_object_profiles", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "profile": { - "name": "profile", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.global_entity_map": { - "name": "global_entity_map", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_entity_id": { - "name": "dest_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "last_synced_at": { - "name": "last_synced_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", - "columns": [ - { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", - "columns": [ - { - "expression": "dest_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_source_app_idx": { - "name": "gem_source_app_idx", - "columns": [ - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_app_idx": { - "name": "gem_dest_app_idx", - "columns": [ - { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "source_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "accountId": { - "name": "accountId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "providerId": { - "name": "providerId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "accessToken": { - "name": "accessToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refreshToken": { - "name": "refreshToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "idToken": { - "name": "idToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "nullsNotDistinct": false, - "columns": [ - "userId", - "providerId" - ] - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", - "nullsNotDistinct": false, - "columns": [ - "providerId", - "accountId" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "inviterId": { - "name": "inviterId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": [ - "inviterId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", - "columns": [ - { - "expression": "userId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"member\".\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_org_idx": { - "name": "member_org_idx", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organization": { - "name": "organization", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"deletedAt\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" - } - }, - "isRLSEnabled": false - }, - "public.permission": { - "name": "permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "resource": { - "name": "resource", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "nullsNotDistinct": false, - "columns": [ - "resource", - "action" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role": { - "name": "role", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permission": { - "name": "role_permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "roleId": { - "name": "roleId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permissionId": { - "name": "permissionId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "conditions": { - "name": "conditions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", - "columns": [ - { - "expression": "organizationId", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", - "columns": [ - { - "expression": "roleId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permissionId", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "COALESCE(\"organizationId\", '__NULL__')", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "tableTo": "role", - "columnsFrom": [ - "roleId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "tableTo": "permission", - "columnsFrom": [ - "permissionId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "impersonatedBy" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'member'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "banReason": { - "name": "banReason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pieces": { - "name": "pieces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", - "primaryKey": false, - "notNull": false - }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.field_mapping": { - "name": "field_mapping", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_canonical": { - "name": "source_canonical", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "mapping_rules": { - "name": "mapping_rules", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "field_mapping_stitch_canonical_unique_idx": { - "name": "field_mapping_stitch_canonical_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_canonical", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "field_mapping_stitch_id_integration_stitch_id_fk": { - "name": "field_mapping_stitch_id_integration_stitch_id_fk", - "tableFrom": "field_mapping", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.integration_stitch": { - "name": "integration_stitch", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "src_connection_id": { - "name": "src_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_connection_id": { - "name": "dest_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_object": { - "name": "source_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "target_object": { - "name": "target_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "sync_condition": { - "name": "sync_condition", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "status": { - "name": "status", - "type": "stitch_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 30 - }, - "schedule_enabled": { - "name": "schedule_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_scheduled_at": { - "name": "last_scheduled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "stitch_name_workspace_unique_idx": { - "name": "stitch_name_workspace_unique_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_workspace_idx": { - "name": "stitch_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_org_idx": { - "name": "stitch_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_src_conn_idx": { - "name": "stitch_src_conn_idx", - "columns": [ - { - "expression": "src_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_dest_conn_idx": { - "name": "stitch_dest_conn_idx", - "columns": [ - { - "expression": "dest_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_status_idx": { - "name": "stitch_status_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "stitch_workspace_org_fk": { - "name": "stitch_workspace_org_fk", - "tableFrom": "integration_stitch", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id", - "org_id" - ], - "columnsTo": [ - "id", - "org_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_src_connection_fk": { - "name": "stitch_src_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "src_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_dest_connection_fk": { - "name": "stitch_dest_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "sync_interval_minutes_positive": { - "name": "sync_interval_minutes_positive", - "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" - } - }, - "isRLSEnabled": false - }, - "public.scheduler_outbox": { - "name": "scheduler_outbox", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "scheduler_outbox_action_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "scheduler_outbox_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "next_retry_at": { - "name": "next_retry_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "scheduler_outbox_poll_idx": { - "name": "scheduler_outbox_poll_idx", - "columns": [ - { - "expression": "next_retry_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'pending'", - "concurrently": false, - "method": "btree", - "with": {} - }, - "scheduler_outbox_stitch_idx": { - "name": "scheduler_outbox_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "scheduler_outbox_stitch_fk": { - "name": "scheduler_outbox_stitch_fk", - "tableFrom": "scheduler_outbox", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sync_cursors": { - "name": "sync_cursors", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "stream_name": { - "name": "stream_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "state_document": { - "name": "state_document", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sync_cursors_stitch_stream_unique_idx": { - "name": "sync_cursors_stitch_stream_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stream_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sync_cursors_stitch_idx": { - "name": "sync_cursors_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sync_cursors_stitch_fk": { - "name": "sync_cursors_stitch_fk", - "tableFrom": "sync_cursors", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", - "schema": "", - "columns": { - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", - "primaryKey": false, - "notNull": true - }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", - "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" - }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true, - "default": "'aurora-prod'" - }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_host_idx": { - "name": "registry_host_idx", - "columns": [ - { - "expression": "database_host_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_region_idx": { - "name": "registry_region_idx", - "columns": [ - { - "expression": "region_context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_connection": { - "name": "app_connection", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "external_id": { - "name": "external_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "auth_type": { - "name": "auth_type", - "type": "auth_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "connection_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "app_name_idx": { - "name": "app_name_idx", - "columns": [ - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_status_idx": { - "name": "tenant_status_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "connection_expires_at_idx": { - "name": "connection_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"app_connection\".\"expires_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_external_id_unique_idx": { - "name": "tenant_external_id_unique_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "external_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tenant_app_display_name_lower_idx": { - "name": "tenant_app_display_name_lower_idx", - "columns": [ - { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "app_name", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"display_name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "tableTo": "organization", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace_connection": { - "name": "ui_workspace_connection", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_connection_conn_idx": { - "name": "workspace_connection_conn_idx", - "columns": [ - { - "expression": "connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { - "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ui_workspace_connection_connection_id_app_connection_id_fk": { - "name": "ui_workspace_connection_connection_id_app_connection_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "ui_workspace_connection_workspace_id_connection_id_pk": { - "name": "ui_workspace_connection_workspace_id_connection_id_pk", - "columns": [ - "workspace_id", - "connection_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace": { - "name": "ui_workspace", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ui_workspace_org_name_lower_unique_idx": { - "name": "ui_workspace_org_name_lower_unique_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ui_workspace_org_idx": { - "name": "ui_workspace_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "ui_workspace_env_idx": { - "name": "ui_workspace_env_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "tableTo": "organization", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ui_workspace_id_org_unique_idx": { - "name": "ui_workspace_id_org_unique_idx", - "nullsNotDistinct": false, - "columns": [ - "id", - "org_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.organization_status": { - "name": "organization_status", - "schema": "public", - "values": [ - "active", - "disabled", - "suspended" - ] - }, - "public.pipeline_layer_enum": { - "name": "pipeline_layer_enum", - "schema": "public", - "values": [ - "L1", - "L2", - "L3", - "L4", - "L5", - "L6" - ] - }, - "public.pipeline_status_enum": { - "name": "pipeline_status_enum", - "schema": "public", - "values": [ - "RECEIVED", - "PROCESSING", - "REPLICATED", - "NORMALIZED", - "SKIPPED", - "PENDING", - "SUCCESS", - "FAIL", - "RETRY", - "DISMISSED" - ] - }, - "public.scheduler_outbox_action_enum": { - "name": "scheduler_outbox_action_enum", - "schema": "public", - "values": [ - "created", - "updated", - "deleted" - ] - }, - "public.scheduler_outbox_status_enum": { - "name": "scheduler_outbox_status_enum", - "schema": "public", - "values": [ - "pending", - "processing", - "succeeded", - "failed" - ] - }, - "public.stitch_status_enum": { - "name": "stitch_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "PAUSED", - "ARCHIVED" - ] - }, - "public.auth_type_enum": { - "name": "auth_type_enum", - "schema": "public", - "values": [ - "OAUTH2", - "API_KEY", - "BASIC" - ] - }, - "public.connection_status_enum": { - "name": "connection_status_enum", - "schema": "public", - "values": [ - "ACTIVE", - "INACTIVE", - "REVOKED", - "EXPIRED", - "PROVISIONING", - "FAILED" - ] - }, - "public.env_type_enum": { - "name": "env_type_enum", - "schema": "public", - "values": [ - "PRODUCTION", - "SANDBOX" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json deleted file mode 100644 index 12fdfb81..00000000 --- a/packages/database/drizzle/meta/_journal.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1774591613330, - "tag": "0000_salty_stryfe", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1775387140030, - "tag": "0002_add_status_enum_values", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1775387539103, - "tag": "0003_add_gem_indexes", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1775387600000, - "tag": "0004_verify_gem_indexes", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1775804313050, - "tag": "0005_greedy_eddie_brock", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1775887098998, - "tag": "0006_quick_puck", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1776610843999, - "tag": "0007_clammy_aaron_stack", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/packages/database/drizzle/tenant/0000_oval_calypso.sql b/packages/database/drizzle/tenant/0000_oval_calypso.sql new file mode 100644 index 00000000..f29bc0f2 --- /dev/null +++ b/packages/database/drizzle/tenant/0000_oval_calypso.sql @@ -0,0 +1,203 @@ +CREATE TYPE "public"."pipeline_layer_enum" AS ENUM('L1', 'L2', 'L3', 'L4', 'L5', 'L6');--> statement-breakpoint +CREATE TYPE "public"."pipeline_status_enum" AS ENUM('RECEIVED', 'PROCESSING', 'REPLICATED', 'NORMALIZED', 'SKIPPED', 'PENDING', 'SUCCESS', 'FAIL', 'RETRY', 'DISMISSED');--> statement-breakpoint +CREATE TYPE "public"."scheduler_outbox_action_enum" AS ENUM('created', 'updated', 'deleted');--> statement-breakpoint +CREATE TYPE "public"."scheduler_outbox_status_enum" AS ENUM('pending', 'processing', 'succeeded', 'failed');--> statement-breakpoint +CREATE TYPE "public"."stitch_status_enum" AS ENUM('ACTIVE', 'PAUSED', 'ARCHIVED');--> statement-breakpoint +CREATE TYPE "public"."auth_type_enum" AS ENUM('OAUTH2', 'API_KEY', 'BASIC');--> statement-breakpoint +CREATE TYPE "public"."connection_status_enum" AS ENUM('ACTIVE', 'INACTIVE', 'REVOKED', 'EXPIRED', 'PROVISIONING', 'FAILED');--> statement-breakpoint +CREATE TYPE "public"."env_type_enum" AS ENUM('PRODUCTION', 'SANDBOX');--> statement-breakpoint +CREATE TABLE "canonical_mappings" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "app_name" varchar(100) NOT NULL, + "category" varchar(50) NOT NULL, + "entity" varchar(100) NOT NULL, + "view_mode" varchar(50) NOT NULL, + "version" varchar(50) DEFAULT 'v1' NOT NULL, + "mapping_config" jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ai_conversations" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "title" varchar(255) DEFAULT 'New Conversation' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ai_conversations_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") +); +--> statement-breakpoint +CREATE TABLE "ai_messages" ( + "id" uuid DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "conversation_id" uuid NOT NULL, + "role" varchar(50) NOT NULL, + "content" text NOT NULL, + "status" varchar(50) DEFAULT 'completed' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ai_messages_tenant_id_id_pk" PRIMARY KEY("tenant_id","id") +); +--> statement-breakpoint +CREATE TABLE "connector_object_profiles" ( + "connection_id" uuid NOT NULL, + "object_name" varchar(255) NOT NULL, + "profile" jsonb DEFAULT '{}' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "connector_object_profiles_connection_id_object_name_pk" PRIMARY KEY("connection_id","object_name") +); +--> statement-breakpoint +CREATE TABLE "global_entity_map" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "stitch_id" uuid NOT NULL, + "source_app_name" varchar(100) NOT NULL, + "source_app_id" uuid NOT NULL, + "source_org_id" varchar(255) NOT NULL, + "source_org_name" varchar(255), + "source_entity_type" varchar(100) NOT NULL, + "source_entity_id" varchar(255) NOT NULL, + "source_ref_layer" varchar(10) NOT NULL, + "source_trace_id" uuid NOT NULL, + "dest_app_name" varchar(100) NOT NULL, + "dest_app_id" uuid NOT NULL, + "dest_org_id" varchar(255) NOT NULL, + "dest_org_name" varchar(255), + "dest_entity_type" varchar(100) NOT NULL, + "dest_entity_id" varchar(255) NOT NULL, + "dest_ref_layer" varchar(10) NOT NULL, + "dest_trace_id" uuid NOT NULL, + "last_synced_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "field_mapping" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "stitch_id" uuid NOT NULL, + "source_canonical" varchar(100) NOT NULL, + "mapping_rules" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "integration_stitch" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar(255) NOT NULL, + "org_id" text NOT NULL, + "workspace_id" uuid NOT NULL, + "src_connection_id" uuid NOT NULL, + "dest_connection_id" uuid NOT NULL, + "source_object" varchar(255) NOT NULL, + "target_object" varchar(255) NOT NULL, + "sync_condition" jsonb DEFAULT '[]'::jsonb NOT NULL, + "status" "stitch_status_enum" DEFAULT 'ACTIVE' NOT NULL, + "sync_interval_minutes" integer DEFAULT 30 NOT NULL, + "schedule_enabled" boolean DEFAULT true NOT NULL, + "last_scheduled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "sync_interval_minutes_positive" CHECK ("integration_stitch"."sync_interval_minutes" > 0) +); +--> statement-breakpoint +CREATE TABLE "scheduler_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "stitch_id" uuid NOT NULL, + "action" "scheduler_outbox_action_enum" NOT NULL, + "status" "scheduler_outbox_status_enum" DEFAULT 'pending' NOT NULL, + "attempts" smallint DEFAULT 0 NOT NULL, + "next_retry_at" timestamp with time zone DEFAULT now() NOT NULL, + "last_error" text, + "processed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sync_cursors" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "stitch_id" uuid NOT NULL, + "stream_name" varchar(200) NOT NULL, + "state_document" jsonb DEFAULT '{"bookmarks":{},"versions":{},"currently_syncing":null}'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "app_connection" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "app_name" varchar(100) NOT NULL, + "external_id" varchar(255) NOT NULL, + "display_name" varchar(255) NOT NULL, + "auth_type" "auth_type_enum" NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp with time zone, + "status" "connection_status_enum" DEFAULT 'ACTIVE' NOT NULL, + "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb, + "schema_plan" varchar(64) DEFAULT 'NAMESPACE_ONLY' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ui_workspace_connection" ( + "workspace_id" uuid NOT NULL, + "connection_id" uuid NOT NULL, + "assigned_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ui_workspace_connection_workspace_id_connection_id_pk" PRIMARY KEY("workspace_id","connection_id") +); +--> statement-breakpoint +CREATE TABLE "ui_workspace" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "org_id" text NOT NULL, + "name" varchar(255) NOT NULL, + "env_type" "env_type_enum" DEFAULT 'PRODUCTION' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ui_workspace_id_org_unique_idx" UNIQUE("id","org_id") +); +--> statement-breakpoint +ALTER TABLE "ai_messages" ADD CONSTRAINT "ai_messages_conversation_fk" FOREIGN KEY ("tenant_id","conversation_id") REFERENCES "public"."ai_conversations"("tenant_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "connector_object_profiles" ADD CONSTRAINT "connector_object_profiles_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_stitch_id_integration_stitch_id_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_source_app_id_app_connection_id_fk" FOREIGN KEY ("source_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "global_entity_map" ADD CONSTRAINT "global_entity_map_dest_app_id_app_connection_id_fk" FOREIGN KEY ("dest_app_id") REFERENCES "public"."app_connection"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "field_mapping" ADD CONSTRAINT "field_mapping_stitch_id_integration_stitch_id_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_workspace_org_fk" FOREIGN KEY ("workspace_id","org_id") REFERENCES "public"."ui_workspace"("id","org_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_src_connection_fk" FOREIGN KEY ("src_connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "integration_stitch" ADD CONSTRAINT "stitch_dest_connection_fk" FOREIGN KEY ("dest_connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scheduler_outbox" ADD CONSTRAINT "scheduler_outbox_stitch_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sync_cursors" ADD CONSTRAINT "sync_cursors_stitch_fk" FOREIGN KEY ("stitch_id") REFERENCES "public"."integration_stitch"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_workspace_id_ui_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."ui_workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ui_workspace_connection" ADD CONSTRAINT "ui_workspace_connection_connection_id_app_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."app_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "canonical_mapping_unique_idx" ON "canonical_mappings" USING btree ("app_name","category","entity","view_mode","version");--> statement-breakpoint +CREATE INDEX "ai_conv_tenant_idx" ON "ai_conversations" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "ai_conv_created_idx" ON "ai_conversations" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "ai_msg_conv_idx" ON "ai_messages" USING btree ("conversation_id");--> statement-breakpoint +CREATE INDEX "ai_msg_tenant_idx" ON "ai_messages" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "ai_msg_timeline_idx" ON "ai_messages" USING btree ("conversation_id","created_at");--> statement-breakpoint +CREATE INDEX "cop_connection_idx" ON "connector_object_profiles" USING btree ("connection_id");--> statement-breakpoint +CREATE UNIQUE INDEX "gem_unique_mapping_idx" ON "global_entity_map" USING btree ("stitch_id","source_app_id","source_entity_id","dest_app_id","dest_entity_type");--> statement-breakpoint +CREATE INDEX "gem_src_lookup_idx" ON "global_entity_map" USING btree ("source_entity_id","source_app_id");--> statement-breakpoint +CREATE INDEX "gem_dest_lookup_idx" ON "global_entity_map" USING btree ("dest_entity_id","dest_app_id");--> statement-breakpoint +CREATE INDEX "gem_source_app_idx" ON "global_entity_map" USING btree ("source_app_id");--> statement-breakpoint +CREATE INDEX "gem_dest_app_idx" ON "global_entity_map" USING btree ("dest_app_id");--> statement-breakpoint +CREATE INDEX "gem_src_trace_idx" ON "global_entity_map" USING btree ("source_trace_id");--> statement-breakpoint +CREATE INDEX "gem_dest_trace_idx" ON "global_entity_map" USING btree ("dest_trace_id");--> statement-breakpoint +CREATE INDEX "gem_stitch_idx" ON "global_entity_map" USING btree ("stitch_id");--> statement-breakpoint +CREATE UNIQUE INDEX "field_mapping_stitch_canonical_unique_idx" ON "field_mapping" USING btree ("stitch_id","source_canonical");--> statement-breakpoint +CREATE UNIQUE INDEX "stitch_name_workspace_unique_idx" ON "integration_stitch" USING btree ("workspace_id",lower("name"));--> statement-breakpoint +CREATE INDEX "stitch_workspace_idx" ON "integration_stitch" USING btree ("workspace_id");--> statement-breakpoint +CREATE INDEX "stitch_org_idx" ON "integration_stitch" USING btree ("org_id");--> statement-breakpoint +CREATE INDEX "stitch_src_conn_idx" ON "integration_stitch" USING btree ("src_connection_id");--> statement-breakpoint +CREATE INDEX "stitch_dest_conn_idx" ON "integration_stitch" USING btree ("dest_connection_id");--> statement-breakpoint +CREATE INDEX "stitch_status_idx" ON "integration_stitch" USING btree ("org_id","status");--> statement-breakpoint +CREATE INDEX "scheduler_outbox_poll_idx" ON "scheduler_outbox" USING btree ("next_retry_at") WHERE status = 'pending';--> statement-breakpoint +CREATE INDEX "scheduler_outbox_stitch_idx" ON "scheduler_outbox" USING btree ("stitch_id");--> statement-breakpoint +CREATE UNIQUE INDEX "sync_cursors_stitch_stream_unique_idx" ON "sync_cursors" USING btree ("stitch_id","stream_name");--> statement-breakpoint +CREATE INDEX "sync_cursors_stitch_idx" ON "sync_cursors" USING btree ("stitch_id");--> statement-breakpoint +CREATE INDEX "app_name_idx" ON "app_connection" USING btree ("app_name");--> statement-breakpoint +CREATE INDEX "tenant_status_idx" ON "app_connection" USING btree ("tenant_id","status");--> statement-breakpoint +CREATE INDEX "connection_expires_at_idx" ON "app_connection" USING btree ("expires_at") WHERE "app_connection"."expires_at" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "tenant_external_id_unique_idx" ON "app_connection" USING btree ("tenant_id","external_id");--> statement-breakpoint +CREATE UNIQUE INDEX "tenant_app_display_name_lower_idx" ON "app_connection" USING btree ("tenant_id","app_name",lower("display_name"));--> statement-breakpoint +CREATE INDEX "workspace_connection_conn_idx" ON "ui_workspace_connection" USING btree ("connection_id");--> statement-breakpoint +CREATE UNIQUE INDEX "ui_workspace_org_name_lower_unique_idx" ON "ui_workspace" USING btree ("org_id","env_type",lower("name"));--> statement-breakpoint +CREATE INDEX "ui_workspace_org_idx" ON "ui_workspace" USING btree ("org_id");--> statement-breakpoint +CREATE INDEX "ui_workspace_env_idx" ON "ui_workspace" USING btree ("org_id","env_type"); \ No newline at end of file diff --git a/packages/database/drizzle/meta/0000_snapshot.json b/packages/database/drizzle/tenant/meta/0000_snapshot.json similarity index 65% rename from packages/database/drizzle/meta/0000_snapshot.json rename to packages/database/drizzle/tenant/meta/0000_snapshot.json index 8693d56a..1bc15eb6 100644 --- a/packages/database/drizzle/meta/0000_snapshot.json +++ b/packages/database/drizzle/tenant/meta/0000_snapshot.json @@ -1,303 +1,298 @@ { - "id": "8065b651-1a84-4883-ac57-edca0623da9b", + "id": "01f8af89-fcb6-4992-a950-0cfd0857f334", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "public.account": { - "name": "account", + "public.canonical_mappings": { + "name": "canonical_mappings", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, - "accountId": { - "name": "accountId", - "type": "text", + "app_name": { + "name": "app_name", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "providerId": { - "name": "providerId", - "type": "text", + "category": { + "name": "category", + "type": "varchar(50)", "primaryKey": false, "notNull": true }, - "userId": { - "name": "userId", - "type": "text", + "entity": { + "name": "entity", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "accessToken": { - "name": "accessToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refreshToken": { - "name": "refreshToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "idToken": { - "name": "idToken", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "accessTokenExpiresAt": { - "name": "accessTokenExpiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refreshTokenExpiresAt": { - "name": "refreshTokenExpiresAt", - "type": "timestamp with time zone", + "view_mode": { + "name": "view_mode", + "type": "varchar(50)", "primaryKey": false, - "notNull": false + "notNull": true }, - "scope": { - "name": "scope", - "type": "text", + "version": { + "name": "version", + "type": "varchar(50)", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'v1'" }, - "password": { - "name": "password", - "type": "text", + "mapping_config": { + "name": "mapping_config", + "type": "jsonb", "primaryKey": false, - "notNull": false + "notNull": true }, - "createdAt": { - "name": "createdAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "updatedAt": { - "name": "updatedAt", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" } }, - "indexes": {}, - "foreignKeys": { - "account_userId_user_id_fk": { - "name": "account_userId_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" + "indexes": { + "canonical_mapping_unique_idx": { + "name": "canonical_mapping_unique_idx", + "columns": [ + { + "expression": "app_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "view_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "onDelete": "cascade", - "onUpdate": "no action" + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} } }, + "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_user_provider_unique": { - "name": "account_user_provider_unique", - "nullsNotDistinct": false, - "columns": [ - "userId", - "providerId" - ] - }, - "account_provider_account_unique": { - "name": "account_provider_account_unique", - "nullsNotDistinct": false, - "columns": [ - "providerId", - "accountId" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.invitation": { - "name": "invitation", + "public.ai_conversations": { + "name": "ai_conversations", "schema": "", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organizationId": { - "name": "organizationId", - "type": "text", + "type": "uuid", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, - "email": { - "name": "email", + "tenant_id": { + "name": "tenant_id", "type": "text", "primaryKey": false, "notNull": true }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", + "title": { + "name": "title", + "type": "varchar(255)", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "'New Conversation'" }, - "expiresAt": { - "name": "expiresAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true - }, - "inviterId": { - "name": "inviterId", - "type": "text", - "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "createdAt": { - "name": "createdAt", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" } }, - "indexes": {}, - "foreignKeys": { - "invitation_organizationId_organization_id_fk": { - "name": "invitation_organizationId_organization_id_fk", - "tableFrom": "invitation", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_role_role_id_fk": { - "name": "invitation_role_role_id_fk", - "tableFrom": "invitation", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" + "indexes": { + "ai_conv_tenant_idx": { + "name": "ai_conv_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "onDelete": "restrict", - "onUpdate": "no action" + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, - "invitation_inviterId_user_id_fk": { - "name": "invitation_inviterId_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": [ - "inviterId" + "ai_conv_created_idx": { + "name": "ai_conv_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "columnsTo": [ + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ai_conversations_tenant_id_id_pk": { + "name": "ai_conversations_tenant_id_id_pk", + "columns": [ + "tenant_id", "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" + ] } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.member": { - "name": "member", + "public.ai_messages": { + "name": "ai_messages", "schema": "", "columns": { "id": { "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" }, - "organizationId": { - "name": "organizationId", + "tenant_id": { + "name": "tenant_id", "type": "text", "primaryKey": false, "notNull": true }, - "userId": { - "name": "userId", - "type": "text", + "conversation_id": { + "name": "conversation_id", + "type": "uuid", "primaryKey": false, "notNull": true }, "role": { "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", "type": "text", "primaryKey": false, "notNull": true }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "status": { + "name": "status", + "type": "varchar(50)", "primaryKey": false, "notNull": true, - "default": "now()" + "default": "'completed'" }, - "deletedAt": { - "name": "deletedAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" } }, "indexes": { - "member_user_org_unique": { - "name": "member_user_org_unique", + "ai_msg_conv_idx": { + "name": "ai_msg_conv_idx", "columns": [ { - "expression": "userId", + "expression": "conversation_id", "isExpression": false, "asc": true, "nulls": "last" - }, + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_msg_tenant_idx": { + "name": "ai_msg_tenant_idx", + "columns": [ { - "expression": "organizationId", + "expression": "tenant_id", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, - "where": "\"member\".\"deletedAt\" IS NULL", + "isUnique": false, "concurrently": false, "method": "btree", "with": {} }, - "member_org_idx": { - "name": "member_org_idx", + "ai_msg_timeline_idx": { + "name": "ai_msg_timeline_idx", "columns": [ { - "expression": "organizationId", + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", "isExpression": false, "asc": true, "nulls": "last" @@ -310,328 +305,280 @@ } }, "foreignKeys": { - "member_organizationId_organization_id_fk": { - "name": "member_organizationId_organization_id_fk", - "tableFrom": "member", - "tableTo": "organization", - "columnsFrom": [ - "organizationId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_userId_user_id_fk": { - "name": "member_userId_user_id_fk", - "tableFrom": "member", - "tableTo": "user", + "ai_messages_conversation_fk": { + "name": "ai_messages_conversation_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", "columnsFrom": [ - "userId" + "tenant_id", + "conversation_id" ], "columnsTo": [ + "tenant_id", "id" ], "onDelete": "cascade", "onUpdate": "no action" - }, - "member_role_role_id_fk": { - "name": "member_role_role_id_fk", - "tableFrom": "member", - "tableTo": "role", - "columnsFrom": [ - "role" - ], - "columnsTo": [ + } + }, + "compositePrimaryKeys": { + "ai_messages_tenant_id_id_pk": { + "name": "ai_messages_tenant_id_id_pk", + "columns": [ + "tenant_id", "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" + ] } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.organization": { - "name": "organization", + "public.connector_object_profiles": { + "name": "connector_object_profiles", "schema": "", "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", + "connection_id": { + "name": "connection_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "slug": { - "name": "slug", - "type": "text", + "object_name": { + "name": "object_name", + "type": "varchar(255)", "primaryKey": false, - "notNull": false + "notNull": true }, - "logo": { - "name": "logo", - "type": "text", + "profile": { + "name": "profile", + "type": "jsonb", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'{}'" }, - "createdAt": { - "name": "createdAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" }, - "updatedAt": { - "name": "updatedAt", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "organization_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "isSystem": { - "name": "isSystem", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false } }, "indexes": { - "organization_slug_unique_idx": { - "name": "organization_slug_unique_idx", + "cop_connection_idx": { + "name": "cop_connection_idx", "columns": [ { - "expression": "slug", + "expression": "connection_id", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, - "where": "\"deletedAt\" IS NULL", + "isUnique": false, "concurrently": false, "method": "btree", "with": {} } }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "organization_id_not_sentinel": { - "name": "organization_id_not_sentinel", - "value": "\"organization\".\"id\" <> '__NULL__'" + "foreignKeys": { + "connector_object_profiles_connection_id_app_connection_id_fk": { + "name": "connector_object_profiles_connection_id_app_connection_id_fk", + "tableFrom": "connector_object_profiles", + "tableTo": "app_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "connector_object_profiles_connection_id_object_name_pk": { + "name": "connector_object_profiles_connection_id_object_name_pk", + "columns": [ + "connection_id", + "object_name" + ] } }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, "isRLSEnabled": false }, - "public.permission": { - "name": "permission", + "public.global_entity_map": { + "name": "global_entity_map", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "stitch_id": { + "name": "stitch_id", + "type": "uuid", + "primaryKey": false, "notNull": true }, - "resource": { - "name": "resource", - "type": "text", + "source_app_name": { + "name": "source_app_name", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "action": { - "name": "action", - "type": "text", + "source_app_id": { + "name": "source_app_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "description": { - "name": "description", - "type": "text", + "source_org_id": { + "name": "source_org_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_org_name": { + "name": "source_org_name", + "type": "varchar(255)", "primaryKey": false, "notNull": false }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "source_entity_type": { + "name": "source_entity_type", + "type": "varchar(100)", "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permission_resource_action_unique": { - "name": "permission_resource_action_unique", - "nullsNotDistinct": false, - "columns": [ - "resource", - "action" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role": { - "name": "role", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, "notNull": true }, - "name": { - "name": "name", - "type": "text", + "source_entity_id": { + "name": "source_entity_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "description": { - "name": "description", - "type": "text", + "source_ref_layer": { + "name": "source_ref_layer", + "type": "varchar(10)", "primaryKey": false, - "notNull": false + "notNull": true }, - "isSystem": { - "name": "isSystem", - "type": "boolean", + "source_trace_id": { + "name": "source_trace_id", + "type": "uuid", "primaryKey": false, - "notNull": true, - "default": false + "notNull": true }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "dest_app_name": { + "name": "dest_app_name", + "type": "varchar(100)", "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "role_name_unique": { - "name": "role_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permission": { - "name": "role_permission", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, "notNull": true }, - "roleId": { - "name": "roleId", - "type": "text", + "dest_app_id": { + "name": "dest_app_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "permissionId": { - "name": "permissionId", - "type": "text", + "dest_org_id": { + "name": "dest_org_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "organizationId": { - "name": "organizationId", - "type": "text", + "dest_org_name": { + "name": "dest_org_name", + "type": "varchar(255)", "primaryKey": false, "notNull": false }, - "conditions": { - "name": "conditions", - "type": "jsonb", + "dest_entity_type": { + "name": "dest_entity_type", + "type": "varchar(100)", "primaryKey": false, - "notNull": false + "notNull": true + }, + "dest_entity_id": { + "name": "dest_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "dest_ref_layer": { + "name": "dest_ref_layer", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "dest_trace_id": { + "name": "dest_trace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" } }, "indexes": { - "idx_role_permission_org_id": { - "name": "idx_role_permission_org_id", + "gem_unique_mapping_idx": { + "name": "gem_unique_mapping_idx", "columns": [ { - "expression": "organizationId", + "expression": "stitch_id", "isExpression": false, "asc": true, "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_role_permission_unique": { - "name": "idx_role_permission_unique", - "columns": [ + }, { - "expression": "roleId", + "expression": "source_app_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "permissionId", + "expression": "source_entity_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "COALESCE(\"organizationId\", '__NULL__')", + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dest_entity_type", + "isExpression": false, "asc": true, - "isExpression": true, "nulls": "last" } ], @@ -639,15 +586,132 @@ "concurrently": false, "method": "btree", "with": {} - } - }, + }, + "gem_src_lookup_idx": { + "name": "gem_src_lookup_idx", + "columns": [ + { + "expression": "source_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_dest_lookup_idx": { + "name": "gem_dest_lookup_idx", + "columns": [ + { + "expression": "dest_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_source_app_idx": { + "name": "gem_source_app_idx", + "columns": [ + { + "expression": "source_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_dest_app_idx": { + "name": "gem_dest_app_idx", + "columns": [ + { + "expression": "dest_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_src_trace_idx": { + "name": "gem_src_trace_idx", + "columns": [ + { + "expression": "source_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_dest_trace_idx": { + "name": "gem_dest_trace_idx", + "columns": [ + { + "expression": "dest_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gem_stitch_idx": { + "name": "gem_stitch_idx", + "columns": [ + { + "expression": "stitch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": { - "role_permission_roleId_role_id_fk": { - "name": "role_permission_roleId_role_id_fk", - "tableFrom": "role_permission", - "tableTo": "role", + "global_entity_map_stitch_id_integration_stitch_id_fk": { + "name": "global_entity_map_stitch_id_integration_stitch_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "integration_stitch", "columnsFrom": [ - "roleId" + "stitch_id" ], "columnsTo": [ "id" @@ -655,30 +719,30 @@ "onDelete": "cascade", "onUpdate": "no action" }, - "role_permission_permissionId_permission_id_fk": { - "name": "role_permission_permissionId_permission_id_fk", - "tableFrom": "role_permission", - "tableTo": "permission", + "global_entity_map_source_app_id_app_connection_id_fk": { + "name": "global_entity_map_source_app_id_app_connection_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "app_connection", "columnsFrom": [ - "permissionId" + "source_app_id" ], "columnsTo": [ "id" ], - "onDelete": "cascade", + "onDelete": "restrict", "onUpdate": "no action" }, - "role_permission_organizationId_organization_id_fk": { - "name": "role_permission_organizationId_organization_id_fk", - "tableFrom": "role_permission", - "tableTo": "organization", + "global_entity_map_dest_app_id_app_connection_id_fk": { + "name": "global_entity_map_dest_app_id_app_connection_id_fk", + "tableFrom": "global_entity_map", + "tableTo": "app_connection", "columnsFrom": [ - "organizationId" + "dest_app_id" ], "columnsTo": [ "id" ], - "onDelete": "cascade", + "onDelete": "restrict", "onUpdate": "no action" } }, @@ -688,86 +752,81 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.session": { - "name": "session", + "public.field_mapping": { + "name": "field_mapping", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", + "stitch_id": { + "name": "stitch_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "token": { - "name": "token", - "type": "text", + "source_canonical": { + "name": "source_canonical", + "type": "varchar(100)", "primaryKey": false, "notNull": true }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "mapping_rules": { + "name": "mapping_rules", + "type": "jsonb", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "'[]'::jsonb" }, - "updatedAt": { - "name": "updatedAt", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, - "notNull": true - }, - "ipAddress": { - "name": "ipAddress", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userAgent": { - "name": "userAgent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true + "notNull": true, + "default": "now()" }, - "impersonatedBy": { - "name": "impersonatedBy", - "type": "text", + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" } }, - "indexes": {}, - "foreignKeys": { - "session_userId_user_id_fk": { - "name": "session_userId_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" + "indexes": { + "field_mapping_stitch_canonical_unique_idx": { + "name": "field_mapping_stitch_canonical_unique_idx", + "columns": [ + { + "expression": "stitch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_canonical", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "session_impersonatedBy_user_id_fk": { - "name": "session_impersonatedBy_user_id_fk", - "tableFrom": "session", - "tableTo": "user", + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "field_mapping_stitch_id_integration_stitch_id_fk": { + "name": "field_mapping_stitch_id_integration_stitch_id_fk", + "tableFrom": "field_mapping", + "tableTo": "integration_stitch", "columnsFrom": [ - "impersonatedBy" + "stitch_id" ], "columnsTo": [ "id" @@ -777,238 +836,98 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.user": { - "name": "user", + "public.integration_stitch": { + "name": "integration_stitch", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, "name": { "name": "name", - "type": "text", + "type": "varchar(255)", "primaryKey": false, - "notNull": false + "notNull": true }, - "email": { - "name": "email", + "org_id": { + "name": "org_id", "type": "text", "primaryKey": false, "notNull": true }, - "emailVerified": { - "name": "emailVerified", - "type": "boolean", + "workspace_id": { + "name": "workspace_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "image": { - "name": "image", - "type": "text", + "src_connection_id": { + "name": "src_connection_id", + "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", + "dest_connection_id": { + "name": "dest_connection_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", + "source_object": { + "name": "source_object", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'member'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "banReason": { - "name": "banReason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banExpires": { - "name": "banExpires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "deletedAt": { - "name": "deletedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expiresAt": { - "name": "expiresAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updatedAt": { - "name": "updatedAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_connection": { - "name": "app_connection", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "tenant_id": { - "name": "tenant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "app_name": { - "name": "app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "external_id": { - "name": "external_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", + "target_object": { + "name": "target_object", "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "auth_type": { - "name": "auth_type", - "type": "auth_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", + "sync_condition": { + "name": "sync_condition", + "type": "jsonb", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "'[]'::jsonb" }, "status": { "name": "status", - "type": "connection_status_enum", + "type": "stitch_status_enum", "typeSchema": "public", "primaryKey": false, "notNull": true, "default": "'ACTIVE'" }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", "primaryKey": false, "notNull": true, - "default": "'PRODUCTION'" + "default": 30 }, - "metadata": { - "name": "metadata", - "type": "jsonb", + "schedule_enabled": { + "name": "schedule_enabled", + "type": "boolean", "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" + "notNull": true, + "default": true + }, + "last_scheduled_at": { + "name": "last_scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false }, "created_at": { "name": "created_at", @@ -1026,32 +945,32 @@ } }, "indexes": { - "app_name_idx": { - "name": "app_name_idx", + "stitch_name_workspace_unique_idx": { + "name": "stitch_name_workspace_unique_idx", "columns": [ { - "expression": "app_name", + "expression": "workspace_id", "isExpression": false, "asc": true, "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" } ], - "isUnique": false, + "isUnique": true, "concurrently": false, "method": "btree", "with": {} }, - "tenant_status_idx": { - "name": "tenant_status_idx", + "stitch_workspace_idx": { + "name": "stitch_workspace_idx", "columns": [ { - "expression": "tenant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", + "expression": "workspace_id", "isExpression": false, "asc": true, "nulls": "last" @@ -1062,78 +981,108 @@ "method": "btree", "with": {} }, - "connection_expires_at_idx": { - "name": "connection_expires_at_idx", + "stitch_org_idx": { + "name": "stitch_org_idx", "columns": [ { - "expression": "expires_at", + "expression": "org_id", "isExpression": false, "asc": true, "nulls": "last" } ], "isUnique": false, - "where": "\"app_connection\".\"expires_at\" IS NOT NULL", "concurrently": false, "method": "btree", "with": {} }, - "tenant_external_id_unique_idx": { - "name": "tenant_external_id_unique_idx", + "stitch_src_conn_idx": { + "name": "stitch_src_conn_idx", "columns": [ { - "expression": "tenant_id", + "expression": "src_connection_id", "isExpression": false, "asc": true, "nulls": "last" - }, + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "stitch_dest_conn_idx": { + "name": "stitch_dest_conn_idx", + "columns": [ { - "expression": "external_id", + "expression": "dest_connection_id", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} }, - "tenant_app_display_name_lower_idx": { - "name": "tenant_app_display_name_lower_idx", + "stitch_status_idx": { + "name": "stitch_status_idx", "columns": [ { - "expression": "tenant_id", + "expression": "org_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "app_name", + "expression": "status", "isExpression": false, "asc": true, "nulls": "last" - }, - { - "expression": "lower(\"display_name\")", - "asc": true, - "isExpression": true, - "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} } }, "foreignKeys": { - "app_connection_tenant_id_organization_id_fk": { - "name": "app_connection_tenant_id_organization_id_fk", - "tableFrom": "app_connection", - "tableTo": "organization", + "stitch_workspace_org_fk": { + "name": "stitch_workspace_org_fk", + "tableFrom": "integration_stitch", + "tableTo": "ui_workspace", + "columnsFrom": [ + "workspace_id", + "org_id" + ], + "columnsTo": [ + "id", + "org_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stitch_src_connection_fk": { + "name": "stitch_src_connection_fk", + "tableFrom": "integration_stitch", + "tableTo": "app_connection", + "columnsFrom": [ + "src_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stitch_dest_connection_fk": { + "name": "stitch_dest_connection_fk", + "tableFrom": "integration_stitch", + "tableTo": "app_connection", "columnsFrom": [ - "tenant_id" + "dest_connection_id" ], "columnsTo": [ "id" @@ -1145,54 +1094,74 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, - "checkConstraints": {}, + "checkConstraints": { + "sync_interval_minutes_positive": { + "name": "sync_interval_minutes_positive", + "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" + } + }, "isRLSEnabled": false }, - "public.connection_storage_registry": { - "name": "connection_storage_registry", + "public.scheduler_outbox": { + "name": "scheduler_outbox", "schema": "", "columns": { - "connection_id": { - "name": "connection_id", + "id": { + "name": "id", "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, - "data_namespace": { - "name": "data_namespace", - "type": "varchar(128)", + "stitch_id": { + "name": "stitch_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "schema_plan": { - "name": "schema_plan", - "type": "varchar(64)", + "action": { + "name": "action", + "type": "scheduler_outbox_action_enum", + "typeSchema": "public", "primaryKey": false, - "notNull": true, - "default": "'NAMESPACE_ONLY'" + "notNull": true }, - "database_host_id": { - "name": "database_host_id", - "type": "varchar(255)", + "status": { + "name": "status", + "type": "scheduler_outbox_status_enum", + "typeSchema": "public", "primaryKey": false, "notNull": true, - "default": "'primary-cluster'" + "default": "'pending'" }, - "region_context": { - "name": "region_context", - "type": "varchar(50)", + "attempts": { + "name": "attempts", + "type": "smallint", "primaryKey": false, - "notNull": true + "notNull": true, + "default": 0 }, - "created_at": { - "name": "created_at", + "next_retry_at": { + "name": "next_retry_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" }, - "updated_at": { - "name": "updated_at", + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, @@ -1200,41 +1169,27 @@ } }, "indexes": { - "registry_namespace_unique_idx": { - "name": "registry_namespace_unique_idx", - "columns": [ - { - "expression": "data_namespace", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "registry_host_idx": { - "name": "registry_host_idx", + "scheduler_outbox_poll_idx": { + "name": "scheduler_outbox_poll_idx", "columns": [ { - "expression": "database_host_id", + "expression": "next_retry_at", "isExpression": false, "asc": true, "nulls": "last" } ], "isUnique": false, + "where": "status = 'pending'", "concurrently": false, "method": "btree", "with": {} }, - "registry_region_idx": { - "name": "registry_region_idx", + "scheduler_outbox_stitch_idx": { + "name": "scheduler_outbox_stitch_idx", "columns": [ { - "expression": "region_context", + "expression": "stitch_id", "isExpression": false, "asc": true, "nulls": "last" @@ -1247,12 +1202,12 @@ } }, "foreignKeys": { - "connection_storage_registry_connection_id_app_connection_id_fk": { - "name": "connection_storage_registry_connection_id_app_connection_id_fk", - "tableFrom": "connection_storage_registry", - "tableTo": "app_connection", + "scheduler_outbox_stitch_fk": { + "name": "scheduler_outbox_stitch_fk", + "tableFrom": "scheduler_outbox", + "tableTo": "integration_stitch", "columnsFrom": [ - "connection_id" + "stitch_id" ], "columnsTo": [ "id" @@ -1267,28 +1222,35 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.connector_object_profiles": { - "name": "connector_object_profiles", + "public.sync_cursors": { + "name": "sync_cursors", "schema": "", "columns": { - "connection_id": { - "name": "connection_id", + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "stitch_id": { + "name": "stitch_id", "type": "uuid", "primaryKey": false, "notNull": true }, - "object_name": { - "name": "object_name", - "type": "varchar(255)", + "stream_name": { + "name": "stream_name", + "type": "varchar(200)", "primaryKey": false, "notNull": true }, - "profile": { - "name": "profile", + "state_document": { + "name": "state_document", "type": "jsonb", "primaryKey": false, "notNull": true, - "default": "'{}'" + "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" }, "created_at": { "name": "created_at", @@ -1306,11 +1268,32 @@ } }, "indexes": { - "cop_connection_idx": { - "name": "cop_connection_idx", + "sync_cursors_stitch_stream_unique_idx": { + "name": "sync_cursors_stitch_stream_unique_idx", "columns": [ { - "expression": "connection_id", + "expression": "stitch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_cursors_stitch_idx": { + "name": "sync_cursors_stitch_idx", + "columns": [ + { + "expression": "stitch_id", "isExpression": false, "asc": true, "nulls": "last" @@ -1323,12 +1306,12 @@ } }, "foreignKeys": { - "connector_object_profiles_connection_id_app_connection_id_fk": { - "name": "connector_object_profiles_connection_id_app_connection_id_fk", - "tableFrom": "connector_object_profiles", - "tableTo": "app_connection", + "sync_cursors_stitch_fk": { + "name": "sync_cursors_stitch_fk", + "tableFrom": "sync_cursors", + "tableTo": "integration_stitch", "columnsFrom": [ - "connection_id" + "stitch_id" ], "columnsTo": [ "id" @@ -1337,22 +1320,14 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": { - "connector_object_profiles_connection_id_object_name_pk": { - "name": "connector_object_profiles_connection_id_object_name_pk", - "columns": [ - "connection_id", - "object_name" - ] - } - }, + "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.pieces": { - "name": "pieces", + "public.app_connection": { + "name": "app_connection", "schema": "", "columns": { "id": { @@ -1362,42 +1337,78 @@ "notNull": true, "default": "gen_random_uuid()" }, - "name": { - "name": "name", + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_name": { + "name": "app_name", "type": "varchar(100)", "primaryKey": false, "notNull": true }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, "display_name": { "name": "display_name", "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "logo_url": { - "name": "logo_url", - "type": "varchar(1024)", + "auth_type": { + "name": "auth_type", + "type": "auth_type_enum", + "typeSchema": "public", "primaryKey": false, - "notNull": false + "notNull": true }, - "package_name": { - "name": "package_name", - "type": "varchar(255)", + "value": { + "name": "value", + "type": "text", "primaryKey": false, "notNull": true }, - "version": { - "name": "version", - "type": "varchar(50)", + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", "primaryKey": false, - "notNull": true + "notNull": false }, - "enabled": { - "name": "enabled", - "type": "boolean", + "status": { + "name": "status", + "type": "connection_status_enum", + "typeSchema": "public", "primaryKey": false, "notNull": true, - "default": true + "default": "'ACTIVE'" + }, + "env_type": { + "name": "env_type", + "type": "env_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PRODUCTION'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "schema_plan": { + "name": "schema_plan", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'NAMESPACE_ONLY'" }, "created_at": { "name": "created_at", @@ -1414,52 +1425,12 @@ "default": "now()" } }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "pieces_name_unique": { - "name": "pieces_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace_connection": { - "name": "ui_workspace_connection", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "connection_id": { - "name": "connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_connection_conn_idx": { - "name": "workspace_connection_conn_idx", + "indexes": { + "app_name_idx": { + "name": "app_name_idx", "columns": [ { - "expression": "connection_id", + "expression": "app_name", "isExpression": false, "asc": true, "nulls": "last" @@ -1469,230 +1440,118 @@ "concurrently": false, "method": "btree", "with": {} - } - }, - "foreignKeys": { - "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { - "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ui_workspace_connection_connection_id_app_connection_id_fk": { - "name": "ui_workspace_connection_connection_id_app_connection_id_fk", - "tableFrom": "ui_workspace_connection", - "tableTo": "app_connection", - "columnsFrom": [ - "connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "ui_workspace_connection_workspace_id_connection_id_pk": { - "name": "ui_workspace_connection_workspace_id_connection_id_pk", - "columns": [ - "workspace_id", - "connection_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ui_workspace": { - "name": "ui_workspace", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "env_type": { - "name": "env_type", - "type": "env_type_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PRODUCTION'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ui_workspace_org_name_lower_unique_idx": { - "name": "ui_workspace_org_name_lower_unique_idx", + "tenant_status_idx": { + "name": "tenant_status_idx", "columns": [ { - "expression": "org_id", + "expression": "tenant_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "env_type", + "expression": "status", "isExpression": false, "asc": true, "nulls": "last" - }, + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_expires_at_idx": { + "name": "connection_expires_at_idx", + "columns": [ { - "expression": "lower(\"name\")", + "expression": "expires_at", + "isExpression": false, "asc": true, - "isExpression": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, + "where": "\"app_connection\".\"expires_at\" IS NOT NULL", "concurrently": false, "method": "btree", "with": {} }, - "ui_workspace_org_idx": { - "name": "ui_workspace_org_idx", + "tenant_external_id_unique_idx": { + "name": "tenant_external_id_unique_idx", "columns": [ { - "expression": "org_id", + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": false, + "isUnique": true, "concurrently": false, "method": "btree", "with": {} }, - "ui_workspace_env_idx": { - "name": "ui_workspace_env_idx", + "tenant_app_display_name_lower_idx": { + "name": "tenant_app_display_name_lower_idx", "columns": [ { - "expression": "org_id", + "expression": "tenant_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "env_type", + "expression": "app_name", "isExpression": false, "asc": true, "nulls": "last" + }, + { + "expression": "lower(\"display_name\")", + "asc": true, + "isExpression": true, + "nulls": "last" } ], - "isUnique": false, + "isUnique": true, "concurrently": false, "method": "btree", "with": {} } }, - "foreignKeys": { - "ui_workspace_org_id_organization_id_fk": { - "name": "ui_workspace_org_id_organization_id_fk", - "tableFrom": "ui_workspace", - "tableTo": "organization", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, + "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ui_workspace_id_org_unique_idx": { - "name": "ui_workspace_id_org_unique_idx", - "nullsNotDistinct": false, - "columns": [ - "id", - "org_id" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.field_mapping": { - "name": "field_mapping", + "public.ui_workspace_connection": { + "name": "ui_workspace_connection", "schema": "", "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", + "workspace_id": { + "name": "workspace_id", "type": "uuid", "primaryKey": false, "notNull": true }, - "source_canonical": { - "name": "source_canonical", - "type": "varchar(100)", + "connection_id": { + "name": "connection_id", + "type": "uuid", "primaryKey": false, "notNull": true }, - "mapping_rules": { - "name": "mapping_rules", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", + "assigned_at": { + "name": "assigned_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, @@ -1700,416 +1559,42 @@ } }, "indexes": { - "field_mapping_stitch_canonical_unique_idx": { - "name": "field_mapping_stitch_canonical_unique_idx", + "workspace_connection_conn_idx": { + "name": "workspace_connection_conn_idx", "columns": [ { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_canonical", + "expression": "connection_id", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} } }, "foreignKeys": { - "field_mapping_stitch_id_integration_stitch_id_fk": { - "name": "field_mapping_stitch_id_integration_stitch_id_fk", - "tableFrom": "field_mapping", - "tableTo": "integration_stitch", + "ui_workspace_connection_workspace_id_ui_workspace_id_fk": { + "name": "ui_workspace_connection_workspace_id_ui_workspace_id_fk", + "tableFrom": "ui_workspace_connection", + "tableTo": "ui_workspace", "columnsFrom": [ - "stitch_id" + "workspace_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.integration_stitch": { - "name": "integration_stitch", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "src_connection_id": { - "name": "src_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_connection_id": { - "name": "dest_connection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_object": { - "name": "source_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "target_object": { - "name": "target_object", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "sync_condition": { - "name": "sync_condition", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "status": { - "name": "status", - "type": "stitch_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 30 - }, - "schedule_enabled": { - "name": "schedule_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_scheduled_at": { - "name": "last_scheduled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "stitch_name_workspace_unique_idx": { - "name": "stitch_name_workspace_unique_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"name\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_workspace_idx": { - "name": "stitch_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_org_idx": { - "name": "stitch_org_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_src_conn_idx": { - "name": "stitch_src_conn_idx", - "columns": [ - { - "expression": "src_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_dest_conn_idx": { - "name": "stitch_dest_conn_idx", - "columns": [ - { - "expression": "dest_connection_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "stitch_status_idx": { - "name": "stitch_status_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "stitch_workspace_org_fk": { - "name": "stitch_workspace_org_fk", - "tableFrom": "integration_stitch", - "tableTo": "ui_workspace", - "columnsFrom": [ - "workspace_id", - "org_id" - ], - "columnsTo": [ - "id", - "org_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_src_connection_fk": { - "name": "stitch_src_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "src_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "stitch_dest_connection_fk": { - "name": "stitch_dest_connection_fk", - "tableFrom": "integration_stitch", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_connection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "sync_interval_minutes_positive": { - "name": "sync_interval_minutes_positive", - "value": "\"integration_stitch\".\"sync_interval_minutes\" > 0" - } - }, - "isRLSEnabled": false - }, - "public.scheduler_outbox": { - "name": "scheduler_outbox", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "scheduler_outbox_action_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "scheduler_outbox_status_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "smallint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "next_retry_at": { - "name": "next_retry_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "scheduler_outbox_poll_idx": { - "name": "scheduler_outbox_poll_idx", - "columns": [ - { - "expression": "next_retry_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'pending'", - "concurrently": false, - "method": "btree", - "with": {} }, - "scheduler_outbox_stitch_idx": { - "name": "scheduler_outbox_stitch_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "scheduler_outbox_stitch_fk": { - "name": "scheduler_outbox_stitch_fk", - "tableFrom": "scheduler_outbox", - "tableTo": "integration_stitch", + "ui_workspace_connection_connection_id_app_connection_id_fk": { + "name": "ui_workspace_connection_connection_id_app_connection_id_fk", + "tableFrom": "ui_workspace_connection", + "tableTo": "app_connection", "columnsFrom": [ - "stitch_id" + "connection_id" ], "columnsTo": [ "id" @@ -2118,238 +1603,60 @@ "onUpdate": "no action" } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sync_cursors": { - "name": "sync_cursors", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "stream_name": { - "name": "stream_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "state_document": { - "name": "state_document", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{\"bookmarks\":{},\"versions\":{},\"currently_syncing\":null}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sync_cursors_stitch_stream_unique_idx": { - "name": "sync_cursors_stitch_stream_unique_idx", - "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stream_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sync_cursors_stitch_idx": { - "name": "sync_cursors_stitch_idx", + "compositePrimaryKeys": { + "ui_workspace_connection_workspace_id_connection_id_pk": { + "name": "ui_workspace_connection_workspace_id_connection_id_pk", "columns": [ - { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sync_cursors_stitch_fk": { - "name": "sync_cursors_stitch_fk", - "tableFrom": "sync_cursors", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" + "workspace_id", + "connection_id" + ] } }, - "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false }, - "public.global_entity_map": { - "name": "global_entity_map", + "public.ui_workspace": { + "name": "ui_workspace", "schema": "", "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "stitch_id": { - "name": "stitch_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_app_name": { - "name": "source_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_app_id": { - "name": "source_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "source_org_id": { - "name": "source_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_org_name": { - "name": "source_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "source_entity_type": { - "name": "source_entity_type", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "source_entity_id": { - "name": "source_entity_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_ref_layer": { - "name": "source_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "source_trace_id": { - "name": "source_trace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_app_name": { - "name": "dest_app_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "dest_app_id": { - "name": "dest_app_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "dest_org_id": { - "name": "dest_org_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "dest_org_name": { - "name": "dest_org_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" }, - "dest_entity_type": { - "name": "dest_entity_type", - "type": "varchar(100)", + "org_id": { + "name": "org_id", + "type": "text", "primaryKey": false, "notNull": true }, - "dest_entity_id": { - "name": "dest_entity_id", + "name": { + "name": "name", "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "dest_ref_layer": { - "name": "dest_ref_layer", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "dest_trace_id": { - "name": "dest_trace_id", - "type": "uuid", + "env_type": { + "name": "env_type", + "type": "env_type_enum", + "typeSchema": "public", "primaryKey": false, - "notNull": true + "notNull": true, + "default": "'PRODUCTION'" }, - "last_synced_at": { - "name": "last_synced_at", + "created_at": { + "name": "created_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" }, - "created_at": { - "name": "created_at", + "updated_at": { + "name": "updated_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, @@ -2357,37 +1664,25 @@ } }, "indexes": { - "gem_unique_mapping_idx": { - "name": "gem_unique_mapping_idx", + "ui_workspace_org_name_lower_unique_idx": { + "name": "ui_workspace_org_name_lower_unique_idx", "columns": [ { - "expression": "stitch_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_entity_id", + "expression": "org_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "dest_app_id", + "expression": "env_type", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "dest_entity_type", - "isExpression": false, + "expression": "lower(\"name\")", "asc": true, + "isExpression": true, "nulls": "last" } ], @@ -2396,17 +1691,11 @@ "method": "btree", "with": {} }, - "gem_src_lookup_idx": { - "name": "gem_src_lookup_idx", + "ui_workspace_org_idx": { + "name": "ui_workspace_org_idx", "columns": [ { - "expression": "source_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_app_id", + "expression": "org_id", "isExpression": false, "asc": true, "nulls": "last" @@ -2417,62 +1706,17 @@ "method": "btree", "with": {} }, - "gem_dest_lookup_idx": { - "name": "gem_dest_lookup_idx", + "ui_workspace_env_idx": { + "name": "ui_workspace_env_idx", "columns": [ { - "expression": "dest_entity_id", + "expression": "org_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "dest_app_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_src_trace_idx": { - "name": "gem_src_trace_idx", - "columns": [ - { - "expression": "source_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_dest_trace_idx": { - "name": "gem_dest_trace_idx", - "columns": [ - { - "expression": "dest_trace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "gem_stitch_idx": { - "name": "gem_stitch_idx", - "columns": [ - { - "expression": "stitch_id", + "expression": "env_type", "isExpression": false, "asc": true, "nulls": "last" @@ -2484,89 +1728,50 @@ "with": {} } }, - "foreignKeys": { - "global_entity_map_stitch_id_integration_stitch_id_fk": { - "name": "global_entity_map_stitch_id_integration_stitch_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "integration_stitch", - "columnsFrom": [ - "stitch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "global_entity_map_source_app_id_app_connection_id_fk": { - "name": "global_entity_map_source_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "source_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "global_entity_map_dest_app_id_app_connection_id_fk": { - "name": "global_entity_map_dest_app_id_app_connection_id_fk", - "tableFrom": "global_entity_map", - "tableTo": "app_connection", - "columnsFrom": [ - "dest_app_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ui_workspace_id_org_unique_idx": { + "name": "ui_workspace_id_org_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "id", + "org_id" + ] } }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false } }, "enums": { - "public.organization_status": { - "name": "organization_status", - "schema": "public", - "values": [ - "active", - "disabled", - "suspended" - ] - }, - "public.auth_type_enum": { - "name": "auth_type_enum", - "schema": "public", - "values": [ - "OAUTH2", - "API_KEY", - "BASIC" - ] - }, - "public.connection_status_enum": { - "name": "connection_status_enum", + "public.pipeline_layer_enum": { + "name": "pipeline_layer_enum", "schema": "public", "values": [ - "ACTIVE", - "INACTIVE", - "REVOKED", - "EXPIRED" + "L1", + "L2", + "L3", + "L4", + "L5", + "L6" ] }, - "public.env_type_enum": { - "name": "env_type_enum", + "public.pipeline_status_enum": { + "name": "pipeline_status_enum", "schema": "public", "values": [ - "PRODUCTION", - "SANDBOX" + "RECEIVED", + "PROCESSING", + "REPLICATED", + "NORMALIZED", + "SKIPPED", + "PENDING", + "SUCCESS", + "FAIL", + "RETRY", + "DISMISSED" ] }, "public.scheduler_outbox_action_enum": { @@ -2597,31 +1802,33 @@ "ARCHIVED" ] }, - "public.pipeline_layer_enum": { - "name": "pipeline_layer_enum", + "public.auth_type_enum": { + "name": "auth_type_enum", "schema": "public", "values": [ - "L1", - "L2", - "L3", - "L4", - "L5", - "L6" + "OAUTH2", + "API_KEY", + "BASIC" ] }, - "public.pipeline_status_enum": { - "name": "pipeline_status_enum", + "public.connection_status_enum": { + "name": "connection_status_enum", "schema": "public", "values": [ - "RECEIVED", - "PROCESSING", - "REPLICATED", - "NORMALIZED", - "SKIPPED", - "PENDING", - "SUCCESS", - "FAIL", - "RETRY" + "ACTIVE", + "INACTIVE", + "REVOKED", + "EXPIRED", + "PROVISIONING", + "FAILED" + ] + }, + "public.env_type_enum": { + "name": "env_type_enum", + "schema": "public", + "values": [ + "PRODUCTION", + "SANDBOX" ] } }, diff --git a/packages/database/drizzle/tenant/meta/_journal.json b/packages/database/drizzle/tenant/meta/_journal.json new file mode 100644 index 00000000..85665fe0 --- /dev/null +++ b/packages/database/drizzle/tenant/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1777718772817, + "tag": "0000_oval_calypso", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/database/package.json b/packages/database/package.json index 857e7f41..c14da0ef 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -28,9 +28,12 @@ }, "scripts": { "build": "tsc", - "db:generate": "pnpm build && drizzle-kit generate", - "db:migrate": "pnpm build && drizzle-kit migrate", - "db:studio": "pnpm build && drizzle-kit studio" + "db:generate": "pnpm build && drizzle-kit generate --config drizzle.config.ts", + "db:migrate": "pnpm build && drizzle-kit migrate --config drizzle.config.ts", + "db:generate:tenant": "pnpm build && drizzle-kit generate --config drizzle.config.tenant.ts", + "db:migrate:tenant": "pnpm build && drizzle-kit migrate --config drizzle.config.tenant.ts", + "db:studio": "pnpm build && drizzle-kit studio --config drizzle.config.ts", + "db:studio:tenant": "pnpm build && drizzle-kit studio --config drizzle.config.tenant.ts" }, "dependencies": { "@nestjs/common": "^11.1.11", diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index f29b761a..86f8a727 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -2,8 +2,8 @@ export * from './client.js'; export * from './schema/global/identity.js'; export * from './schema/global/storage_registry.js'; export * from './schema/global/pieces.js'; -export * from './schema/global/canonical_mappings.js'; export * from './schema/tenant/tenant.js'; +export * from './schema/tenant/canonical_mappings.js'; export * from './schema/tenant/workspace.js'; export * from './schema/tenant/stitches.js'; export * from './schema/tenant/gem.js'; diff --git a/packages/database/src/schema/global/canonical_mappings.ts b/packages/database/src/schema/global/canonical_mappings.ts deleted file mode 100644 index fbb1a9df..00000000 --- a/packages/database/src/schema/global/canonical_mappings.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { pgTable, uuid, varchar, jsonb, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; -import { sql } from 'drizzle-orm'; - -/** - * Storage for canonical JSON property mappings. - * Maps raw piece (foreign app) shapes into Nexiom's strictly-typed internal objects. - */ -export const canonicalMappings = pgTable('canonical_mappings', { - id: uuid('id').defaultRandom().primaryKey(), - - /** e.g. "salesforce", "quickbooks" -> Matches Piece name */ - appName: varchar('app_name', { length: 100 }).notNull(), - - /** e.g. "TMS", "Accounting", "CRM" */ - category: varchar('category', { length: 50 }).notNull(), - - /** e.g. "Load", "Invoice" */ - entity: varchar('entity', { length: 100 }).notNull(), - - /** e.g. "summary", "financial", "execution" (Supports lazy loading constraints) */ - viewMode: varchar('view_mode', { length: 50 }).notNull(), - - /** Optional overriding for a specific tenant -> if null, it is global */ - tenantId: varchar('tenant_id', { length: 100 }), - - /** Versioning strategy e.g. "v1" */ - version: varchar('version', { length: 50 }).notNull().default('v1'), - - /** The generic JSON document dictating the transformation paths */ - mappingConfig: jsonb('mapping_config').notNull(), - - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull().$onUpdate(() => new Date()), -}, (table) => { - return { - /** - * Enforce tenant-aware uniqueness with partial indexes. - * PostgreSQL treats NULLs as not equal, so we need two separate indexes: - * - One for tenant-scoped rows (WHERE tenantId IS NOT NULL) - * - One for global rows (WHERE tenantId IS NULL) - */ - tenantMappingIdx: uniqueIndex('canonical_mapping_tenant_idx') - .on(table.tenantId, table.appName, table.category, table.entity, table.viewMode, table.version) - .where(sql`${table.tenantId} IS NOT NULL`), - globalMappingIdx: uniqueIndex('canonical_mapping_global_idx') - .on(table.appName, table.category, table.entity, table.viewMode, table.version) - .where(sql`${table.tenantId} IS NULL`) - }; -}); \ No newline at end of file diff --git a/packages/database/src/schema/global/storage_registry.ts b/packages/database/src/schema/global/storage_registry.ts index f77cb54f..d44f0a51 100644 --- a/packages/database/src/schema/global/storage_registry.ts +++ b/packages/database/src/schema/global/storage_registry.ts @@ -30,6 +30,9 @@ export const tenantStorageRegistry = pgTable( // e.g. 'us-east-1' | 'eu-central-1' regionContext: varchar('region_context', { length: 50 }).notNull(), + // The lifecycle state of the database (WARM, ACTIVE, SUSPENDED, DELETED) + status: varchar('status', { length: 20 }).default('ACTIVE').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull().$onUpdate(() => new Date()), }, diff --git a/packages/database/src/schema/tenant/canonical_mappings.ts b/packages/database/src/schema/tenant/canonical_mappings.ts new file mode 100644 index 00000000..8c4b2fd8 --- /dev/null +++ b/packages/database/src/schema/tenant/canonical_mappings.ts @@ -0,0 +1,38 @@ +import { pgTable, uuid, varchar, jsonb, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; + +/** + * Customer-specific canonical JSON property mappings. + * + * Maps raw piece (foreign app) shapes into Nexiom's strictly-typed internal + * canonical format. Each customer owns and manages their own mappings via the UI. + * + * Lives in the tenant DB — one set of mappings per customer, fully isolated. + */ +export const canonicalMappings = pgTable('canonical_mappings', { + id: uuid('id').defaultRandom().primaryKey(), + + /** e.g. "salesforce", "quickbooks" → matches Piece name */ + appName: varchar('app_name', { length: 100 }).notNull(), + + /** e.g. "TMS", "Accounting", "CRM" */ + category: varchar('category', { length: 50 }).notNull(), + + /** e.g. "Load", "Invoice" */ + entity: varchar('entity', { length: 100 }).notNull(), + + /** e.g. "summary", "financial", "execution" (supports lazy loading) */ + viewMode: varchar('view_mode', { length: 50 }).notNull(), + + /** Versioning strategy e.g. "v1" */ + version: varchar('version', { length: 50 }).notNull().default('v1'), + + /** The JSON document dictating transformation paths — managed via UI */ + mappingConfig: jsonb('mapping_config').notNull(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull().$onUpdate(() => new Date()), +}, (table) => [ + // Unique per (app, category, entity, viewMode, version) within this tenant DB + uniqueIndex('canonical_mapping_unique_idx') + .on(table.appName, table.category, table.entity, table.viewMode, table.version), +]); diff --git a/packages/database/src/schema/tenant/chat.ts b/packages/database/src/schema/tenant/chat.ts index 0c655b85..35962973 100644 --- a/packages/database/src/schema/tenant/chat.ts +++ b/packages/database/src/schema/tenant/chat.ts @@ -1,6 +1,6 @@ import { pgTable, uuid, varchar, text, timestamp, index, primaryKey, foreignKey } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; -import { organization } from '../global/identity.js'; +// NOTE: No cross-DB FK to organization — tenant isolation enforced by TenantDatabaseManager routing. /** * Storage for ChatGPT-style conversational grouping. @@ -47,22 +47,13 @@ export const aiMessages = pgTable('ai_messages', { }; }); -// Relationships -export const aiConversationsRelations = relations(aiConversations, ({ one, many }) => ({ - tenant: one(organization, { - fields: [aiConversations.tenantId], - references: [organization.id], - }), +// Relationships (within-tenant only — no cross-DB relations to global organization table) +export const aiConversationsRelations = relations(aiConversations, ({ many }) => ({ messages: many(aiMessages), })); export const aiMessagesRelations = relations(aiMessages, ({ one }) => ({ - tenant: one(organization, { - fields: [aiMessages.tenantId], - references: [organization.id], - }), conversation: one(aiConversations, { - // Must match the composite PK fields: [aiMessages.tenantId, aiMessages.conversationId], references: [aiConversations.tenantId, aiConversations.id], }), diff --git a/packages/database/src/schema/tenant/tenant.ts b/packages/database/src/schema/tenant/tenant.ts index 29be2e31..9271b766 100644 --- a/packages/database/src/schema/tenant/tenant.ts +++ b/packages/database/src/schema/tenant/tenant.ts @@ -1,6 +1,7 @@ import { pgTable, uuid, varchar, text, timestamp, jsonb, index, uniqueIndex, pgEnum } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; -import { organization } from '../global/identity.js'; +// NOTE: No cross-DB FK to organization — tenant isolation is enforced by TenantDatabaseManager +// routing: every request resolves tenantId from auth context and connects to the correct tenant DB. // Shared environment discriminator — used by both app_connection and ui_workspace. // Defined here (tenant.ts) so workspace.ts can import it without a circular dep. @@ -34,7 +35,10 @@ export type AppConnectionStatus = (typeof AppConnectionStatus)[keyof typeof AppC */ export const appConnections = pgTable('app_connection', { id: uuid('id').defaultRandom().primaryKey(), - tenantId: text('tenant_id').notNull().references(() => organization.id, { onDelete: 'cascade' }), + // tenantId identifies which organization this connection belongs to. + // No FK to organization — cross-database FKs are not supported in Postgres. + // Tenant isolation is enforced by TenantDatabaseManager: each org routes to its own DB. + tenantId: text('tenant_id').notNull(), // Provider name — validated against PROVIDER_REGISTRY in application code appName: varchar('app_name', { length: 100 }).notNull(), diff --git a/packages/database/src/schema/tenant/workspace.ts b/packages/database/src/schema/tenant/workspace.ts index 3689396d..8630166c 100644 --- a/packages/database/src/schema/tenant/workspace.ts +++ b/packages/database/src/schema/tenant/workspace.ts @@ -10,9 +10,9 @@ import { unique, } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; -import { organization } from '../global/identity.js'; import { appConnections, envTypeEnum } from './tenant.js'; export { envTypeEnum } from './tenant.js'; +// NOTE: No cross-DB FK to organization — tenant isolation enforced by TenantDatabaseManager routing. // --------------------------------------------------------------------------- @@ -27,10 +27,8 @@ export { envTypeEnum } from './tenant.js'; */ export const uiWorkspaces = pgTable('ui_workspace', { id: uuid('id').defaultRandom().primaryKey(), - // text — matches organization.id which is also text - orgId: text('org_id') - .notNull() - .references(() => organization.id, { onDelete: 'cascade' }), + // orgId identifies the owning organization. No cross-DB FK to organization table. + orgId: text('org_id').notNull(), name: varchar('name', { length: 255 }).notNull(), envType: envTypeEnum('env_type').notNull().default('PRODUCTION'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), diff --git a/packages/dbmanager/src/impl/sql-database-manager.ts b/packages/dbmanager/src/impl/sql-database-manager.ts index fb7a384a..02cd80be 100644 --- a/packages/dbmanager/src/impl/sql-database-manager.ts +++ b/packages/dbmanager/src/impl/sql-database-manager.ts @@ -377,15 +377,19 @@ export class SqlDatabaseManager { private async provisionCanonicalTables(schemaName: string): Promise { try { - // Find appName from connection_storage_registry - const res = await this.db.$client.query(` - SELECT ac.app_name - FROM public.connection_storage_registry csr - JOIN public.app_connection ac ON ac.id = csr.connection_id - WHERE csr.data_namespace = $1 - `, [schemaName]); + // Find appName by deriving connection_id from schemaName (e.g. ws_{connection_id_with_underscores}) + const connectionIdCandidate = schemaName.startsWith('ws_') ? schemaName.slice(3).replace(/_/g, '-') : null; + const isUuid = connectionIdCandidate && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(connectionIdCandidate); - const appName = res.rows[0]?.app_name as string | undefined; + let appName: string | undefined; + if (isUuid) { + const res = await this.db.$client.query(` + SELECT app_name + FROM public.app_connection + WHERE id = $1 + `, [connectionIdCandidate]); + appName = res.rows[0]?.app_name as string | undefined; + } if (appName && this.domainProvisionerResolver) { const provisioner = this.domainProvisionerResolver(appName); diff --git a/packages/dbmanager/src/interfaces.ts b/packages/dbmanager/src/interfaces.ts index db84212b..055a9f29 100644 --- a/packages/dbmanager/src/interfaces.ts +++ b/packages/dbmanager/src/interfaces.ts @@ -1,5 +1,7 @@ import type { DrizzleDb } from '@nexiom/database'; +export const DB_MANAGER = 'DB_MANAGER'; + export enum SchemaPlan { /** * Only the empty PostgreSQL schema namespace (e.g., "ws_abc123") diff --git a/packages/queue/src/constants.ts b/packages/queue/src/constants.ts index ebbf39bf..ec35c471 100644 --- a/packages/queue/src/constants.ts +++ b/packages/queue/src/constants.ts @@ -9,6 +9,7 @@ export enum QueueName { DeliveryQueue = "delivery-queue", AiCopilotQueue = "ai-copilot-queue", GitopsQueue = "gitops-queue", + TenantProvisionQueue = "tenant-provision-queue", // Dead-letter queues — activated after 5 failed attempts InboundQueueDLQ = "inbound-queue-dlq", @@ -17,6 +18,7 @@ export enum QueueName { DeliveryQueueDLQ = "delivery-queue-dlq", AiCopilotQueueDLQ = "ai-copilot-queue-dlq", GitopsQueueDLQ = "gitops-queue-dlq", + TenantProvisionQueueDLQ = "tenant-provision-queue-dlq", } /** Injection token — use to inject QueueService across the application. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8cd62fa..97e94810 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,7 +119,7 @@ importers: version: 3.0.3 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -312,6 +312,94 @@ importers: specifier: ^5.5.3 version: 5.9.3 + apps/tenant-provisioner: + dependencies: + '@nestjs/common': + specifier: ^11.0.1 + version: 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.2 + version: 4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/schedule': + specifier: ^6.1.1 + version: 6.1.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nexiom/database': + specifier: workspace:* + version: link:../../packages/database + '@nexiom/queue': + specifier: workspace:* + version: link:../../packages/queue + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3) + pg: + specifier: ^8.16.3 + version: 8.16.3 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + uuid: + specifier: ^13.0.0 + version: 13.0.0 + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.3 + '@eslint/js': + specifier: ^9.18.0 + version: 9.39.2 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.14(@swc/cli@0.8.0(@swc/core@1.15.11))(@swc/core@1.15.11)(@types/node@22.19.3) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11) + '@swc/core': + specifier: ^1.15.11 + version: 1.15.11 + '@types/node': + specifier: ^22.10.7 + version: 22.19.3 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 + '@types/uuid': + specifier: ^11.0.0 + version: 11.0.0 + '@vitest/coverage-v8': + specifier: ^2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + eslint: + specifier: ^9.18.0 + version: 9.39.2(jiti@2.6.1) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + unplugin-swc: + specifier: ^1.5.1 + version: 1.5.9(@swc/core@1.15.11)(rollup@4.55.1) + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) + apps/web: dependencies: '@ai-sdk/react': @@ -376,7 +464,7 @@ importers: version: 1.13.2 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -584,7 +672,7 @@ importers: version: 3.0.3 better-auth: specifier: ^1.4.10 - version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) + version: 1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -757,6 +845,9 @@ importers: '@nexiom/database': specifier: workspace:* version: link:../../../packages/database + '@nexiom/dbmanager': + specifier: workspace:* + version: link:../../../packages/dbmanager '@nexiom/piece-framework': specifier: workspace:* version: link:../../../packages/pieces/platform/framework @@ -16269,30 +16360,7 @@ snapshots: bcryptjs@3.0.3: {} - better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): - dependencies: - '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.21 - '@noble/ciphers': 2.1.1 - '@noble/hashes': 2.0.1 - better-call: 1.1.7(zod@4.3.5) - defu: 6.1.4 - jose: 6.1.3 - kysely: 0.28.9 - nanostores: 1.1.0 - zod: 4.3.5 - optionalDependencies: - drizzle-kit: 0.31.8 - drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3) - next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) - pg: 8.16.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - vitest: 2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) - - better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): + better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): dependencies: '@better-auth/core': 1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.10(@better-auth/core@1.4.10(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7(zod@4.3.5))(jose@6.1.3)(kysely@0.28.9)(nanostores@1.1.0)) @@ -16313,7 +16381,7 @@ snapshots: pg: 8.16.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - vitest: 2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) + vitest: 2.1.9(@types/node@24.10.4)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@24.10.4)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1) better-auth@1.4.10(drizzle-kit@0.31.8)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3))(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3))(pg@8.16.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@2.1.9(@types/node@22.19.3)(happy-dom@20.1.0)(jsdom@27.4.0(@noble/hashes@2.0.1))(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.3)(typescript@5.9.3))(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)): dependencies: @@ -16332,7 +16400,7 @@ snapshots: optionalDependencies: drizzle-kit: 0.31.8 drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(kysely@0.28.9)(pg@8.16.3) - next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) + next: 16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) pg: 8.16.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -19230,34 +19298,6 @@ snapshots: rxjs: 7.8.2 zod: 4.3.5 - next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): - dependencies: - '@next/env': 16.1.1 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001780 - postcss: 8.4.31 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) - optionalDependencies: - '@next/swc-darwin-arm64': 16.1.1 - '@next/swc-darwin-x64': 16.1.1 - '@next/swc-linux-arm64-gnu': 16.1.1 - '@next/swc-linux-arm64-musl': 16.1.1 - '@next/swc-linux-x64-gnu': 16.1.1 - '@next/swc-linux-x64-musl': 16.1.1 - '@next/swc-win32-arm64-msvc': 16.1.1 - '@next/swc-win32-x64-msvc': 16.1.1 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.58.2 - sass: 1.97.3 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - optional: true - next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): dependencies: '@next/env': 16.1.1 @@ -19286,34 +19326,6 @@ snapshots: - babel-plugin-macros optional: true - next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3): - dependencies: - '@next/env': 16.1.1 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001780 - postcss: 8.4.31 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(react@19.2.3) - optionalDependencies: - '@next/swc-darwin-arm64': 16.1.1 - '@next/swc-darwin-x64': 16.1.1 - '@next/swc-linux-arm64-gnu': 16.1.1 - '@next/swc-linux-arm64-musl': 16.1.1 - '@next/swc-linux-x64-gnu': 16.1.1 - '@next/swc-linux-x64-musl': 16.1.1 - '@next/swc-win32-arm64-msvc': 16.1.1 - '@next/swc-win32-x64-msvc': 16.1.1 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.58.2 - sass: 1.97.3 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - optional: true - node-abort-controller@3.1.1: {} node-addon-api@7.1.1: @@ -21006,14 +21018,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): - dependencies: - client-only: 0.0.1 - react: 19.2.3 - optionalDependencies: - '@babel/core': 7.28.5 - optional: true - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): dependencies: client-only: 0.0.1 @@ -21022,12 +21026,6 @@ snapshots: '@babel/core': 7.29.0 optional: true - styled-jsx@5.1.6(react@19.2.3): - dependencies: - client-only: 0.0.1 - react: 19.2.3 - optional: true - stylehacks@6.1.1(postcss@8.5.6): dependencies: browserslist: 4.28.1 diff --git a/scripts/init-localstack.sh b/scripts/init-localstack.sh index 3b5cf7f1..ae22090e 100755 --- a/scripts/init-localstack.sh +++ b/scripts/init-localstack.sh @@ -16,6 +16,7 @@ QUEUES=( "normalized-queue" "delivery-queue" "gitops-queue" + "tenant-provision-queue" ) # Step 1: Create all DLQs first (no redrive policy); idempotent — ignore From 8490b5f0ea97b9bf1ba25f5dc22a9767f08a9262 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 08:52:51 +0000 Subject: [PATCH 8/9] fix: apply CodeRabbit auto-fixes Fixed 15 file(s) based on 20 unresolved review comments. Co-authored-by: CodeRabbit --- apps/api/src/db/database-manager.ts | 46 +++++++++------ .../modules/gitops/gitops-webhook.guard.ts | 7 +++ .../workspaces/capacity-manager.service.ts | 25 ++++----- apps/api/src/scripts/admin-bootstrap.ts | 20 ++++--- apps/api/src/shared/db.utils.spec.ts | 6 ++ .../provisioner/tenant-provision.worker.ts | 56 +++++++++++++------ apps/tenant-provisioner/tsconfig.json | 1 - apps/tenant-provisioner/vitest.config.ts | 3 + .../pipeline/normalized-outbox.worker.spec.ts | 5 +- .../pipeline/normalized-outbox.worker.ts | 8 ++- .../ai/core/src/categories/mapping.service.ts | 22 +++++++- package.json | 8 +-- packages/database/drizzle.config.ts | 2 +- packages/queue/src/index.ts | 1 + pnpm-workspace.yaml | 6 ++ 15 files changed, 146 insertions(+), 70 deletions(-) diff --git a/apps/api/src/db/database-manager.ts b/apps/api/src/db/database-manager.ts index 6ae76dcc..239eca43 100644 --- a/apps/api/src/db/database-manager.ts +++ b/apps/api/src/db/database-manager.ts @@ -881,7 +881,13 @@ export class DatabaseManager { ? `${parsedUrl.username}${parsedUrl.password ? ':' + parsedUrl.password : ''}@` : ''; hostUrl = `${parsedUrl.protocol}//${auth}${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}`; - } catch { + } catch (err) { + const redactedUrl = process.env.DATABASE_URL + ? process.env.DATABASE_URL.replace(/:\/\/[^@]*@/, '://***:***@') + : '(not set)'; + console.warn( + `⚠️ Failed to parse DATABASE_URL: ${redactedUrl}. Error: ${err instanceof Error ? err.message : String(err)}. Falling back to default.`, + ); hostUrl = 'postgresql://user:password@localhost:5432'; } @@ -922,25 +928,27 @@ export class DatabaseManager { // ── Step 4: Connect to tenant DB and write app_connection fixtures ─────── const tenantUrl = `${hostUrl.replace(/\/$/, '')}/${tenantDbName}`; - const tenantPool = new Pool({ connectionString: tenantUrl, max: 5 }); - const tenantDb = drizzle(tenantPool, { schema: dbSchema }); + let tenantPool: Pool | undefined; - const { SchemaPlan } = await import('@nexiom/dbmanager'); - const { TenantDatabaseManager } = await import('@nexiom/dbmanager'); - const { getDomainProvisioner } = await import('@nexiom/piece-framework'); + try { + tenantPool = new Pool({ connectionString: tenantUrl, max: 5 }); + const tenantDb = drizzle(tenantPool, { schema: dbSchema }); - const schemaMgr = new TenantDatabaseManager( - globalDb as unknown as import('@nexiom/database').DrizzleDb, - (_hostIdentifier: string) => { - const pool2 = new Pool({ connectionString: tenantUrl, max: 20 }); - return drizzle(pool2, { - schema: dbSchema, - }) as unknown as import('@nexiom/database').DrizzleDb; - }, - getDomainProvisioner, - ); + const { SchemaPlan } = await import('@nexiom/dbmanager'); + const { TenantDatabaseManager } = await import('@nexiom/dbmanager'); + const { getDomainProvisioner } = await import('@nexiom/piece-framework'); + + const schemaMgr = new TenantDatabaseManager( + globalDb as unknown as import('@nexiom/database').DrizzleDb, + (_hostIdentifier: string) => { + const pool2 = new Pool({ connectionString: tenantUrl, max: 20 }); + return drizzle(pool2, { + schema: dbSchema, + }) as unknown as import('@nexiom/database').DrizzleDb; + }, + getDomainProvisioner, + ); - try { const fixtures = [ { id: '00000000-0000-0000-0000-000000000001', @@ -1050,7 +1058,9 @@ export class DatabaseManager { ' Do NOT edit the value column manually — it holds AES-GCM ciphertext.', ); } finally { - await tenantPool.end(); + if (tenantPool) { + await tenantPool.end(); + } await globalClient.end(); } } diff --git a/apps/api/src/modules/gitops/gitops-webhook.guard.ts b/apps/api/src/modules/gitops/gitops-webhook.guard.ts index 1610cbbf..67aa26d1 100644 --- a/apps/api/src/modules/gitops/gitops-webhook.guard.ts +++ b/apps/api/src/modules/gitops/gitops-webhook.guard.ts @@ -97,6 +97,13 @@ export class GitopsWebhookGuard implements CanActivate { const providedDigest = signature.slice(7); + // Validate that providedDigest is exactly 64 hex characters (SHA-256 output) + if (!/^[a-fA-F0-9]{64}$/.test(providedDigest)) { + throw new UnauthorizedException( + 'Invalid X-Hub-Signature-256: digest must be exactly 64 hexadecimal characters', + ); + } + // Get raw request body (must be available via express.raw() middleware) const rawBody = (request as RawBodyRequest).rawBody; if (!rawBody) { diff --git a/apps/api/src/modules/workspaces/capacity-manager.service.ts b/apps/api/src/modules/workspaces/capacity-manager.service.ts index 644e6fb1..2637ec4e 100644 --- a/apps/api/src/modules/workspaces/capacity-manager.service.ts +++ b/apps/api/src/modules/workspaces/capacity-manager.service.ts @@ -2,10 +2,9 @@ import { Injectable, Inject, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { DATABASE_CONNECTION } from '@nexiom/database'; import type { DrizzleDb } from '@nexiom/database'; -import { QueueService, QueueName } from '@nexiom/queue'; +import { QueueService, QueueName, type ProvisionDatabaseEvent } from '@nexiom/queue'; import { sql } from 'drizzle-orm'; import { v4 as uuidv4 } from 'uuid'; -import type { ProvisionDatabaseEvent } from './capacity-manager.types.js'; /** The minimum number of WARM databases we always want in the pool. */ const WARM_POOL_TARGET = 5; @@ -59,7 +58,10 @@ export class CapacityManagerService { const event: ProvisionDatabaseEvent = { poolSlotId, hostUrl }; const dbName = `nexiom_tenant_${poolSlotId.replace(/-/g, '_')}`; - // 1. Insert INITIALIZING slot to prevent double-queueing on next cron tick + // 1. Dispatch the job first (if this fails, we don't create orphaned rows) + await this.queueService.send(QueueName.TenantProvisionQueue, event); + + // 2. Insert INITIALIZING slot only after successful queue send await this.db.execute( sql`INSERT INTO tenant_storage_registry (tenant_id, database_name, database_host_url, region_context, status, created_at, updated_at) @@ -74,8 +76,6 @@ export class CapacityManagerService { )`, ); - // 2. Dispatch the job - await this.queueService.send(QueueName.TenantProvisionQueue, event); this.logger.debug(` → Queued provision job: poolSlotId=${poolSlotId}`); } } catch (err) { @@ -94,20 +94,19 @@ export class CapacityManagerService { } /** - * Derives the base host URL (no database name) from DATABASE_URL. + * Derives the base host URL (no database name, no credentials) from DATABASE_URL. * This is sent in the queue message so the tenant-provisioner knows - * which Postgres cluster to provision on. + * which Postgres cluster to provision on. Credentials are stripped for security. */ private deriveHostUrl(): string { const raw = process.env.DATABASE_URL ?? ''; try { const parsed = new URL(raw); - const auth = parsed.username - ? `${parsed.username}${parsed.password ? ':' + parsed.password : ''}@` - : ''; - return `${parsed.protocol}//${auth}${parsed.hostname}${parsed.port ? ':' + parsed.port : ''}`; - } catch { - return 'postgresql://user:password@localhost:5432'; + return `${parsed.protocol}//${parsed.hostname}${parsed.port ? ':' + parsed.port : ''}`; + } catch (err) { + throw new Error( + `Failed to parse DATABASE_URL: ${err instanceof Error ? err.message : String(err)}`, + ); } } } diff --git a/apps/api/src/scripts/admin-bootstrap.ts b/apps/api/src/scripts/admin-bootstrap.ts index 93330ec3..55783a0f 100644 --- a/apps/api/src/scripts/admin-bootstrap.ts +++ b/apps/api/src/scripts/admin-bootstrap.ts @@ -44,7 +44,10 @@ const getDbClient = () => { // --- Helpers --- -async function seedRbac(db: NodePgDatabase) { +async function seedRbac( + client: Client, + db: NodePgDatabase, +) { console.log('3️⃣ Seeding RBAC (Roles & Permissions)...'); // Get validated env vars (throws if missing) @@ -55,15 +58,14 @@ async function seedRbac(db: NodePgDatabase) { systemTenantId: getRequiredSystemTenantId(), }; - await seedSystemRbac( - db as unknown as NodePgDatabase, - config, - console, - ); + // Create a new Drizzle instance scoped to the identity schema + const identityDb = drizzle(client, { schema: identitySchema }); + await seedSystemRbac(identityDb, config, console); } // Shared helper to elevate a user to System Owner async function elevateToOwner( + client: Client, db: NodePgDatabase, email: string, skipSeed?: boolean, @@ -101,7 +103,7 @@ async function elevateToOwner( .onConflictDoNothing(); if (!skipSeed) { - await seedRbac(db); + await seedRbac(client, db); } // Find user @@ -248,7 +250,7 @@ async function bootstrapAdmin() { await client.connect(); const db = drizzle(client, { schema }); - await elevateToOwner(db, EMAIL); + await elevateToOwner(client, db, EMAIL); console.log(`Basic setup complete.`); } finally { await client.end(); @@ -401,7 +403,7 @@ async function forceResetAdmin() { } // 4. Elevate to System Owner - await elevateToOwner(db, EMAIL); + await elevateToOwner(client, db, EMAIL); console.log(` Credentials: ${EMAIL} / ********`); } finally { await client.end(); diff --git a/apps/api/src/shared/db.utils.spec.ts b/apps/api/src/shared/db.utils.spec.ts index 65133e64..16705bfe 100644 --- a/apps/api/src/shared/db.utils.spec.ts +++ b/apps/api/src/shared/db.utils.spec.ts @@ -8,6 +8,7 @@ describe('db.utils', () => { describe('extractPgError', () => { it('returns null for non-objects', () => { expect(extractPgError(null)).toBeNull(); + expect(extractPgError(undefined)).toBeNull(); expect(extractPgError('string')).toBeNull(); expect(extractPgError(123)).toBeNull(); }); @@ -40,6 +41,11 @@ describe('db.utils', () => { const error = { cause: { code: 123 } }; expect(extractPgError(error)).toBeNull(); }); + + it('returns null for non-string direct code', () => { + const error = { code: 123 }; + expect(extractPgError(error)).toBeNull(); + }); }); describe('isUniqueViolation', () => { diff --git a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts index 554dd0ec..1794b009 100644 --- a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts +++ b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts @@ -1,11 +1,10 @@ import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; -import { QueueService, QueueName } from "@nexiom/queue"; +import { QueueService, QueueName, type ProvisionDatabaseEvent } from "@nexiom/queue"; import { Client as PgClient, Pool } from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/node-postgres/migrator"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import type { ProvisionDatabaseEvent } from "./provision-database.event.js"; /** * TenantProvisionWorker @@ -34,7 +33,15 @@ export class TenantProvisionWorker implements OnModuleInit { this.logger.log( `Received ProvisionDatabaseEvent — poolSlotId=${event.poolSlotId}`, ); - await this.provision(event); + try { + await this.provision(event); + } catch (err) { + this.logger.error( + `Failed to provision database for poolSlotId=${event.poolSlotId}: ${err instanceof Error ? err.message : String(err)}`, + err instanceof Error ? err.stack : undefined, + ); + throw err; + } }, ); } @@ -79,7 +86,7 @@ export class TenantProvisionWorker implements OnModuleInit { `SELECT 1 FROM pg_database WHERE datname = $1`, [dbName], ); - if ((existing.rowCount ?? 0) === 0) { + if (existing.rows.length === 0) { await adminClient.query(`CREATE DATABASE "${dbName}"`); this.logger.log(` ✓ Created database: ${dbName}`); } else { @@ -91,16 +98,23 @@ export class TenantProvisionWorker implements OnModuleInit { } private async runMigrations(dbName: string, hostUrl: string): Promise { - const tenantUrl = `${hostUrl.replace(/\/$/, "")}/${dbName}`; - - const rootDir = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../../../../", - ); - const migrationsFolder = path.join( - rootDir, - "packages/database/drizzle/tenant", - ); + // Recompose full connection string by reading credentials from DATABASE_URL + const dbUrl = process.env.DATABASE_URL ?? ''; + const parsedEnv = new URL(dbUrl); + const auth = parsedEnv.username + ? `${parsedEnv.username}${parsedEnv.password ? ':' + parsedEnv.password : ''}@` + : ''; + const tenantUrl = `${hostUrl.replace(/\/$/, "").replace(/^([^:]+:\/\/)/, `$1${auth}`)}/${dbName}`; + + const migrationsFolder = + process.env.MIGRATIONS_DIR ?? + path.join( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../../", + ), + "packages/database/drizzle/tenant", + ); const pool = new Pool({ connectionString: tenantUrl, max: 2 }); try { @@ -120,15 +134,21 @@ export class TenantProvisionWorker implements OnModuleInit { try { // Update the INITIALIZING slot to WARM - await adminClient.query( + const result = await adminClient.query( `UPDATE tenant_storage_registry SET status = 'WARM', updated_at = NOW() WHERE tenant_id = $1`, [`WARM-${poolSlotId}`], ); - this.logger.log( - ` ✓ Registered WARM slot in registry: WARM-${poolSlotId}`, - ); + if (result.rowCount === 0) { + this.logger.warn( + ` ⚠️ No row updated in tenant_storage_registry for poolSlotId=${poolSlotId} (tenant_id=WARM-${poolSlotId}). The INITIALIZING row may not exist.`, + ); + } else { + this.logger.log( + ` ✓ Registered WARM slot in registry: WARM-${poolSlotId}`, + ); + } } finally { await adminClient.end(); } diff --git a/apps/tenant-provisioner/tsconfig.json b/apps/tenant-provisioner/tsconfig.json index 38723f38..58e2d58f 100644 --- a/apps/tenant-provisioner/tsconfig.json +++ b/apps/tenant-provisioner/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "types": ["vitest/globals"], "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, diff --git a/apps/tenant-provisioner/vitest.config.ts b/apps/tenant-provisioner/vitest.config.ts index 15212413..814020f1 100644 --- a/apps/tenant-provisioner/vitest.config.ts +++ b/apps/tenant-provisioner/vitest.config.ts @@ -5,6 +5,9 @@ export default defineConfig({ test: { globals: true, root: './', + typecheck: { + tsconfig: './tsconfig.spec.json', + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], diff --git a/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts b/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts index ceb738f1..e08dd6dc 100644 --- a/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts +++ b/apps/worker/src/modules/pipeline/normalized-outbox.worker.spec.ts @@ -189,7 +189,10 @@ describe("NormalizedOutboxWorker", () => { }); it("should handle schema query errors securely without throwing", async () => { - tenantDb.from.mockRejectedValueOnce(new Error("Schema query error")); + // Mock rejection from where() chain, simulating a query error in production code + globalDb.from.mockReturnValueOnce({ + where: vi.fn().mockRejectedValueOnce(new Error("Schema query error")), + }); await expect(worker.processOutbox()).resolves.toBeUndefined(); }); diff --git a/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts b/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts index 63f868d3..6a57fe06 100644 --- a/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts +++ b/apps/worker/src/modules/pipeline/normalized-outbox.worker.ts @@ -47,7 +47,8 @@ export class NormalizedOutboxWorker { // 2. Use TenantDatabaseManager to connect to the specific physical tenant DB. const tenantDb = await this.dbManager.getTenantDb(tenant.tenantId); - // 3. Query app_connection inside the Global DB to find all ACTIVE connections for this tenant. + // 3. Query app_connection inside the Global DB to find all ACTIVE connections for this tenant + // that have normalized_outbox provisioned (OUTBOUND_ACTIVE or GATEWAY_ACTIVE plans). const connections = await this.globalDb .select({ id: appConnections.id, appName: appConnections.appName }) .from(appConnections) @@ -55,9 +56,14 @@ export class NormalizedOutboxWorker { and( eq(appConnections.status, "ACTIVE"), eq(appConnections.tenantId, tenant.tenantId), + sql`${appConnections.schemaPlan} IN ('OUTBOUND_ACTIVE', 'GATEWAY_ACTIVE')`, ), ); + if (connections.length === 0) { + return; + } + // 4. Run drainWorkspaceOutbox on each schema derived from the connection. for (const connection of connections) { const schemaName = getWorkspaceSchemaName( diff --git a/engine/ai/core/src/categories/mapping.service.ts b/engine/ai/core/src/categories/mapping.service.ts index b9c363f9..6b07f51a 100644 --- a/engine/ai/core/src/categories/mapping.service.ts +++ b/engine/ai/core/src/categories/mapping.service.ts @@ -27,7 +27,7 @@ export class MappingService { version: string = 'v1' ): Promise | null> { const tenantDb = await this.dbManager.getTenantDb(tenantId); - + // Look for mapping config in the tenant's dedicated database const mappingRecord = await tenantDb.select({ config: canonicalMappings.mappingConfig }) .from(canonicalMappings) @@ -47,6 +47,26 @@ export class MappingService { return mappingRecord[0].config as Record; } + // If no tenant-specific mapping found, fall back to global canonical mapping + const globalDb = await this.dbManager.getGlobalDb(); + const globalMappingRecord = await globalDb.select({ config: canonicalMappings.mappingConfig }) + .from(canonicalMappings) + .where( + and( + eq(canonicalMappings.appName, appName), + eq(canonicalMappings.category, category), + eq(canonicalMappings.entity, entity), + eq(canonicalMappings.viewMode, viewMode), + eq(canonicalMappings.version, version) + ) + ) + .limit(1); + + if (globalMappingRecord.length > 0) { + this.logger.debug({ appName, category, entity, viewMode }, 'Loaded Global Mapping Configuration'); + return globalMappingRecord[0].config as Record; + } + // --- TEMPORARY MOCK MAPPING FOR PHASE 1 TESTING --- // Only enabled in non-production environments and requires exact context match const isNonProdMockEnabled = process.env.NODE_ENV !== 'production'; diff --git a/package.json b/package.json index 05e68423..118c68b5 100644 --- a/package.json +++ b/package.json @@ -58,12 +58,6 @@ "overrides": { "axios": "1.13.2", "esbuild": "0.25.12" - }, - "onlyBuiltDependencies": [ - "@nestjs/core", - "@swc/core", - "esbuild", - "sharp" - ] + } } } \ No newline at end of file diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 246c45e9..5ced658e 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -45,7 +45,7 @@ if (!process.env.DATABASE_URL) { // Global DB schema ONLY — identity, registry, pieces. // Tenant-specific tables are managed by drizzle.config.tenant.ts. export default { - schema: ['./dist/schema/global/**/*.js', './dist/schema/tenant/**/*.js'], + schema: ['./dist/schema/global/**/*.js'], out: './drizzle/global', dialect: 'postgresql', dbCredentials: { diff --git a/packages/queue/src/index.ts b/packages/queue/src/index.ts index ac9266d6..cfa345ad 100644 --- a/packages/queue/src/index.ts +++ b/packages/queue/src/index.ts @@ -11,3 +11,4 @@ export type { QueueModuleAsyncOptions, } from "./queue.module.js"; export { createQueueModuleOptions } from "./queue.options.js"; +export type { ProvisionDatabaseEvent } from "./events/provision-database.event.js"; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 590eb693..ebe850bd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,3 +8,9 @@ packages: - 'packages/pieces/application/*' - 'packages/domain/*' +onlyBuiltDependencies: + - '@nestjs/core' + - '@swc/core' + - 'esbuild' + - 'sharp' + From 5c08d19ede17b1be1c36bf2e5122be8962d95c82 Mon Sep 17 00:00:00 2001 From: Pramod Narayana Date: Mon, 4 May 2026 18:16:40 +0530 Subject: [PATCH 9/9] db per tenant migration code review --- apps/api/src/db/database-manager.ts | 2 +- .../capacity-manager.service.spec.ts | 2 ++ .../workspaces/capacity-manager.service.ts | 6 +++++- .../workspaces/capacity-manager.types.ts | 13 ------------- apps/api/src/scripts/admin-bootstrap.ts | 7 ++----- .../provisioner/provision-database.event.ts | 19 ------------------- .../tenant-provision.worker.spec.ts | 15 ++++++++++++--- .../provisioner/tenant-provision.worker.ts | 12 ++++++++---- .../ai/core/src/categories/mapping.service.ts | 6 +++--- .../src/events/provision-database.event.ts | 4 ++++ 10 files changed, 37 insertions(+), 49 deletions(-) delete mode 100644 apps/api/src/modules/workspaces/capacity-manager.types.ts delete mode 100644 apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts create mode 100644 packages/queue/src/events/provision-database.event.ts diff --git a/apps/api/src/db/database-manager.ts b/apps/api/src/db/database-manager.ts index 239eca43..7f18f433 100644 --- a/apps/api/src/db/database-manager.ts +++ b/apps/api/src/db/database-manager.ts @@ -2,7 +2,7 @@ import { execSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createCipheriv, randomBytes } from 'node:crypto'; -import { Client } from 'pg'; +import { Client, type Pool } from 'pg'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { eq, sql } from 'drizzle-orm'; import * as schema from './schema.js'; diff --git a/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts b/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts index ff676c60..5754c196 100644 --- a/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts +++ b/apps/api/src/modules/workspaces/capacity-manager.service.spec.ts @@ -35,6 +35,8 @@ describe('CapacityManagerService', () => { vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => {}); errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {}); vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {}); + + process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/testdb'; }); describe('replenishPool', () => { diff --git a/apps/api/src/modules/workspaces/capacity-manager.service.ts b/apps/api/src/modules/workspaces/capacity-manager.service.ts index 2637ec4e..705e36ae 100644 --- a/apps/api/src/modules/workspaces/capacity-manager.service.ts +++ b/apps/api/src/modules/workspaces/capacity-manager.service.ts @@ -2,7 +2,11 @@ import { Injectable, Inject, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { DATABASE_CONNECTION } from '@nexiom/database'; import type { DrizzleDb } from '@nexiom/database'; -import { QueueService, QueueName, type ProvisionDatabaseEvent } from '@nexiom/queue'; +import { + QueueService, + QueueName, + type ProvisionDatabaseEvent, +} from '@nexiom/queue'; import { sql } from 'drizzle-orm'; import { v4 as uuidv4 } from 'uuid'; diff --git a/apps/api/src/modules/workspaces/capacity-manager.types.ts b/apps/api/src/modules/workspaces/capacity-manager.types.ts deleted file mode 100644 index e6bbf98e..00000000 --- a/apps/api/src/modules/workspaces/capacity-manager.types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Shared type for the ProvisionDatabaseEvent published by the - * CapacityManagerService and consumed by the TenantProvisionWorker. - * - * Re-exported here so the API does not need to import from - * apps/tenant-provisioner directly. - */ -export interface ProvisionDatabaseEvent { - /** Pre-generated UUID used both as the pool slot placeholder and DB name suffix. */ - poolSlotId: string; - /** The base Postgres host URL (without the database name path). */ - hostUrl: string; -} diff --git a/apps/api/src/scripts/admin-bootstrap.ts b/apps/api/src/scripts/admin-bootstrap.ts index 55783a0f..2684def1 100644 --- a/apps/api/src/scripts/admin-bootstrap.ts +++ b/apps/api/src/scripts/admin-bootstrap.ts @@ -44,10 +44,7 @@ const getDbClient = () => { // --- Helpers --- -async function seedRbac( - client: Client, - db: NodePgDatabase, -) { +async function seedRbac(client: Client) { console.log('3️⃣ Seeding RBAC (Roles & Permissions)...'); // Get validated env vars (throws if missing) @@ -103,7 +100,7 @@ async function elevateToOwner( .onConflictDoNothing(); if (!skipSeed) { - await seedRbac(client, db); + await seedRbac(client); } // Find user diff --git a/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts b/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts deleted file mode 100644 index dc1a678e..00000000 --- a/apps/tenant-provisioner/src/modules/provisioner/provision-database.event.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Describes the shape of a message on the TenantProvisionQueue. - * - * The Capacity Manager publishes these messages; the TenantProvisionWorker - * consumes them and creates a physically isolated Postgres database. - */ -export interface ProvisionDatabaseEvent { - /** - * A pre-generated UUID used both as the ephemeral tenantId placeholder - * (e.g. WARM-) and the database name suffix. - */ - poolSlotId: string; - - /** - * The target Postgres host URL (without the database path). - * e.g. 'postgresql://user:password@localhost:5432' - */ - hostUrl: string; -} diff --git a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts index bf8b1902..ef32b35a 100644 --- a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts +++ b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { TenantProvisionWorker } from "./tenant-provision.worker.js"; import { QueueName } from "@nexiom/queue"; import type { QueueService } from "@nexiom/queue"; -import type { ProvisionDatabaseEvent } from "./provision-database.event.js"; +import type { ProvisionDatabaseEvent } from "@nexiom/queue"; // --- Mocks --- const mockQuery = vi.fn(); @@ -41,6 +41,12 @@ describe("TenantProvisionWorker", () => { beforeEach(() => { vi.clearAllMocks(); + mockQuery.mockReset(); + mockConnect.mockReset(); + mockEnd.mockReset(); + mockPoolEnd.mockReset(); + + process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/testdb"; queueServiceMock = { consume: vi.fn(), @@ -73,7 +79,7 @@ describe("TenantProvisionWorker", () => { it("creates database, runs migrations, and registers slot as WARM", async () => { // Mock PG to simulate the database does NOT exist initially - mockQuery.mockResolvedValueOnce({ rowCount: 0 }); // SELECT 1 FROM pg_database ... + mockQuery.mockResolvedValueOnce({ rowCount: 0, rows: [] }); // SELECT 1 FROM pg_database ... mockQuery.mockResolvedValueOnce({ rowCount: 0 }); // CREATE DATABASE mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE tenant_storage_registry @@ -114,7 +120,10 @@ describe("TenantProvisionWorker", () => { it("skips database creation if it already exists, but still runs migrations and updates registry", async () => { // Mock PG to simulate the database DOES exist - mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // SELECT 1 FROM pg_database ... + mockQuery.mockResolvedValueOnce({ + rowCount: 1, + rows: [{ "?column?": 1 }], + }); // SELECT 1 FROM pg_database ... mockQuery.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE tenant_storage_registry await handler({ diff --git a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts index 1794b009..da3fa10c 100644 --- a/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts +++ b/apps/tenant-provisioner/src/modules/provisioner/tenant-provision.worker.ts @@ -1,5 +1,9 @@ import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; -import { QueueService, QueueName, type ProvisionDatabaseEvent } from "@nexiom/queue"; +import { + QueueService, + QueueName, + type ProvisionDatabaseEvent, +} from "@nexiom/queue"; import { Client as PgClient, Pool } from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/node-postgres/migrator"; @@ -99,11 +103,11 @@ export class TenantProvisionWorker implements OnModuleInit { private async runMigrations(dbName: string, hostUrl: string): Promise { // Recompose full connection string by reading credentials from DATABASE_URL - const dbUrl = process.env.DATABASE_URL ?? ''; + const dbUrl = process.env.DATABASE_URL ?? ""; const parsedEnv = new URL(dbUrl); const auth = parsedEnv.username - ? `${parsedEnv.username}${parsedEnv.password ? ':' + parsedEnv.password : ''}@` - : ''; + ? `${parsedEnv.username}${parsedEnv.password ? ":" + parsedEnv.password : ""}@` + : ""; const tenantUrl = `${hostUrl.replace(/\/$/, "").replace(/^([^:]+:\/\/)/, `$1${auth}`)}/${dbName}`; const migrationsFolder = diff --git a/engine/ai/core/src/categories/mapping.service.ts b/engine/ai/core/src/categories/mapping.service.ts index 6b07f51a..f7073d8a 100644 --- a/engine/ai/core/src/categories/mapping.service.ts +++ b/engine/ai/core/src/categories/mapping.service.ts @@ -1,7 +1,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { eq, and } from 'drizzle-orm'; import { PinoLogger } from 'nestjs-pino'; -import { canonicalMappings } from '@nexiom/database'; +import { canonicalMappings, DATABASE_CONNECTION, type DrizzleDb } from '@nexiom/database'; import { DB_MANAGER, type DatabaseManager } from '@nexiom/dbmanager'; @Injectable() @@ -9,6 +9,7 @@ export class MappingService { constructor( private readonly logger: PinoLogger, @Inject(DB_MANAGER) private readonly dbManager: DatabaseManager, + @Inject(DATABASE_CONNECTION) private readonly globalDb: DrizzleDb, ) { this.logger.setContext(MappingService.name); } @@ -48,8 +49,7 @@ export class MappingService { } // If no tenant-specific mapping found, fall back to global canonical mapping - const globalDb = await this.dbManager.getGlobalDb(); - const globalMappingRecord = await globalDb.select({ config: canonicalMappings.mappingConfig }) + const globalMappingRecord = await this.globalDb.select({ config: canonicalMappings.mappingConfig }) .from(canonicalMappings) .where( and( diff --git a/packages/queue/src/events/provision-database.event.ts b/packages/queue/src/events/provision-database.event.ts new file mode 100644 index 00000000..2729873b --- /dev/null +++ b/packages/queue/src/events/provision-database.event.ts @@ -0,0 +1,4 @@ +export interface ProvisionDatabaseEvent { + poolSlotId: string; + hostUrl: string; +}